getting your head around Ren'py: for coders

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
TheSHAD0W
Newbie
Posts: 9
Joined: Wed Jun 18, 2014 8:03 am
Contact:

getting your head around Ren'py: for coders

#1 Post by TheSHAD0W »

Are you an experienced coder who needed to write some custom Ren'py stuff, and then started beating your head on the desk because you couldn't figure out what you were doing wrong? This guide is for you.

Ren'py uses paradigms you may not be familiar with; it is an unstructured programming language, designed to make it easier for a novice author to write a visual novel. Most coders expect a structured language, and until you are able to get the model of how Ren'py runs straight in your head, coding can be painful. It may be based on Python but there are some very important differences. Here is a summary of the things most likely to trip you up.

Note that this not a complete description of how to work with Ren'py, but should be enough to help point the way towards doing what you need to.

This will likely be updated in the future as I find more things to clarify...



(1) Labels, jumps and calls.

If you're used to most modern programming language, you might think that "label" is like a function definition. It isn't. It's more analogous to a line number from BASIC or F0RTRAN. It's just a reference to where a jump or call (GOSUB/GOTO) points. When program flow hits the next label, it's not going to return from where you called it, it's just going to plow right through.

There are a few special labels reserved for Ren'py, like "start" and "main_menu". Check the documentation for full details.



(2) Program flow isn't what you expect it to be.

Ren'py has two phases of operation: Init and Game. Init is performed at Ren'py start-up; then the main menu code is called, which launches the game. Okay? But look at this code:

Code: Select all

label start:

    image jason = "images/jason.jpg"

    $ s = "images/sam.jpg"
    image sam = s

    screen howdy:
        text "Howdy [myname]!" align (0.5,0.5)
        $ count += 1
        text str(count) align (0.5,0.6)

    define myname = "Max"

    init:
        $ count = 0
        $ yourname = "Sam"
        default hisname = "Jason"

    show screen howdy
    "Well, this is interesting."
Ren'py helpfully goes through and performs things in an order you might not expect... The order of operation is:

(1) image jason is defined.
(2) tries to define image sam, but s hasn't been set yet, so this will generate an error.
(3) screen howdy is defined but not evaluated.
(4) myname is defined in init phase.
(5) the init block is evaluated. count and yourname are defined. hisname, however, is not yet.
(6) Just before main menu, hisname is set.
(7) main menu is shown, through which you start the game.
(8) screen howdy may be evaluated for prediction purposes, incrementing count.
(9) s is set, for whatever good it does now...
(10) screen howdy is queued up for display.
(11) "well, this is interesting" is fed to the say screen, which is queued up for display.
(12) screen howdy may be re-evaluated, incrementing count again. (This is why modifying variables in screens can have unexpected results.)
(13) screens say and howdy are displayed and Ren'py waits for input.

Ren'py will run through image and screen definitions, defines, and init blocks at init time; default definitions, even in init blocks, wait until just before main menu is called.



(3) Saving and rollback.

Ren'py is designed to be able to roll backwards through the game, at least to some extent, so you can rewatch the last few bits of the performance and even change your mind on recent decisions. The same system that allows this rollback is used to gather the data needed to save the current game.

Variables defined after init time are actually defined as attributes of the object "store". At each interaction (see below), "store" is pickled and placed on a stack. When you roll back, the top of the stack is popped off and used to overwrite the store. When a game is saved a copy of the stack along with the current position in the script and the state of the display are saved.

In the above code example, note that s and hisname, s because it was set in program flow after start and hisname because it was initialized in default, exist in the store and participate in rollback, and their current values are saved when the game is saved. The rest do not.

Function definitions and some other objects are not pickleable, and their presence in the store will cause errors. This is why functions and classes should only be defined at init.

In order to save space, in .rpy files "list" and "dict" objects are actually "RevertableList" and "RevertableDict" objects. These data structures store change information which ties into the rollback system and keeps it from having to store redundant information. This is largely transparent, but may trip you up if you're trying something complex. Note that if you are including .py files, lists and dicts created there are generated normally, and will produce exceptions if inserted into the store.



(4) Displayables and interactions.

You can think of Ren'py operating in two threads; it may not actually be implemented that way, but it's a good way to conceptualize it. One thread handles displayable updates, and the other handles script flow and interactions.

Every visual element in Ren'py is a displayable. A displayable may be a combination of other displayables. Even the layers on the screen are displayables. A displayable is an object which has code attached and may or may not also have various image-related objects which can be placed on the display. Looking at the structure of creator-defined displayables (https://www.renpy.org/doc/html/udd.html) will give you some idea as to how they work. A screen is just another displayable, whose structure is created using screen language (or manually).

Displayables are re-evaluated and re-displayed at each interaction (a point where user input is expected), and also at points which may be scheduled by the displayable itself. Applying a transform containing movement to a displayable operates by scheduling that re-evaluation for you. (It can often be handy to create a displayable that doesn't directly display anything, but accesses that scheduling and handles events for you.) They may also be evaluated prior to their actually being displayed as a part of the prediction (caching) system.

The other thread is the one that steps through the script and tells the engine to show/hide displayables and to perform interactions. An interaction may simply be a pause, or it may be a character speaking. When this thread hits that point, it is performing an interaction, which updates the display.


(5) Prediction.

Ren'py has a prediction system, which looks ahead into the code and tries to preload and cache images and keep things running smoothly. It's fairly smart at this but can be fooled.

One problem with the prediction system is it often can't delve into displayables coded using Python, resulting in cache misses and pauses while images are loaded. Ren'py has python functions, start_predict() and stop_predict() which you can use in your code to alleviate this problem.

There's also an edge case where, if you're using a transform written in transform language to switch between images, Ren'py will preload all the images in the transform at once. For a transform with a lot of image switching you may need to use a python transform function instead and arrange the caching manually.



(6) Screens, styles, and screen Actions.

The screen data structure is a major part of Ren'py. Read the documentation carefully... The screen language interpreter is designed to run through in a single-pass, so sometimes the automatic sizing doesn't go the way you expect, so you may need to tweak that manually. If you're having trouble modifying the dimensions of a single element, try wrapping it in a vbox/hbox and tweak *that* instead.

The styles system is basic in concept but can get very complicated in practice, and figuring out how to adjust the single element of a style you have to tweak can be difficult. The object inspector can help, you'll have to play with it. Again, read the documentation carefully.

Actions are, as you'd think, actions performed when certain things occur in screens, like button-presses, mouse-overs, etc. Note that while an action looks like a simple function call in the .rpy code, it definitely isn't. An Action is a class definition, which creates an instance that can be called like a function when it's performed, but also has other attributes that Ren'py uses to tell whether a button is active or selected, for instance. Calling a function there instead of using an Action will result in the function being called every time the screen is re-evaluated, which can be a lot. The action Function() can be used to call a standard function on its performance; there are other tricks to do so but they aren't recommended. To get the most out of it you'll need to subclass the base class "Action" or one of the other Actions.

Diving into the actual code for Ren'py is kind of a must for real coders, and looking at how Actions are defined in real use is a good starting point. Action definitions are stored in renpy/renpy/common/00action_*.rpy . (One example to browse is https://github.com/renpy/renpy/blob/mas ... _other.rpy)



(7) All variables are global. (With exceptions.)

If you have a for loop in one file using "count", expect to have "count" used in another file overwritten. This is handy if you're a novice and only use a few variables to store things, but if you're doing real coding it can be a headache.

There are some exceptions though:

(a) Prefixing a variable with double-underscore will make it "local"; it will automatically be prefixed with some information tying it to the name of the file it's in. So "__count" in one file will be different from another - *unless* the files have the same names (and are in different folders). Then you can still have collisions.

(b) Variables created in "python hide" blocks are local.

(c) Variables created inside a function or class definition are also local (as you'd expect).
Last edited by TheSHAD0W on Sun Oct 21, 2018 2:32 pm, edited 5 times in total.

User avatar
Pando
Regular
Posts: 29
Joined: Wed Oct 08, 2014 7:57 am
Projects: Crossfire
Organization: Agape Studios
IRC Nick: Pando
Github: Scylardor
Contact:

Re: getting your head around Ren'py: for coders

#2 Post by Pando »

Very useful information. :)

Should this be a sticky ?
Image

User avatar
madomadomadotsuki
Newbie
Posts: 8
Joined: Mon Sep 15, 2014 9:37 pm
Location: Dream World
Contact:

Re: getting your head around Ren'py: for coders

#3 Post by madomadomadotsuki »

Thanks a lot for writing out this post. Coming into this with hopes of coding more in Python, it definitely was a huge head rush diving into screen language and the default Ren'py code.

leeto
Newbie
Posts: 4
Joined: Fri Feb 24, 2017 9:35 pm
Location: The Cloud
Contact:

Re: getting your head around Ren'py: for coders

#4 Post by leeto »

As a python dev first and ren'py developer second, this is a big help. Thanks! +1

User avatar
ISAWHIM
Veteran
Posts: 318
Joined: Sun Nov 06, 2016 5:34 pm
Contact:

Re: getting your head around Ren'py: for coders

#5 Post by ISAWHIM »

That helped me a lot too. +9,000 to you!

User avatar
Hazel
Regular
Posts: 80
Joined: Sun Jun 26, 2016 10:10 am
Projects: Wreath of Roses, BookSLEEPer, Girlfriend Material
Tumblr: zincalloygames
itch: zincalloy
Contact:

Re: getting your head around Ren'py: for coders

#6 Post by Hazel »

This is incredibly helpful. Bookmarked.
Image Image

BookSLEEPer (writer) * Girlfriend Material (writer/programmer)

User avatar
bosinpai
Regular
Posts: 100
Joined: Sat Nov 22, 2014 9:11 am
Contact:

Re: getting your head around Ren'py: for coders

#7 Post by bosinpai »

Thanks a lot!

Let's add that renpy.call() will drop all your Python call stack, i.e. you never return from it, even if you used a renpy "return" statement.
Instead, renpy will jump to the next statement in the renpy script.

Typically:

Code: Select all

$ ret = renpy.call("mylabel")
if ret:
  # something
causes an error at the "if" about ret being undefined.

The reason are interestingly detailed in https://github.com/renpy/renpy/issues/959 (i.e. only the renpy - not python - call stack can be stored in savegames) but this is surprising.

The workarounds appear to involve:
- recoding your python loop in renpy using the limited (but savegame-savvy) while/if/jump statements
- relying on renpy's "_return" variable
- possibly using renpy.call_in_new_context(): savegames will use the current renpy (not python) statement as a checkpoint, but this disables the renpy rollback for the duration of call_in_new_context.
- recoding RenPy with a fuzzy continuations framework (or not)

In case you need to set _return from Python, this is probably a bad sign, and that's not possibly directly AFAICS, but a trick is to use a simple wrapper:

Code: Select all

label myreturn(val):
    return val
    
python:
    def myfun:
        ...
        renpy.call("myreturn", "something")

Post Reply

Who is online

Users browsing this forum: No registered users