Hi all!
Breaking Fast is moving forward. This last week we worked on the menu navigation part, which includes the menu screens through which players navigate until the race begins. As for the technical side, I want to discuss the two main issues you have to consider when implementing this aspect of a game: how to manage the flow between screens, and how to pass information among screens. Let's get started!
Flow Management Schemes
There are two main "schemes" (I can't think of a better word) during menu navigation. In the first scheme, each screen is actually a different screen. This implies that, upon changing among screens, we swap the contents of one screen with the contents of the other screen. This can be implemented as a function in the screen manager:
function changeScreen(newScreen)
currentScreen.unload()
currentScreen = newScreen
currentScreen.load()
end
In order to manage transitions among screens (i.e. the implementation of the screen manager), it is useful to think of Finite State Machines. You can watch a series of two videos that I prepared for gaining insight on the matter.
In games it is also very common to use another scheme, in which the contents of different screens are all drawn, but only one of them has the focus at any moment. Think for example of a racing game in which you have to choose a scenario and then, the time of the race. You can implement this by using the aforementioned scheme, but it feels kind of overkill to unload one screen and to load another one just for the purpose of selecting the time. It seems more natural that the same screen presents both options, but that the player can only change one of them at any time. Actually, we can stop thinking of different screens in this situation. This "move the focus" approach can be implemented by means of screen phases or states.
In particular, in Lua, we can hold a table of states, and we iterate over this table in order to draw and update the screen. We also have a variable that holds the value of the current state (the current focus), and we delegate the interaction of the player (be it with the keyboard or a gamepad) to the actual focused state. Some self-explanatory code is shown next:
function load()
table.insert(states, state1)
table.insert(states, state2)
currentStateFocus = state1
end
function draw()
for _, v in ipairs(states) do
v.draw()
end
end
function update(dt)
for _, v in ipairs(states) do
v.update(dt)
end
end
function onKeyPressed(key)
currentStateFocus.onKeyPressed(key)
end
Communication between Screens and States
Passing information among screens is essential. In particular for Breaking Fast, consider the following flow of actions:
- Players select the characters that will participate in the race.
- Players select the scenario in which the race will take place.
- Players select the duration of the race.
Consider the figure above, in which we mix the two menu navigation schemes explained before. Each transition among screens and states conveys both new information and the information carried by older transitions. In the end, when we prepare the race screen, we need to know all the options that have been chosen by the players through the different screens. Fortunately, implementing this in Lua is very simple thanks to a powerful feature of the language: function overriding of imported modules. Consider the following module, which models the Time Selection State:
local timeSelectionState = {}
-- other functions
function timeSelectionState.toScenarioSelection()
end
function timeSelectionState.toGame(timeChosen)
end
return timeSelectionState
As you can see, we are defining two empty functions. Any module that imports this module can re-define (i.e. override) these functions. So for example, consider the Scenario Selection Screen module, which contains the Time Selection State:
local scenarioSelectionScreen = {}
-- other functions
function timeSelectionState.toScenarioSelection()
currentStateFocus = scenarioSelectionState
end
function timeSelectionState.toGame(timeChosen)
for _, v in ipairs(states) do
v.unloadResources()
end
scenarioSelectionScreen.toGame(numberOfPlayers, playersInfo, scenarioChosen, timeChosen)
end
function scenarioSelectionScreen.toGame(numberOfPlayers, playersInfo, scenarioChosen, timeChosen)
end
return scenarioSelectionScreen
As you can observe, this module re-defines toScenarioSelction() and toGame() functions. In particular, toGame() unloads all the states of this screen, forwards the racing time that has been chosen by the player, and pads it to more information accumulated during previous phases. In turn, scenarioSelectionScreen defines an empty toGame() function that cam be re-defined by the module importing it. This latter module could therefore add some more information and pass it to the Race Screen.
And this is it! As always, I hope you enjoyed this tutorial, and if you have any question, don't hesitate to ask! Any feedback is also well appreciated.
See you!
FM