Tudo é um objeto
Tudo ganha vida na forma de um objeto.

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!