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 set pcLoc to townPer and let a call to townPer.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.

vars.rpy

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:

  1. At the start of the player character's day it does any housekeeping such as resetting their energy, then it wakes them up.
  2. 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.
  3. Otherwise it calls the .choice label for their location. This would typically present the player with a menu of things they can do there.

mainPer.rpy

    #
    # 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.

mainPer.rpy (cont)

    # 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.

  1. It builds a label name from the player character's location (pcLoc) and the passed localLabel. So if pcLoc was "homePer" and the localLabel was "wake" it would build the label homePer.wake.
  2. It checks to see if this label exists in the game or not.
  3. If it exists, it calls it using call expression.
  4. If it does not exist it prints a debug message.

mainPer.rpy (cont)

    # 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.

  1. If the player character is already at the required location it does nothing and returns immediately.
  2. It starts a loop calling pcLoc.travelTo(destLoc). It stops when the player character is in the right location.
  3. But if the .travelTo label is missing it prints a debug message and sets pcLoc to the required destination so the game can keep running.
  4. 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.rpy

label 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: