すべてはオブジェクト

すべてがオブジェクトの形で命を吹き込まれる。

とてもたくさんのオブジェクト

オブジェクト

私はオブジェクトを使うのが好きです。あとで必要になる情報を全部入れておけるからです。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 の camera クラスを参照します。そのうえで自分の変更を加えます。

---@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 に追加され、必要に応じてまた削除されます。上の camera クラスで、それが次の行で行われているのをすでに見たと思います。

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

ただしこれは、見つかった Camera 型(クラス)のすべての Entity をテーブルで返します。この場合カメラは常に1つなので不便ですし、Camera が確実に存在することを100%確かめたいのです。

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

最後に

クラスとオブジェクトについて求めていた点は、これですべて満たされました。

  • クラス
  • クラスは拡張できる
  • クラスは上書きできる
  • entity はマネージャーで管理される
  • entity はどこでも使える

この選択はできるだけ早くして、最終的なコードはこれからも大きく変わると考えておくことがとても大切です。すべてを構造的に整えておけば、あとで大いに役立ちます。

もし Lua が entity の数をさばききれなくなったら、この部分を C に置き換えることもできます!