Discover Python and Patterns (8): Game Loop pattern

Published March 11, 2020
Advertisement

The time has come to see our first design pattern: the Game Loop Pattern! This pattern can give us many good ideas to refactor our game in a very effective way.

This post is part of the Discover Python and Patterns series

The Game Loop pattern

There is several version of the Game Loop pattern. Here I present a simple case with a single thread. The following 5 functions form this pattern:

The Game Loop pattern

  • The init() function is called at startup to initialize the game and its data. In the following, I name this data the game state.
  • The processInput() function is called at each iteration of the game to manage the controls (keyboard, mouse, pad).
  • The update() function changes the game state. The result of processInput() is used to update the game state, as well as automatic processes.
  • The render() function handles display. It “convert” the game state into visual content.
  • The run() function contains the game loop. In many cases, this loop looks like this one:
   def run():
       init()
       while True:
           processInput()
           update()
           render()

The Game Loop pattern, as for all patterns, is a recipe that gives you ideas on how to solve a problem. There is no unique way to use it: it depends on cases.

Following a pattern forces you to consider problems you may not be thinking about. For instance, splitting user input, data updates, and rendering is not the first thing that came to mind when we created the “guess the number” game. However, according to experimented developers that already created many games, it seems that this splitting is essential. So, right now as beginners, we follow this advice, and later we will understand why it matters. And please trust me: the day you see it all, you will get amazed by all the genius behind these ideas!

Guess the number game: init() function

Let’s start using the pattern with the init() function:

   def init():    
       return None, random.randint(1,10)

This function returns an initial game state. I named game data “the game state” because games can be seen as finite state machines. For this game, the state is made of:

1) The game status: this is the general status of the game, represented by a string:

  • “win”: the player wins and the game is over;
  • “end”: the player leaves the game;
  • “lower”: the player is still playing, and proposed a number lower than the magic one;
  • “higher”: same but for a higher number.

2) The magic number: the number the player has to find.

Bundle all game data is an important task; we’ll see that with more details in the next posts.

Guess the number game: processInput() function

   def processInput():
       while True:
           word = input("What is the magic number? ")
           if word == "quit":
               return None
           try:
               playerNumber = int(word)
               break
           except ValueError:
               print("Please type a number without decimals!")
               continue
           
       return playerNumber

This function asks the player for a number. It handles all problems related to user input, like checking that the entered number is correct. It returns the number, or None if the player wants to stop the game.

For users of this function, like the run() function, it is like a magic box that returns instructions from the player. It does not matter how they are collected. It could be from a keyboard, a mouse, a pad, the network, or even from an AI.

Guess the number game: update() function

The update() function updates the game state using the player’s instructions:

   def update(gameStatus,magicNumber,playerNumber):
       if playerNumber is None:
           gameStatus = "end"
       elif playerNumber == magicNumber:
           gameStatus = "win"
       elif magicNumber < playerNumber:
           gameStatus = "lower"
       elif magicNumber > playerNumber:
           gameStatus = "higher"
           
       return gameStatus, magicNumber

In our case, the player’s instruction is playerNumber, and the game status and the magic number form the game state. The function updates the game status depending on the value of playerNumber.

Note that we don’t use gameStatus as an input, and never change the value of magicNumber. So, we could think that we can remove gameStatus from the arguments and magicNumber from the return values. Except if this is the very last version of this game and that we must reduce computational complexity, this is not a good idea. Maybe in future improvements of the game, we will need to update the game according to gameStatus or change the value of magicNumber. From a design point of view, this current definition of inputs and outputs is robust and has no reason to change.

Guess the number game: render() function

The render() function displays the current game state. It should work whatever happens, to always give a clear view of the game:

   def render(gameStatus,magicNumber):
       if gameStatus == "win":
           print("This is correct! You win!")
       elif gameStatus == "end":
           print("Bye!")
       elif gameStatus == "lower":
           print("The magic number is lower")
       elif gameStatus == "higher":
           print("The magic number is higher")
       else:
           raise RuntimeError("Unexpected game status {}".format(gameStatus))

The input of this function is the game state and has no output. The process is simple: display a message according to the value of gameStatus.

Note that we also handle the case where gameStatus has an unexpected value. It is a good habit, it greatly helps the day you update the game and forget to update some parts.

Guess the number game: runGame() function

The runGame() function is the core of the game and uses all the previous functions:

   def runGame():
       gameStatus, magicNumber = init()
       while gameStatus != "win" and gameStatus != "end":
           playerNumber = processInput()
           gameStatus, magicNumber = update(gameStatus,magicNumber,playerNumber)
           render(gameStatus,magicNumber)

You can see the flow:

  • The init() function returns an initial game state;
  • The processInput() function collects instructions from the player;
  • The update() function uses the instructions to update the game state;
  • The render() function displays the game state.

Guess the number game: final code

The final code, with documentation in function headers:

   # Import the random package
   import random
   def init():
       """
       Initialize game
       
       Outputs:
         * gameStatus
         * magicNumber
       """
       # Generate a random Magic number
       return None, random.randint(1,10)
       
   def processInput():
       """
       Handle player's input
       
       Output:
         * playerNumber: the number entered by the player, or None if the player wants to stop the game
       """
       while True:
           # Player input
           word = input("What is the magic number? ")
           # Quit if the player types "quit"
           if word == "quit":
               return None
           # Int casting with exception handling
           try:
               playerNumber = int(word)
               break
           except ValueError:
               print("Please type a number without decimals!")
               continue
           
       return playerNumber
   def update(gameStatus,magicNumber,playerNumber):
       """
       Update game state
       
       Inputs:
         * gameStatus: the status of the game
         * magicNumber: the magic number to find
         * playerNumber: the number entered by the player
       Output:
         * gameStatus: the status of the game
         * magicNumber: the magic number to find
       """
       if playerNumber is None:
           gameStatus = "end"
       elif playerNumber == magicNumber:
           gameStatus = "win"
       elif magicNumber < playerNumber:
           gameStatus = "lower"
       elif magicNumber > playerNumber:
           gameStatus = "higher"
           
       return gameStatus, magicNumber
       
   def render(gameStatus,magicNumber):
       """
       Render game state
       
       Input:
         * gameStatus: the status of the game, "win", "end", "lower" or "higher"
       """
       # Cases
       if gameStatus == "win":
           print("This is correct! You win!")
       elif gameStatus == "end":
           print("Bye!")
       elif gameStatus == "lower":
           print("The magic number is lower")
       elif gameStatus == "higher":
           print("The magic number is higher")
       else:
           raise RuntimeError("Unexpected game status {}".format(gameStatus))
       
   def runGame():
       gameStatus, magicNumber = init()
       while gameStatus != "win" and gameStatus != "end":
           playerNumber = processInput()
           gameStatus, magicNumber = update(gameStatus,magicNumber,playerNumber)
           render(gameStatus,magicNumber)
                       
     
   # Launch the game
   runGame()

Now we saw the essential basics, in the next post I’ll be able to show you how to use game graphics!

0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement