Tout est un objet
Tout prend vie sous forme d’objet.

Objet
J’aime travailler avec des objets, on peut y mettre toutes les informations dont on aura besoin plus tard. Lua n’a pas de système de classes comme celui que je connais dans d’autres langages. Il me fallait quelque chose qui permette plus tard de surcharger ou d’étendre des classes. Et je parle ici du point de vue du jeu qui utilise engine3.
L’idée est qu’engine3 puisse être publié pour que tout le monde puisse l’utiliser. Le jeu lui-même peut alors rester privé. Les deux existent côte à côte et se complètent. Après quelques tests, j’y suis arrivé, et on peut maintenant créer des objets à partir de classes.
Un exemple de classe, engine3/core/camera.lua (version réduite).
local Callback = require "lib.engine3.src.framework.core.callback"
---@class Camera: Object
---@field config Config
---@field gameCanvas Canvas
---@field x integer
---@field y integer
---@field width integer
---@field height integer
---@field followSpeed integer
---@field targetX integer
---@field targetY integer
---@field bound boolean
---@field bounds_min_x integer
---@field bounds_min_y integer
---@field bounds_max_x integer
---@field bounds_max_y integer
---@field callbackAfterStop function
---@field callback Callback
---@field super self
---@overload fun(data: CameraOptions):Camera
local Camera = Object:extend("Camera")
Camera:implement(Callback)
---@class CameraOptions
---@field x? integer Default 0
---@field y? integer Default 0
---@field width? integer Default canvas width
---@field height? integer Default canvas height
---@field followSpeed integer Default 1
function Camera:new(data)
if data == nil then
data = {}
end
self.config = entityManager:findOne(nil, "Config")
self.gameCanvas = entityManager:findOne({name = "gameCanvas"}, "Canvas")
self.x = data.x or 0
self.y = data.y or 0
self.width = data.width or self.gameCanvas:getWidth()
self.height = data.height or self.gameCanvas:getHeight()
self.followSpeed = data.followSpeed or 1
self.targetX = nil
self.targetY = nil
self.bound = true
self.bounds_min_x = nil
self.bounds_min_y = nil
self.bounds_max_x = nil
self.bounds_max_y = nil
self.callbackAfterStop = {}
entityManager:add(self)
end
---@param x integer
---@param y integer
---@param w integer
---@param h integer
function Camera:setBounds(x, y, w, h)
self.bound = true
self.bounds_min_x = x
self.bounds_min_y = y
self.bounds_max_x = x + w
self.bounds_max_y = y + h
end
---@param dt number
function Camera:update(dt)
-- core things
end
function Camera:setBounds(x, y, w, h)
self.bound = true
self.bounds_min_x = x
self.bounds_min_y = y
self.bounds_max_x = x + w
self.bounds_max_y = y + h
end
Imaginons que la caméra du jeu doive se comporter un peu différemment de la caméra par défaut d’engine3. Je crée alors le fichier game/framework/core/camera.lua et je fais référence à la classe camera d’engine3. Ensuite, j’apporte mes modifications.
---@class Camera: Object
---@field followEntity Actor|Animation
---@overload fun():Camera
local Camera = require("lib.engine3.src.framework.core.camera"):extend("Camera")
-- extra new function
---@return Actor|Animation
function Camera:getFollowEntity()
-- do things
return self.followEntity
end
-- overwrite the original
---@param dt number
function Camera:update(dt)
if self.followEntity then
-- do something
end
-- optional but possible, call the original camera update
Camera.super.update(self, dt)
end
Entity Manager
C’est bien beau, tous ces objets différents (à partir de maintenant, j’appelle un objet une entity), mais il faut pouvoir les trouver et les utiliser dans tout le code. Il fallait donc créer quelque chose pour tout gérer.
EntityManager:implement(Callback)
function EntityManager:new()
self.entities = {}
self.lastEntityId = 0
self.callbackAfterAdd = {}
self.callbackAfterDelete = {}
end
function EntityManager:createId()
end
function EntityManager:add(entities)
end
function EntityManager:del(entity, args)
...
if entity.release then
entity:release(args)
end
...
end
function EntityManager:delAll(args)
end
function EntityManager:getEntityIndex(entity)
end
function EntityManager:find(filter, className, enitities)
end
function EntityManager:findOne(filter, className, entities)
end
function EntityManager:filter(data, filter)
end
C’est en fait le cœur du jeu, ou l’une de ses nombreuses parties importantes. Si quelque chose tourne mal ici, vous avez un problème…
Quand une entity est créée, elle est ajoutée à l’Entity Manager et éventuellement retirée ensuite. Dans la classe camera ci-dessus, vous avez déjà vu que ça se fait avec la ligne ci-dessous.
entityManager:add(self)
Chaque classe que j’écris contient cette ligne, pour qu’elle s’ajoute elle-même via l’entityManager.
Dans la classe, on peut aussi créer une fonction release, exécutée automatiquement quand l’entityManager supprime l’objet. C’est très important pour garder la mémoire sous contrôle. On ne veut pas garder tous les assets comme les images, mais les retirer de la mémoire à un moment donné.
function Camera:release()
-- do things when the entityManger delete this object
end
En pratique
Quand j’ai une nouvelle classe quelque part et que j’ai besoin d’une entity, par exemple la caméra, je peux le faire de plusieurs façons.
local cameras = entityManager:find(nil,"Camera")
Mais cela renvoie un tableau avec toutes les Entities trouvées du type Camera (classe). Ce n’est pas pratique, puisqu’ici il y a toujours 1 seule caméra, et je veux être sûr à 100 % que la Camera existe.
local camera = entityManager:findOne(nil, "Camera")
function EntityManager:findOne(filter, className, entities)
local objects = self:find(filter, className, entities)
if #objects == 1 then
return objects[1]
elseif #objects > 1 then
error("Cannot get exactly one object, because it was found (" .. #objects .. ") times !")
end
if className ~= nil then
error("findOne() object does not exist :" .. className)
else
dump(filter)
error("findOne() Cannot find object")
end
end
C’est un wrapper autour de find, qui renvoie une erreur quand l’objet n’existe pas.
Quelques autres exemples
local entities = entityManager:find({name = "bird", visible = true}, "Frameset")
local entities = entityManager:find({name = "bird", visible = true}, "Frameset", customEntities)
-- special magic
local entities = entityManager:find({createdBy = Item}, "Frameset")
-- special magic
local entities = entityManager:find({collidableA = "CONTAINS_DATA"}, "Frameset")
-- special magic
local entities = entityManager:find({isObject = require "lib.engine3.src.framework.core.canvas", excludeEntities = self.canvases})
Pour finir
Tous les points que je cherchais concernant les classes et les objets sont maintenant couverts
- des classes
- les classes sont extensibles
- les classes peuvent être surchargées
- les entities sont contrôlées par un manager
- je peux utiliser les entities partout
Il est très important de faire ce choix le plus tôt possible et de partir du principe que le code final changera encore beaucoup. Si vous organisez tout de façon structurée, vous en profiterez beaucoup plus tard.
Si Lua ne pouvait plus gérer le nombre d’entities, on pourrait convertir cette partie en C !