Всё есть объект
Всё оживает в виде объекта.

Объект
Я люблю работать с объектами, в них можно положить всю информацию, которая понадобится позже. В Lua нет системы классов, к которой я привык в других языках. Мне нужно было что-то, чтобы позже можно было переопределять или расширять классы. И речь именно о стороне игры, которая использует engine3.
Идея в том, чтобы engine3 можно было выпустить и им мог пользоваться кто угодно. Саму игру при этом можно оставить закрытой. Они существуют рядом и дополняют друг друга. После нескольких тестов это получилось, и теперь объекты можно создавать на основе классов.
Пример класса engine3/core/camera.lua (сокращённая версия).
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
Допустим, камера в игре должна вести себя немного иначе, чем стандартная камера engine3. Тогда я создаю файл game/framework/core/camera.lua и ссылаюсь на класс камеры из engine3. Затем вношу свои изменения.
---@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
Все эти разные объекты это хорошо (дальше я буду называть объект entity), но их нужно находить и использовать по всему коду. Значит, нужно было сделать что-то, чтобы всем этим управлять.
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
По сути это сердце игры, или одна из многих важных частей. Если здесь что-то пойдёт не так, у вас проблема…
Когда entity создаётся, она добавляется в Entity Manager и при необходимости удаляется. В классе камеры выше вы уже видели, что это делается строкой ниже.
entityManager:add(self)
Эта строка есть в каждом классе, который я пишу, чтобы он сам добавлялся через entityManager.
В классе можно также создать функцию release, которая автоматически выполняется, когда entityManager удаляет объект. Это очень важно для контроля памяти. Не хочется хранить все ассеты вроде изображений, а в какой-то момент нужно выгружать их из памяти.
function Camera:release()
-- do things when the entityManger delete this object
end
Использование
Когда у меня где-то есть новый класс и мне там нужна entity, например камера, это можно сделать разными способами.
local cameras = entityManager:find(nil,"Camera")
Но это вернёт таблицу со всеми найденными Entities типа Camera (класс). Это неудобно, ведь в этом случае камера всегда одна, а я хочу быть на 100 % уверен, что Camera существует.
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
Это обёртка вокруг find, которая выдаёт ошибку, если объекта нет.
Ещё несколько примеров
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})
Напоследок
Всё, что я хотел от классов и объектов, теперь есть
- классы
- классы можно расширять
- классы можно переопределять
- entities под контролем менеджера
- entities можно использовать где угодно
Очень важно сделать этот выбор как можно раньше и исходить из того, что итоговый код ещё сильно изменится. Если всё организовать структурированно, потом это очень пригодится.
Если Lua перестанет справляться с количеством entities, эту часть можно переписать на C!