Alles ist ein Objekt

Alles wird in Form eines Objekts lebendig.

Jede Menge Objekte

Objekt

Ich arbeite gern mit Objekten, man kann darin alle Informationen unterbringen, die man später braucht. Lua hat kein Klassensystem, wie ich es aus anderen Sprachen kenne. Ich brauchte etwas, womit Klassen später überschrieben oder erweitert werden können. Und damit meine ich aus Sicht des Spiels, das engine3 verwendet.

Die Idee dahinter ist, dass engine3 veröffentlicht werden kann, damit jeder es nutzen kann. Das Spiel selbst kann man dann privat halten. Beide existieren nebeneinander und ergänzen sich. Nach ein paar Tests hat das geklappt, und jetzt können Objekte anhand von Klassen erstellt werden.

Eine Beispielklasse, engine3/core/camera.lua (gekürzte 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

Angenommen, die Kamera im Spiel soll sich ein bisschen anders verhalten als die Standardkamera von engine3. Dann lege ich die Datei game/framework/core/camera.lua an und verweise auf die Kameraklasse von engine3. Danach mache ich meine Anpassungen.

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

Schön und gut, all diese verschiedenen Objekte (ich nenne ein Objekt ab jetzt Entity), aber sie müssen im ganzen Code gefunden und benutzt werden können. Es musste also etwas her, mit dem sich alles verwalten lässt.

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

Das ist eigentlich das Herz des Spiels, oder eines der vielen wichtigen Teile. Wenn hier etwas schiefgeht, hast du ein Problem…

Wenn eine Entity erstellt wird, wird sie dem Entity Manager hinzugefügt und gegebenenfalls auch wieder entfernt. In der Kameraklasse oben konntest du schon sehen, dass das mit der folgenden Zeile passiert.

entityManager:add(self)

In jeder Klasse, die ich schreibe, steht diese Zeile, damit sie sich selbst über den entityManager hinzufügt.

In der Klasse kann man auch eine release-Funktion anlegen, die automatisch ausgeführt wird, wenn der entityManager das Objekt entfernt. Das ist sehr wichtig, um den Speicher im Griff zu behalten. Man will Assets wie Bilder nicht alle aufbewahren, sondern irgendwann aus dem Speicher holen.

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

Im Einsatz

Wenn ich irgendwo eine neue Klasse habe und dort eine Entity brauche, zum Beispiel die Kamera, geht das auf verschiedene Arten.

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

Das gibt allerdings eine Tabelle mit allen gefundenen Entities vom Typ Camera (Klasse) zurück. Das ist unpraktisch, da es in diesem Fall immer genau 1 Kamera gibt, und ich will 100 % sicher sein, dass die Camera auch vorhanden ist.

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

Das ist ein Wrapper um find und wirft einen Fehler, wenn das Objekt nicht vorhanden ist.

Ein paar weitere Beispiele sind

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

Zum Schluss

Alle Punkte, die ich bei Klassen und Objekten gesucht habe, sind jetzt abgedeckt

  • Klassen
  • Klassen sind erweiterbar
  • Klassen sind überschreibbar
  • Entities werden von einem Manager kontrolliert
  • Entities kann ich überall verwenden

Es ist sehr wichtig, diese Entscheidung so früh wie möglich zu treffen und davon auszugehen, dass sich der endgültige Code noch stark verändern wird. Wenn man alles strukturiert aufsetzt, hat man später viel davon.

Falls Lua die Menge an Entities nicht mehr bewältigt, kann man diesen Teil nach C umsetzen!