Alt er et objekt

Alt kommer til live i form af et objekt.

Rigtig mange objekter

Objekt

Jeg kan godt lide at arbejde med objekter, man kan putte al den information i dem, man får brug for senere. Lua har ikke et klassesystem som dem, jeg kender fra andre sprog. Jeg havde brug for noget, som gjorde det muligt senere at overskrive eller udvide klasser. Og så mener jeg set fra spillet, som bruger engine3.

Tanken bag er, at engine3 kan udgives, så alle kan bruge det. Selve spillet kan man så holde privat. De to eksisterer side om side og supplerer hinanden. Efter nogle tests lykkedes det, og nu kan der laves objekter ud fra klasser.

Et eksempel på en klasse, engine3/core/camera.lua (begrænset version).

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

Lad os sige, at kameraet i spillet skal opføre sig lidt anderledes end standardkameraet i engine3. Så laver jeg filen game/framework/core/camera.lua og henviser til engine3’s camera-klasse. Derefter laver jeg mine ændringer.

---@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 nok med alle de forskellige objekter (fra nu af kalder jeg et objekt en entity), men de skal kunne findes og bruges i hele koden. Der skulle altså laves noget til at styre det hele.

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

Det er faktisk hjertet i spillet, eller en af de mange vigtige dele. Går der noget galt her, har du et problem…

Når en entity bliver oprettet, bliver den tilføjet til Entity Manager og eventuelt fjernet igen. I camera-klassen ovenfor kunne du allerede se, at det sker med linjen herunder.

entityManager:add(self)

Den linje står i alle klasser, jeg skriver, så de selv bliver tilføjet via entityManager.

I klassen kan man også lave en release-funktion, som automatisk køres, når entityManager fjerner objektet. Det er meget vigtigt for at holde styr på hukommelsen. Man vil ikke gemme alle assets som billeder, men på et tidspunkt fjerne dem fra hukommelsen.

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

I brug

Når jeg et sted har en ny klasse og skal bruge en entity, fx kameraet, kan det gøres på forskellige måder.

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

Det giver dog en tabel tilbage med alle fundne Entities af typen Camera (klasse). Det er ikke praktisk, da der i det her tilfælde altid kun er 1 kamera, og jeg vil være 100 % sikker på, at Camera findes.

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

Det er en wrapper omkring find, som giver en fejl, når objektet ikke findes.

Nogle 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 sidst

Alle de punkter, jeg søgte angående klasser og objekter, er nu dækket

  • klasser
  • klasser kan udvides
  • klasser kan overskrives
  • entities styres af en manager
  • entities kan jeg bruge overalt

Det er meget vigtigt at træffe det valg så tidligt som muligt og regne med, at der stadig kommer mange ændringer i den endelige kode. Hvis man sætter det hele struktureret op, får man meget glæde af det senere.

Skulle Lua ikke længere kunne klare antallet af entities, kan den del laves om til C!