游戏开发:基础模块之任务系统设计方案

  • 写一写游戏项目的基础模块的实现思路,之任务系统:

起引导、活跃、成就等作用的任务系统,是游戏常见的业务需求;

实现上可以分为几个部分:

  1. 任务类设计;
  2. 任务对象管理;
  3. 事件管理;
  4. hook机制;

一. 事件管理模块

一个以事件类型(event type)为单位,进行注册和回调触发的管理模块。

模块需要实现:
1)支持独立上下文;
2)事件触发支持控制优先级;
3)回调handler需要支持逻辑热更;
4)保护模式下执行;

实现思路:

  1. 支持独立上下文。模块逻辑实现为原型,提供默认上下文对象。通过参数控制创建独立上下文或者使用默认上下文。
local EventMgr = { singleton = true }
--[[ ... --]]
local default_instance = setmetatable({ _evpool = {} }, {__index = EventMgr})

local EventMgrFactory = {
    new = function(singleton)
        singleton = singleton or EventMgr.singleton
        if singleton then
            return default_instance
        end
        return setmetatable({ _evpool = {} }, {__index = EventMgr})
    end
}
  1. 事件处理优先级。单个触发帧的格式为:
local event = { module_ctx, cmd, priority, extra }
-- 这里通过第三个参数priority控制当前帧的触发优先级;

模块上下文维护一个_evpool:

_evpool = {
    event_type = {
        {module_ctx, cmd, priority, extra},
        -- ...
    },
    -- ...
}

这是event_type对应需要处理的事件帧有序列表,列表根据priority进行倒序排列。注册事件帧时insert到对应priority的位置,事件触发时顺序处理所有事件帧。

-- 注册事件帧
function EventMgr:register(et, ctx, cmd, prior, extra)
    assert(type(ctx) == "table" and type(cmd) == "string" and type(ctx.cmd) == "function")
    prior = prior or 0
    assert(type(prior) == "number")
    local ev = {ctx, cmd, prior, extra}
    if not self._evpool[et] then
        self._evpool[et] = {}
    end
    local evs = self._evpool[et]
    local pos = 0
    for i, elem in ipairs(evs) do
        if elem[3] < prior then
            pos = i
            break
        end
    end
    if pos > 0 then table.insert(evs, pos, ev) else table.insert(evs, ev) end
end

-- 触发事件类型
function EventMgr:trigger(et, ...)
    local evs = self._evpool[et]
    if not evs or #evs == 0 then return end
    for _, elem in ipairs(evs) do
        local ctx, cmd, extra = elem[1], elem[2], elem[4]
        if extra then
            xpcall(ctx[cmd], traceback, ctx, extra, ...)
        else
            xpcall(ctx[cmd], traceback, ctx, ...)
        end
    end
end
  1. 支持逻辑热更。思路是保持逻辑实现和数据的分离,不引入运行时状态。这里将事件帧的回调处理函数记录为module_ctx + cmd,而不直接引用住函数,这样事件触发时通过 **module_ctx[cmd] **的方式调用,使对外部模块的热更对事件触发仍然生效,代价是字符串内存消耗可观。

二. 任务对象管理

一个任务的属性应该包括:

mission = {
    id,		-- 任务唯一id
    name,	-- 任务名称
    progress,	-- 任务进度
    reward,		-- 领奖标记
    -- ...
}

维护任务属性的逻辑以元表的方式引入,实现逻辑数据的分离:

local tplt = {
    get_id = function(self) return self.id end,
    get_da = function(self) return self.da end,
    get_kv = function(self, k) return self.da[k] end,
    set_kv = function(self, k, v) self.da[k] = v end,
    get_name = function(self) return self.name end,
    -- ...
}

mission_obj = setmetable({
	id,		-- 任务唯一id
    name,	-- 任务名称
    progress,	-- 任务进度
    reward,		-- 领奖标记
    -- ...
}, {__index = tplt})

那么可以通过tplt的继承多态来实现不同的任务可能关联的overload逻辑;

-- 模板库,可支持持续扩展
-- 实际上我们将每个模板实现在单独的文件中,创建任务时通过指定文件路径获取模板设置元表
local tplt_base = {
    get_id = function(self) return self.id end,
    -- ...
}

local tplt_1 = setmetable({
	get_id = function()
    	-- overload
    end
}, {__index = tplt_base})

-- local tplt_2

-- ...

任务逻辑需要实现self的创建和销毁动作,那么提供一个外部管理模块:mission_obj_mgr

local mission_obj_mgr = {}

local TEMPLATE_PATH = "./templates/"

function mission_obj_mgr.create_obj(id, tplt_name)
    assert(tplt, "id not tplt")
    local tplt = require(TEMPLATE_PATH .. tplt_name)
    return setmetatable({id = id, da = {}}, {__index = tplt})
end

function mission_obj_mgr.destroy_obj(obj)
    obj:delete()
end

三.任务对象的事件注册触发hook支持

作为基础功能系统,任务系统的特性:
1.受外部功能系统驱动,任务对象需要注册监听对应关注的事件;
2.任务对象数量多,往往数以百计;

多个对象关注同一事件时,触发流程需要遍历处理所有对象,可能出现繁忙问题,在任务系统中实现hook层,管理任务对象的事件注册触发分发逻辑;

Hook层维护一个callbackCMD —> mission_obj的映射,约束任务对象中针对同一事件的处理回调需要统一的函数名称,Hook层实现统一的hookHandler,注册和接收事件管理器的回调;

function Hook:getCmd2Obj()
    if not self._cmd2obj then
        self._cmd2obj = {}
    end
    return self._cmd2obj
end

function Hook:addListener(et, ctx, prior, cmd)
    tryRenewObj = function(cbCtx, cmd, ...)
        local objectIds = self:getCmd2Obj()[cmd]
        if not objectIds or #objectIds == 0 then
            return
        end
    end
    EventManager.register(et, ctx, tryRenewObj, prior, cmd)
end

事件回调统一通过tryRenewObj向维护中的mission_obj列表进行触发。

posted @ 2024-06-04 21:57  linxx-  阅读(39)  评论(0编辑  收藏  举报