Tutto è un oggetto

Tutto prende vita sotto forma di oggetto.

Tantissimi oggetti

Oggetto

Mi piace lavorare con gli oggetti, ci puoi mettere tutte le informazioni che ti servono dopo. Lua non ha un sistema di classi come quello che conosco da altri linguaggi. Mi serviva qualcosa con cui, più avanti, sovrascrivere o estendere le classi. E intendo dal punto di vista del gioco che usa engine3.

L’idea di fondo è che engine3 possa essere pubblicato in modo che chiunque possa usarlo. Il gioco stesso invece puoi tenerlo privato. I due esistono uno accanto all’altro e si completano. Dopo qualche test ci sono riuscito, e ora si possono creare oggetti a partire da classi.

Un esempio di classe, engine3/core/camera.lua (versione ridotta).

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

Supponiamo che la camera nel gioco debba comportarsi in modo un po’ diverso dalla camera predefinita di engine3. Allora creo il file game/framework/core/camera.lua e faccio riferimento alla classe camera di engine3. Poi faccio le mie modifiche.

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

Belli tutti questi oggetti diversi (da qui in poi chiamo un oggetto entity), ma devono poter essere trovati e usati in tutto il codice. Serviva quindi qualcosa per gestire tutto.

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

Questo è in pratica il cuore del gioco, o uno dei tanti pezzi importanti. Se qui qualcosa va storto, hai un problema…

Quando viene creata un’entity, viene aggiunta all’Entity Manager ed eventualmente anche rimossa. Nella classe camera qui sopra hai già visto che succede con la riga qui sotto.

entityManager:add(self)

Ogni classe che scrivo contiene quella riga, così si aggiunge da sola tramite l’entityManager.

Nella classe puoi anche creare una funzione release, che viene eseguita automaticamente quando l’entityManager rimuove l’oggetto. È molto importante per tenere sotto controllo la memoria. Non vuoi tenere tutti gli asset come le immagini, ma a un certo punto toglierli dalla memoria.

function Camera:release()
    -- do things when the entityManger delete this object
end

In uso

Quando ho una nuova classe da qualche parte e mi serve un’entity, per esempio la camera, posso farlo in diversi modi.

local cameras = entityManager:find(nil,"Camera")

Questo però restituisce una tabella con tutte le Entity trovate di tipo Camera (classe). Non è comodo, visto che in questo caso c’è sempre 1 sola camera, e voglio essere sicuro al 100% che la Camera ci sia.

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

Questo è un wrapper attorno a find, e dà un errore quando l’oggetto non c’è.

Alcuni altri esempi sono

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})

Per finire

Tutti i punti che cercavo per classi e oggetti ora sono coperti

  • classi
  • le classi sono estendibili
  • le classi sono sovrascrivibili
  • le entity sono sotto controllo grazie a un manager
  • posso usare le entity ovunque

È molto importante fare questa scelta il prima possibile e mettere in conto che il codice finale cambierà ancora molto. Se imposti tutto in modo strutturato, dopo ne avrai un grande vantaggio.

Se Lua non riuscisse più a gestire il numero di entity, è possibile convertire questa parte in C!