Tudo é um objeto

Tudo ganha vida na forma de um objeto.

Muitos, muitos objetos

Objeto

Gosto de trabalhar com objetos, dá para colocar neles todas as informações de que você vai precisar depois. Lua não tem um sistema de classes como o que conheço de outras linguagens. O que eu precisava era de algo que permitisse, mais tarde, sobrescrever ou estender classes. E falo do ponto de vista do jogo que usa a engine3.

A ideia por trás disso é que a engine3 possa ser publicada para que qualquer pessoa possa usá-la. O jogo em si você pode manter privado. Os dois existem lado a lado e se complementam. Depois de alguns testes, deu certo, e agora dá para criar objetos a partir de classes.

Um exemplo de classe, engine3/core/camera.lua (versão reduzida).

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

Imagine que a câmera no jogo precise se comportar um pouco diferente da câmera padrão da engine3. Então crio o arquivo game/framework/core/camera.lua e faço referência à classe camera da engine3. Depois faço minhas alterações.

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

Muito bom ter todos esses objetos diferentes (daqui em diante chamo um objeto de entity), mas eles precisam ser encontrados e usados em todo o código. Então era preciso criar algo para gerenciar tudo.

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

Isso é, na verdade, o coração do jogo, ou uma das muitas partes importantes. Se algo der errado aqui, você tem um problema…

Quando uma entity é criada, ela é adicionada ao Entity Manager e, se for o caso, removida depois. Na classe camera acima você já viu que isso é feito com a linha abaixo.

entityManager:add(self)

Toda classe que escrevo tem essa linha, para que ela se adicione sozinha pelo entityManager.

Na classe você também pode criar uma função release, que é executada automaticamente quando o entityManager remove o objeto. Isso é muito importante para manter a memória sob controle. Você não quer guardar todos os assets, como imagens, e sim tirá-los da memória em algum momento.

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

Na prática

Quando tenho uma classe nova em algum lugar e preciso de uma entity, por exemplo a câmera, posso fazer isso de vários jeitos.

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

Mas isso retorna uma tabela com todas as Entities encontradas do tipo Camera (classe). Não é prático, já que neste caso sempre existe 1 câmera, e quero ter 100% de certeza de que a 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

Isso é um wrapper em volta do find e dá um erro quando o objeto não existe.

Alguns outros exemplos

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 os pontos que eu buscava sobre classes e objetos agora estão cobertos

  • classes
  • classes podem ser estendidas
  • classes podem ser sobrescritas
  • entities ficam sob controle de um manager
  • posso usar entities em qualquer lugar

É muito importante tomar essa decisão o quanto antes e contar com o fato de que o código final ainda vai mudar muito. Se você organizar tudo de forma estruturada, vai se beneficiar muito disso depois.

Se o Lua não der mais conta da quantidade de entities, dá para converter essa parte para C!