Skip to content

Advanced Gamestates

noooway edited this page Feb 9, 2017 · 19 revisions

As the number of game objects increases, it becomes cumbersome to use if-else to maintain gamestates. It is more convenient to put each gamestate into a separate file. In this part I want to introduce such a more sophisticated gamestates system.

The scheme is following: each gamestate is represented by a table, containing it's own draw, update and other LÖVE callbacks, as well as all the necessary game objects. Reference to this table is stored in a special variable in the main program. Each LÖVE callback is redirected to an appropriate gamestate callback. Transition between the gamestates is achieved by changing the variable to point to another gamestate table.

Let's start from the gamestates module. It defines a variable current_state that points to the currently active gamestate, and two functions: set_state and state_event - responsible for switching gamestates and redirecting callbacks.

local gamestates = {}

local current_state = nil                                   --(*1)

function gamestates.state_event( function_name, ... )       
   if current_state and type( current_state[function_name] ) == 'function' then
      current_state[function_name](...)                     --(*2)
   end
end

function gamestates.set_state( new_state, ... )
   gamestates.state_event( 'exit' )                          --(*3)
   local old_state = current_state
   current_state = new_state                                 --(*4)
   if current_state.load then
      gamestates.state_event( 'load', old_state, ... )       --(*5)
      current_state.load = nil
   end
   gamestates.state_event( 'enter', old_state, ... )         --(*6)
end

return gamestates

(*1): current_state points to the currently active gamestate.
(*2): If function with function_name is present in the gamestate, this function is called. Ellipsis ... is used to forward arguments of the state_event to the current_state[function_name]. In Lua, ... is called a vararg expression. In the argument list of a function, it indicates, that the function accepts arbitrary number of arguments. In the body of the function, ... acts like a multivalued function, returning all the collected arguments.
(*3): When gamestates change, "exit" callback of the current state is called (if it is defined).
(*4): current_state now points to the new gamestate.
(*5): If the new state is loaded for the first time, and it has a "load" callback, then this callback is called. "Load" is supposed to be executed only once, so it is deleted from the table after the first use.
(*6): After that, "enter" callback is called. Both "load" and "enter" receive old gamestate as an argument (this is not used now, but it will turn convenient later).

In the main.lua LÖVE callbacks are redirected to gamestates.state_event according to the following definitions:

function love.update( dt )
   gamestates.state_event( "update", dt )
end

function love.draw()
   gamestates.state_event( "draw" )
end

function love.keyreleased( key, code )
   gamestates.state_event( "keyreleased", key, code )
end

After that, each gamestate can be moved into a separate file. For example, the menu.lua for the "menu" state is:

local menu = {}

function menu.update( dt )
end

function menu.draw()
   love.graphics.print("Menu gamestate. Press Enter to continue.",
                       280, 250)
end

function menu.keyreleased( key, code )
   if key == "return" then
      gamestates.set_state( game, { current_level = 1 } )
   elseif key == 'escape' then
      love.event.quit()
   end    
end

return menu

The usefulness of such an approach can be seen most clearly for the "game" gamestate: all the game objects are now local to this state.

local ball = require "ball"
local platform = require "platform"
local bricks = require "bricks"
local walls = require "walls"
local collisions = require "collisions"
local levels = require "levels"

local game = {}

.....

function game.update( dt )
   ball.update( dt )
   platform.update( dt )
   bricks.update( dt )
   walls.update( dt )
   collisions.resolve_collisions( ball, platform, walls, bricks )
   game.switch_to_next_level( bricks, ball, levels )
end

function game.draw()
   ball.draw()
   platform.draw()
   bricks.draw()
   walls.draw()
end

.....

return game

There are several minor subtleties that need an attention to make this scheme work.

The first one regards the requiring of the "gamestates" module. I explicitly load this module only from the main.lua, store it in a global variable and do so before any other gamestates are required, so that the "gamestates" definitions are accessible from the "menu", "game", "gamepaused", "gamefinished" modules.

gamestates = require "gamestates"
.....
menu = require "menu"
game = require "game"
gamepaused = require "gamepaused"
gamefinished = require "gamefinished"

The reason for this is following. The functions inside this module have to be accessible from each of the gamestate modules. This can be achieved by requiring it in each gamestate, i.e. by placing local gamestates = require "gamestates" in each of the menu.lua, game.lua, etc. The problem is, current_state variable, storing active gamestate, has to be the same among each of the gamestates. Since this variable is declared local, it can be possible that on each require a new closure with this variable is created. In practice this doesn't happen, because the interpreter caches the loaded modules. Still, I explicitly make the corresponding table global.

The second problem concerns gamestate switching. For example, to switch from the "menu" to the "game", it is necessary to call gamestates.set_state( game, ..... ). When "menu" module is required, game is just an ordinary variable. If it is not defined at the moment of requiring the menu.lua, it's value will be nil. However, to correctly switch gamestates it should point to the table game containing all the game functions. Therefore, it is necessary to declare all the tables corresponding to gamestates in advance and only then require them.

menu = {}
game = {}
gamepaused = {}
gamefinished = {}
menu = require "menu"
game = require "game"
gamepaused = require "gamepaused"
gamefinished = require "gamefinished"

Third. In the "gamepaused" state I want to display the ball, the platform, the bricks and the walls as the background. However, these objects are now local to the "game", and are not available in the "gamepaused". The solution is to pass them as arguments in the set_state call.

function game.keyreleased( key, code )
   .....
   elseif  key == 'escape' then
      gamestates.set_state( gamepaused, { ball, platform, bricks, walls } )
   end    
end

On entering the "gamepaused", it is necessary to store the received game objects somewhere. I use local game_objects variable, available to the whole module.

local game_objects = {}

function gamepaused.enter( prev_state, ... )
   game_objects = ...
end

After that, in the draw callback it is possible to iterate over the contents of the game_objects table. If any of it's members happens to have draw method, this method is called.

function gamepaused.draw()
   for _, obj in pairs( game_objects ) do
      if type(obj) == "table" and obj.draw then
         obj.draw()
      end
   end
   love.graphics.print( 
      "Game is paused. Press Enter to continue or Esc to quit", 
      50, 50 )
end

Finally, upon exiting the "gamepaused" it is necessary to delete the references to the used game objects.

function gamepaused.exit()
   game_objects = nil 
end`

Fourth. The "game" gamestate can be entered from the "menu", "gamefinished" and "gamepaused" states. When it is entered for the first time, it is convenient to construct the walls, because they do not change during the game.

function game.load( prev_state, ... )
   walls.construct_walls()
end

When "game" is entered from the "menu" or from the "gamefinished", it should start from the first level. When it is resumed from the "gamepaused", it should continue from the point where it has stopped. It is convenient to pass the level number into game.enter as an optional argument. If it is present, corresponding level is loaded. If it is missing, it is assumed that all objects are already in place and it is safe to continue:

function game.enter( prev_state, ... )
   args = ...
   if args.current_level then
      levels.current_level = args.current_level
      local level = levels.require_current_level()
      bricks.construct_level( level )      
      ball.reposition()   
   end      
end

Using such an approach, transitions from the "menu" and "gamefinished" are

function menu.keyreleased( key, code )
   if key == "return" then
      gamestates.set_state( game, { current_level = 1 } )
   .....
   end    
end

function gamefinished.keyreleased( key, code )
   if key == "return" then
      gamestates.set_state( game, { current_level = 1 } )
   .....
   end    
end

It is also convenient to use gamestates.set_state to switch between the levels: the next level is passed to this function as an optional argument.

function game.switch_to_next_level( bricks, ball, levels )
   if bricks.no_more_bricks then
      if levels.current_level < #levels.sequence then
         gamestates.set_state( game, { current_level = levels.current_level + 1 } )
      else
         gamestates.set_state( gamefinished )
      end
   end
end

    Home
    Acknowledgements
    Todo

Chapter 1: Prototype

  1. The Ball, The Brick, The Platform
  2. Game Objects as Lua Tables
  3. Bricks and Walls
  4. Detecting Collisions
  5. Resolving Collisions
  6. Levels

    Appendix A: Storing Levels as Strings
    Appendix B: Optimized Collision Detection (draft)

Chapter 2: General Code Structure

  1. Splitting Code into Several Files
  2. Loading Levels from Files
  3. Straightforward Gamestates
  4. Advanced Gamestates
  5. Basic Tiles
  6. Different Brick Types
  7. Basic Sound
  8. Game Over

    Appendix C: Stricter Modules (draft)
    Appendix D-1: Intro to Classes (draft)
    Appendix D-2: Chapter 2 Using Classes.

Chapter 3 (deprecated): Details

  1. Improved Ball Rebounds
  2. Ball Launch From Platform (Two Objects Moving Together)
  3. Mouse Controls
  4. Spawning Bonuses
  5. Bonus Effects
  6. Glue Bonus
  7. Add New Ball Bonus
  8. Life and Next Level Bonuses
  9. Random Bonuses
  10. Menu Buttons
  11. Wall Tiles
  12. Side Panel
  13. Score
  14. Fonts
  15. More Sounds
  16. Final Screen
  17. Packaging

    Appendix D: GUI Layouts
    Appendix E: Love-release and Love.js

Beyond Programming:

  1. Game Design
  2. Minimal Marketing (draft)
  3. Finding a Team (draft)

Archive

Clone this wiki locally