Basics of Ren'Py #9
Welcome everybody! It's great to have you all back.
Today's tutorial relies heavily on variables, which you should be more than familiar with by now - Aside from Basics #7 which revolves around them entirely, they've been with us since the beginning - first storing Character objects in Basics #2, used in many examples along the way, their ability to store all kinds of values has brought us here.
They can affect the plot and they can be used in screens. They can be changed and saved in a save file, granting them the ability to affect an entire playthrough. But they're still bound to respective playthroughs - anytime a new game is started, whatever the variables were before is put aside, and the default values are restored.
This is where persistent data comes in - they are special variables that retain their values not only across playthroughs with different save files, but even after the game is closed and launched again.
And we'll learn how to work with them today. Exciting, isn't it?
What's even more exciting is that I got my friend and a fellow tutorial writer, Feniks, to help me with this tutorial!
They've agreed to guide you through the second half of this tutorial.
While I'll explain the basics and feature examples with simple data types (int and bool), Feniks will explain how objects can be made persistent as well, allowing you to carry over pretty much anything!
Hi everyone! Fen from feniksdev.com here. I'm very pleased to help bring this tutorial to you with Lez, and I hope you find it useful.
If you'd like to learn more about general coding practices and how they apply to Ren'Py to do things like track choices and compare points, you can check out some of my tutorials on the topic over at feniksdev.com. But first, on to the tutorial!
Starting off light as always, the first script contains just a default statement that sets a variable named bottles to 99, and a start label that keeps infinitely decreasing that count - eventually going below 0 into negatives, should we get that far into the label.
default bottles = 99 label start(): "[bottles] bottles of beer on the wall." "You take one down and pass it around." $ bottles -= 1 "Now [bottles] bottles of beer are on the wall." jump start # Repeat this label
Let me start by bringing up the point I bring up every single time I get the chance: we're using the default statement throughout all today's code blocks to declare variables, because values of said variables will be changing. We are not using the define statement, which is meant for values that never change - like Characters or Displayables.
The code itself is easy to understand, especially given that I've pretty much explained it above already - start label goes through three dialogue lines, during which value of bottles is decreased by 1, and jumps to the beginning, repeating the process infinitely.
Let's change this "regular" variable into a persistent variable. All we need to do to achieve that is include a persistent. before the variable name.
In practice, the specifics of how this works aren't very important, just keep in mind that the spelling and the period are both important. If we want to be technical, it causes the variable to actually become an attribute of an object called persistent, which is what's responsible in Ren'Py for keeping track of all persistent data.
It still acts as a variable, mind you. Persistent variables are nearly identical to their regular siblings. If I change the bottles variable from the first example to be persistent, it will still work like nothing has happened.
default persistent.bottles = 99 label start(): "[persistent.bottles] bottles of beer on the wall." "You take one down and pass it around." $ persistent.bottles -= 1 "Now [persistent.bottles] bottles of beer are on the wall." return # Back to the Main Menu
Now that the variable is persistent, it will no longer reset upon exiting the label, be it by quitting the game in the middle of it, or by getting to the end. This is a point I've emphasised by changing the jump statement into return which will take you back to the Main Menu.
This is one of those times where I recommend trying the code out for yourself. Create a new project in Ren'Py launcher and copy the code over.
If you Start the game, go through it once (removing 1 bottle), exit the project and re-launch it, dialogue after Starting the game again will show the preserved number of 98.
Now that we understand the theory behind persistent variables, let's look at using them in practical situations.
Let's start with an example using a variable with a bool value. You might remember bool stands for boolean, which means values of True and False.
Code below uses a variable, named prologue_seen, to record whether the player has already been through the game's prologue. Declared with the default value of False, player is bound to go through the prologue until this variable is changed to True. This is, of course, done at the end of the prologue.
default persistent.prologue_seen = False label start(): if persistent.prologue_seen: "You've already seen the prologue at least once." menu: "Would you like to skip it?" "Yes": jump after_prologue "No": pass "This is the story of one ordinary programmer." "He once wrote a really short prologue." $ persistent.prologue_seen = True jump after_prologue
Now that prologue_seen is True and there's no way of setting it back to False, anytime the game is Started from now on, a menu statement will be encountered, giving player the choice of skipping the prologue.
For the second example, I've decided to go with an integer, AKA a whole number. This example is longer, but not hard to understand, I promise.
First, go through the code and see if you can understand what's going on by yourselves. I will of course include an explanation below, but if you've been following previous Basics of Ren'Py tutorials, you should be able to figure it out!
define boss = Character("Lord Enteludar") default persistent.deaths_to_boss = 0 label final_boss_intro(): if persistent.deaths_to_boss == 0: boss "So, you're finally here." boss "You got past all of my friends that you've encountered." boss "I hope you don't expect me to go easy on you." elif persistent.deaths_to_boss == 1: boss "I'm sorry about that. It's just something I've always wondered about." boss "Hopefully you're ready this time." elif persistent.deaths_to_boss == 2: boss "That's twice now that I got you." boss "You sure you want to try again?" elif persistent.deaths_to_boss == 3: boss "I'm going to try and beat you in as short time as possible." boss "Get a stopwatch, see how long you can last!" elif persistent.deaths_to_boss == 4: boss "Seriously? That..." boss "That wasn't even close." elif persistent.deaths_to_boss == 5: boss "I'm getting the feeling this will go on for a while." boss "You don't mind if I stop counting your failures, do you?" boss "Not like you have a say in it." else: # Covers deaths 6 and above. boss "let's just get to the point." boss "Here I come!" jump final_boss_fight label final_boss_fight(): ######################### # Fight takes place here. ######################### if boss_beaten: jump final_boss_outro else: $ persistent.deaths_to_boss += 1 jump game_over
Lord Enteludar keeps track of player's unsuccessful fights against him. After some intro dialogue, the boss fight would take place, resulting in the player either winning or losing.
Winning would move the story along, possibly display an ending, since this is a boss fight after all. Losing increases the value of deaths_to_boss variable by 1, resulting in Lord Enteludar having different dialogue the next time the fight is attempted.
That example was inspired by a well-known RPG game, which is referenced inside the code three times. However, a feature like this expects the player to get stuck a lot at that section of the game, causing them to quit before the fight is done and death is recorded - causing it to not be remembered.
One of the ways this could be solved is by renaming the int variable to boss_encounters instead, declared with the default of 0. Then, you would increment the variable at the beginning of every fight rather than the end, making it impossible for the player to avoid it.
I've shown you examples with True/False and an integer. Data structures like lists or dictionaries can be persistent as well, and so can be classes containing them - although it gets a bit dodgy there.
And this is where I let Feniks take over. Don't worry, their texts are just as full of knowledge as mine are.
Thanks Lez! Using persistent gives you some interesting gameplay ideas, doesn't it? It can even be used for more complicated information, as you'll see next.
What if you want to save more complex information persistently? While integers, strings, lists, dictionaries and the like can store most information you'd want to persist beyond individual playthroughs, there may be a time when you want to preserve a more complex data structure like an instance of a class (an object) in persistent data.
Ren'Py will let you do this, but there are some things you must set up for it to work properly. To understand what can and cannot be saved in persistent, we must first look at the concept of pickling, as the first thing Ren'Py requires of a class to be able to save it persistently is that the class is pickleable.
Pickling is a Python-specific serialization format. It takes data structures like dictionaries, strings, and objects, and turns them into a series of bytes to save for later - basically a bunch of 1s and 0s. The opposite of pickling is unpickling, which takes the series of bytes and turns it back into the original data.
Why do we need to pickle data in the first place? Well, we need some way of storing and reading information for features like Ren'Py's save/load system. This can be done in many ways - for example, you could have a text file which you add a new line to every time data changes during the game. Then, when you load a save file, you read back the text file line-by-line to figure out what to set the variables to.
Problem is, text files are very large and inconsistent compared to other methods, and you would need to set up some standardized way of writing the variable name and value yourself so you can read it back later to get the values again.
Enter pickling: while Python-specific, it allows you to convert data into a standardized format (computer-readable bytes) to save and read back later. Since it is standardized, we can count on it to be the same across all systems using Python. It is much smaller than something like a text file for the same amount of data.
That said, not everything can be pickled. If you can't pickle something, you can't save it, so it's important to make sure anything you want to save can be pickled.
I wish pickling worked like that in real life. Can you imagine preserving fruit and vegetables by pickling them, then being able to unpickle them back into the original anytime you want?
That would be super cool.
Common variable types like integers, strings, lists, and dictionaries are automatically pickleable. If you're only using pickleable variable types inside your class, your class is probably pickleable without having to do anything else. You'll know if this is an issue if you get errors saying "Cannot pickle X" when saving, or if Ren'Py has trouble unpickling your class when loading (or reloading during development).
The second thing Ren'Py requires of classes in order to save them persistently is that it has an equality method. This is a special method which is called when you use the == operator to compare an instance of that class to something else. Its name is eq, for EQuality. Ren'Py uses this method to compare data when loading.
For the most part, any equality implementation will look roughly similar unless your class is really weird. Let's look at an example.
Let's say you have a phone feature in your game, and you want the player to be able to customize the theme of the phone UI. This theme choice should be persistent, as it's an aesthetic UI choice rather than one tied to an individual playthrough (similar to an app having a light/dark mode UI toggle).
If you have enough themes, or enough parts to the theme, it makes sense to store this information in a class to organize it. Below is an example of a simple class which holds information for primary, secondary, and tertiary theme colours:
class PhoneTheme: def __init__(self, primary, secondary, tertiary): self.primary = primary self.secondary = secondary self.tertiary = tertiary
Since the only things that will be saved in this class are strings with colour codes, it's automatically pickleable, so that's taken care of. To make sure Ren'Py can accurately compare our class instances when loading, the next step is to declare the equality method.
class PhoneTheme: def __init__(self, primary, secondary, tertiary): self.primary = primary self.secondary = secondary self.tertiary = tertiary def __eq__(self, other): """Declare the equality operator for two PhoneTheme objects.""" # First, check if the other object is a PhoneTheme object if isinstance(other, PhoneTheme): # Check if all their attributes are the same return (self.primary == other.primary and self.secondary == other.secondary and self.tertiary == other.tertiary) # If not, they can't possibly be the same else: return False def __ne__(self, other): """The result of != is the opposite of ==""" return not self.__eq__(other)
To declare the equality method, declare a method with the name __eq__ that takes one additional parameter (besides self). Conventionally it is called other, as it is the "other" object we are comparing the *current* object (self) to. Similarly, to declare the inequality method (used by the != operator), declare a method with the name __ne__ that takes one additional other parameter just like __eq__.
You must declare *both* an __eq__ method and a __ne__ method for Python 2.7 (aka Ren'Py <8.0), even if the latter is just the opposite result of the former. Python 3.0+ (Ren'Py >=8.0) doesn't require this, but it's not a bad idea to include it anyway.
The first thing you should do in an equality method is usually check that other is an instance of the same class.
If it isn't, for most classes, this means they are not equal and you should return False. That's what the line...
if isinstance(other, PhoneTheme):
...does - checks if other is a PhoneTheme object.
If it *is* an instance of the same class, then it's safe to check its attributes.
In this case, the two objects are the same if they share all the same attributes (e.g. self.primary == other.primary and so on). You may or may not compare every attribute in the class to determine if two objects are the same; it'll depend on the class and what you're trying to do. In a use case like this, where we simply want to know if every attribute is the same for both objects, we can use the __dict__ attribute. It's a special attribute in same way that __eq__ is a special method. __dict__ is a dictionary of all the attributes in the class and their associated values, which you can compare directly to the __dict__ attribute of other.
def __eq__(self, other): if isinstance(other, PhoneTheme): return (self.__dict__ == other.__dict__) else: return False
This won't be useful for all classes, however, especially if you need to be specific.
For example, imagine you had a Recipe class with a name, list of ingredients, and a learned attribute to check if the player knows the recipe or not. Two recipes with the same name and ingredients should be considered the same recipe, even if one is learned and the other is not.
In that case, you would use code like the first example, but you would only compare the values of the name and ingredients attributes inside __eq__ and ignore learned.
Finally, the last piece of the puzzle to get a class to work with persistent is to declare the class in a python early block instead of a regular init python.
This is because persistent data is loaded very early, before init python blocks are run. Declaring your class in a python early block ensures it will be declared early enough for Ren'Py to know about it by the time it's loading persistent data.
So, all in all, here's a full example of a persistent-ready class, PhoneTheme, and an instance of that class declared in persistent.phone:
python early: class PhoneTheme: def __init__(self, primary, secondary, tertiary): self.primary = primary self.secondary = secondary self.tertiary = tertiary def __eq__(self, other): """Declare the equality operator for two PhoneTheme objects.""" # First, check if the other object is a PhoneTheme object if isinstance(other, PhoneTheme): # Check if all their attributes are the same return (self.primary == other.primary and self.secondary == other.secondary and self.tertiary == other.tertiary) # If not, they can't possibly be the same else: return False def __ne__(self, other): """The result of != is the opposite of ==""" return not self.__eq__(other) default persistent.phone = PhoneTheme("#080808", "#151515", "#A1A1A1")
Are you still there readers? That was a lot to take in.
Persistent data is useful across the board, from CG Galleries that store images the player encounters during their playthroughs, to gimmicks that have the player close and re-launch the game - like those used in Doki Doki Literature Club!.
With me having covered the basic data types and Fen covering the classes, you should now be geared with knowledge to tackle such challenges.
Let's do our usual summary! Today we have gone over plenty, even if it may not look like it:
- What persistent data is.
- How to declare persistent variables.
- How to change and use them to affect the dialogue.
- What pickling is and why it's important.
- What equality (__eq__) and inequality (__ne__) methods are
- How to use these special methods to make objects usable by Ren'Py's persistent system.
- How to declare a class in a python early block so it can be persisted.
I'm not used to collaborations, but I like how this one turned out. It is packed with so much knowledge..! Put it to good use readers. Feniks and Lez signing out.