Semuanya adalah objek

Semuanya hidup dalam bentuk objek.

Banyak sekali objek

Objek

Saya suka bekerja dengan objek, kamu bisa menyimpan semua informasi yang nanti dibutuhkan di dalamnya. Lua tidak punya sistem class seperti yang saya kenal dari bahasa lain. Yang saya butuhkan adalah sesuatu yang memungkinkan class ditimpa atau diperluas di kemudian hari. Maksud saya dari sisi game yang memakai engine3.

Ide dasarnya adalah engine3 bisa dirilis supaya semua orang bisa memakainya. Game-nya sendiri bisa tetap privat. Keduanya berdiri berdampingan dan saling melengkapi. Setelah beberapa uji coba, ini berhasil, dan sekarang objek bisa dibuat berdasarkan class.

Contoh class, engine3/core/camera.lua (versi terbatas).

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

Misalkan kamera di dalam game harus berperilaku sedikit berbeda dari kamera bawaan engine3. Maka saya membuat file game/framework/core/camera.lua dan merujuk ke class camera milik engine3. Lalu saya membuat perubahan saya.

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

Semua objek yang berbeda-beda itu memang menyenangkan (mulai sekarang saya menyebut objek sebagai entity), tapi semuanya harus bisa ditemukan dan dipakai di seluruh kode. Jadi harus dibuat sesuatu untuk mengelola semuanya.

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

Ini sebenarnya jantung dari game, atau salah satu dari banyak bagian penting. Kalau ada yang salah di sini, kamu punya masalah…

Saat sebuah entity dibuat, ia akan ditambahkan ke Entity Manager dan bila perlu dihapus lagi. Di class camera di atas kamu sudah bisa melihat bahwa ini dilakukan dengan baris di bawah.

entityManager:add(self)

Setiap class yang saya tulis punya baris itu, sehingga class itu menambahkan dirinya sendiri lewat entityManager.

Di dalam class kamu juga bisa membuat fungsi release, yang otomatis dijalankan saat entityManager menghapus objeknya. Ini sangat penting untuk menjaga memori tetap terkendali. Kamu tidak ingin menyimpan semua aset seperti gambar, melainkan pada suatu saat mengeluarkannya dari memori.

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

Penggunaan

Kalau saya punya class baru di suatu tempat dan butuh sebuah entity, misalnya kamera, ada beberapa cara untuk melakukannya.

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

Tapi ini akan mengembalikan tabel berisi semua Entity yang ditemukan dengan tipe Camera (class). Itu tidak praktis, karena dalam hal ini selalu hanya ada 1 kamera, dan saya ingin 100% yakin bahwa Camera memang ada.

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

Ini adalah wrapper di sekitar find, dan memberi error kalau objeknya tidak ada.

Beberapa contoh lain

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

Penutup

Semua poin yang saya cari terkait class dan objek sekarang sudah terpenuhi

  • class
  • class bisa diperluas
  • class bisa ditimpa
  • entity dikendalikan oleh sebuah manager
  • entity bisa saya pakai di mana saja

Sangat penting untuk membuat pilihan ini secepat mungkin dan memperhitungkan bahwa kode akhirnya masih akan banyak berubah. Kalau kamu menyusun semuanya secara terstruktur, nanti kamu akan sangat terbantu.

Kalau Lua tidak sanggup lagi memproses jumlah entity, bagian ini bisa diubah ke C!