玩家定时器模块的设计与实现
Contents
前言
游戏开发中,玩家一些定时刷新的物品、任务,若每个对象都增加一个setTimeout函数,数量就很恐怖了,尤其是玩家数量一多就更废了。本文针对这一现象,设计了一种线性定时器,它同时只有一个定时事件处于激活状态,当前定时事件处理完毕会启动下一个定时事件。
案例
玩家角色可以穿多件时装,时装又有时间限制,时装时间到期后,需要服务端把到期的时装卸下。
分析
方案一
为每个装扮增加一个定时器,到期时卸下装扮。 这就需要面对玩家有N个装扮,就需要同时启动N个定时器的问题。
方案二
不做定时器,每次每次客户端请求一下,所有时装遍历一遍进行到期卸载的逻辑处理。 由于无脑遍历,付出的计算量和实现的功能相比,有点得不偿失,尤其是涉及到更多的定时时。
方案三
做一个数组,数组内每个成员记录装扮ID和过期时间,并对时间从小到大排序。 装扮模块增加一个定时器,时间是数组第一个元素时间,时间到期卸载对应装扮,并pop指定此成员。
[
{
SkinId:30001,
endTime:1000000,
},
{
SkinId:30002,
endTime:2000000,
},
]
此方案单对装扮是比较完美,但仍不能避免玩家多种不同类型的定时事件,会同时启动多个定时器的问题。
方案四
其实就是方案三的改良版,把设计一个定时器模块,此模块内部维护一个数组,数据内成员具有基本属性{type:事件类型,endTime:响应时间,id:目标ID…},数组内部依然对每个成员按响应事件按从下到大排序。
此方案实现了玩家身上,同时只有一个定时器模块响应的需求,同时又能高效的处理按时间监听的事件。
设计实现
//监听类型
const LISTENER_TYPE = {
APPEARANCE: 1,
};
/**
* @description 生成监听
* @param {LISTENER_TYPE} type
* @param {Number} end_time
* @param {*} id //索引可以是对象
*/
function NewListener(type, end_time, id) {
return {
type: type,
end_time: end_time,
id: id,
};
}
/**
* @description 增加监听
* @param {Object} userData 玩家
* @param {NewListener} listener
*/
function AddListener(userData, listener) {
MF.Log.logDebug('AddListener: ', JSON.stringify(listener));
if(!userData.Timer) {
userData.Timer = [];
}
if(listener.end_time <= 0 || listener.type === undefined) {
MF.Log.logError('AddListener: ', JSON.stringify(listener));
return;
}
userData.Timer.push(listener);
for(let i = userData.Timer.length - 1; i >= 1; i--) {
if(userData.Timer[i-1].end_time <= userData.Timer[i].end_time) {
break;
}
let tmp = userData.Timer[i];
userData.Timer[i] = userData.Timer[i-1];
userData.Timer[i-1] = tmp;
}
}
/**
* @description 添加并踢出雷同的,更新序列
* !!添加并向前排除相同的
* @param {Object} userData 玩家
* @param {NewListener} listener
*/
function UpdateListener(userData, listener) {
if(!userData.Timer) {
userData.Timer = [];
}
if(listener.end_time <= 0 || listener.type === undefined) {
MF.Log.logError('AddListener: ', JSON.stringify(listener));
return;
}
userData.Timer.push(listener);
for(let i = userData.Timer.length - 1; i >= 1; i--) {
if(userData.Timer[i-1].type === listener.type &&
userData.Timer[i-1].end_time !== listener.end_time &&
JSON.stringify(userData.Timer[i-1].id) === JSON.stringify(listener.id)) {
userData.Timer.splice(i-1, 1);
i--;
continue;
}
if(userData.Timer[i-1].end_time <= userData.Timer[i].end_time) {
continue;
}
let tmp = userData.Timer[i];
userData.Timer[i] = userData.Timer[i-1];
userData.Timer[i-1] = tmp;
}
}
/**
* @description 运行
* @param {Object} userData 玩家
*/
function AutoRun(userData) {
let curTime = MF.Util.CurTimeStamp();
for(let i = 0; i < userData.Timer.length; i++) {
let listener = userData.Timer[i];
if(curTime < listener.end_time) {
break;
}
EndTimerEvent(userData, listener);
userData.Timer.splice(i, 1);
i--;
}
}
/**
* @description 触发监听事件
* @param {Object} userData 玩家
* @param {NewListener} listener
*/
function EndTimerEvent(userData, listener) {
let type = listener.type;
switch(type) {
case LISTENER_TYPE.APPEARANCE: {
WW.Appearance.OnEndTimerEvent(userData, listener);
break;
}
default: {
MF.Log.logError('Listener type not defined: ', JSON.stringify(arguments));
}
}
}
总结
上述是一个方案四的变种,AutoRun函数没有用定时器,因为项目中玩家数据是在Redis中存放的,所以在每次Redis取玩家数据后调用AutoRun函数。如果玩家数据在内存中可以自行改写AutoRun函数。
Author wangkm
LastMod 2018/07/26