Semuanya adalah objek
Semuanya hidup dalam bentuk 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!