游戏开发:基础模块之任务系统设计方案
- 写一写游戏项目的基础模块的实现思路,之任务系统:
起引导、活跃、成就等作用的任务系统,是游戏常见的业务需求;
实现上可以分为几个部分:
- 任务类设计;
- 任务对象管理;
- 事件管理;
- hook机制;
一. 事件管理模块
一个以事件类型(event type)为单位,进行注册和回调触发的管理模块。
模块需要实现:
1)支持独立上下文;
2)事件触发支持控制优先级;
3)回调handler需要支持逻辑热更;
4)保护模式下执行;
实现思路:
- 支持独立上下文。模块逻辑实现为原型,提供默认上下文对象。通过参数控制创建独立上下文或者使用默认上下文。
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
}
- 事件处理优先级。单个触发帧的格式为:
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
- 支持逻辑热更。思路是保持逻辑实现和数据的分离,不引入运行时状态。这里将事件帧的回调处理函数记录为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列表进行触发。
本文来自博客园,作者:linxx-,转载请注明原文链接:https://www.cnblogs.com/linxx-/p/18231841