Tout est un objet

Tout prend vie sous forme d’objet.

Énormément d’objets

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 !