Logo Signal From The Stars

Everything is an object

Everything comes to life in the form of an object.

Martin avatar
  • Martin
  • 5 min read
many many objects

Object

I like working with objects, you can put all the information you need later in it. Lua does not have a class system like I know from other languages. What I needed was something with which classes could be overwritten or extended at a later point. And then I mean from the perspective of the game that uses engine3.

The underlying idea is that engine3 can be released so that everyone can use it. The game itself can then be kept private. The two then exist side by side and complement each other. After some testing this worked and objects can now be created using classes.

An example class engine3/core/camera.lua (limited 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

Suppose the camera in the game needs to behave slightly differently than the default camera of engine3, then I create the file game/framework/core/camera.lua and point to the engine3 camera class. Then I make my adjustments.

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

All those different objects are nice (I’ll call an object entity from now on), but they have to be found/used throughout the code. So something had to be made to manage everything.

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

This is actually the heart of the game or one of the many important ones, if something goes wrong here you have a problem…

When an entity is created it will be added to the Entity Manger and possibly also removed. In the camera class above you could already see that this goes with the rule below.

entityManager:add(self)

In every class I write that line is there so it will add itself via the entityManger.

In the class you can also create a release function, this will be executed automatically when the entityManger removes the object. This is very important to keep memory under control. You don’t want to keep all assets like images but at some point remove them from memory.

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

In use

When I have a new class somewhere and I need an entity there, for example the camera, then that can be done in different ways.

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

However, this will return a table with all found Entities of type Camera (class), this is not useful as in this case it is always 1 camera, and I want to be 100% sure that the Camera is also present.

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

This is a wrapper around find , and returns an error if the object is not present.

Some other examples are

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

Finally

All my points that I was looking for regarding classes and objects are now covered

  • classes
  • classes are extensible
  • classes are overwritable
  • entities are under the control of a manager
  • entities I can use anywhere

It is very important to make that choice as soon as possible and assume that many changes will still take place in the final code. If you structure everything, you will benefit a lot from it later.

Suppose Lua can no longer handle the processing of the number of entities, then it is possible to convert this part to C!


Loading external comment system...

βœ‰οΈ Stay informed!

Receive the latest news for free and motivate me to make this adventure a success!