Welcome to the sponsor-exclusive content for the Ren'Py Patreon. Sponsors like you ensure this page exists. Thank you.
Hello again, and welcome to another Ren’Py developer update. This month was a month of fixes and minor features, that should lead to Ren’Py 7.4.3 being released in the next few days, just in time for NaNoRenO. I’ll cover a few of the more important changes that will be coming. Then I’ll turn to the bulk of this month’s article, a set of tips and tricks for maintaining save compatibility in games that see repeated releases.
The biggest changes to Ren’Py 7.4.3 will be ones that hopefully not many people see at all, as they are intended to make sure that Ren’Py runs well on different platforms.
On Windows, I’m changing the C libraries that Ren’Py is linked against, to make them more ‘normal’ - instead of the ancient libraries that we had been using, Ren’Py will switch to using C libraries provided with Windows Vista and later. This, in conjunction with improvements in the icon changing code added by new contributor Andri, should make antivirus programs less likely to detect compiled games.
I’ve also improved compatibility with NVidia graphics cards. Ren’Py had been interacting poorly with an NVidia option called ‘threaded optimizations’, so Ren’Py will now talk to the video card to turn that off when a game is running.
On Android, I’ve been spending a lot of effort trying to ensure compatibility with different devices - so far this year, I’ve used Patreon contributions to pick up 3 cell phones and a tablet to test with, all but one of them obsolete hardware that Ren’Py still supports. Getting this hardware has led to improvements which will be in the new version.
There have been a bunch of improvements to OpenGL shaders and the meshes that are passed to them, especially when a shader has been a applied to text.
There have also been a bunch of improvements to modal focus handling. In addition to some fixes, it’s now possible to declare a Window or Frame modal, which means that while the mouse is inside the window, clicks will not be passed to displayables hidden behind the window.
Look forward to the full changelog when Ren’Py 7.4.3 comes out in the next few days.
Another great honor was judging the third annual Ren’Py game jam at Stony Brook University. Stony Brook is my alma mater and the place where I was a student when I first started working on Ren’Py, so it’s always an honor to have them host their Ren’Py contest.
The prizes were games contributed by Sekai Project, Ithaqua Labs, SypherZent, and Pixels and Pins. Special thanks to Flat Chest Dev, even if the game was a bit too adult for the university setting.
The winner was a game called SBUwU, about dating in the Science Fiction Forum, a 50-year-old club I was part of as an undergraduate. So it was really interesting to see it immortalized in visual novel form.
I don’t know where to put this, so I figure I’d stick it here. This month’s postcard art by Adirosa, which you can see above, has Eileen in a dress worn by Ada Lovelace, who realized that Charles Babbage’s design for an Analytical Engine could be used to represent things that aren’t numbers:
[The Analytical Engine] might act upon other things besides number, were objects found whose mutual fundamental relations could be expressed by those of the abstract science of operations, and which should be also susceptible of adaptations to the action of the operating notation and mechanism of the engine…Supposing, for instance, that the fundamental relations of pitched sounds in the science of harmony and of musical composition were susceptible of such expression and adaptations, the engine might compose elaborate and scientific pieces of music of any degree of complexity or extent.
While the Analytical Engine couldn’t be built in the 1800s, eventually we became able to build electronic computing machines. Now we routinely use computers to represent things like words, pictures, music, and stories, as well as everything else.
I came across this quote while researching what to write on the postcard, and thought it too good not to share. I find it amazing that something that was a thought 180 years ago has permeated the world.
This month, I had a number of people come to me on the Ren’Py discord channel and ask about save compatibility, especially with games that have multiple major releases. It’s an interesting question, so I figured I’d expand the answer into an article.
An interesting change in the time Ren’Py has been around is games that are being released multiple times, with changes to the content. This somewhat surprised me. When designing Ren’Py originally, I’d assumed that the usual process would be for a creator to develop and release their game, perhaps add a few bug fixes, and then move on to another project.
This was before things like Patreon and Steam Early Access, both of which encourage a stream of releases. In this new model, the game is released multiple times, with new and changed content each time, over the course of many release cycles. I’ll call this the ‘repeated release’ model, and I’ll try to call out things that make sense to do now to prepare for repeated releases.
Ren’Py is flexible enough to support both development models, though in all cases, you need to be careful with how you change your game after the first public release. While there will always be some changes that Ren’Py isn’t able to handle - think about a game where the entire script is replaced by something unrelated - it’s possible to adjust to many changes.
Many of the techniques here are based on those I use inside Ren’Py. Just like how you have to update your games to handle old save data, I have to update Ren’Py to ensure old saves load.
The process of loading a save involves making sure both control and data can be loaded correctly, so I’ll cover these in turn.
In computing in general, the word “control” refers to the part of a program that is running or about to run. When Ren’Py loads a game, control is placed at the start of a statement, with Ren’Py figuring out which label control should be transferred to.
Usually, control problems manifest themselves as the “Couldn’t find a place to stop rolling back.” error. If you’re experiencing that when testing a new release, this is the section for you.
Ensure the chain of .rpyc files isn’t broken. An article I wrote a while back goes deep into how .rpyc files are the key to the save system. I won’t completely repeat it, but I’ll try to cover some of the most important parts here.
Ren’Py generates .rpyc files using old .rpyc files, if they exist, and new .rpy files. The .rpyc files aren’t just a cache of information - the .rpyc files contain new information that’s needed to load saves. As such, it’s important that the .rpyc files from an old version be present when creating a new release of the game.
A key to loading games is to make sure there is a chain of .rpyc files, so that the .rpyc files for release 2 are based on those from release 1, the .rpyc files for release 3 are based on those for release 2, and so on.
This is a spot where I’ve seen developers get into trouble, and sometimes it’s trouble that’s hard to recover from. For example, I’ve seen games released from automated systems that don’t preserve the .rpycs between builds, and hence loading becomes impossible. A similar problem occurs if two developers take turns releasing the game, and break the chain of .rpyc files that way.
Finally, if you have to downgrade Ren’Py, it makes sense to restore the old .rpyc files. There’s no guarantee the .rpyc files created in newer versions of Ren’Py will work in older versions.
Avoid moving Ren’Py script between .rpy files. This is true for statements that execute, like labels, say statements, and menu choices. When statements are in the same file, in roughly the same order, Ren’Py can match them and bring the player back to where they left off. Moving things between files makes this matching impossible, and so can lead to loading problems.
As an exception to this, renaming a file is fine, provided you rename the corresponding .rpyc. Ren’Py won’t have a problem if you move day1.rpy to day1a.rpy, provided you also move day1.rpyc to day1a.rpyc.
When creating a game for repeated release, it makes sense to spend time figuring out how to organize your games into .rpy files before the first release. Once the first release has been made, changing this organization becomes a bit harder.
Use labels. Ren’Py can always match a label with itself, and so the more labels your game has, the more likely Ren’Py will be able to figure out control. In 7.4.2, Ren’Py looks about 128 statements back for a label. That will likely be changing to 1024 statements in 7.4.3.
This is especially important for repeated releases. Labels are very cheap, so it makes sense to include them in many places in your game, to help the load system.
Call-from. When you distribute your game, Ren’Py will rewrite a call statement from:
call mylabel
to:
call mylabel from _call_mylabel_1
The from clause added here is actually shorthand for creating a label
after the call statement, so the above would be equivalent to:
call mylabel
label _call_mylabel_1:
pass
Ren’Py uses these labels to find the place a call should return to, even if the game has changed. If you later remove the call statement, you should turn the from clause into a label, and keep that in your game. Otherwise, Ren’Py might not be able to find a place to return to, and the game will eventually break.
config.load_failed_label While the techniques described above should prevent control problems, it’s possible that games that have been released without them might be in a place where it’s impossible to recover from. (For example, there isn’t a good way to recover from having made multiple releases with a broken chain of .rpyc files.)
To solve this, Ren’Py has a variable config.load_failed_label. If
Ren’Py otherwise can’t find a place to load, and this variable is set to
a label, Ren’Py will jump to this label rather than reporting a failure.
This then lets you use the game’s data to figure out where the player
should be, jump them to that place, and let the game continue.
One way to use this would be in a game that has a structure where it returns to the same place every time. For example, if a player sleeps at home every day, on a failed load, it might be possible to just have them waking up in bed.
Another way to use this, one suitable for repeated releases, is to maintain
a recovery variable throughout the game, and if something goes
wrong, jump to that. For example:
default recovery = "start"
define config.load_failed_label = "load_failed"
label load_failed:
jump expression recovery
This could bring the player back to the start of the current chapter or some other well-known location. Just note that the call stack is cleared when this label is jumped to, so it’s important not to jump to a label that expects to be called.
The other part of save compatibility is the data inside your game - the variables that are saved, and the objects reachable from those variables. Depending on how your game changes, this data might need to be upgraded for the new release. Ren’Py generally can’t do this automatically - the data used by a pure visual novel, life simulation game, or rpg can vary wildly.
At the same time, there are a number of techniques that make sense to apply over and over, and so it makes sense to talk about them here. These are the same techniques that I use to make sure that saves created with an older Ren’Py load on newer versions of the engine.
The ``default`` statement.
For pure visual novels and other simple games, the only data migration that
makes sense is to use the default statement to declare variables,
especially those created after the first release. The default statement
should be used in preference to a python statement right after the start
label. That’s because if you have:
label start:
$ oldvar = True
e "Please save here."
And then change it to:
label start:
$ oldvar = True
$ newvar = False
e "Please save here."
if newvar:
e "An exception will occur before things get here."
The new variable will not be defined in the save loaded. The behavior of the
default statement is to check to see if the variable has ever been set,
and if it’s not been defined at load or rollback, to set it. So if we were
to write:
default newvar = False
label start:
$ oldvar = True
e "Please save here."
if newvar:
e "This won't run, since newvar is False. But no exception!"
Of course, there’s no reason oldvar couldn’t become a default statement too.
The default statement won’t change the value of the variable if it’s been
set, however. If you need to do that, please use the after_load label, as
described below.
Classes. For more complicated games, it usually makes sense to store some of your data in classes. For fields with simple values, like numbers or strings, the default values can be included as part of the class. For example, say we have a class:
class Enemy(object):
def __init__(self, name):
self.name = name
and we want to add a field to it:
class Enemy(object):
def __init__(self, name):
self.name = name
self.hp = hp
the instances of the class loaded from an old save won’t have the hp field
associated with them. If it’s a simple value, it often makes sense to add
a class field with the value to the class:
class Enemy(object):
hp = 42
def __init__(self, name):
self.name = name
self.hp = hp
This takes advantage of the way Python works, where if a field doesn’t exist on an instance, it’s looked up on the class. It will run into problems if the field is something changeable, like a list, dictionary, or set. In that case, what I tend to do is to default the field to None, and create it when it’s used:
class Ally(object):
inventory = None
def __init__(self, inventory):
self.inventory = set(inventory)
def add_to_inventory(self, item):
if self.inventory is None:
self.inventory = set()
self.inventory.add(item)
Note that in all these cases, we’re only considering the addition of a field, not a change in the value of the field. I usually write in a way that can accept the old value of the field, so it doesn’t have to change.
To be loaded, a class has to be reachable under the name it was saved under. So if I wanted to rename Enemy to Monster, I could do that, so long as I created an alias to the old name:
init python:
Enemy = Monster
Classes can be moved from one .rpy file to another, and as long as the class keeps the same name, it should be loadable.
Dictionaries.
I’m aware that some games use dictionaries instead of classes, such that it
might make sense to want to add a new key to a dictionary. There are even
a few places, like the implementation of the play music statement, where
Ren’Py does this because classes aren’t ready to be used at that point.
The way I usually handle upgraded dictionaries is to call the .get method when a dictionary might not always have a key. For example, say we have:
default monster = { "base_hp" : 100 }
$ fight_hp = monster["base_hp"]
We could change this to:
default monster = { "base_hp" : 100, "bonus_hp" : 50 }
$ fight_hp = monster["base_hp"] + monster.get("bonus_hp", 50)
If you’re planning to make a game for repeated release, I encourage you to learn how classes work. But if you’ve already released a game with dictionary data structures, this can help you upgrade it.
The ``after_load`` label.
Finally, Ren’Py has the label, after_load, that is called after
a save file is loaded. This allows you to use arbitrary Python to update
your game’s data.
One thing that’s very useful here is the _version variable, which defaults
to config.version when a game is first created. By updating this after
each data migration occurs, it’s possible to choose what is done.
Here’s an example of how it can work, something that migrates data by
combining a first_name and last_name variable into a single
player_name variable, a reasonable thing to do because some people have
only one name:
label after_load:
python:
if _version == "1.0":
$ player_name = first_name + " " + last_name
_version = "1.1"
return
This is something incredibly small, but there is a lot you can do in after_load,
like walking through data structures to update them.
Of course, the best data migration is the one that doesn’t have to happen, so if you’re planning for repeated releases, it makes sense to think about what kind of data you’ll be using in the future.
There’s a lot here, and for many Ren’Py games, Ren’Py handles save compatibility for you. If you’re a solo developer, keep the .rpyc files, and make minor updates like fixing typos or adding translations, Ren’Py will generally do the right thing when it comes to save compatibility.
If you’re making repeated releases, it makes sense to think about save compatibility as you change things. It’s much easier to deal with migrations one change at a time than to go back between releases to figure out what’s changed.
As for me, I plan to work to make the repeated release workflow easier. One change I have planned is to make .rpyc files interact better with version control, probably by making it simpler to check .rpyc files from older releases without newer .rpyc files overwriting them.