2.x时间管理

挂机养成类游戏中战斗加速的需求非常普遍
需要加速的不光有帧动画 骨骼动画 还有音频 缓动 定时器等等

但是并没有一个接口来统一控制上述系统的时间间隔

下面直接上代码
*只做了简单测试 另外写的时候不是很清醒 如有错误请轻喷

import Manager from "./Manager";

/**时间管理类 @description 用于控制时间流速 */
class TimeMgr extends Manager {

    private _speed: number = 1;
    /**全局时间流速 */
    public get speed(): number {
        return this._speed;
    }
    public set speed(newValue: number) {
        this._speed = newValue;
        //同步全局时间流速
        this.speed_audio = this._speed;
        this.speed_skeleton = this._speed;
        this.speed_action = this._speed;
        this.speed_animation = this._speed;
        this.speed_collision = this._speed;
        this.speed_physics = this._speed;
        this.speed_timer = this._speed;
    }

    private _speed_audio: number = 1;
    /**音频时间流速 */
    public get speed_audio(): number {
        return this._speed_audio;
    }
    public set speed_audio(newValue: number) {
        this._speed_audio = newValue;
        hh.audioMgr.musicSource.audio._element._currentSource.playbackRate.value = this._speed_audio;
        hh.audioMgr.effectSources.forEach(effectSource => {
            if (effectSource.audio._element) effectSource.audio._element._currentSource.playbackRate.value = this._speed_audio;
        });
    }

    private _speed_skeleton: number = 1;
    /**骨骼时间流速 */
    public get speed_skeleton(): number {
        return this._speed_skeleton;
    }
    public set speed_skeleton(newValue: number) {
        this._speed_skeleton = newValue;
        sp.timeScale = this._speed_skeleton;
        dragonBones.timeScale = this._speed_skeleton;
    }

    private _speed_action: number = 1;
    /**动作时间流速 */
    public get speed_action(): number {
        return this._speed_action;
    }
    public set speed_action(newValue: number) {
        this._speed_action = newValue;
    }

    private _speed_animation: number = 1;
    /**动画时间流速 */
    public get speed_animation(): number {
        return this._speed_animation;
    }
    public set speed_animation(newValue: number) {
        this._speed_animation = newValue;
    }

    private _speed_collision: number = 1;
    /**碰撞时间流速 */
    public get speed_collision(): number {
        return this._speed_collision;
    }
    public set speed_collision(newValue: number) {
        this._speed_collision = newValue;
    }

    private _speed_physics: number = 1;
    /**物理时间流速 */
    public get speed_physics(): number {
        return this._speed_physics;
    }
    public set speed_physics(newValue: number) {
        this._speed_physics = newValue;
    }

    private _speed_physics3D: number = 1;
    /**3D物理时间流速 */
    public get speed_physics3D(): number {
        return this._speed_physics3D;
    }
    public set speed_physics3D(newValue: number) {
        this._speed_physics3D = newValue;
    }

    private _speed_timer: number = 1;
    /**定时器时间流速 */
    public get speed_timer(): number {
        return this._speed_timer;
    }
    public set speed_timer(newValue: number) {
        this._speed_timer = newValue;
        cc.director._scheduler._timeScale = this._speed_timer;
    }

    private _timers: Map<Timer['tag'], Timer[]> = null;
    /**定时器任务标签索引存放对象 */
    private get timersByTag(): Map<Timer['tag'], Timer[]> {
        if (this._timers === null) {
            this._timers = new Map();
        }
        return this._timers;
    }

    private _timersByID: Map<Timer['id'], Timer> = null;
    /**定时器任务ID索引存放对象 */
    private get timersByID(): Map<Timer['id'], Timer> {
        if (this._timersByID === null) {
            this._timersByID = new Map();
        }
        return this._timersByID;
    }

    protected init() {
        //hook动作间隔帧时间的计算
        const { value: update_action } = Reflect.getOwnPropertyDescriptor(cc.ActionManager.prototype, 'update');
        Reflect.defineProperty(cc.director._actionManager, 'update', {
            value: function (dt: number) {
                update_action.call(this, dt * hh.timeMgr.speed_action / hh.timeMgr.speed_timer);
            },
        });
        //hook动画间隔帧时间的计算
        const { value: update_animation } = Reflect.getOwnPropertyDescriptor(cc.AnimationManager.prototype, 'update');
        Reflect.defineProperty(cc.director._animationManager, 'update', {
            value: function (dt: number) {
                update_animation.call(this, dt * hh.timeMgr.speed_animation / hh.timeMgr.speed_timer);
            },
        });
        //hook碰撞间隔帧时间的计算
        const { value: update_collision } = Reflect.getOwnPropertyDescriptor(cc.CollisionManager.prototype, 'update');
        Reflect.defineProperty(cc.director._collisionManager, 'update', {
            value: function (dt: number) {
                update_collision.call(this, dt * hh.timeMgr.speed_collision / hh.timeMgr.speed_timer);
            },
        });
        //hook物理间隔帧时间的计算
        const { value: update_physics } = Reflect.getOwnPropertyDescriptor(cc.PhysicsManager.prototype, 'update');
        Reflect.defineProperty(cc.director._physicsManager, 'update', {
            value: function (dt: number) {
                update_physics.call(this, dt * hh.timeMgr.speed_physics / hh.timeMgr.speed_timer);
            },
        });
        //hook物理间隔帧时间的计算
        const { value: update_physics3D } = Reflect.getOwnPropertyDescriptor(cc.Physics3DManager.prototype, 'update');
        Reflect.defineProperty(cc.director._physics3DManager, 'update', {
            value: function (dt: number) {
                update_physics3D.call(this, dt * hh.timeMgr.speed_physics3D / hh.timeMgr.speed_timer);
            },
        });
    }

    /**
     * 调度定时器超时任务方法
     * @param cb 定时器回调函数
     * @param target 定时器回调对象
     * @param delay 延迟触发时间(单位ms)
     * @example //回调函数传参
     * let cb: Function = function (num1: number, num2: number): number { return this.add(num1, num2) }.bind(this, 1, 2);
     * hh.timeMgr.setTimeout(cb, this, 3);
     */
    public setTimeout(cb: Timer['cb'], target: Timer['target'], delay: number): Timer['id'] {
        //执行定时器
        let timerID: Timer['id'] = this.setTimer(cb, target, { tag: 'timeout', interval: 0, repeat: 1, delay: delay });
        //清理定时器
        for (let timer of cc.director._scheduler._hashForTimers[this.timersByID.get(timerID)._id].timers) {
            let cancel = timer.cancel;
            Reflect.defineProperty(timer, 'cancel', {
                value: function () {
                    cancel.call(this);
                    hh.timeMgr.clearTimer(timerID);
                },
            });
        }
        //返回定时器任务ID
        return timerID;
    }

    /**
     * 调度定时器间隔任务方法
     * @param cb 定时器回调函数
     * @param target 定时器回调对象
     * @param interval 触发间隔时间(单位ms填0为每帧触发)
     * @example //回调函数传参
     * let cb: Function = function (num1: number, num2: number): number { return this.add(num1, num2) }.bind(this, 1, 2);
     * hh.timeMgr.setInterval(cb, this, 3);
     */
    public setInterval(cb: Timer['cb'], target: Timer['target'], interval: number): Timer['id'] {
        //执行定时器
        return this.setTimer(cb, target, { tag: 'interval', interval: interval, repeat: cc.macro.REPEAT_FOREVER, delay: interval });
    }

    /**
     * 取消定时器超时任务方法
     * @param id 定时器任务ID
     */
    public clearTimeout(id: Timer['id']): boolean {
        return this.clearTimer(id);
    }

    /**
     * 取消定时器间隔任务方法
     * @param id 定时器任务ID
     */
    public clearInterval(id: Timer['id']): boolean {
        return this.clearTimer(id);
    }

    /**
     * 取消定时器任务方法
     * @param id 定时器任务ID
     */
    public clearTimer(id: Timer['id']): boolean {
        //获取定时器
        let timer: Timer = this.timersByID.get(id);
        if (!timer) return false;
        //取消定时器
        cc.director._scheduler.unschedule(timer.cb, timer);
        //清理定时器
        this.timersByID.delete(timer.id);
        hh.tools.removeItems(this.timersByTag.get(timer.tag), timer);
        return true;
    }

    /**
     * 调度定时器任务方法
     * @param cb 定时器回调函数
     * @param target 定时器回调对象
     * @param opts 定时器参数对象
     */
    public setTimer<T extends {
        /**定时器任务标签(不填则默认为default) */
        tag?: Timer['tag'],
        /**触发间隔时间(单位ms不填则默认为0于每帧触发) */
        interval?: number,
        /**重复次数(不填则默认为一直持续触发) */
        repeat?: number,
        /**首次触发延迟时间(单位ms不填则默认为0于下帧触发) */
        delay?: number,
    }>(cb: Timer['cb'], target: Timer['target'], opts?: T): Timer['id'];
    public setTimer(cb: Timer['cb'], target: any, interval?: number, repeat?: number, delay?: number): Timer['id'];
    public setTimer<T extends { tag?: Timer['tag'], interval?: number, repeat?: number, delay?: number }>(cb: Timer['cb'], target: Timer['target'], opts: T | number = {} as any, ..._: any[]): Timer['id'] {
        //解构参数对象
        let tag: Timer['tag'],
            interval: number,
            repeat: number,
            delay: number;
        if (arguments.length === [cb, target].length || (arguments.length === [cb, target, opts].length && typeof opts === 'object')) {
            tag = (opts as T)?.tag ?? 'default';
            interval = (opts as T)?.interval / 1000 ?? 0;
            repeat = (opts as T)?.repeat ?? cc.macro.REPEAT_FOREVER;
            delay = (opts as T)?.delay / 1000 ?? 0;
        }
        else {
            tag = 'default';
            interval = arguments[2] / 1000 ?? 0;
            repeat = arguments[3] ?? cc.macro.REPEAT_FOREVER;
            delay = arguments[4] / 1000 ?? 0;
        }
        //创建定时器
        let timer: Timer = { cb: cb, target: target, tag: tag, id: performance.now() };
        //执行定时器
        cc.director._scheduler.enableForTarget(timer);
        if (interval === 0 && repeat === cc.macro.REPEAT_FOREVER && delay === 0) {
            timer.update = cb as Timer['update'];
            cc.director._scheduler.scheduleUpdate(timer, cc.Scheduler.PRIORITY_NON_SYSTEM, cc.director._scheduler.isTargetPaused(timer));
        }
        else cc.director._scheduler.schedule(cb, timer, interval, repeat - 1, delay === 0 ? cc.director._deltaTime : delay, cc.director._scheduler.isTargetPaused(timer));
        //缓存定时器
        this.timersByID.set(timer.id, timer);
        if (!this.timersByTag.get(tag)?.length) this.timersByTag.set(tag, []);
        this.timersByTag.get(tag).push(timer);
        //返回定时器任务ID
        return timer.id;
    }

    public correctTimerByID(id: Timer['id'], timeScale: number) {
        for (let timer of cc.director._scheduler._hashForTimers[this.timersByID.get(id)._id].timers) {
            timer._interval *= 1 / timeScale;
            if (this.timersByID.get(id).tag === 'interval') timer._delay *= 1 / timeScale;
        }
    }

    public correctTimersByTag(tag: Timer['tag'], timeScale: number) {
        this.timersByTag.get(tag)?.forEach(_timer => {
            for (let timer of cc.director._scheduler._hashForTimers[_timer._id].timers) {
                timer._interval *= 1 / timeScale;
                if (tag === 'interval') timer._delay *= 1 / timeScale;
            }
        });
    }

    /**
     * 分帧执行方法
     * @param process 处理函数(若存在返回值则作为是否中止执行的判断条件)
     * @param opts 执行参数对象
     * @param args 处理函数参数
     */
    public async framing(process: (
        /**当前执行次数索引 */
        index: number,
    ) => any, opts: {
        /**总执行次数 */
        total: number,
        /**单次执行次数(默认为1次) */
        step?: number,
        /**单次执行间隔(单位ms默认为100ms) */
        interval?: number,
        /**绑定处理函数的this指向对象 */
        target?: any,
    }) {
        return new Promise<void>(async (resolve, reject) => {
            //解构参数对象
            let { total, step = 1, interval = 100, target } = opts ?? {};
            //声明累计执行计数
            let count: number = 0;
            //执行分帧处理
            _framing();

            //声明分帧处理方法
            async function _framing() {
                //获取本次执行次数
                let once: number = Math.min(total - count, step);
                //循环执行处理函数
                for (let i: number = 0; i < once; i++) {
                    //达成条件中止执行
                    if (await process.call(target, count)) {
                        reject(count);
                        return;
                    }
                    //统计当前执行次数
                    count++;
                }
                //继续执行剩余次数
                if (count < total) setTimeout(_framing, interval);
                else resolve();
            }
        });
    }

    /**
     * 获取当前时间方法
     * @param opts 日期格式参数对象
     * @returns 返回当前指定日期格式时间文本字符串
     */
    public getLocalDate<T extends keyof {
        /**年-月-日 */
        'YYYY_MM_DD',
        /**时-分-秒 */
        'HH_MM_SS',
    }>(opts?: {
        /**日期格式(默认为完整格式) */
        format?: T,
        /**世界时间(默认为本地时间) */
        isWorld?: boolean,
    }): string {
        //解构参数对象
        let { format, isWorld = false } = opts ?? {};
        //获取当前时间
        isWorld = isWorld ? 'UTC' : '' as any;
        let time: Date = new Date();
        let year: number = time[`get${isWorld}FullYear`](); //年
        let month: number = time[`get${isWorld}Month`]() + 1; //月
        let day: number = time[`get${isWorld}Date`](); //日
        let hour: number = time[`get${isWorld}Hours`](); //时
        let minute: number = time[`get${isWorld}Minutes`](); //分
        let second: number = time[`get${isWorld}Seconds`](); //秒
        let millisecond: number = time[`get${isWorld}Milliseconds`](); //毫秒
        let week: string[] = ['日', '一', '二', '三', '四', '五', '六'];
        let index: number = time[`get${isWorld}Day`](); //星期几
        //格式化时分秒毫秒
        hour = hour.toString().length === 1 ? '0' + hour : hour as any;
        minute = minute.toString().length === 1 ? '0' + minute : minute as any;
        second = second.toString().length === 1 ? '0' + second : second as any;
        if (millisecond.toString().length === 1) millisecond = '00' + millisecond as any;
        if (millisecond.toString().length === 2) millisecond = '0' + millisecond as any;
        //返回当前时间
        if (format === 'YYYY_MM_DD') return `${year}年${month}月${day}日`;
        if (format === 'HH_MM_SS') return `${hour}时${minute}分${second}秒`;
        return `${year}年${month}月${day}日\r\n${hour}时${minute}分${second}秒${millisecond}毫秒\r\n星期${week[index]}`;
    }

    /**
     * 全局延时阻塞方法
     * @param time 等待时间(单位ms默认为延迟一帧)
     */
    public async wait(time: number = 0) {
        return new Promise<void>(resolve => setTimeout(resolve, time));
    }

}

/**定时器绑定目标 */
type Timer = {
    /**定时器任务ID */
    id: ReturnType<Performance['now']>,
    /**定时器任务回调 */
    cb: Parameters<cc.Scheduler['schedule']>[0],
    /**定时器任务目标 */
    target: Parameters<cc.Scheduler['schedule']>[1],
    /**定时器任务标签 */
    tag: 'default' | 'timeout' | 'interval',
    /**定时器ID */
    _id?: `Scheduler.${number}`,
    /**定时器更新回调 */
    update?: cc.Scheduler['update'],
};

export { TimeMgr as default };

需求参考
代码参考

1赞

另外有个疑问 为啥我手动new一个scheduler然后enableForTarget绑定目标再调用schedule不会触发回调
使用姿势应该没错啊 还是有什么已知bug