Ren'Py A better way to sequence events. I'm getting overwhelmed by if/elif ladders with variables

SnubbLR

Newbie
Game Developer
Sep 13, 2017
79
644
So I'm working on a semi open-world game, but am extremely new to coding in general. After doing some initial research, I figured using variables to keep track of certain scenes would be a good way. And initially it worked out great, but it's now getting to a point where I get lost in if/elif ladders. This is an example of how it's starting to look:

Python:
label start:
#variables

    $ alleyvisit = 0
    $ helenavisit = 0
    $ lilithvisit = 0
    $ yogavisit = 0
    $ farmvisit = 0
    $ maxfound = 0
######################
label helenahouse:
 if alleyvisit == 0:
        scene 00helenahouse01
        "I have no reason to go here at this time"
        menu:
            "Return to map":
                call worldmap
            "Cheat skip alley (Remove later)":
                $ alleyvisit += 1
                call worldmap
    elif alleyvisit == 1 and helenavisit == 0:
        jump helenaintro
    elif alleyvisit == 1 and helenavisit == 1:
        scene 00helenahouse01
        ys "I should probably visit Lily in the alley again, before coming back here"
        call worldmap
    elif alleyvisit == 2 and helenavisit == 1:
        jump helenaintru
    elif alleyvisit ==2 and helenavisit ==2 and lilithvisit == 0:
        scene 00helenahouse01
        y "I should consult with Lilith on what to do next"
        call worldmap
    elif alleyvisit ==2 and helenavisit ==2 and lilithvisit == 1:
        jump helenalily1
        "(First spying)"
        call worldmap
    elif helenavisit ==3 and farmvisit ==0:
        scene 00helenahouse01
        ys "I should come back later to see what [h] and [l] are up to"
        ys "Maybe exploring the downtown area for my next projects?"
        call worldmap
    elif helenavisit ==3 and farmvisit ==1:
        jump helenalily2
    elif helenavisit ==4 and maxfound ==0:
        scene 00helenahouse01
        ys "I should probably try to find Helena's dog Max before coming back here"
        call worldmap
This is the code that's being run when clicking on a specific location (The house of Helena in this case) on a worldmap callscreen. The if/elif lines are just becoming longer and longer. And this is just one of many locations... Is there any way you could point me in the direction of something more efficient yet not too complex for a beginner like me? Or maybe I'm just using the variables in an inefficient way?

Thank you for taking your time helping beginners like me. This is truly a great community.
 

sillyrobot

Engaged Member
Apr 22, 2019
2,163
1,884
Probably a better way for the example shown is a finite state machine.

Essentially, you have several states the game can be in with specific ways to exit to other known states. You have a variable that holds the current state and when the conditions are met, you change it to the new state as you exit. The next time the user comes in, the new state is presented.
 

K.T.L.

Keeping Families Together
Donor
Mar 5, 2019
718
1,423
It's worth remembering that you can do things like:
Code:
$ call_label = "helenahouse_" + str(alleyvisit) + "_" + str(farmvisit)
That gives you something like "helena_house_0_1" then you create a label with that name and call it with "call expression call_label."

It won't reduce the amount of 'laddering' but it will enable you to separate all your decision making code from the dialogue and scene coding. Makes it a lot easier to see what you're doing, IMO.
 

probably_dave

Member
Jun 3, 2017
133
363
Hi Abuuu, Good luck with learning to code, it's a fairly long journey, but the more practice the better

Usually, for this type of 'event management' I would usually use a Controller object to manage which 'events' are played when, however, as you're extremely new at coding, I wouldn't recommend this for you.

Instead, I'll give a few pointers on how to manage the if, elif, and trouble your are having. This is based on the code provided:

Rather than use if X and Y, split you code up so you have nested IF statements. This will make it easier to handle. For example:
Python:
if alleyvisit == 0:
    # blah blah blah
elif alleyvisit == 1:
    if helenavisit == 0:
        #blah blah blah
    elif helenavisit == 1:
        # blah blah blah
    else:
        # default behaviour
Having nested if statements will make your life a bit easier than multiple 'ands'. Keeping it in a consistant order will also help with your planning (i.e. try to avoid starting with if helenavisit == 4 and then having helenavisit == 1 afterwards).

Also, if you have numerous different events for the same higher level values, separate them out their own label and call it.
For example, if you had 10 lilithvisist events all when helenavisit is 2, have these in a seperate section:

Python:
    ...
    elif helenavisit == 4:
        call lilithvisit_helenavisit4
    ...
...
label lilithvisit_helendavisit4:
   if lilithvisit == 1
   ...

Try and also comment as much as you go, at least one line so you know when the event should be called. This can save you having to remember what state each and every number represents.

You should also be careful with your 'jump' and 'call'. You have a call after a jump, and as jump will not return to it's original place, everything after it will not be run. In your example: "(First spying)",call worldmap will never be run as there is a jump helenalily1 before it.

I would also suggest splitting each location into separate rpy files. This will make it easier for you to manage in the long term, especially if you have multiple locations.

Again, this is not the best way of doing it, however, it is fairly simple for a new starter and should be manageable for fairly straight forward VNs. As you progress and gain more experience, you can start to look for more complex solutions
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,611
2,258
As far as laddering goes... the only real solution is to use jump (or call) as soon as possible. It won't help with the complexity, but it will help with the separation necessary to help keep things straighter in your head.

There are probably some technical performance hits by coding this way, rather than if / elif pairings. But let me remind you of something that became true in the mid-1980's... programmer timer is more expensive that processor time. If you spend 10 minutes thinking about something the CPU is going to save 0.1 nanoseconds doing... it's wasted 9 minutes of YOUR time. Unless you're coding for google or NASA, inefficiency is fine if it brings code simplicity and readability.

I'm imagining your code more like :-

Python:
default alleyvisit = 0
default helenavisit = 0
default lilithvisit = 0
default yogavisit = 0
default farmvisit = 0
default maxfound = 0

label start:

    # blah, blah, code...


label helenahouse:

    if alleyvisit == 0:
        jump alley_visit_zero

    if alleyvisit == 1:
        jump alley_visit_one

    if alleyvisit == 2:
        jump alley_visit_two

    if alleyvisit == 3:
        jump alley_visit_three

    if alleyvisit == 4:
        jump alley_visit_four

  

label alley_visit_zero:

    scene 00helenahouse01
    "I have no reason to go here at this time"
    menu:
        "Return to map":
            call worldmap
        "Cheat skip alley (Remove later)":
            $ alleyvisit += 1
            call worldmap


label alley_visit_one:

    if helenavisit == 0:
        jump helenaintro
  
    scene 00helenahouse01
    ys "I should probably visit Lily in the alley again, before coming back here"
    call worldmap


label alley_visit_two:
  
    if helenavisit == 1:
        jump helenaintru
  
    if helenavisit == 2:
        if lilithvisit == 0:
            scene 00helenahouse01
            y "I should consult with Lilith on what to do next"
            call worldmap
        else:
            jump helenalily1

        # "(First spying)"
        # call worldmap
  
    if helenavisit == 3:
        if farmvisit ==0:
            scene 00helenahouse01
            ys "I should come back later to see what [h] and [l] are up to"
            ys "Maybe exploring the downtown area for my next projects?"
            call worldmap
        else:
            jump helenalily2
      
    elif helenavisit == 4:
        if maxfound == 0:
            scene 00helenahouse01
            ys "I should probably try to find Helena's dog Max before coming back here"
            call worldmap
        else:
            # blah, blah.

This code doesn't actually make sense, without more context - but I assume my example is similiar enough to your example that you can draw some parallels.

You can simplify things a little by using call expression as kitkat102 has laid out. But neither solution is going to help with your main problem...

If you are getting lost in the myriad of variables - that's really on you. Experience will teach you alternate ways of thinking about things and offer different solutions to the same problems. But keeping it all straight in your head isn't something for which there is a programming solution. There are programming solutions to make the code simpler - but those are design decisions and design is sometimes hard.

For example, I would probably code things with True / False variables for some of those visits. Then disable the button / menu choice that jumps to theses bits of code if the value of "visited_helena" was True. That is... don't offer to visit someone you've already dealt with. But maybe you really do want to count how many times someone has been visited as a number - or maybe the "I should come back..." type messages really are necessary as game hints. I don't know, so my thoughts are purely theoretical.

Not sure how much that actually helps, but maybe another viewpoint will nudge you towards a solution you can live with.

As an aside, I am somewhat concerned about your code for call worldmap. call implies that control will return to that same section of code at some point. Calling a label and not returning is bad. Given how it is expressed here, I would expect jump worldmap - but maybe that all important context I'm missing is the reason why I shouldn't be concerned.
 
Last edited:
  • Like
Reactions: anne O'nymous
Apr 24, 2020
192
257
My first thought would be to try and see if some of those can be turned into "quests" instead of individual objectives.
It can be really tough trying to figure out where in the story you are, if you are keeping track of it through individual actions rather than story points.

If you can't get to story point E without having first done A, B, C and D, then it's a lot easier to just write if D: rather than if A and B and C and D:.
 

sillyrobot

Engaged Member
Apr 22, 2019
2,163
1,884
Here's a bare bones example of a finite state machine of the first 3 states.

Code:
class GameState: #simple class that currently only holds the function to be called by this state; typically holds other information specific to this state
    def __init__( self, fcn ):
        self.fcn = fcn

    def Do( self ):
        self.fcn()

# all alley states currently defined
define alleyStateStart = 0
define alleyStateVisited   = 1
define alleyStateNeedToVisitLilly = 2

define alleyStateList = [ new GameState( AlleySceneState0 ),  new GameState( AlleyHelenaIntro ), new GameState( AlleyVisitLilly )]

default currentAlleyStateIndex = alleyStateStart 

#NB entries in the list must correspond to the ascending order of the defined states without gaps since that is used as the index into the list
#Adding new states is easy.   I'm tempted to use a dictionary rather than a list to prevent mismatches and simplify removing states as the machine evolves.  

label AlleySceneState0():
    scene 00helenahouse01
    "I have no reason to go here at this time"
    menu:
        "Return to map":
            #don't do anything

        "Cheat skip alley (Remove later)":
            $ currentAlleyStateIndex = alleyStateVisited   #note that each block updates the current state directly 

label AlleyHelenaIntro():
    # show and/or do stuff
    $currentAlleyStateIndex = alleyStateNeedToVisitLilly

label AlleyVisitLilly():
    # show and do stuff
    $currentAlleyStateIndex = alleyStateStart # resetting the state to the beginning, to show that the journey is not one-way

label WorldMap:
    # a system to track which area the player tries to interact with; assume it is the alley

    alleyStateList[ currentAlleyStateIndex ].Do()

    jump WorldMap
 
Last edited:

hiya02

Member
Oct 14, 2019
171
98
If your game is going to have generous amount of events/NPC story arcs, then I'd also suggest you to look into state machines. It will make things more manageable when your game grows in scope. There are way too many Renpy VNs out there with the traditional if-elif mess :confused:
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,233
What's probably the most efficient answer (from the coder side) is a mix between kitkat102 and 79flavors answer.


Here's a bare bones example of a finite state machine of the first 3 states.
Why so complicated code ?

Code:
define alleyStateList = [ "AlleySceneState0",  "AlleyHelenaIntro", "AlleyVisitLilly"]
default currentAlleyStateIndex = alleyStateStart

label whatever:
    jump expression alleyStateList[currentAlleyStateIndex]
Would do the same than you code.

The interest of an object here would be to limit the code in Ren'py, by deporting in into the object with something like :
Code:
    class StateAlley( renpy.python.RevertableObject ):
        def __call__( self ):
            if    alleyvisit == 0: return "alley1"
            elif alleyvisit == 1: return "helenaintro" if helenavisit == 0 else "alley2"
            elif alleyvisit == 2 and helenavisit == 1: return "helenaintru"
            [...]

define alleyState = StateAlley()

label alley1:
        scene 00helenahouse01
        "I have no reason to go here at this time"
        menu:
            "Return to map":
                call worldmap
            "Cheat skip alley (Remove later)":
                $ alleyvisit += 1
                call worldmap

label alley2:
        scene 00helenahouse01
        ys "I should probably visit Lily in the alley again, before coming back here"
        call worldmap

label helenahouse:
    jump expression alleyState()
Yet the same could (and would) be better in a function instead of an object.


But the most optimal code would be something like that :
Code:
label helenahouse:
    if alleyvisit <= 2:
        jump expression "alley_visit{}_helena{}_lilith{}".format( alleyvisit, helenavisit, lilithvisit )
    elif helenavisit == 3:
        jump expression "helena_visit{}_farm{}".format( helenavisit, farmvisit )
    elif helenavisit == 4:
        jump expression "helena_visit{}_found{}".format( helenavisit, maxfound )
    else:
        "Something goes wrong"


label alley_visit0_helena0_lilith0:
        scene 00helenahouse01
        "I have no reason to go here at this time"
        menu:
            "Return to map":
                call worldmap
            "Cheat skip alley (Remove later)":
                $ alleyvisit += 1
                call worldmap

label alley_visit1_helena0_lilith0:
        jump helenaintro

label alley_visit1_helena1_lilith0:
        scene 00helenahouse01
        ys "I should probably visit Lily in the alley again, before coming back here"
        call worldmap

label alley_visit2_helena1_lilith0:
        jump helenaintru

label alley_visit2_helena2_lilith0:
        scene 00helenahouse01
        y "I should consult with Lilith on what to do next"
        call worldmap

label alley_visit2_helena2_lilith1:
        jump helenalily1
        "(First spying)"
        call worldmap

label helena_visit3_farm0:
        scene 00helenahouse01
        ys "I should come back later to see what [h] and [l] are up to"
        ys "Maybe exploring the downtown area for my next projects?"
        call worldmap

label helena_visit3_farm1:
        jump helenalily2

label helena_visit4_found0:
        scene 00helenahouse01
        ys "I should probably try to find Helena's dog Max before coming back here"
        call worldmap
 

sillyrobot

Engaged Member
Apr 22, 2019
2,163
1,884
What's probably the most efficient answer (from the coder side) is a mix between kitkat102 and 79flavors answer.




Why so complicated code ?
Because, although I was a developer decades ago, python wasn't a language I used. Much of the syntax sugar is still unfamiliar to me.

<snip code>
The point of the class isn't to take over state control, but to hold data necessary to an individual state as more complex states almost inevitably require (for example a counter for the number of times a particular state is reached that affects the possible transition states). You're right my bare-bones example shouldn't have bothered including the class at all but instead had the list be the functions themselves. The class is an obvious enhancement I included out of habit. I wouldn't try to include state expression in the class simply because I'm not comfortable enough with ren'py to fully grok if including ren'py statements (such as scene or dialogue) among the python is useful or a disaster waiting to be debugged.

The primary value of the finite state machine is the logic moving between any current state and possible next states tends to to small, self-contained, and limits the number and depth of logic nesting. The context of the state is known. If you are in state X then all the attributes required to reach state X are in place and do not need to be checked.

Adding new states involves affecting a small a code base (effectively just the states that can transition to the new state) and doesn't increase nesting depth and bypasses the common errors that come from that.
 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Donor
Respected User
Jun 10, 2017
10,978
16,233
The point of the class isn't to take over state control,
I agree, but at least the class was given a reason to exist, what isn't at all the case in your example, even reduced to a simple function.

Your class, is just a rewriting of the str class. Write it like this, and your code will do exactly the same thing (minus the typo due to your lack of knowledge regarding Ren'py) without changing a single line except the definition of the class itself :
Code:
class GameState( str ):
    def Do( self ):
        return self
Therefore, it present absolutely no interest since, like I said,
Code:
define alleyStateList = [ GameState( AlleySceneState0 ),  GameState( AlleyHelenaIntro ), GameState( AlleyVisitLilly )]
and,
Code:
define alleyStateList = [ "AlleySceneState0",  "AlleyHelenaIntro", "AlleyVisitLilly" ]
are strictly identical in their behavior. This whatever the GameState inherit from an object or from str, or if there's a function in place of the class. This because labels aren't callable, therefore your :
Code:
    def Do( self ):
        self.fcn()
should be wrote :
Code:
    def Do( self ):
        renpy.jump( self.fcn )
And your :
Code:
label WorldMap:
    alleyStateList[ currentAlleyStateIndex ].Do()
is just another way to write :
Code:
label WorldMap:
    jump expression alleyStateList[ currentAlleyStateIndex ]
But while this simplify the if structure, it also complicate the code itself, since it will need a secondary variable as step counter for the list/array. This simply because the variables alleyvisit, helenavisit and all, can perfectly be needed elsewhere in the code. Therefore, they have to be kept, in addition to the new currentAlleyStateIndex.
In short, you just replaced a bunch of conditions depending on many variables, by a bunch of conditions depending on a single variable, then simplified the way to deal with those conditions. But it have a side effect, since it froze the if structure. It's not anymore possible to insert an intermediate state, because it would change the meaning of the currentAlleyStateIndex value ; the step 5 would become the step 6, and so on. And it would also not permit for the player to access this intermediate step, because he's already too advanced in the event.


but to hold data necessary to an individual state as more complex states almost inevitably require (for example a counter for the number of times a particular state is reached that affects the possible transition states).
A more complex state that you already have. Why rely on a counter that will have to be incremented, when you can rely on the condition that already exist ? And there, using an object would be effectively interesting :
Code:
init python:
    class DeportedIf( renpy.python.RevertableObject ):
        def __init__( self, label, condition ):
            self.label = label
            self.condition = condition
        def __call__( self ):
            return eval( self.condition )
Then the if structure pass from what it is initially to :
Code:
define alleyTrigger = [ DeportedIf( "alleyVisit0", "alleyvisit == 0" ),
                 DeportedIf( "helenaintro", "alleyvisit == 1 and helenavisit == 0" ),
                 DeportedIf( "alleyVisit2", "alleyvisit == 1 and helenavisit == 1" ),
                 DeportedIf( "helenaintru", "alleyvisit == 2 and helenavisit == 1" ),
                 DeportedIf( "alleyVisit4", "alleyvisit ==2 and helenavisit ==2 and lilithvisit == 0" ),
                 DeportedIf( "helenalily1", "alleyvisit ==2 and helenavisit ==2 and lilithvisit == 1" ),
                 DeportedIf( "alleyVisit6", "helenavisit ==3 and farmvisit ==0" ),
                 DeportedIf( "helenalily2", "helenavisit ==3 and farmvisit ==1" ),
                 DeportedIf( "alleyVisit8", "helenavisit ==4 and maxfound ==0" ) ]

label helenahouse:
    $ i = 0
    while i < len( alleyTrigger ):
        if alleyTrigger[i]():
            jump expression alleyTrigger[i].label
        $ i += 1

    [whatever "else" code you want here]
With the labels being :
Code:
label alleyVisit0:
        scene 00helenahouse01
        "I have no reason to go here at this time"
        menu:
            "Return to map":
                call worldmap
            "Cheat skip alley (Remove later)":
                $ alleyvisit += 1
                call worldmap

label alleyVisit2:
        scene 00helenahouse01
        ys "I should probably visit Lily in the alley again, before coming back here"
        call worldmap

label alleyVisit4:
        scene 00helenahouse01
        y "I should consult with Lilith on what to do next"
        call worldmap

label alleyVisit6:
        scene 00helenahouse01
        ys "I should come back later to see what [h] and [l] are up to"
        ys "Maybe exploring the downtown area for my next projects?"
        call worldmap

label alleyVisit8:
        scene 00helenahouse01
        ys "I should probably try to find Helena's dog Max before coming back here"
        call worldmap
While it not totally get ride of the if structure, since it deport it as condition for each state, it present some advantages compared to your approach.
Firstly, it don't rely on an external value, the conditions aren't changed, they are kept like they are.
Secondly, because of that it's easier to remember what the conditions for a given state effectively are.
Thirdly, it's not frozen. You can insert as many intermediate state that you want, the player will see them if he met the requirements.
 
  • Like
Reactions: Shadow Fiend