Logo Signal From The Stars

Alles is een object

Alles komt tot leven in de vorm van een object.

Martin avatar
  • Martin
  • 5 min read
Super veel objecten

Object

Ik werk graag met objecten, je kan er alle informatie in kwijt die je later nodig hebt. Lua heeft geen class systeem zoals ik ken van andere talen. Wat ik nodig had was iets waarmee op een later punt classes overschreven of uigebreid konden worden. En dan bedoel ik vanuit het spel gezien die gebruik maakt van engine3.

Het achterliggende idee is dat engine3 uitgebracht kan worden zodat iedereen dit kan gebruiken. Het spel zelf kun je dan private houden. De twee bestaan dan naast elkaar en vullen elkaar aan. Na wat testen is dit het gelukt en kunnen er nu objecten gemaakt worden aan de hand van classes.

Een voorbeeld class engine3/core/camera.lua (beperkte versie).

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

Stel dat de camera in het spel zich net iets anders moet gaan gedragen dan de default camera van engine3 dan maak ik het bestand game/framework/core/camera.lua en verwijs naar de engine3 camera class. Vervolgens maak ik mijn aanpassingen.

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

Leuk al die verschillende objecten (ik noem een object vanaf nu entity), maar ze moeten door de hele code gevonden/gebruikt gaan worden. Er moest dus iets gemaakt worden alles te kunnen managen.

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

Dit is eigenlijk het hart van het spel of een van de vele belangrijke, wanneer hier iets fout gaat heb je een probleem…

Wanneer een entity wordt gemaakt zal dit worden toegevoegd aan de Entity Manger en eventueel ook weer verwijderd. In de camera class hierboven kon je al zien dat dit gaat met de onderstaande regel.

entityManager:add(self)

In elke class die ik schrijf staat die regel zodat het zichzelf zal toevoegen via de entityManger.

In de class kun je ook een release functie maken, dit wordt automatisch uitgevoerd wanneer de enitityManger het object verwijderd. Dit is zeer belangrijk om geheugen onder controle te houden. Je wilt assets zoals afbeeldingen niet allemaal bewaren maar op een gegeven moment uit het geheugen halen.

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

In gebruik

Wanneer ik ergens een nieuwe class heb en ik heb daar een entity nodig bijv. de camera, dan kan dat op verschillende manier.

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

Dit zal echter een tabel teruggeven met alle gevonden Entities van het type Camera (class), dit is niet handig aangezien het in dit geval altijd 1 camera is, en ik wil er 100% zeker van zijn dat de Camera ook aanwezig is.

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

Dit is een wrapper om find heen, en en geeft een error wanneer het object niet aanwezig is.

Enkele andere voorbeelden zijn

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

Tot slot

Al mijn punten die ik zocht aangaande classes en objecten zijn nu gedekt

  • classes
  • classes zijn uitbreidbaar
  • classes zijn overschrijfbaar
  • entities staan onder controle door een manager
  • entities kan ik overal gebruiken

Het is zeer belangrijk om die keuze zo snel mogelijk te maken en er vanuit gaan dat er nog veel veranderingen gaan plaats vinden in de uiteindelijke code. Wanneer je alles gestructeerd uitzet heb je daar later veel profijt van.

Stel dat Lua de verwerking van het aantal entities niet meer aan kan, dan is het mogelijk om dit deel om te zetten naar C!