Ren'Py Best way to track and trigger events in Renpy?

Phazyn

Newbie
Game Developer
Feb 25, 2019
52
276
Hey all,

I've fairly new to coding in renpy and most of my learning has been from the renpy site, YouTube tutorials and reverse engineering from other renpy games. I'm currently reading one thread now but I figured I would ask my question here as I'm not sure that thread is exactly related. I'm curious about some best practices when it comes to tracking and triggering events. Would if and elif statements be the best way to track if an even is ready? I have a central location that the player has to return back to often and based on their progress in the game certain events trigger. would if/elif statements be the best way to check for whether a certain event should play or is there something more elegant and streamlined than a MASSIVE pile of if/elifs?

Thanks for reading and any help or insight you can offer!
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,581
2,219
I think the answer is probably the eternal "it depends".

Whilst I'm sure people will have their opinions, mine is that there isn't really a single best practice.

Certainly the most common solution you will find is for RenPy games to have variables to indicate either whether an event has happened yet, or to track the progress through an event. Each variable being separate and sometimes being a True/False boolean flag or a number... or realistically, anything else you can think of.

Basically track any actual choice made by the player (or combination of choices) and then code something later to react to those choices. If you track a choice with a variable, but never actually use that variable... no real harm done. But if you didn't track something, then later decide to add it in retroactively - you're in for a world of pain and compromise.

The main deciding factors in my mind though are your programming background and your logical thinking. You may be new to RenPy - but the coding ideas from other languages are easily transferable.

By which I mean, you could have a lot of seemingly random variables and corresponding if:/else: statements. If you're highly organized, that can either result in spaghetti or something elegant. At the other end of the spectrum, you could have custom classes, with events held almost like in a database, with predefined triggers and logical dependencies to make the most awesome and flexible event handling RenPy has ever seen. Or again, everything in between.

Edit: If I had one piece of advice, it would be that if you do write something based largely on if:/else: statements... and the code nested within the if:/else: statements is more than a dozen lines long... use jump instead to a label where those dozen+ statements are coded instead. It'll generally avoid complex nesting and sections of code that are too long to realistically keep track of.

If you're new to coding in general... keep it simple. Use simple variables. Try to keep the name of those variables consistent. Don't just have a "fucked_alice" variable in chapter 1, only to realize you'll need another "fucked_alice2" variable in chapter 3... instead aim for something like "ch1_fucked_alice" and "ch3_fucked_alice"... or something... whatever makes sense to you.

The most important factor is that you understand it, not that others would write it the same way. Don't try to be clever for clever's sake.
 
Last edited:

harem - king

Member
Feb 26, 2018
327
382
Attempting to do conditionals without if/elif (or python switch statements) tends to be messy and not very worth the time trying to avoid the if/elif statements.

That said, there are many, many, different ways to go about actually formulating your conditionals with the if/elif blocks and that's usually where the optimization can come in.
 
  • Like
Reactions: Phazyn

hiya02

Member
Oct 14, 2019
169
95
If you already know that you will have LOTS of events, then using if/elif WILL become a bug-prone, unreadable mess that is pain to maintain. Sometimes you can avoid long ladder-ifs by using maps or polymorphism.
 
  • Like
Reactions: Phazyn

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,364
15,281
I think the answer is probably the eternal "it depends".
Exactly.

It goes from events split location by location :
Code:
label McBedroom:
    if [this condition]:
        jump whatever
    elif [that condition]:
        jump whatever
    [...]
pass by events split quest by quest :
Code:
label mainLoop:
    call triggerQuest1
    if not _return is None:
        jump expression _return
    call triggerQuest2
    if not _return is None:
        jump expression _return
   [...]

label triggerQuest1:
    if [this condition]:
        return "quest1Event1"
    elif [that condition]:
        return "quest1Event2"
    [...]
to pure Python :
Code:
init python:
    def trigger():
        for e in triggers:
            if eval( e.condition ) is True:
                return e.label
        return None

    class Trigger( renpy.python.RevertableObject ):
        def __init__( trigger, target ):
            self.conditon = trigger
            self.label = target

define triggers = [ Trigger( "quest1 == 2 and day >= 12 and girlLove >= 10", "quest1event2" ),
                            Trigger( "quest5 == 1 and day >= 3 and money >= 100", "quest5event1" ),
                            [...] ]

label mainLoop:
    $ target = trigger()
    if not target is None:
        jump expression target
    [...]
And there's more between each.
 

Phazyn

Newbie
Game Developer
Feb 25, 2019
52
276
Thank you all so much for your input and for your specific examples. This has already informed me immensely. The bits about using jumps to organize was particularly helpful. I'm fairly new to all of this so some things I was not even aware of and the Renpy documentation sometimes goes over my head. I'm gonna do some googling for this but of chance is there a useful resource that breaks down and explains all the different "if"s, "if not"s "elif"s, etc? I'm curious to learn more. Thanks again so much for all your help!
 

8InchFloppyDick

Member
Game Developer
Apr 4, 2020
134
381
Too bad python doesn't provide a switch() statement. I find it makes for much tidier code than long if/then 'ladders'. There are ways to implement a switch() in python, but they are a tad cludgy. Once your coding skills mature you might want to look into it.
 

hakarlman

Engaged Member
Jul 30, 2017
2,097
3,274
Is this part needed?
Code:
class Trigger( renpy.python.RevertableObject ):
I think you can just do this:
Code:
class Trigger():
Here's why (I think):
-----------------
Pytom said,
The answer is yes, but Ren'Py does this for you automatically, when you're in a Ren'Py (.rpy, rather than .py) context.


For example, in .rpy code:
Code:
{ "foo" : "bar", "bar" : "baz", "baz" : "foo" }
.... creates a RevertableDict, rather than a dict. Similarly, in the Ren'Py context, object is bound to RevertableObject. So unless you plan on writing python code in a .py file, there's no need to use the names in renpy.python directly. If you are using .py files, you should inherit from (and create) renpy.store.object, renpy.store.list, renpy.store.dict, and renpy.store.set.
 

hakarlman

Engaged Member
Jul 30, 2017
2,097
3,274
Exactly.

It goes from events split location by location :
Code:
label McBedroom:
    if [this condition]:
        jump whatever
    elif [that condition]:
        jump whatever
    [...]
pass by events split quest by quest :
Code:
label mainLoop:
    call triggerQuest1
    if not _return is None:
        jump expression _return
    call triggerQuest2
    if not _return is None:
        jump expression _return
   [...]

label triggerQuest1:
    if [this condition]:
        return "quest1Event1"
    elif [that condition]:
        return "quest1Event2"
    [...]
to pure Python :
Code:
init python:
    def trigger():
        for e in triggers:
            if eval( e.condition ) is True:
                return e.label
        return None

    class Trigger( renpy.python.RevertableObject ):
        def __init__( trigger, target ):
            self.conditon = trigger
            self.label = target

define triggers = [ Trigger( "quest1 == 2 and day >= 12 and girlLove >= 10", "quest1event2" ),
                            Trigger( "quest5 == 1 and day >= 3 and money >= 100", "quest5event1" ),
                            [...] ]

label mainLoop:
    $ target = trigger()
    if not target is None:
        jump expression target
    [...]
And there's more between each.
For some reason I keep getting an error: triggers is not defined, but if I switch it to default, then it works. Odd. Define seems like the correct approach here given the static nature of event triggers.

Code:
#doesn't work
define triggers = ...

#works
default triggers = ...
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,364
15,281
For some reason I keep getting an error: triggers is not defined, but if I switch it to default, then it works. Odd. Define seems like the correct approach here given the static nature of event triggers.
In this case, don't use default, but pass the declaration of the list in the init block :
Code:
init python:
    triggers = [ Trigger( "quest1 == 2 and day >= 12 and girlLove >= 10", "quest1event2" ),
                            Trigger( "quest5 == 1 and day >= 3 and money >= 100", "quest5event1" ),
                            [...] ]

   def trigger():
        for e in triggers:
[...]
It will have same effect than using define. But like this time "triggers" is created before the code using it, there should be no errors.
 
Last edited:
  • Like
Reactions: hakarlman

khumak

Engaged Member
Oct 2, 2017
3,623
3,660
I found that default seemed to be more forgiving than define when it comes to save game compatibility, especially if you have a habit of going back and tweaking older events.

As far as avoiding a situation where the game becomes a bug infested mess, I found that the most important way to avoid that was to write the code in small sections and then test each little section rather than do 8 hours of coding with no testing and then try and figure out where it all went wrong... Testing each little section as you go is a lot faster because when you do have a bug, it's usually pretty obvious where it is. It's in that newly written section of code.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,364
15,281
I found that default seemed to be more forgiving than define when it comes to save game compatibility, especially if you have a habit of going back and tweaking older events.
It's mostly because define isn't supposed to be used with variables that will be saved.
Both defined and defaulted variables will be created before you load the save file, and will be redefined if the variables exist in the said save file. The difference is what happen when the variable do not exist in the save file.
A defaulted variable will have its value reset to the default one, while a defined variable will keep its actual value.
But if this happen, it's because of a design flaw in the game, not because of Ren'py. A variable that will have its value changed must be created with default, or at worse in a label if you're sure that all player will pass through this point even from an already started game.


Testing each little section as you go is a lot faster because when you do have a bug, it's usually pretty obvious where it is. It's in that newly written section of code.
Yes, it will catch the obvious bugs, but 100% bugs free sections do not mean 100% bugs free code.
Take Main Seduction by example. The author use two variables, dom and sub, but never achieved to remember what they mean. At the level of a section of the code, everything is correct, but at the level of the game, you pass your time changing between a dom MC to a sub MC, sometimes more than once in what was a single update.
 

khumak

Engaged Member
Oct 2, 2017
3,623
3,660
Yes, it will catch the obvious bugs, but 100% bugs free sections do not mean 100% bugs free code.
Take Main Seduction by example. The author use two variables, dom and sub, but never achieved to remember what they mean. At the level of a section of the code, everything is correct, but at the level of the game, you pass your time changing between a dom MC to a sub MC, sometimes more than once in what was a single update.
Yeah for me, it mostly makes it easy to catch the constant stream of stupid little syntax errors I make. Missing brackets, missing quotes, miss spelled variables, etc. I find those MUCH harder to sort out if I spend several hours coding with no testing in between. Most of my other mistakes are just broken image links that I will sometimes miss when I don't test the bad choices as thoroughly as the good choices. Usually fan reports are how I end up fixing those.

Usually if I make a logic mistake it's when I decide that the way I'm tracking some aspect of the game is too much of a hassle and decide to streamline it by adding a new variable. For instance maybe MC is gathering evidence for blackmail and I keep having to do something like:


if event1=true and event2=true and ... eventN=true then something


I might go back and make a variable called evidence as a counter for tracking the success or failure of those events. Then I would have to go back and tweak all of those other events with something like this and sometimes miss a spot or two:


if evidence>4 then something