Todo es un objeto

Todo cobra vida en forma de objeto.

Muchísimos objetos

Objeto

Me gusta trabajar con objetos, puedes meter en ellos toda la información que vas a necesitar después. Lua no tiene un sistema de clases como el que conozco de otros lenguajes. Lo que necesitaba era algo con lo que más adelante se pudieran sobrescribir o ampliar clases. Y me refiero desde el punto de vista del juego que usa engine3.

La idea de fondo es que engine3 se pueda publicar para que cualquiera lo use. El juego en sí lo puedes mantener privado. Los dos conviven y se complementan. Después de algunas pruebas lo conseguí, y ahora se pueden crear objetos a partir de clases.

Un ejemplo de clase, engine3/core/camera.lua (versión reducida).

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

Supongamos que la cámara del juego tiene que comportarse un poco distinto a la cámara por defecto de engine3. Entonces creo el archivo game/framework/core/camera.lua y hago referencia a la clase camera de engine3. Después hago mis cambios.

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

Muy bonitos todos esos objetos distintos (a partir de ahora a un objeto lo llamo entity), pero hay que poder encontrarlos y usarlos en todo el código. Así que había que hacer algo para gestionarlo todo.

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

Esto es en realidad el corazón del juego, o una de sus muchas partes importantes. Si aquí algo sale mal, tienes un problema…

Cuando se crea una entity, se añade al Entity Manager y, si hace falta, también se elimina. En la clase camera de arriba ya viste que se hace con la línea de abajo.

entityManager:add(self)

Todas las clases que escribo tienen esa línea, para que se añadan solas a través del entityManager.

En la clase también puedes crear una función release, que se ejecuta automáticamente cuando el entityManager elimina el objeto. Es muy importante para mantener la memoria bajo control. No quieres guardar todos los recursos, como imágenes, sino sacarlos de la memoria en algún momento.

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

En uso

Cuando tengo una clase nueva en algún sitio y necesito una entity, por ejemplo la cámara, puedo hacerlo de varias formas.

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

Pero esto devuelve una tabla con todas las Entities encontradas del tipo Camera (clase). No es práctico, porque en este caso siempre hay 1 cámara, y quiero estar 100% seguro de 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

Esto es un wrapper alrededor de find, y da un error cuando el objeto no existe.

Algunos otros ejemplos son

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

Para terminar

Todos los puntos que buscaba sobre clases y objetos ya están cubiertos

  • clases
  • las clases se pueden ampliar
  • las clases se pueden sobrescribir
  • las entities están controladas por un manager
  • puedo usar las entities en cualquier parte

Es muy importante tomar esta decisión cuanto antes y contar con que el código final todavía va a cambiar mucho. Si lo organizas todo de forma estructurada, después le sacarás mucho partido.

¡Si Lua ya no pudiera con el número de entities, esta parte se podría pasar a C!