Main Loop
A sandbox or life-sim style game where the player is free to roam benefits from having a main loop to coordinate the essential tasks such as checking the player's energy and time of day. Without a main loop each activity the player chooses has to check for the no energy or day over conditions.
This skeleton example is in several parts, one for the main loop
here, and one for each location to allow a player to free-roam
between locations.
It also uses the date and period calendar
presented before as well as the debug tool so
you can see how it works.
Note: The Per
suffix relates to this being
the date/period version in my project.
Common Location Labels
The design works on the idea that each location has a number of local labels that perform particular functions. It is a rudimentary interface:
-
.arrive
-
Called when the PC arrives in a new location.
Typically describes them arriving.
It should set
pcLoc
to the current location. -
.choice
- Called when the player should make a choice about what the player character should do at the location. Typically would present a menu of choices.
-
.travelTo(destLoc)
-
Called when the player character needs to leave this location
for another.
Typically describes their travel.
It should set
pcLoc
to the new location. Optionally it may only perform part of the journey. For example leaving home to go to work may involve getting the bus into town first, so in this case it would describe that and setpcLoc
totownPer
and let a call totownPer.travelTo('work')
complete the journey. -
.wake
- Called when the player wakes up in a location. Describes them waking up.
Variables
These variables are used by the main loop.
-
The
gameOver
boolean is used to control when the loop should exit. It would be set by any event that ends the game. -
The
newDay
boolean indicates that a new day has started after the player character has slept. It is used to trigger day-based changes and wake the player. -
The
energy
int records how much energy the player has. If their energy gets too low they have to return home and sleep. -
The
pcLoc
string records where the player is as a Ren'Py label for that location. Each of these location labels has a number of known calls that the main loop can use. -
The
pcMoney
integer records how much money the player character has. It's not really used in this example.
default gameOver = False # Game over flag, set by endings default newDay = False # Flag to run start of day processing default energy = 10 # Player energy default pcLoc = 'homePer' # Current player location default pcMoney = 100 # Current player money
Main Loop
The main loop does three basic things:
- At the start of the player character's day it does any housekeeping such as resetting their energy, then it wakes them up.
- It checks to see if they have run out of energy, or it has become too late. If so the player character goes home and sleeps.
-
Otherwise it calls the
.choice
label for their location. This would typically present the player with a menu of things they can do there.
# # Main game loop. # label mainPer: $ dbgLabel('mainPer') while not gameOver: if newDay: # Start of day processing. # call .newDay from main_per_new_day # Set state for start of new day call .callLocLabel('wake') from main_per_wake # Have the PC wake up $ newDay = False elif energy <= 0 or isLastPeriod(): # Player character must return home to sleep now. # $ dbgLabel('main', None, 'force home/sleep energy={} lastPeriod={}', energy, isLastPeriod()) call .travelTo('homePer') from main_per_day_over_travel call homePer.sleep from main_per_day_over_sleep else: # Allow the player to choose what to do. # call .callLocLabel('choice') from main_per_choice # Game over. # $ dbgLabel('mainPer', None, 'done') return
If you need to check for scheduled events (perhaps the player character
has a meeting, or a date) this can easily be added to the main loop
before the else
that allows the player free choice.
New day
The .newDay
routine is very simple in this outline.
It just sets the player character's energy back to 10.
This is a good place to clear down any day-related variables that
have been used.
# Do housekeeping before the player wakes. # label .newDay: $ dbgLabel('mainPer', 'newDay') $ energy = 10 return
Call Location Label
Central to the design is being able to call specific labels for the
player character's location. This is done by the .callLocLabel
Ren'Py function which takes a localLabel
as a parameter.
-
It builds a label name from the player character's location
(
pcLoc
) and the passedlocalLabel
. So ifpcLoc
was "homePer" and thelocalLabel
was "wake" it would build the labelhomePer.wake
. - It checks to see if this label exists in the game or not.
-
If it exists, it calls it using
call expression
. - If it does not exist it prints a debug message.
# Call a local label at the player location if it exists. # label .callLocLabel(localLabel): $ dbgLabel('mainPer', 'callLocLabel', 'localLabel="{}" pcLoc={}', localLabel, pcLoc) $ renpy.dynamic('renLabel') $ renLabel = pcLoc + '.' + localLabel if renpy.has_label(renLabel): call expression renLabel from main_call_local_label_dyn else: dbg "In main: location label [renLabel] does not exist." return
Travel To Location
Finally the implementation of .travelTo(destLoc)
is a
little more complicated as it has to handle the possibility that
a journey may involve multiple steps.
- If the player character is already at the required location it does nothing and returns immediately.
-
It starts a loop calling
pcLoc.travelTo(destLoc)
. It stops when the player character is in the right location. -
But if the
.travelTo
label is missing it prints a debug message and setspcLoc
to the required destination so the game can keep running. -
It calls
pcLoc.arrive
using.callLocLabel('arrive')
.
# Have the player travel to a new location. # label .travelTo(destLoc): $ dbgLabel('mainPer', 'travelTo', 'destLoc="{}" pcLoc={}', destLoc, pcLoc) if pcLoc == destLoc: return $ renpy.dynamic('renLabel', 'travel') $ travel = True while travel: $ renLabel = pcLoc + '.travelTo' if renpy.has_label(renLabel): call expression renLabel pass (destLoc) from main_travel_to_dyn $ travel = pcLoc != destLoc else: dbg "In main.travelTo: location label [renLabel] does not exist." # Force player location to continue. $ travel = False $ pcLoc = destLoc # Arrive at the location. call .callLocLabel('arrive') from main_travel_to_arrive return
Example use
With the variables set up, and the main loop taking care of running the game, starting is very brief:
script.rpylabel start: if config.developer: $ debugLabels = True # Debug on in developer mode. show screen datePeriodScr # Show date/period screen. # Run main loop. # call mainPer from start_main # Final message. # "Thank you for playing [config.name!t]." hide screen datePeriodScr # Hide date/period screen. return
Other parts of the main loop example
This may seem a little abstract (indeed it is intentionally so). Hopefully it will become clearer when examining some of the location files:
- Player's home
- The player character's home.
- Town centre
- The town centre and hub.
- Workplace
- The player character's place of work.