How to create a Screen Manager

When you create a game or some tool in a “complete” way, it is often desired to achieve some sort of a set of screens/views throughout which the user navigates. There are several ways to achieve an engine that will allow us to manage all these screens, from simple code (but less convenient), to the most complex and/or difficult to understand, but easy to use after all. The goal of this tutorial is obviously not to give you a fixed idea of what a screen manager may look like (since there are many different types), but to offer you a set of choice. This tutorial is quite long as it presents comprehensive examples for the practice part and some necessary theory too, so stay focused !

First, what we mean by “Screen Manager” is a way of organizing the code so in such way that we can isolate the “drawing” parts from the functional parts. The clearer and visible this border is,  the neater and more understandable the code gets, therefore allowing easier integration of our “Screen Manager”.

Take this example : we want to make a game with : a simple menu, the game itself, a help page and a page of high-scores. At first glance, the number of screens is limited and fixed before the creation by our specifications. That number is low (4) and so it is easy to consider something like:

function on.create()
  screen = 0 -- 0=menu, 1=game, 2=help, 3=highscores
end
 
function on.paint(gc)
  if screen == 0 then -- we draw the menu
  elseif screen == 1 then -- we draw the game
  elseif screen == 2 then -- we draw the help
  elseif screen == 3 then -- we draw the highscores
  end
end
 
function on.enterKey()
  if screen == 0 then
    screen == 1
  elseif screen == 1 then
....

Ok! No need to go further! We get it. To create an ugly, tedious-to-write code, this would be the best solution.
It is primarily designed for very small applications easily doable.

There is however a less ugly way, using arrays of functions :

function on.create() -- or on.construction with OS >= 3.2
  menu, game, help, highscores = 1, 2, 3, 4
  screen = menu
  paints = {}
  enterKeys = {}
end
 
function on.paint(gc)
  paints[screen](gc)
end
 
function on.enterKey(gc)
  enterKeys[screen](gc)
end
 
paints[menu] = function(gc)... end -- we draw the menu here
paints[jeu] = function(gc) ... end -- we draw the game here
...
enterKeys[menu] = function(gc)... end -- menu handling here
enterKeys[jeu] = function(gc) ... end -- game handling here
....

Here, it is clear that reading the code will be much easier than before. Moreover, the use of labels instead of the values themselves ​​significantly improves reading the code !
Feel free to “abuse” of that… ! You can, if you have too many “constants”, using an array of constants in order to organize them, like this:

screen = {menu=1, jeu=2, aide=3, records=4}
--> screen.menu == 1

Or, if you want something even more “automatic” (no need to count/shift the numbers) :

function createEnv(t)
  local env = {}
  for i, v in ipairs(t) do
    env[v] = i
  end
  return env
end
screen = createEnv({"menu", "game", "help", "highscores"})
--> screen.menu == 1

That being said, handling different screens remains archaic for other uses than games. It is of course possible to create a more elaborate system that facilitates the use and coding. What could be better?

You guessed it … use classes ! (if you’re not familiar with them, go to our tutorial !)

What we will code is actually a superset of the TI-Nspire API. You know the on.paint(), on.arrowKey() etc. events … Well, we will pass them along to our screen manager. The interest of coding a “parent class” here is that we are sure that a screen has specific methods, which will be called from standard events, without the fear of forgetting any when we create a new screen.

First, we must create our Screen class:

Screen = class()
function Screen:init() end

First, a screen does not need special initialization. Let’s recall that again : what we define is a superset of the API TI-Nspire, so we will write (without defining the content!) an exhaustive list of event-driven methods that will be available:

function Screen:paint(gc) end
function Screen:arrowKey(key) end
function Screen:timer() end
function Screen:charIn(ch) end
....

These functions are called “virtual”, and will be redefined later, for each particular screen. Because the class contains here only virtual functions, it is also called “virtual” (a bit of vocabulary isn’t bad 🙂 ). The Screen Manager involves creating a virtual class, a model, a “buffer” that guarantee us the existence of functions when we will code the specific event-driven parts:

activeScreen = Screen()
function on.paint(gc) activeScreen:paint(gc) end
function on.arrowKey(key) activeScreen:arrowKey(key) end
function on.timer() activeScreen:timer() end
function on.charIn(ch) activeScreen:charIn(ch) end

Thus, when we want to add the code for a screen, we code as if we coded a screen! Just think that “Menu” is “on.” :

Menu = class(Screen)
function Menu:init() end
function Menu:paint(gc) gc:drawRectangle(0, 0, 50, 50) end
function Menu:arrowKey(key) ... end
activeScreen = Menu()

This technique is widely used but also has a weakness: each screen change leads to recreating a screen object. In some cases it can be very useful (reset the game), in others, it is useless (the menu does not move). One can always store it in variables, or only pass the class itself as parameter. But there are even smarter choices to make.

Indeed, we can often see that to couple this technique with a stack. A stack is a special list: a data structure called “first in, first out” (or FIFO). The use of a stack here is to store initialized displays in an ordered list, while keeping the previous process as we want.

So we will add the stack management part. To do this, we must change the access to ActiveScreen by ActiveScreen(), replace the assignments of ActiveScreen by a function that will add to the screen stack (PushScreen()) and  we must not forget to pop the screens out once it’s no longer used.

Here’s how that goes, and this is the final tutorial code:

------ Screen Manager
Screen = class()
 
function Screen:init() end
 
-- virtual functions to override
function Screen:paint(gc) end
function Screen:timer() end
function Screen:charIn(ch) end
function Screen:arrowKey(key) end
function Screen:escapeKey() end
function Screen:enterKey() end
function Screen:tabKey() end
function Screen:contextMenu() end
function Screen:backtabKey() end
function Screen:backspaceKey() end
function Screen:clearKey() end
function Screen:mouseMove(x, y) end
function Screen:mouseDown(x, y) end
function Screen:mouseUp() end
function Screen:rightMouseDown(x, y) end
function Screen:help() end
 
local Screens = {}
 
function PushScreen(screen)
    table.insert(Screens, screen)
    platform.window:invalidate()
end
 
function PullScreen()
    if #Screens > 0 then
        table.remove(Screens)
        platform.window:invalidate()
    end
end
 
function activeScreen()
    return Screens[#Screens] and Screens[#Screens] or Screen
end
 
-- Link events to ScreenManager
function on.paint(gc)
   for _, screen in pairs(Screens) do
        screen:paint(gc)
    end
end
 
function on.timer()
    for _, screen in pairs(Screens) do
        screen:timer()
    end
end
 
function on.charIn(ch) activeScreen():charIn(ch) end
function on.arrowKey(key) activeScreen():arrowKey(key) end
function on.escapeKey()	activeScreen():escapeKey() end
function on.enterKey() activeScreen():enterKey() end
function on.tabKey() activeScreen():tabKey() end
function on.contextMenu() activeScreen():contextMenu() end
function on.backtabKey() activeScreen():backtabKey() end
function on.backspaceKey() activeScreen():backspaceKey() end
function on.clearKey() activeScreen():clearKey() end
function on.mouseDown(x, y) activeScreen():mouseDown(x, y) end
function on.mouseUp() activeScreen():mouseUp() end
function on.mouseMove(x, y) activeScreen():mouseMove(x, y) end
function on.rightMouseDown(x, y) activeScreen():rightMouseDown(x, y) end
function on.help() activeScreen():help() end
 
function on.create() PushScreen(Menu()) end
function on.resize() end

NB: being exhaustive at this point is not required.
However, the number of functions defined in the Screen class must match the number of defined events.

Leave a Reply