Implementing simple game states using tables

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
RNavega
Party member
Posts: 246
Joined: Sun Aug 16, 2020 1:28 pm

Implementing simple game states using tables

Post by RNavega »

These are some notes on how to implement simple game states using Lua tables.
If it walks like a duck, swims like a duck, and quacks like a duck, then it's a duck.
In programming, "duck typing" is when you consider an object of a certain type if it has certain functions and properties, rather than the actual class or type that you declared the object as.
In other words, you don't care where the object came from, as long as it has some specific functions and/or properties available that you can use.

In Lua, you can store functions inside a table. You can put them in some keys for example:

Code: Select all

local tableA = {
    function1 = function()
        print("Function 1 of table A")
    end,
    function2 = function()
        print("Function 2 of table A")
    end,
    function3 = function()
        print("Function 3 of table A")
    end
}
-- And adding more functions after the table was created:
function tableA:function4()
        print("Function 4 of table A")
end
You can do the same with another table, and have keys with the same names as keys from table A.

Code: Select all

local tableB = {
    function1 = function()
        print("Function 1 of table B")
    end,
    function2 = function()
        print("Function 2 of table B")
    end,
    function3 = function()
        print("Function 3 of table B")
    end
}
Since those tables have key names in common, if you store either table A or table B in a variable, then you know that that variable will have a table with at least the keys "function1", "function2" and "function3".

Code: Select all

local unknownTable = tableA
unknownTable.function1() -- Prints "Function 1 of table A"

unknownTable = tableB
unknownTable.function1() -- Prints "Function 1 of table B"
The same variable, unknownTable, can store either of the tables, and call the keys that they have in common, and you don't care which table is stored in that variable.

That's exactly what you'll use to implement game states in your game.
If all states have the same set of functions (or rather, the same keys that point to functions), then you don't care what state is stored in the "current state" variable, you can execute the functions from it all the same.

I think it's a good idea to think of a game state as three distinct steps:
- A beginning moment, where you can prepare, load or initialize some things that the state will need.
- A middle moment (which lasts a long time), where you keep updating the game under that state, until the state wants the game to change to another state;
- An ending moment, where you can finish, destroy or cleanup some things which were only necessary in that state and won't be needed anymore in the next state(s).

Since Löve works with callbacks (love.update, love.keypressed, love.draw etc.), that "middle moment" of each state table can be represented by a unique set of functions, with each function corresponding to one of those callbacks.
This means that whatever game state is active, its callbacks will be called and that state will have control over the program.

So you could make a Main Menu state that looks like this:

Code: Select all

local mainMenuState = {}
function mainMenuState:prepare()
    print("The start of the Main Menu state")
end

-- Callbacks:
function mainMenuState:update(dt)
    print("Updating the game with the Main Menu state")
end
function mainMenuState:keypressed(key)
    print("Listening to keys with the Main Menu state")
end

function mainMenuState:finish()
    print("Cleaning up at the end of the Main Menu state")
end
...and it would be used in this way:

Code: Select all

local currentState

function love.load()
    -- Set the initial state to Main Menu, and prepare it.
    currentState = mainMenuState
    currentState:prepare()
end

function love.update(dt)
    currentState:update(dt)
end

function love.draw()
    currentState:draw()
end

function love.keypressed(key)
    currentState:keypressed(key)
end
How do you change between states?

It's better to write a function for changing states in your program, this way you're sure that the prepare() and finish() functions of the states that you're changing between are always called.

Code: Select all

function changeGameState(newState)
    -- Right now the 'currentState' variable is storing the active state.
    -- Call its finish() function, if it exists.
    if currentState.finish then
        currentState:finish()
    end
    
    -- Change to the new state and call its prepare() function
    -- if it exists.
    currentState = newState
    if currentState.prepare then
        currentState:prepare()
    end
end
To use that changeGameState() function you can call it at any point in your program.
HOWEVER, since that function changes the state immediately (instead of "queueing" it to be done later), you need to use it with a return statement so that it exits out of whatever function you're running at that moment, to avoid the function continuing from that point and expecting some resources that might've already been deleted by the finish() function of the state.
For example:

Code: Select all

function mainMenuState:keypressed(key)
    if key == 'return' and currentOption == OPTION_START_GAME then
        return changeGameState(loadLevel1State)
    end
    
    (...)
    
    print("We're still in the Main Menu state")
end
...since the changeGameState() function is called with a 'return', that keypressed function will exit right at that moment and will never run whatever else might be in the function, which might rely on resources that were already cleared by the mainMenuState:finish() function that was called inside changeGameState(). This means that the "We're still in the Main Menu state" print call will not be called, because the keypressed function will have returned before it reaches that point.

What if a state doesn't need to do any preparation or finishing?

There's a couple of options. You can set the "prepare" and "finish" keys of a state table to nil, or set them to a dummy function that is empty and doesn't do anything. If you go the nil route, make sure that the code for changing between states will test for those keys being nil before using them, as seen in the changeGameState above.

I want to show some animated graphics when I go from the main menu to a certain game level. Do I use the prepare() or finish() functions from my states to do that?

You don't use either.
The prepare() and finish() functions are meant for instantaneous changes, like the loading of small resources, changing some global variables, freeing some objects etc.
If you need for the game to "hold" on a certain screen and show some animated graphics or be interactive while there's a countdown or heavier resource loading etc, then you can create a state whose sole purpose is to serve as a transition or intermediary step between the two other states.
That is, instead of having the states change like this:

Code: Select all

mainMenuState -> level1State
...you'd program a new state, and have them change like this:

Code: Select all

mainMenuState -> loadLevel1State -> level1State
Then from within the update/draw/etc callbacks in the "loadLevel1State" you can do the animated waiting screen, and when the work is done, change to the level 1 state.
User avatar
Gunroar:Cannon()
Party member
Posts: 1088
Joined: Thu Dec 10, 2020 1:57 am

Re: Implementing simple game states using tables

Post by Gunroar:Cannon() »

Wow. Very informative.
The risk I took was calculated,
but man, am I bad at math.

-How to be saved and born again :huh:
Post Reply

Who is online

Users browsing this forum: No registered users and 156 guests