前言

游戏开发中,玩家一些定时刷新的物品、任务,若每个对象都增加一个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函数。