Comment réaliser un Gestionnaire d’Ecrans

Lorsque l’on crée un jeu ou un outil quelconque de manière « complète », on désire souvent réaliser un panel d’écrans dans lequel l’utilisateur naviguera. Il existe plusieurs manières de réaliser un moteur qui nous permettra de gérer tous ces écrans, du plus simple à coder mais le moins pratique, au plus difficile à comprendre mais facile d’utilisation. L’objectif de ce tutoriel n’est bien évidemment pas de vous donner une idée fixe de ce à quoi ressemble un gestionnaire d’écran (il en existe une multitude de types), mais de vous présenter un panel de choix. Ce tutoriel est long car exhaustif en exemples et théorie, ne lâchez pas le fil!

Tout d’abord, ce que l’on entend par « gestionnaire d’écrans » c’est une manière d’organiser le code afin que l’on puisse isoler les parties de dessin des parties fonctionnelles. Plus cette frontière est marquée, plus le code est propre et lisible, et permettra donc d’intégrer plus facilement notre fameux « gestionnaire d’écrans ».

Prenons un exemple, nous souhaitons réaliser un jeu simple avec un menu, le jeu, une page d’aide et une page de records. A première vue, le nombre d’écrans est limité et fixé avant la création par notre cahier des charges. Ce même nombre est faible (4) et il est donc facile d’envisager quelque chose en ce sens :

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 ! inutile d’aller plus loin ! On a compris. Pour créer un code moche, long à écrire et j’en passe, c’est la meilleure solution. Il est avant tout destiné aux applications de très petite taille car facilement implémentable.

Il y a cependant moyen de faire moins moche, en utilisant des tableaux de fonctions.

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

Ici, il est évident que la lecture du code sera bien plus simple qu’auparavant. Par ailleurs, l’utilisation d’étiquettes au lieu de valeurs propres améliore considérablement la lecture du code ! N’hésitez pas à en abuser ! Vous pouvez, si vous avez trop de « constantes », utiliser un tableau de constantes afin de les organiser, comme ceci :

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

Ou encore pour faire quelque chose d’automatisé (plus besoin de compter/décaler les chiffres) :

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

Ceci étant, la gestion des différents écrans reste archaïque pour des usages autres que les jeux. Il est bien entendu possible de créer un système plus élaboré qui facilite l’utilisation et le coding. Quoi demander de plus ?

Vous l’aurez deviné, il faut utiliser … les classes !

Ce que l’on va s’apprêter à coder est en fait une surcouche de l’API TI-Nspire. Vous connaissez les évènements on.paint(), on.arrowKey() etc … et bien nous allons by-passer leur utilisation par notre gestionnaire d’écrans. L’intérêt de coder une classe mère ici est que nous sommes sûr qu’un écran a telles ou telles méthodes; méthodes que l’on appellera depuis les évènements standards sans risquer d’en oublier lorsque l’on code un nouvel écran.

Tout d’abord, il faut créer notre classe Screen :

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

Pour commencer, un écran n’a pas besoin d’initialisation spéciale. Rappelons le encore une fois, ce que nous définissons est une surcouche de l’API TI-Nspire, nous allons donc écrire (sans en définir le contenu !!) exhaustivement la liste des fonctions évènementielles qui sont disponibles :

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

Ces fonctions sont qualifiées de « virtuelles » qui devrons être redéfinies . Parce que la classe ne comprend ici que des fonctions virtuelles, elle est également qualifiée de virtuelle (un peu de vocabulaire ne fait pas de mal). Le gestionnaire d’écrans passe par la création d’une classe virtuelle, un modèle, un tampon qui nous garanti l’existence des fonctions lorsque l’on va coder la liaison évènementielle :

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

Ainsi, lorsque l’on veut rajouter le code pour un écran, on code comme si on ne codait qu’un écran ! Il suffit de penser que « Menu: » est « 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()

Cette technique est très utilisée mais elle a cependant un point faible : à chaque changement d’écran on recréer un objet d’écran. Dans certains ça peut être très utile (réinitialisation du jeu), dans d’autres c’est inutile (le menu ne bouge pas). On peut toujours, stocker ça dans des variables, ou bien passer uniquement la classe elle même en paramètre. Mais il y a encore mieux.

En effet, il est souvent d’usage de coupler cette technique avec un pile. Une pile est liste spéciale : une structure de donnée surnommée « first in, first out » (ou encore FIFO), littéralement, « premier rentré, premier sorti ». L’utilisation d’une pile ici permet de stocker les écrans initialisés dans une liste ordonnée, tout en conservant le fonctionnement précédent.

Nous allons donc rajouter la gestion de la pile. Pour cela il faut changer les accès à activeScreen par activeScreen(), remplacer les assignations de activeScreen par une fonction qui va rajouter l’écran à la pile (PushScreen()) et ne pas oublier de dépiler les écrans une fois qu’on ne s’en sert plus.

Voici ce que cela donne :

------ 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 : le fait d’être exhaustif à ce point n’est pas obligé. Il faut par contre que le nombre de fonctions dans la classe Screen corresponde au nombre d’évènements définis.

Laisser un commentaire