Mọi thứ đều là đối tượng

Mọi thứ trở nên sống động dưới dạng đối tượng.

Rất nhiều đối tượng

Đối tượng

Tôi thích làm việc với đối tượng, bạn có thể đặt vào đó mọi thông tin mình sẽ cần sau này. Lua không có hệ thống lớp như tôi biết ở các ngôn ngữ khác. Tôi cần một thứ cho phép sau này ghi đè hoặc mở rộng các lớp. Ý tôi là nhìn từ phía trò chơi sử dụng engine3.

Ý tưởng đằng sau là engine3 có thể được phát hành để ai cũng dùng được. Còn trò chơi thì có thể giữ riêng tư. Hai thứ tồn tại song song và bổ sung cho nhau. Sau vài lần thử nghiệm, tôi đã làm được, và giờ có thể tạo đối tượng từ các lớp.

Một ví dụ về lớp, engine3/core/camera.lua (phiên bản rút gọn).

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

Giả sử camera trong trò chơi cần hoạt động hơi khác camera mặc định của engine3. Khi đó tôi tạo tệp game/framework/core/camera.lua và tham chiếu tới lớp camera của engine3. Sau đó tôi thực hiện những thay đổi của mình.

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

Có nhiều đối tượng khác nhau thì hay đấy (từ giờ tôi gọi đối tượng là entity), nhưng chúng phải được tìm thấy và sử dụng trong toàn bộ mã. Vì vậy cần có thứ gì đó để quản lý tất cả.

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

Đây thực ra là trái tim của trò chơi, hoặc một trong nhiều phần quan trọng. Nếu có gì sai ở đây, bạn sẽ gặp rắc rối…

Khi một entity được tạo, nó sẽ được thêm vào Entity Manager và có thể bị xóa sau đó. Trong lớp camera ở trên, bạn đã thấy việc này được làm bằng dòng bên dưới.

entityManager:add(self)

Mọi lớp tôi viết đều có dòng đó, để nó tự thêm mình qua entityManager.

Trong lớp, bạn cũng có thể tạo hàm release, được chạy tự động khi entityManager xóa đối tượng. Điều này rất quan trọng để kiểm soát bộ nhớ. Bạn không muốn giữ lại mọi tài nguyên như hình ảnh, mà đến lúc nào đó phải giải phóng chúng khỏi bộ nhớ.

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

Cách dùng

Khi tôi có một lớp mới ở đâu đó và cần một entity, ví dụ camera, có thể làm theo nhiều cách.

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

Tuy nhiên cách này trả về một bảng chứa mọi Entity tìm thấy thuộc loại Camera (lớp). Như vậy không tiện, vì trong trường hợp này luôn chỉ có 1 camera, và tôi muốn chắc chắn 100% là Camera có tồn tại.

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

Đây là lớp bao quanh find, và sẽ báo lỗi khi đối tượng không tồn tại.

Một vài ví dụ khác là

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

Lời kết

Mọi điểm tôi tìm kiếm về lớp và đối tượng giờ đã được đáp ứng

  • lớp
  • lớp có thể mở rộng
  • lớp có thể ghi đè
  • entity được một manager kiểm soát
  • entity có thể dùng ở mọi nơi

Rất quan trọng là phải đưa ra lựa chọn này càng sớm càng tốt và chấp nhận rằng mã cuối cùng sẽ còn thay đổi nhiều. Nếu bạn thiết lập mọi thứ có cấu trúc, sau này bạn sẽ được lợi rất nhiều.

Nếu Lua không còn xử lý nổi số lượng entity, có thể chuyển phần này sang C!