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