Archives pour l'étiquette screen

Un nouveau Screen Manager plus intelligent

Ca ne fait pas si longtemps que Jim Bauwens nous a agréablement surpris avec une façon élégante de personnaliser l’objet « gc » avec vos propres fonctions. Mais il frappe encore !

En effet, il a trouvé/créé un nouveau moyen, plus intelligent qu’avant, de créer un Screen Manager. Si vous n’êtes pas familier avec ce superbe concept, allez-donc vous renseigner 😉
Pour ceux qui le sont, par contre, vous devez savoir qu’une partie du code nécessaire est le listing/lien complet des fonctions à utiliser, des événements vers les screens.

Par exemple :

function on.arrowKey(key) activeScreen:arrowKey(key) end
function on.timer() activeScreen:timer() end
 
-- long list of all handled events...       Then, somewhere else :
 
function myScreen:arrowKey(key)
    -- Your awesome code goes here
end
function myScreen:timer()
    print("I'm the myScreen timer, ticking....")
end
 
-- etc.

Ca marche, certes, mais … c’est un peu barbant.

Allons voir ce que Jim a créé.
Je définis cette nouvelle façon comme « plus intelligente » parce qu’avec ce code, vous n’aurez même plus besoin explicitement de vous tracasser sur la gestion des événements comme vous en avez l’habitude (avec des on.arrowKey, on.enterKey… ou le bon vieux on.paint ). Oui, vous avez bien lu : plus besoin de « function on.paint(gc) … » etc. !

« Qu’est-ce que c’est que cette magie noire », me direz-vous ?
Hé bien, encore une fois, tout repose sur l’utilisation futée des métatables Lua.
En gros, les métatables sont un ensemble de propriétés définissant le comportement d’un objet (généralement une table).
Il existe une propriété « __index » que vous pouvez définir, qui va décrire comment la table réagira quand le script appelle un élément non-défini de celle-ci. Assez utile, croyez-moi 😉
Bref, le truc c’est que quand vous écrivez « function on.paint(gc) », tout ce que vous faites, c’est implémenter la méthode « paint » de la table « on » (d’où le point).
Ce que l’on souhaite pouvoir faire, c’est de se débarasser de cette décalration explicite, et de rediriger l’événement « paint » vers tel ou tel Screen (et les autres événements aussi…)
Bref, nous allons donc utiliser une méthode « eventDistributer » qui prend comme arguments tout ce qui lui est passé, grâce à l’utilisation de « … » (l’événement suivi de ses paramètres, si c’est le cas), et qui les « passe » au Screen que l’on veut (tout en vérifiant que cela est bien possible pour le screen en question):

local triggeredEvent = "paint"  -- first declaration, it will be overwritten anyway
local eventDistributer = function (...)
     local currentScreen = GetScreen()
     if currentScreen[triggeredEvent] then
         currentScreen[triggeredEvent](currentScreen, ...)
     end
end

Ce code devrait vous paraître assez clair.

Ce qu’il nous reste désormais à faire, c’est de lier cette fonction à l’__index de la métatable de « on » (vous remarquerez l’intelligente utilisation des closures) :

local eventCatcher = {}
 
eventCatcher.__index = function (tbl, event)
    triggeredEvent = event
    return eventDistributer
end
 
setmetatable(on, eventCatcher)

Ce code permet au script de comprendre que quand un événement, sans handler explicite, est déclenché, il devra exécuter cette fonction (qui retourne (closure) une fonction qui prendra les arguments de l’événement et qui redirigera le tout via l’eventDistributer, donc vers l’event handler approprié du bon Screen). Je conçois que ceci peut vous paraître un peu confus/complexe, mais vous finirez par comprendre 🙂

Bref, voila le code complet du Screen Manager et de l’eventDistributer.
(Comme d’habitude, vous aurez à créer (et push) vos Screens, par la suite)

local screens = {}
local screenLocation = {}
local currentScreen = 0
 
function RemoveScreen(screen)
    screen:removed()
    table.remove(screens, screenLocation[screen])
    screenLocation[screen] = nil
    currentScreen = #screens -- sets the current screen as the top one on the stack.
    if #screens<=0 then print("Uh oh. This shouldn't have happened ! You must have removed too many screens.") end
end
 
function PushScreen(screen)
    -- if already activated, remove it first (so that it will be on front later)
    if screenLocation[screen] then
        RemoveScreen(screen)
    end
 
    table.insert(screens, screen)
    screenLocation[screen] = #screens
 
    currentScreen = #screens
    screen:pushed()
end
 
function GetScreen()
    return screens[currentScreen] or RootScreen
end
 
Screen = class()
 
function Screen:init() end
 
function Screen:pushed() end
function Screen:removed() end
 
RootScreen = Screen() -- "fake", empty placeholder screen.
 
local eventCatcher = {}
local triggeredEvent = "paint"
 
local eventDistributer = function (...)
     local currentScreen = GetScreen()
     if currentScreen[triggeredEvent] then
         currentScreen[triggeredEvent](currentScreen, ...)
     end
end
 
eventCatcher.__index = function (tbl, event)
    triggeredEvent = event
    return eventDistributer
end
 
-- finally linking everything
setmetatable(on, eventCatcher)

Je suis sur que parmi vous lecteurs, nombreux sont ceux intéressés par un .tns d’exemple directement, « tout prêt » 😉 Je vous ai préparé ceci : cliquez ici pour télécharger le fichier. C’est un exemple de base avec quelques événements, mais sans une seule fonction on.xxx définie explicitement, et dans 2 screens. J’ai plutôt assez bien commenté le code pour que vous puissiez comprendre rapidement 🙂

Pour un exemple plus terre-à-terre, montrant en même temps cette technique et celle de la personnalisation du gc, je vous suggère de télécharger le jeu Memory de Jim, ici.