Call and Return (Ren'Py)
If jump
is equivalent to your mother saying
"Go do the dishes" (after which you slope off to play computer games)
then call
is equivalent to
"Go do the dishes then come straight back so I can tell you what to
do next." A trivial example in Ren'Py script might run something like
this:
label mumChores: mother "Go do the dishes." call dishes mother "Put out the trash." pc "Aww, mum!" call trash return label dishes: "You do the dishes." return label trash: "You put out the trash." return
In each case the return
marks the end of the task and
the point where control should transfer back to the caller.
The labels dishes
and trash
become
sub-routines that complete a specific task. They can be re-used
in the unlikely(?) event you choose to do one of these chores
voluntarily, or if your father has a similar set of chores for
you to do, possibly in a different order.
The self-contained and re-useable nature of these sub-routines is
what makes them useful. If this had been built with jumps instead
and we wanted to add a dadChores
label then both
dishes
and trash
would have to have
conditional code to jump back to the right list of chores.
Another benefit is that the structure of the code and story can be made obvious without traipsing around multiple files:
label start: call act1 call act2 call act3 return label act1: call act1Scene1 call act1Scene2 call act1Scene3 return ...
How does it work?
Hidden from view is the call stack. The call stack is a list of places to go back to.
- When a call is made it puts the position of the next line in the script on to the end of the stack.
- When a return is encountered it takes the last entry off the stack and jumps to that place in the script, which is the line after the call.
Functions (Ren'Py)
You can pass parameters when you make a call, and the subroutine can
return a value, making it a function. Returned values are slightly
strange in Ren'Py being passed back in the special variable
_return
.
In this next example there's a trivial combat function called
fight
which takes the name of the opponent monster, and
its hit points as parameters and returns True
if the player
survives:
# Fight a single monster. # Returns True if the player lives. label fight(monName, monHp): if pcHp >= monHp: "The quick skirmish with the [monName] goes your way, but not without it wounding you." $ pcHp -= monHp // 4 return True else: "Despite your best effort the [monName] bests you in combat." return False # Fight a goblin. # Returns True if the player lives. label fightGoblin: "This room is full of crates and boxes. As you are checking to see if there's anything useful inside a goblin jumps out from behind one, startling you!" call fight("goblin", 4) if _return: "You find a couple of coins on the corpse." $ pcMoney += 2 return _return
The second label fightGoblin
uses the fight function to have
the player fight a goblin with 4 hit points. If the player survives they
find two gold pieces. It also returns True
if the player
survives and False
if they died.
Local scopes
Another advantage of call and return is that we can create variables that
only exist for the duration of the subroutine using
renpy.dynamic
. These can hide global variables:
# Fight a single monster. # Returns True if the player lives. label fight(monName, monHp): $ renpy.dynamic('hpDiff') $ hpDiff = pcHp - monHp if hpDiff >= 0: "The quick skirmish with the [monName] goes your way, but not without it wounding you." $ pcHp -= monHp // 4 return True else: "Despite your best effort the [monName] bests you in combat." return False
When the return
is encountered the local variable is
deleted automatically.
Note: This is the opposite of a Python function where
variables are local by default, and you must declare if you want to
access a global one.
Call from
There's one issue with using call and return that relates to how Ren'Py
locates labels when a new version of a game moves lines about in the
script file. To ensure this works the line after each call (the one that
goes on the call stack) needs to be labelled. Ren'Py provides a shorthand
for this using call ... from lbl
:
label fightGoblin: ... call fight("goblin", 4) from fightGoblin_fight
When you build a distribution you can have Ren'Py automatically add any
missing from
labels, but they tend to be ugly, so it can
be better to add your own.