相对精确的计时器(精确到0.025秒)

游戏中经常用到的
因为帧频会影响时间,所以定时器走着走着就不准确了。
根据论坛一个框架结构,简单改了个定时器管理类。
支持切后台,超时回调
支持暂停,恢复
支持N个定时器统一管理
(3.X版本)

TimerManager.ts

/**

 * 更加精确的计时器

 */

import { Component } from "cc";

const guid = function () {

    let guid: string = "";

    for (let i = 1; i <= 32; i++) {

        let n = Math.floor(Math.random() * 16.0).toString(16);

        guid += n;

        if ((i == 8) || (i == 12) || (i == 16) || (i == 20))

            guid += "-";

    }

    return guid;

}

export class TimerManager {

    private static times: any = {};

    private schedules: any = {};

    private _scheduleCount: number = 1;

    private initTime: number = (new Date()).getTime();      // 当前游戏进入的时间毫秒值

    private component: Component;

    // 服务器时间与本地时间间隔

    private _$serverTimeElasped: number = 0;

    constructor(component: Component) {

        // super();

        this.component = component;

        this.schedule(this.onUpdate, 1/60);

    }

    /**

     * 设置服务器时间与本地时间间隔

     * @param val

     */

    public serverTimeElasped(val?: number): number {

        if (val) {

            this._$serverTimeElasped = val;

        }

        return this._$serverTimeElasped;

    }

    /**

     * 格式化日期显示 format= "yyyy-MM-dd hh:mm:ss";

     * @param format

     * @param date

     */

    public format(format: string, date: Date): string {

        let o: any = {

            "M+": date.getMonth() + 1,                      // month

            "d+": date.getDate(),                           // day

            "h+": date.getHours(),                          // hour

            "m+": date.getMinutes(),                        // minute

            "s+": date.getSeconds(),                        // second

            "q+": Math.floor((date.getMonth() + 3) / 3),    // quarter

            "S": date.getMilliseconds()                     // millisecond

        }

        if (/(y+)/.test(format)) {

            format = format.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));

        }

        for (let k in o) {

            if (new RegExp("(" + k + ")").test(format)) {

                format = format.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ("00" + o[k]).substr(("" + o[k]).length));

            }

        }

        return format;

    }

    /** 获取游戏开始到现在逝去的时间 */

    public getTime(): number {

        return this.getLocalTime() - this.initTime;

    }

    /** 获取本地时间刻度 */

    public getLocalTime(): number {

        return Date.now();

    }

    public schedule(callback: Function, interval: number): string {

        let UUID = `schedule_${this._scheduleCount++}`

        this.schedules[UUID] = callback;

        this.component.schedule(callback, interval);

        return UUID;

    }

    public scheduleOnce(callback: Function, delay: number = 0): string {

        let UUID = `scheduleOnce_${this._scheduleCount++}`;

        this.schedules[UUID] = callback;

        this.component.scheduleOnce(() => {

            let cb = this.schedules[UUID];

            if (cb) {

                cb();

            }

            this.unschedule(UUID);

        }, Math.max(delay, 0));

        return UUID;

    }

    public unschedule(uuid: string) {

        let cb = this.schedules[uuid];

        if (cb) {

            this.component.unschedule(cb);

            delete this.schedules[uuid];

        }

    }

    public unscheduleAllCallbacks() {

        for (let k in this.schedules) {

            this.component.unschedule(this.schedules[k]);

        }

        this.schedules = {};

    }

    onUpdate(dt: number) {

        // 后台管理倒计时完成事件

        for (let key in TimerManager.times) {

            let data = TimerManager.times[key];

            data.update(dt);

        }

    }

    /** 游戏最小划时记录时间数据 */

    public save() {

        for (let key in TimerManager.times) {

            TimerManager.times[key].recordTime = this.getTime();

        }

    }

    /** 游戏最大化时回复时间数据 */

    public load() {

        for (let key in TimerManager.times) {

            let data:Timer = TimerManager.times[key];

            if(data.isSaveTime){

                // 经过了多少时间,单位为秒

                let interval = ((this.getTime() - (data.recordTime || this.getTime())) / 1000);

                let overTimes = interval/data.step;

                data.curTimes = data.curTimes + overTimes;

                if (data.curTimes > data.totalTime) {

                    if (data.onDelayCompleteCallback) {

                        data.onDelayCompleteCallback.call(data.object);                     // 触发超时回调事件  

                    }

                }

            }

           

            data.recordTime = 0;

        }

    }

    public getTimer(id:string):Timer{

        if (TimerManager.times[id])

            return TimerManager.times[id];

        return null;

    }

    /**

     * 注册指定对象的倒计时属性更新

     * @param object 回调对象

     * @param step 时间间隔

     * @param totalTime 执行次数(n+1次)

     * @param onStepCallback 执行回调

     * @param onDelayCompleteCallback 超时回调

     * @param isSaveTime 是否保存切后台时间

     * @returns

     */

    public addTimer(object: any, step:number,totalTime: number = -1, onStepCallback: Function, onDelayCompleteCallback?:Function,isSaveTime:boolean = false) {

        let data: Timer = new Timer(step,totalTime,onStepCallback,object,isSaveTime);

        data.id = guid();

        data.onDelayCompleteCallback = onDelayCompleteCallback; // 超时完成事件

        TimerManager.times[data.id] = data;

        return data.id;

    }

    /** 注消指定对象的倒计时属性更新 */

    public clearTimer(id: string) {

        if (TimerManager.times[id])

            delete TimerManager.times[id];

    }

}

/** 定时跳动组件 */

export class Timer {

    public id:string

    public onStepCallback: Function | null = null;

    public onDelayCompleteCallback: Function | null = null;

    public object: any | null = null;

    public totalTime:number = -1;//总次数,-1为无限

    public recordTime:number = 0;//某个时间点保存的时间

    public curTimes:number = 0;//当期触发次数

    public isSaveTime:boolean = false;//是否记录切后台时间

    private _elapsedTime: number = 0;

    private _isStop:boolean = false;

   

    public get elapsedTime(): number {

        return this._elapsedTime;

    }

    private _step: number = 0;

    /** 触发间隔时间(秒) */

    get step(): number {

        return this._step;

    }

    set step(step: number) {

        this._step = step;                     // 每次修改时间

        this._elapsedTime = 0;                 // 逝去时间

    }

    public get progress(): number {

        return this._elapsedTime / this._step;

    }

    /**

     *

     * @param step 时间间隔

     * @param totalTimes 总次数,-1为无限

     * @param callback 回调

     * @param object 回调者

     */

    constructor(step: number = 0,totalTimes:number = -1,callback?:Function,object?:any,isSaveTime?:boolean) {

        this.step = step;

        this.onStepCallback = callback;

        this.object = object;

        this.totalTime = totalTimes;

        this.curTimes = 0;

        this.isSaveTime = isSaveTime;

        this.onStepCallback?.call(this.object,this.curTimes,(this.totalTime - this.curTimes));

    }

    public update(dt: number) {

        if (this._isStop){

            return;

        }

        this._elapsedTime += dt;

        if (this._elapsedTime >= this._step) {

            this._elapsedTime -= this._step;

            if(this.totalTime == -1){

                this.onStepCallback?.call(this.object);

                return true;

            }else{

               

                if (this.curTimes < this.totalTime){

                    this.curTimes++;

                    this.onStepCallback?.call(this.object,this.curTimes,(this.totalTime - this.curTimes));

                    return true;

                }

            }

            return false;

           

        }

        return false;

    }

    // 重置定时器

    public reset(step?:number,totalTimes?:number) {

        this._elapsedTime = 0;

        this.curTimes = 0;

        if (step){

            this.step = step;

        }

        if (totalTimes){

            this.totalTime = totalTimes;

        }

    }

    public isStop(){

        return this._isStop;

    }

    // 停止

    public stop(){

        this._isStop = true;

        this.reset();

    }

    // 开始

    public start(){

        this._isStop = false;

        this.reset();

    }

    // 暂停

    public pause(){

        this._isStop = true;

    }

    // 恢复

    public resume(){

        this._isStop = false;

    }

}

实例代码:

 /**
 * 实例代码

 * /

// import { _decorator, Component, game, js, Game } from 'cc';

// import { Timer, TimerManager } from './TimerManager';

// const { ccclass, property } = _decorator;

 

// @ccclass('Main')

// export class Main extends Component {

//     private _timerId = null;

//     private timerManager:TimerManager;

//     start () {

//         let date = new Date().getTime();

//         console.log(this.getDateStr(date));

//         this.timerManager = new TimerManager(this);

//         // this._timerId = this.timerManager.addTimer(this,0.025,1000,this.onTimerHandler);

//         this._timerId = this.timerManager.addTimer(this,1,10,this.onTimerHandler,this.onDelayTimerHandler,true);

//         // 游戏显示(进入前台)

//         game.on(Game.EVENT_SHOW, () => {

//             this.timerManager.load();

//         });

   

//         // 游戏隐藏事件(进入后台)

//         game.on(Game.EVENT_HIDE, () => {

//             this.timerManager.save();

//         });

//     }

//     click(){

//         let timer = this.timerManager.getTimer(this._timerId);

//         if (timer.isStop()){

//             // 恢复

//             timer.resume();

//         }else{

//             // 暂停

//             timer.pause();

//         }

//     }

//     /**

//      *

//      * @param curTimes 已经过时间

//      * @param remainTimes 剩下的时间

//      */

//      onTimerHandler(curTimes,remainTimes){

//         console.log("==onTimerHandler:",curTimes,remainTimes);

//         if(remainTimes == 0){

//             let date = new Date().getTime();

//             console.log(this.getDateStr(date));

//         }

//     }

//     /**超时执行 */

//     onDelayTimerHandler(){

//         console.log("==onDelayTimerHandler");

//     }

//     getDateStr(time:any) {

//         var date = new Date(time);

//         var y = date.getFullYear();

//         var m = date.getMonth() + 1;

//         var d = date.getDate();

//         var h = date.getHours();

//         var mm = date.getMinutes();

//         var s = date.getSeconds();

//         return js.formatStr("%s-%s-%s %s:%s:%s", y, m, d, h, mm, s);

//     }

// }
6赞

:100:

没看明白大佬的代码,能简单介绍下实现原理吗?

根据帧频的间隔时间去修正定时器的触发回调。
官方的定时器一般30后就会出现偏差,
用这个不会有这种问题。
但对于精准度,是根据你程序逻辑的复杂度来的

:joy:感觉2.x这方面好像没太大毛病啊。。。3.x这么难受的么?

  1. 假设有一个定时器设定在 0.5s 后执行,但是下一帧需要运行 1s (不是16ms),那么你这个定时器的是在什么时候执行呢?

  2. 假设有一个定时器设定在 0.5s 后执行,但是下一帧需要 0.4s,下下一帧需要 0.4s,那么你这个定时器期望是在什么时候执行呢?

所以我说相对精确。
当你的代码卡顿。
第一个问题,则不准确了。
第二个问题,则可以正常运行。

另外,代码中可能存在的问题为「内存泄漏」

TimeManager 的构造函数中,持有了一个外部的 Component ,但是TimeManager中并没有任何释放 Component 的做法或者方法

一个很简单的泄漏例子为:节点已销毁,Component 销毁,但是TimerManager还持有component,并且可能在后续过程中再次调用 TimeManager 去调用 component

no,no,no
TimerMananger绑定的component是常驻节点。
只初始化一次。

既然如此,我觉得可以直接封装好这个构造函数,不要外部传component,或者直接构造时,构造函数自己内部创建常驻节点

要是有具体一点的 例子

  1. 引擎的schedule 什么时候不精确?原因是什么?
  2. 你的这个 schedule 为什么能解决这个问题,怎么解决

可能会更加好

schedule和引擎一样的。。放这里至少方便管理。

我主要用的是addTimer。这里的精确也是说timer

好了。撸代码了