-
Notifications
You must be signed in to change notification settings - Fork 17
Advanced Gamestates
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
Feedback is crucial to improve the tutorial!
Let me know if you have any questions, critique, suggestions or just any other ideas.
Chapter 1: Prototype
- The Ball, The Brick, The Platform
- Game Objects as Lua Tables
- Bricks and Walls
- Detecting Collisions
- Resolving Collisions
- Levels
Appendix A: Storing Levels as Strings
Appendix B: Optimized Collision Detection (draft)
Chapter 2: General Code Structure
- Splitting Code into Several Files
- Loading Levels from Files
- Straightforward Gamestates
- Advanced Gamestates
- Basic Tiles
- Different Brick Types
- Basic Sound
- 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
- Improved Ball Rebounds
- Ball Launch From Platform (Two Objects Moving Together)
- Mouse Controls
- Spawning Bonuses
- Bonus Effects
- Glue Bonus
- Add New Ball Bonus
- Life and Next Level Bonuses
- Random Bonuses
- Menu Buttons
- Wall Tiles
- Side Panel
- Score
- Fonts
- More Sounds
- Final Screen
- Packaging
Appendix D: GUI Layouts
Appendix E: Love-release and Love.js
Beyond Programming: