Skip to content

Advanced Gamestates

noooway edited this page Feb 14, 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.

The definition of the state_event is:

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

(*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.

The set_state function expects a state name as a string. It requires the corresponding gamestate module. To prevent multiple requirement of the same modules, they are cached in the loaded table.

local loaded = {}                                                    --(*1)

function gamestates.set_state( state_name, ... )
   gamestates.state_event( 'exit' )                                  --(*2)
   local old_state_name = get_key_for_value( loaded, current_state ) --(*3)   
   current_state = loaded[ state_name ]                              --(*4)
   if not current_state then
      current_state = require( state_name )                          --(*5)
      loaded[ state_name ] = current_state
      gamestates.state_event( 'load', old_state_name, ... )          
   end
   gamestates.state_event( 'enter', old_state_name, ... )            --(*6)
end

return gamestates

(*1): The already required gamestates are stored in the loaded table with gamestate names used as keys.
(*2): When gamestates change, "exit" callback of the current state is called (if it is defined).
(*3): get_key_for_value traverses loaded table and extracts the name under which the current_state is stored in that table.
(*4): If the new state has been loaded already, current_state will point to that state.
(*5): If the new state hasn't been loaded already, it is required and cached in the loaded table. After that, 'load' callback of that state is called.
(*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. The functions of this module have to be accessible from each of the gamestates. This can be achieved by placing local gamestates = require "gamestates" in each of the menu.lua, game.lua, etc. However, that would result in circular dependencies. To avoid that, I explicitly load this module only from the main.lua and store it in a global variable.

gamestates = require "gamestates"

Second. 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`

Third. 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 and 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