Alt er et objekt

Alt får liv i form av et objekt.

Veldig mange objekter

Objekt

Jeg liker å jobbe med objekter, du kan legge all informasjon du trenger senere, i dem. Lua har ikke et klassesystem slik jeg kjenner det fra andre språk. Jeg trengte noe som gjorde det mulig å overskrive eller utvide klasser senere. Og da mener jeg sett fra spillet som bruker engine3.

Tanken bak er at engine3 kan gis ut slik at alle kan bruke det. Selve spillet kan du da holde privat. De to eksisterer side om side og utfyller hverandre. Etter litt testing lyktes det, og nå kan det lages objekter ut fra klasser.

Et eksempel på en klasse, engine3/core/camera.lua (begrenset versjon).

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

Si at kameraet i spillet skal oppføre seg litt annerledes enn standardkameraet i engine3. Da lager jeg filen game/framework/core/camera.lua og viser til kameraklassen i engine3. Deretter gjør jeg endringene mine.

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

Fint med alle disse ulike objektene (fra nå av kaller jeg et objekt en entity), men de må kunne finnes og brukes i hele koden. Det måtte altså lages noe for å styre alt.

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

Dette er egentlig hjertet i spillet, eller en av de mange viktige delene. Går noe galt her, har du et problem…

Når en entity lages, legges den til i Entity Manager og fjernes eventuelt igjen. I kameraklassen over kunne du allerede se at dette skjer med linjen under.

entityManager:add(self)

Den linjen står i hver klasse jeg skriver, slik at den legger seg til selv via entityManager.

I klassen kan du også lage en release-funksjon, som kjøres automatisk når entityManager fjerner objektet. Det er svært viktig for å holde kontroll på minnet. Du vil ikke ta vare på alle ressurser som bilder, men fjerne dem fra minnet på et tidspunkt.

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

I bruk

Når jeg har en ny klasse et sted og trenger en entity der, for eksempel kameraet, kan det gjøres på flere måter.

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

Dette gir imidlertid tilbake en tabell med alle funnede Entities av typen Camera (klasse). Det er upraktisk, siden det i dette tilfellet alltid er 1 kamera, og jeg vil være 100 % sikker på at Camera finnes.

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

Dette er en wrapper rundt find og gir en feil når objektet ikke finnes.

Noen andre eksempler er

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

Til slutt

Alle punktene jeg lette etter når det gjelder klasser og objekter, er nå dekket

  • klasser
  • klasser kan utvides
  • klasser kan overskrives
  • entities kontrolleres av en manager
  • entities kan jeg bruke overalt

Det er svært viktig å ta dette valget så tidlig som mulig og regne med at den endelige koden fortsatt kommer til å endre seg mye. Setter du opp alt strukturert, har du stor nytte av det senere.

Hvis Lua ikke lenger skulle klare antallet entities, kan denne delen gjøres om til C!