Ren'Py Tutorial [Beginner -> Intermediate] Future-Proofing State for Visual Novels

Exanadae

Newbie
Dec 19, 2018
84
512
Background

I'm a software developer who likes to play VNs with the source-code open in another window so I can be sure I'm not missing anything important. This means I see a lot of Ren'Py code. It's always bugged me that many developers track state haphazardly, so I decided to throw up a guide to how developers could (in my opinion) make life easier for themselves in the long run.

Introduction

It felt like this was always going to happen.

Six months into development, six episodes of your game published like clockwork and working hard on episode seven. But that one character. That one minor character who turned into a Patreon favorite. That one character you hadn't been tracking affection for because she was just the bank teller for fuck's sake. You grimace, go back through the code and add relationship-counter updates to every past interaction, and write those terrible words into the changelog.

* To access the scenes with Wendy, you will need to start a new game.
But it didn't have to be that way. With a little bit of preparation your players could have boned (or not boned) Wendy without ever knowing it wasn't part of your master-plan all along.

Counters Bad. Event Logs Good.

Knowing that wendy_affection == 7 tells you that Wendy likes you, but it doesn't tell you why she likes you. The problem with using counters to track game state in a VN is that every time you react to some in-game event by adding to (or subtracting from) a counter, you're throwing away important information about where that number came from. Other ways this can bite you in the ass include:
  • You've been scrupulously tracking how virtuous your MC has been behaving, but you realize too late that you also want to know how virtuous they've been behaving while other people were watching.
  • You mistakenly wrote += instead of -= in that conversation branch where you told your best friend she looks like a pig, and didn't notice until after you released the game
  • You want to make a callback to something that may have happened in a previous episode, but you didn't record whether it happened because it wasn't important at the time
  • You want your game to be unpredictable, but anyone who wants to know what their decisions mean in future episodes can look at the script to see what variables were updated today.
The solution is straightforward: record everything. Computers have gigabytes of memory these days, you can spend a few KB on an event log.

Start with an empty list:

Python:
default choices = []
Now every time the player makes any non-trivial choice, record that they did it in the list. Don't try to second-guess yourself here. If it's in an interaction menu, it's probably worth recording.

Python:
menu:
    "Punch him in the face!":
        choices.append("ep4bosspunched")
        "You knock your boss out cold. This might look bad in your performance review."
    "Slap him on the back":
        choices.append("ep4bossslapped")
        "Choking back bile, you pretend to laugh along with what [boss] just said about your mother."
Already, you'll be getting simple call-backs to previous occurrences for free.

Python:
mc "I don't know what you're getting so uptight about. Everybody does it."

if "ep2spiedlonger" in choices:
    mc "And you still haven't explained to me what you were doing with that goat in the bathroom."
A good example of a game that uses this technique to good effect is Jessica O'Neil's Hard News, which is constantly making short dialogue insertions and changes based on its log of every decision you've ever made. But for some reason JOHN hasn't taken the next obvious step, which is:

Replace your counters with functions.

Python:
init python:
    def boss_friendliness():
        bf = 0
        if "ep4bosspunched" in choices:
            bf -= 5
        if "ep4bossslapped" in choices:
            bf += 2
        if "ep6reacharound" in choices:
            bf += 10
        return bf
Moving these calculations from variables to functions gives you the power to add new ones whenever you want, or to change your mind about the effect of player decisions in a way that is 100% compatible with every existing save-game. You also get handy central locations where you can see every choice that might impact any given statistic, rather than having to global-search through all your scripts to find every place a variable is touched.

"But isn't it more efficient to keep this in a variable rather than recalculate it every time?" Sure in theory, but finding an item in a reasonably-sized list should take microseconds on any computer worth sticking in your pocket. I promise Ren'Py is doing so many far, far more expensive things every time you throw a line of text on the screen that you're not going to notice the difference.

Example Code

This is just the beginning, obviously. I threw together a quick sample Ren'Py project with some examples of how this can work in practice, and some helper-functions to streamline common operations:

 
Last edited:

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,112
14,768
Interesting approach, but most authors having zero knowledge in coding, I'm not sure that it's not too advanced for them. Not that they are idiots or can't learn, but it's like putting on a 10 meters diving board and saying "jump", someone who don't even know how to swim.
Many of them have just a vague idea about what def mean, and your example already make them deal with lambdas.


This said:
# A quick disclaimer, Python is not my best language so there might be
# simpler ways to do some of the stuff I do below.
yep :
Python:
    def choose(choice, max=-1):
        if max <= 0 or choices_log.count( choice ) < max:
            choices_log.append(choice)
there's already a count method, no need to create it.


It's past 5AM here, so no guaranties, but those should do the same :
Python:
    def made_choices(*choices):
        return len( [ a for a in choices if a in choices_log ] ) == len( choices )

    def score(scores):
        return reduce( [ scores.get( a, 0 ) for a in choices_log ] )
while being perhaps a little less abstruse for people not knowing Python, since they don't need a lambda.

Python:
    class Scores():
        @property
        def horniness( self ):
            return score({
              [...]

        @property
        def bravery( self ):
            return score({
              [...]

        @property
        def sneakiness( self ):
            return score({
              [...]

        @property
        def persistence( self ):
            return score({
              [...]

        @property
        def curiosity( self ):
            # once again a lambda free approach.
            return len( [ a for a in choices_log if a.startswith( "0100" ) ] )
[...]
define scores = Scores()
It can seem useless, but it permit to avoid the use of temporary attributes later:
Code:
    "Your bravery is [scores.bravery]"
    "Your horniness is [scores.horniness]"
    "Your sneakiness is [scores.sneakiness]"
    "Your persistence is [scores.persistence]"
    "Your curiosity is [scores.curiosity]"
Also, but regarding Ren'py this time :
Code:
    label about_this_game:

    # This system supports multi-choice conversation menus for free, and
    # if later you want to know "Did the MC ask the mysterious stranger
    # about the poker game?" it's in the log.

    menu:
        "What is this all about?":
            $ choose("0100what")
            [...]
        "Demo now! Explain later!" if not made_choices("0100what", "0100when", "0100why", "0100how"):
            d "Sure thing, chief."
is probably better this way :
Code:
default about_this_game = []
[...]
    menu:
        "What is this all about?":
            $ choose("0100what")
            [...]
            show concerned
        "Demo now! Explain later!":
            d "Sure thing, chief."
            jump start_demo

    menu about_this_game:
        set about_this_game
        "Why is that a good idea?":
            $ choose("0100why")
            [...]
            jump about_this_game
        "When should I use this?":
            $ choose("0100when")
            [...]
            jump about_this_game
        "How does it work?":
            $ choose("0100how")
            [...]
            jump about_this_game
        "I heard enough" if len( about_this_game ) < 3:
            d "Sure thing, chief."

label start_demo:
    d "Now before we start, I have a few hypothetical questions to ask you."
It's always easier if you limit the conditions to the strict minimum. It's as much possibilities of errors that are removed.
 

Exanadae

Newbie
Dec 19, 2018
84
512
Thanks! All good feedback.

The demo project was intended to be more "here's some stuff you could potentially cut/paste to make your life easier without entirely understanding it" than a useful tutorial, which is why I kept to the bare basics in the original post, but I agree it might be a bit too much of a complexity jump.

Although I'm skeptical that list comprehensions are any more innately... er... comprehensible than lambdas, which is why I went with more straight-up procedural code in the comments.
 

anne O'nymous

I'm not grumpy, I'm just coded that way.
Modder
Respected User
Donor
Jun 10, 2017
10,112
14,768
The demo project was intended to be more "here's some stuff you could potentially cut/paste to make your life easier without entirely understanding it" than a useful tutorial, [...]
Which is the way I understood it, and the reason I commented. The easier is the "ready to go" code, the less errors people will do.
The real "ready to go" code don't exist, people will always have to adapt it to their needs, and some will try to go really far for this. So, the more the said code stay near to the basis, the better it will be.


Although I'm skeptical that list comprehensions are any more innately... er... comprehensible than lambdas, which is why I went with more straight-up procedural code in the comments.
I don't think they are, reason why I qualified them as perhaps less confusing, and not better approach.
With a little change, the commentaries above the functions using them will describe them totally. This while with lambda you can just give an example of code having the same result.
 

lancelotdulak

Active Member
Nov 7, 2018
556
549
I'd.. slightly change this.

Learn to use matrixes/multidimension variables.. (psuedocode)
Constant charNAME
Angie = 1;
Erin =2;
Janice = 3 (etc)..

stats
Anger = 1;
Hornyness = 2;
Like = 3;
HasHadSex = 4 ; etc etc

Charstate
isattracted = 1
sexpossible=2
quest1complete = 3
quest2complete = 4 etc etc


charName[NAME]][stat]
charName[NAME][charState]

In some languages you do this with variables. others you do it with classes or structures.. EXACTLY this is why programmers love structures and classes... object oriented programming.
If you never use some or any of the variables for a certain character it doesnt matter.. it's a zero.. that is never referenced
on the other hand after 50 updates you can STILL use that variable for that character. Indeed even if you dont want ot use it for the secretary model you think is ugly.. you can have generic tests like "if mc brought flowers charName[character].like = charname[character.like] + 1. So you never plan on using that character.. doesnt matter. it doesnt hurt to store the data. 2 years from now when you DO want to use it.. the data is saved.. you dont have to say "hey i gave X a story arc so ..everyone has to restart the game from start because neither old variables nor old saves work anymore"

As a rule in programming make everything as macro and open ended as you can and there is almost never a reason to throw away data. even data youre not intentionally connecting. Way easier to write that generic storeage test than to type out one for each characters name etc.


Edit: if this isnt clear to anyone ill be happy to explain. This kind of thinking is how C and c++ programmers think by default and it is a good way to think for All languages
 

Honey hunters

Member
Jan 23, 2020
118
26
Hello everyone , I'm beginner for developing game and if you don't mind can you please help me some stuff to find free for developing my game in daz 3d , I'm searching the genital morph items for male and Female (like Vaginal , Penis , public hair ) items , soo where should i get this item free .... Please help me
 
  • Angry
Reactions: Luckzor

IncredibleH

New Member
Jul 2, 2020
2
0
I just tried to implement this (without lambdas as i don't like them) and ran in a problem with the score method and wanted to share my fix. Don't know if i am just doing it wrong as i am not new to coding, but to python and Ren'py.

Python:
    def score(scores):
        return reduce( [ scores.get( a, 0 ) for a in choices_log ] )
raised an error that reduce expects 2 arguments.

I got it to work like this:

Python:
    import operator
    [...]
    def score(scores):
        return reduce( [ operator.add, scores.get( a, 0 ) for a in choices_log ] )
 

mr.moonmi

Newbie
Game Developer
Jun 21, 2020
52
1,349
What am I doing wrong?)
var_mom.png
Code:
Python:
init python:
        def mom_score():
            s_mom = 0
            if "ch1helpmom" in choices:
                s_mom += 1
            return s_mom
 

79flavors

Well-Known Member
Respected User
Jun 14, 2018
1,557
2,170


You're missing the other half of the puzzle.
You've copied the def boss_friendliness(): code, but the OP didn't really cover how to use it.

I'm guessing you've got a watch s_mom in there, and it's failing because it can't find a variable called s_mom. That's because, in this example so far, s_mom is a variable that only exists within the function (it's a local variable).

I think the way this code is designed to work is something like:

Python:
default choices = []

init python:
    def mom_score():
        s_mom = 0
        if "ch1helpmom" in choices:
            s_mom += 1
        return s_mom

label start:

    scene black with fade:
    $ choices.append("ch1helpmom")

    if mom_score() >= 1:
        "Mom is happy."

    return

.... and it's that if mom_score() type usage that I think you were missing.

You could store the results of the function in another variable, but that seems at odds with what the OP of this thread seemed to be working toward.

But if you did, it might looks something like:

Python:
default choices = []
default s_mom = 0

init python:
    def mom_score():
        mom_score = 0
        if "ch1helpmom" in choices:
            mom_score += 1
        return mom_score

label start:

    scene black with fade:
    $ choices.append("ch1helpmom")

    $ s_mom = mom_score()

    if s_mom >= 1:
        "Mom is happy."

    return

I've only renamed the variables within the function only to highlight that it is separate. I'd need to test it, but it would probably be fine to use s_mom in both places... since the one within the function would be local and the one "outside" is actually really store.s_mom. That said, I wouldn't risk it and give them two separate names to avoid the risk.

Finally, there's the other option of just getting the function to manipulate the s_mom variable directly.

Python:
default choices = []
default s_mom = 0

init python:
    def mom_score():
        store.s_mom = 0
        if "ch1helpmom" in choices:
            store.s_mom += 1
        return

label start:

    scene black with fade:
    $ choices.append("ch1helpmom")

    $ mom_score()

    if s_mom >= 1:
        "Mom is happy."

    return

In this case, the function is more of a "update_mom_score()" rather than what it's original name implies.

Anyway, the final 2 answers are what I posted to the discord group. The first answer is what I think the OP was aiming for.
 
Last edited:

hkproduction

HkProduction Official Account
Game Developer
Sep 22, 2021
163
547
Hi Exanadae , thank you for this useful guide. I stumbled upon this thread while looking for a way to make my VN future-proof.

I have 1 question about this approach; What happens if the user selects a choice, then use the rollback feature, and then choose another choice. In that case, the choices array will contains all of the selected choices right?

How should we deal with that? 1 “idiotic” way I can think of is clear all choices before showing the menu, so when they select, only 1 choice is added to the array.
 

Munitions Mori

New Member
Game Developer
Nov 9, 2020
6
112
Thank you! This system is a core part of my first game, and it works great. Your well-written descriptions, easily-digestible examples, and whole ass demo I could dissect (and relentlessly borrow from) are why I finished anything - and using your system gave me the confidence to even dip my toes in other bits of coding.

I have 1 question about this approach; What happens if the user selects a choice, then use the rollback feature, and then choose another choice. In that case, the choices array will contains all of the selected choices right?
In my experience it automatically rolls back the choice. I worried about that too, but I never had any problems with it.
 
  • Like
Reactions: hkproduction