Alles ist ein Objekt
Alles wird in Form eines Objekts lebendig.

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!