Unity 多时间源Timer定时器实战分享:健壮性、高效性、多线程安全与稳定性能全面解析

news2025/5/21 0:03:01

简介

Timer 是一个 Unity 环境下高效、灵活的定时任务调度系统,支持以下功能:

•支持多种时间源(游戏时间 / 非缩放时间 / 真实时间)

•支持一次性延迟执行和重复执行

•提供 ID、回调、目标对象等多种查询和销毁方式

•内建任务池(对象复用)避免频繁 GC

•支持线程安全的任务添加/移除

•提供完整生命周期管理


最新更新

2.1

TimerTask类中新增了一个字段 private volatile bool IsActived = true;

  • 解决bug, 在主线程或者多线程中 调用Kill方法,定时器仍然会执行一帧

bug原因总结:
为了考虑多线程的安全性,所有定时器的移除任务会先加入队列s_PendingRemoveQueue, 会在Update里去处理移除定时器任务。
这必然导致了,无法实时的去移除定时器任务,所以,当前版本在每个定时器移除的时候 额外设置了标志位,并使用了volatile关键字,避免了多线程缓存不可见问题

2.0

Timer 是一个 Unity 环境下高效、灵活的定时任务调度系统,支持以下功能:

•支持多种时间源(游戏时间 / 非缩放时间 / 真实时间)

•支持一次性延迟执行和重复执行

•提供 ID、回调、目标对象等多种查询和销毁方式

•内建任务池(对象复用)避免频繁 GC

•支持线程安全的任务添加/移除

•提供完整生命周期管理

1.0

一个 轻量 高效 高精度 零GC, 协程定时器

核心特性

特性描述
多时间源支持 GameTimeUnscaledTimeRealTime
对象池优化避免频繁分配,提升性能
安全多线程添加/移除任务队列线程安全,主线程调度
支持任务查找可通过 ID、Action、目标对象查找定时任务
一次性与循环任务支持延迟一次性任务和循环执行任务
稳定运行通过 MonoBehaviour 生命周期运行,自动初始化,独立运行场景生命周期外

时间源类型

csharp复制编辑public enum TimerTimeSource {
    GameTime,      // Time.time
    UnscaledTime,  // Time.unscaledTime
    RealTime       // DateTimeOffset.UtcNow
}


快速开始

延迟执行一个函数

csharp复制编辑Timer.Delay(2.0f, () => Debug.Log("2秒后执行"));

循环执行(每1秒执行一次,无限循环)

csharp复制编辑Timer.Loop(1.0f, () => Debug.Log("每秒触发一次"));

循环执行(立即执行 + 执行5次)

csharp复制编辑Timer.Loop(1.0f, MyCallback, TimerTimeSource.UnscaledTime, immediate: true, times: 5);


API 说明

创建定时任务

方法说明
Timer.Delay(float delay, Action callback, TimerTimeSource timeSource)延迟执行一次回调
Timer.Loop(float interval, Action callback, TimerTimeSource timeSource, bool immediate, int times)间隔时间循环执行回调,支持立即执行和限定次数

查找定时任务

方法说明
Timer.Find(long id)根据唯一 ID 查找任务
Timer.Find(Action func)查找所有指定方法的任务
Timer.Find(object target)查找指定目标对象绑定的方法任务

终止定时任务

方法说明
Timer.Kill(long id)根据 ID 终止任务
Timer.Kill(Action func)终止所有指定方法的任务
Timer.Kill(object target)终止指定对象上的所有任务
Timer.Kill<T>()根据类名终止所有任务(包括 lambda)
Timer.KillAll()清理所有任务

内部机制

•对象池机制:任务对象使用 ConcurrentQueue<TimerTask> 循环复用,避免 GC。

•双缓冲快照列表:主线程调度时快照任务列表,避免遍历冲突。

•排序插入调度:内部任务按到期时间排序,保障调度精度与性能。

•自动初始化:通过 [RuntimeInitializeOnLoadMethod] 自动创建 Timer 挂载 GameObject。


注意事项

•使用成员方法而非 lambda 可提升可控性(利于 Kill 操作)。

•不支持精确毫秒调度,适合用于逻辑调度、UI、冷却、延迟等。

•RealTime 不受 Unity 时间系统影响,可跨暂停/切后台使用。


示例场景

csharp复制编辑public class Example : MonoBehaviour
{
    void Start()
    {
        Timer.Delay(5f, OnTimeout); // 5秒后执行一次
    }

    void OnTimeout()
    {
        Debug.Log("延迟执行完毕");
    }

    void OnDestroy()
    {
        Timer.Kill(this); // 清理当前实例上所有定时器
    }
}


扩展建议

•✅ 可拓展支持 Coroutine(协程回调)

•✅ 可拓展带参数回调、异步支持(如返回 Task)

•✅ 可集成 ECS 环境中运行

•✅ 可接入编辑器模式(EditorApplication.update)

测试用例 - 单元测试 TestFramework

以下为 Timer 工具类的单元测试用例列表,涵盖各核心功能模块,确保运行时稳定性与正确性:


✅ 延迟任务测试

  • 延迟任务是否在正确时间后执行

  • 多个延迟任务能否独立调度

  • 销毁延迟任务是否生效


✅ 循环任务测试

  • 循环任务是否按照设定间隔执行

  • immediate = true 是否立即执行第一次

  • 指定次数的循环任务是否能按期停止

  • 无限循环任务是否正常执行直到手动销毁

  • 多种时间源(GameTime / UnscaledTime / RealTime)下是否都正常运行


✅ 销毁接口测试

  • Kill(id) 是否准确销毁对应任务

  • Kill(callback) 是否能移除注册的回调

  • Kill(target) 是否能销毁目标对象的所有任务

  • Kill<T>() 是否能销毁所有绑定到某类型的任务

  • KillAll() 是否能清空所有任务


✅ 查找任务测试

  • Find(id) 是否能准确找到指定任务

  • Find(callback) 是否能正确返回绑定回调的任务列表

  • Find(target) 是否能正确返回目标对象创建的任务


✅ 对象池与 GC 测试

  • 创建/销毁任务是否有 GC 分配(使用 Unity Profiler 验证)

  • 高频任务创建(>10w)后是否仍稳定运行

  • 重复使用任务对象是否回收到池中


✅ 线程安全测试(如支持)

  • 在主线程和协程中交替添加/销毁任务是否安全

  • 高并发添加任务(1000+)是否有竞态或异常抛出


✅ 特殊场景测试

  • 场景切换后未销毁任务是否仍运行(针对非 MonoBehaviour 静态类)

  • 编辑器下运行是否正常(Editor 模式)

  • 时间源切换是否引发错乱

  • 时间倒退或跳变是否能恢复(如 RealTime 回拨)

单元测试脚本在 插件下载里

插件下载

百度云盘知识库分享

u3d_免费高速下载|百度网盘-分享无限制

源码

Runtime部分:

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

/// <summary>
/// Timer 是一个用于管理定时任务的工具类,允许创建、查找和取消基于时间的回调操作。
/// 它支持多种时间源类型,包括游戏时间、非缩放时间和真实时间,以满足不同的应用场景需求。
/// 定时任务可以是一次性的延迟执行任务,也可以是重复执行的循环任务。
/// 类内部维护了一个任务池,用于高效地复用定时任务对象,从而减少内存分配和垃圾回收的压力。
/// 提供了静态方法来创建、查询和终止定时任务,并支持通过唯一标识符、回调方法或目标对象进行任务检索。
///
/// Anchor: ChenJC
/// Time: 2022/10/09
/// Feedback: Isysprey@foxmail.com
/// Example: https://blog.csdn.net/qq_39162566/article/details/113105351
/// </summary>
public class Timer : MonoBehaviour
{
    /// <summary>
    /// 激活中的TimerTask对象
    /// </summary>
    private static readonly List<TimerTask> s_ActiveTasks = new List<TimerTask>( );

    /// <summary>
    /// 定时器的ID计数
    /// </summary>
    private static long s_TimerID = 0x7f;

    /// <summary>
    /// 闲置TimerTask对象 : 线程安全
    /// </summary>
    private static readonly ConcurrentQueue<TimerTask> s_FreeTasks = new ConcurrentQueue<TimerTask>( );

    /// <summary>
    /// 待添加的定时器任务队列 : 线程安全
    /// </summary>
    private static readonly ConcurrentQueue<TimerTask> s_PendingAddQueue = new ConcurrentQueue<TimerTask>( );

    /// <summary>
    /// 待移除的定时器任务队列 : 线程安全
    /// </summary>
    private static readonly ConcurrentQueue<TimerTask> s_PendingRemoveQueue = new ConcurrentQueue<TimerTask>( );

    /// <summary>
    /// 锁对象,用于确保在多线程环境下对定时器任务列表的安全访问和修改。
    /// </summary>
    private static readonly object s_Locker = new object( );

    /// <summary>
    /// 双缓冲池
    /// </summary>
    private static List<TimerTask>[] s_Snapshots = { new List<TimerTask>( ), new List<TimerTask>( ) };

    /// <summary>
    /// 双缓冲池当前下标
    /// </summary>
    private static int s_ActiveSnapshotIndex = 0;

    /// <summary>
    /// 
    /// </summary>
    private static bool s_ClearAll = false;

    /// <summary>
    /// 游戏时间:  缓存 Time.time当前帧 提供对外的业务访问
    /// </summary>
    public static float GameTime { private set; get; }

    /// <summary>
    /// 不受Time.timeScale限制的游戏时间: 缓存 Time.unscaledTime当前帧
    /// </summary>
    public static float UnscaledTime { private set; get; }

    /// <summary>
    /// 世界真实时间: 缓存 DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0当前帧
    /// </summary>
    public static double RealTime { private set; get; }


    #region Add timer task

    /// <summary>
    /// 延迟定时器 在指定时间后调用一次回调方法
    /// </summary>
    /// <param name="delay"> 延迟时长: 秒 </param>
    /// <param name="func"> 调用的方法回调 </param>
    /// <param name="timeSource"> 时间来源类型: 默认使用游戏时间 </param>
    /// <returns> 返回定时器的唯一标识符 用于后续操作 如取消定时器 </returns>
    public static long Delay( float delay, Action func, TimerTimeSource timeSource = TimerTimeSource.GameTime )
    {
        return Loop( delay, func, timeSource, 1 );
    }

    /// <summary>
    /// 创建一个循环定时器,按照指定的时间间隔重复调用回调方法。
    /// </summary>
    /// <param name="interval">时间间隔: 秒</param>
    /// <param name="func">需要调用的回调方法</param>
    /// <param name="timeSource">时间来源类型: 默认使用游戏时间</param>
    /// <param name="times">循环次数: 0 表示无限循环,默认为 0</param>
    /// <returns>返回定时器的唯一标识符,用于后续操作,如取消定时器</returns>
    public static long Loop( float interval, Action func, TimerTimeSource timeSource = TimerTimeSource.GameTime, int times = 0 )
    {
        //检查参数合法性
        if ( func != null )
        {
            var target = func.Target;
            var method = func.Method;

            if ( target == null || method.IsStatic )
            {
                Debug.LogWarning(
                    "[Timer] Detected lambda or static method as func. " +
                    "Kill(object target) may not work for such tasks. " +
                    "Consider using class instance methods instead." );
            }
        }

        //从free池中 获取一个闲置的TimerTask对象
        var timer = GetFreeTimerTask( );
        timer.LifeCycle = interval;
        timer.TimeSource = timeSource;
        timer.Func = func;
        timer.Times = times == 0 ? long.MaxValue : times;
        timer.ID = Interlocked.Increment( ref s_TimerID );
        timer.Refresh( );

        //推入到 等待添加的线程安全队列中
        s_PendingAddQueue.Enqueue( timer );
        return timer.ID;
    }

    #endregion


    #region Find Timer

    private static List<TimerTask> FindTasks( Predicate<TimerTask> matchedCondition )
    {
        var snapshot = GetCurrentTaskSnapshot( );
        List<TimerTask> result = snapshot.FindAll( matchedCondition );
        foreach ( var t in s_PendingAddQueue )
        {
            if ( matchedCondition( t ) )
                result.Add( t );
        }
        return result;
    }

    /// <summary>
    /// 查找具有指定唯一标识符的定时任务。
    /// </summary>
    /// <param name="ID">要查找的定时任务的唯一标识符。</param>
    /// <returns>返回与指定ID匹配的定时任务的副本。如果未找到匹配的任务,则返回null。</returns>
    public static TimerTask Find( long ID )
    {
        List<TimerTask> freeTasks = null;
        lock ( s_Locker )
        {
            freeTasks = FindTasks( t => t.ID == ID );
        }
        return freeTasks != null && freeTasks.Count > 0 ? freeTasks[ 0 ].Clone( ) : null;
    }

    /// <summary>
    /// 查找与指定回调方法关联的所有定时任务。
    /// 该方法会遍历当前活动的定时任务列表,筛选出与提供的回调方法匹配的任务,并返回这些任务的副本列表。
    /// </summary>
    /// <param name="func">要查找的回调方法。如果为 null,则不会返回任何任务。</param>
    /// <returns>包含与指定回调方法匹配的定时任务副本的列表。如果没有找到匹配的任务,则返回空列表。</returns>
    public static List<TimerTask> Find( Action func )
    {
        List<TimerTask> freeTasks = null;
        lock ( s_Locker )
        {
            freeTasks = FindTasks( t => t.Func == func );
        }

        List<TimerTask> result = new List<TimerTask>( );
        freeTasks?.ForEach( task => result.Add( task.Clone( ) ) );
        return result;
    }

    /// <summary>
    /// 查找与指定目标对象关联的所有定时任务。
    /// 该方法通过遍历活动任务列表,匹配任务回调函数的目标对象,返回所有符合条件的任务副本。
    /// </summary>
    /// <param name="target">要查找的目标对象,用于匹配定时任务回调函数的目标。</param>
    /// <returns>返回一个包含所有匹配定时任务副本的列表。如果没有找到匹配的任务,则返回空列表。</returns>
    public static List<TimerTask> Find( object target )
    {
        List<TimerTask> freeTasks = null;
        lock ( s_Locker )
        {
            freeTasks = FindTasks( t => t.Func != null && t.Func.Target == target );
        }
        List<TimerTask> result = new List<TimerTask>( );
        freeTasks?.ForEach( task => result.Add( task.Clone( ) ) );
        return result;
    }

    #endregion


    #region Clear timer

    /// <summary>
    /// 通过ID 清理定时器
    /// </summary>
    /// <param name="ID">定时器标签</param>
    /// <returns></returns>
    public static void Kill( long ID )
    {
        List<TimerTask> freeTasks = null;
        lock ( s_Locker )
        {
            freeTasks = FindTasks( t => t.ID == ID );
        }

        if ( freeTasks != null )
        {
            Kill( freeTasks );
        }
    }


    /// <summary>
    /// 通过类型来Kill
    /// @ps: 移除同类型的所有成员方法定时器  包含( lambda 和 其它类实例 )
    /// </summary>
    /// <param name="clsType"></param>
    public static void Kill<T>( )
    {
        var clsName = typeof( T ).FullName;
        List<TimerTask> freeTasks = null;

        lock ( s_Locker )
        {
            freeTasks = FindTasks( t =>
            {
                if ( null != t.Func && null != t.Func.Target )
                {
                    var fullname = t.Func.Target.GetType( ).FullName;
                    var currentClsNameClip = fullname.Split( '+' );
                    if ( currentClsNameClip.Length > 0 && currentClsNameClip[ 0 ] == clsName )
                    {
                        return true;
                    }
                }
                return false;
            } );
        }

        if ( freeTasks != null )
        {
            Kill( freeTasks );
        }
    }

    /// <summary>
    /// 通过方法 清理定时器
    /// </summary>
    /// <param name="func">处理方法</param>
    /// <returns></returns>
    public static void Kill( Action func )
    {
        List<TimerTask> freeTasks = null;
        lock ( s_Locker )
        {
            freeTasks = FindTasks( t => t.Func == func );
        }

        if ( freeTasks != null )
        {
            Kill( freeTasks );
        }
    }


    /// <summary>
    /// 清理当前类的所有方法
    /// 避免Lambda 可能会存在问题,请尽量使用类成员方法注册定时器
    /// </summary>
    /// <param name="func">处理方法</param>
    /// <returns></returns>
    public static void Kill( object target )
    {
        List<TimerTask> freeTasks = null;
        lock ( s_Locker )
        {
            freeTasks = FindTasks( t => t.Func != null && t.Func.Target == target );
        }

        if ( freeTasks != null )
        {
            Kill( freeTasks );
        }
    }



    private static void Kill( List<TimerTask> tasks ) => tasks.ForEach( t => t.Cancel( ) );


    /// <summary>
    /// 清理所有定时器 一定要确保所有定时器能完全清理掉
    /// </summary>
    public static void KillAll( ) => s_ClearAll = true;

    #endregion


    #region Core

    /// <summary>
    /// 初始化定时器系统。此方法在场景加载之前自动调用,用于设置定时器的核心环境。
    /// 它会清理所有激活和闲置的定时任务列表,确保定时器系统在一个干净的状态下启动。
    /// 同时,创建一个名为 "Timer" 的全局游戏对象,并附加 Timer 组件,以保证定时器系统在整个应用程序生命周期内持续运行。
    /// 该方法通过 [RuntimeInitializeOnLoadMethod] 特性标记,确保在游戏启动时自动执行,无需手动调用。
    /// </summary>
    [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )]
    static void Init( )
    {
        s_ActiveTasks.Clear( );
        s_FreeTasks.Clear( );
        DontDestroyOnLoad( new GameObject( "Timer", typeof( Timer ) ) );
    }

    /// <summary>
    /// TimerTaskComparer 是一个用于比较两个定时任务的工具类,实现了 IComparer<TimerTask> 接口。
    /// 它的主要功能是根据定时任务的到期时间(ExpirationTime)对任务进行排序。
    /// 该类通过 Compare 方法定义了排序逻辑,确保定时任务按照其到期时间的先后顺序排列。
    /// 此比较器被内部用于维护定时任务的有序性,从而提高任务调度和管理的效率。
    /// </summary>
    private class TimerTaskComparer : IComparer<TimerTask>
    {
        public int Compare( TimerTask x, TimerTask y )
        {
            return x.GetTimeUntilNextExecution( ).CompareTo( y.GetTimeUntilNextExecution( ) );
        }
    }

    /// <summary>
    /// TimerTaskComparer 是一个用于比较两个定时任务的工具类,实现了 IComparer<TimerTask> 接口。
    /// </summary>
    private static readonly TimerTaskComparer s_Comparer = new TimerTaskComparer( );


    /// <summary>
    /// 将定时任务插入到活动任务列表中,并保持列表的有序性。
    /// </summary>
    /// <param name="task"></param>
    private static void InsertTaskSorted( TimerTask task )
    {
        int index = s_ActiveTasks.BinarySearch( task, s_Comparer );
        if ( index < 0 )
            index = ~index;
        s_ActiveTasks.Insert( index, task );
    }

    /// <summary>
    /// 定义定时器时间来源类型,用于指定定时器的时间基准。
    /// GameTime: 使用游戏时间,受时间缩放影响,适用于常规游戏逻辑。
    /// UnscaledTime: 使用未缩放的游戏时间,不受时间缩放影响,适用于暂停或慢动作等场景。
    /// RealTime: 使用系统实时时间,完全独立于游戏时间,适用于与游戏逻辑无关的精确计时。
    /// </summary>
    public enum TimerTimeSource
    {
        GameTime,
        UnscaledTime,
        RealTime
    }

    /// <summary>
    /// TimerTask 表示一个定时任务,用于在指定的时间或间隔内执行特定的操作。
    /// 该类封装了定时器任务的核心逻辑,包括生命周期、执行时间、执行次数以及回调函数。
    /// 定时任务支持不同的时间源类型,如游戏时间、非缩放时间和真实时间。
    /// 定时任务对象可以通过回收机制复用,以提高性能和减少内存分配。
    /// </summary>
    public class TimerTask
    {
        public long ID;
        public float LifeCycle;
        public double ExpirationTime;
        public long Times;
        public Action Func;
        public TimerTimeSource TimeSource;
        private volatile bool IsActived = true;


        /// <summary>
        /// 取消当前定时器任务的执行。
        /// </summary>
        public void Cancel( )
        {
            if ( !IsActived )
                return;
            IsActived = false;
            s_PendingRemoveQueue.Enqueue( this );
        }


        /// <summary>
        /// 判断当前定时器任务是否处于活动状态。
        /// </summary>
        /// <returns></returns>
        public bool IsActive( ) => IsActived;


        /// <summary>
        /// 返回一个副本,避免一些获取的操作 对定时器直接操作,避免可能的线程安全问题
        /// </summary>
        /// <returns></returns>
        public TimerTask Clone( )
        {
            var task = new TimerTask( )
            {
                ID = ID,
                LifeCycle = LifeCycle,
                ExpirationTime = ExpirationTime,
                Times = Times,
                Func = Func,
                TimeSource = TimeSource
            };
            return task;
        }


        /// <summary>
        /// 获取当前时间 根据定时器的类型来获取 世界真实时间,游戏内时间,游戏内非缩放时间
        /// </summary>
        /// <param name="timeSource">时间来源类型: 游戏时间、不受缩放影响的游戏时间或世界真实时间</param>
        /// <returns>返回对应时间来源类型的当前时间值</returns>
        private double GetCurrentTime( TimerTimeSource timeSource )
        {
            return timeSource switch
            {
                TimerTimeSource.GameTime => Timer.GameTime,
                TimerTimeSource.UnscaledTime => Timer.UnscaledTime,
                TimerTimeSource.RealTime => Timer.RealTime,
                _ => Timer.GameTime,
            };
        }


        /// <summary>
        /// 获取当前时间 根据定时器的时间来源类型返回对应的时间值
        /// </summary>
        /// <returns>返回与定时器时间来源类型相对应的当前时间值</returns>
        public double GetCurrentTime( ) => GetCurrentTime( TimeSource );

        /// <summary>
        /// 释放回收当前定时器
        /// </summary>
        public void Recycle( )
        {
            ID = 0;
            LifeCycle = 0;
            ExpirationTime = 0;
            Times = 0;
            Func = null;
            TimeSource = TimerTimeSource.GameTime;
            IsActived = true;
            s_FreeTasks.Enqueue( this );
        }

        /// <summary>
        /// 刷新下一次更新的时间
        /// </summary>
        public void Refresh( )
        {
            ExpirationTime = GetCurrentTime( ) + LifeCycle;
        }


        /// <summary>
        /// 判断当前定时器任务是否已达到下一次执行的时间点。
        /// 该方法通过比较当前时间与任务的过期时间来确定是否需要执行下一步操作。
        /// </summary>
        /// <returns>返回布尔值,如果当前时间大于或等于任务的过期时间,则返回 true,否则返回 false。</returns>
        public bool Next( ) => GetCurrentTime( ) >= ExpirationTime;


        /// <summary>
        /// 计算当前定时器任务距离下次执行的时间间隔。
        /// </summary>
        /// <returns></returns>
        public double GetTimeUntilNextExecution( ) => ExpirationTime - GetCurrentTime( );
    }

    /// <summary>
    /// 刷新当前活动任务的快照。该方法用于在多线程环境下安全地更新任务快照,
    /// 确保在遍历任务列表时不会因任务的动态添加或移除而导致数据不一致。
    /// 快照通过索引切换的方式进行更新,避免直接修改当前正在使用的任务列表。
    /// 该方法在类的内部被调用,通常与定时任务的管理和执行逻辑配合使用。
    /// </summary>
    private static void RefreshSnapshot( )
    {
        lock ( s_Locker )
        {
            s_ActiveSnapshotIndex = 1 - s_ActiveSnapshotIndex;
            var snapshot = s_Snapshots[ s_ActiveSnapshotIndex ];
            snapshot.Clear( );
            snapshot.AddRange( s_ActiveTasks );
        }
    }

    /// <summary>
    /// 获取当前活动的任务快照列表,包含所有正在运行的定时任务。
    /// 该方法返回一个只读的定时任务列表,用于查询或遍历当前所有活动的任务。
    /// 快照是双缓冲池的一部分,确保在多线程环境下任务列表的一致性和安全性。
    /// </summary>
    /// <returns>返回当前活动的任务快照列表,其中包含所有正在执行的定时任务。</returns>
    private static List<TimerTask> GetCurrentTaskSnapshot( ) => s_Snapshots[ s_ActiveSnapshotIndex ];

    /// <summary>
    /// 在每一帧更新定时器系统的内部状态。
    /// 该方法负责刷新时间快照、处理待执行的任务、管理活跃任务,并在满足执行条件时调用任务回调函数。
    /// 它确保任务根据其设定的时间间隔或延迟被正确执行。
    /// 此外,如果某个任务在执行过程中发生异常,它也会通过日志记录错误,以实现优雅的异常处理。
    /// </summary>
    private void Update( )
    {
        GameTime = Time.time;
        UnscaledTime = Time.unscaledTime;
        RealTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds( ) / 1000.0;

        //检查清除所有定时器的标志位
        if ( s_ClearAll )
        {
            s_ClearAll = false;
            s_ActiveTasks.ForEach( task => task.Recycle( ) );
            s_ActiveTasks.Clear( );
            s_PendingAddQueue.Clear( );
            s_PendingRemoveQueue.Clear( );
        }
        else
        {
            //新增的任务
            while ( s_PendingAddQueue.TryDequeue( out var task ) )
            {
                InsertTaskSorted( task );
            }

            //移除的任务
            while ( s_PendingRemoveQueue.TryDequeue( out var task ) )
            {
                s_ActiveTasks.Remove( task );
                task.Recycle( );
            }

            //刷新快照
            RefreshSnapshot( );
        }


        TimerTask t = null;
        for ( int i = 0; i < s_ActiveTasks.Count; ++i )
        {
            t = s_ActiveTasks[ i ];
            if ( !t.IsActive( ) ) continue;
            if ( t.Next( ) )
            {

                try
                {
                    t.Func?.Invoke( );
                }
                catch ( Exception e )
                {
                    Debug.LogError( $"TimerTask Exception: {e}" );
                }

                --t.Times;
                if ( t.Times == 0 )
                {
                    t.Cancel( );
                }
                else
                {
                    t.Refresh( );
                    s_ActiveTasks.Remove( t );
                    InsertTaskSorted( t );
                    break;
                }
            }
            else break;
        }
    }


    /// <summary>
    /// 从定时任务的空闲队列中获取一个可用的 TimerTask 对象。
    /// 如果空闲队列中没有可用对象,则创建一个新的 TimerTask 实例。
    /// 该方法用于优化定时任务的内存使用,通过复用已回收的 TimerTask 对象减少频繁的内存分配。
    /// </summary>
    /// <returns>返回一个可用的 TimerTask 对象,该对象可能来自空闲队列或新创建的实例。</returns>
    private static TimerTask GetFreeTimerTask( )
    {
        if ( !s_FreeTasks.TryDequeue( out var task ) )
        {
            task = new TimerTask( );
        }

        return task;
    }

    #endregion
}

Editor部分:

#if UNITY_EDITOR

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using static Timer;
using Object = UnityEngine.Object;

[CustomEditor( typeof( Timer ) )]
public class TimerInspector : Editor
{
    FieldInfo activeTaskClsInfo;
    private void OnEnable( )
    {
        activeTaskClsInfo = typeof( Timer ).GetField( "s_ActiveTasks", BindingFlags.Static | BindingFlags.NonPublic );
    }

    public override void OnInspectorGUI( )
    {
        base.OnInspectorGUI( );

        if ( !Application.isPlaying )
        {
            return;
        }
        object v = activeTaskClsInfo.GetValue( null );
        var timerTasks = v as List<TimerTask>;
        EditorGUILayout.LabelField( $"TotalTimerCount: {timerTasks.Count}" );
        for ( int i = 0; i < timerTasks.Count; i++ )
        {
            var task = timerTasks[ i ];
            if ( null != task.Func )
            {
                string caller = task.Func.Target == null ? task.Func.Method.DeclaringType.FullName : task.Func.Target.GetType( ).FullName.Split( '+' )[ 0 ];

                EditorGUILayout.BeginHorizontal( );
                if ( EditorGUILayout.LinkButton( "Jump" ) )
                {
                    Jump2ScriptLinesByClsName( caller, task.Func );
                }
                EditorGUILayout.LabelField( $"[{i + 1}] {caller}->{task.Func.Method.Name}" );
                EditorGUILayout.EndHorizontal( );
            }
            else
            {
                EditorGUILayout.BeginHorizontal( );
                if ( EditorGUILayout.LinkButton( "Jump" ) )
                {
                    EditorUtility.DisplayDialog( "Error", "The invoking mode of the timer is incorrect!", "Confirm" );
                }
                EditorGUILayout.LabelField( $"[{i + 1}] null" );
                EditorGUILayout.EndHorizontal( );
            }
        }
    }



    public void Jump2ScriptLinesByClsName( string className, Action func )
    {
        string[] clssAssetGuids = AssetDatabase.FindAssets( className );
        if ( clssAssetGuids.Length > 0 )
        {

            var scripts = Array.FindAll<string>( clssAssetGuids, guid =>
            {
                string path = AssetDatabase.GUIDToAssetPath( guid );

                if ( path.EndsWith( ".cs" ) )
                {
                    string classFlag = $" class {className}";
                    string[] classs = Array.FindAll<string>( File.ReadAllLines( path ), l =>
                    {
                        return l.Contains( classFlag );
                    } );

                    return Array.Find<string>( classs, s =>
                    {
                        int _ = s.IndexOf( classFlag ) + classFlag.Length;
                        if ( _ < s.Length && ( _ + 1 >= s.Length || !Char.IsLetter( s[ _ + 1 ] ) ) )
                        {
                            return true;
                        }

                        return false;
                    } ) != null;
                }

                return false;
            } );
            if ( scripts.Length > 1 )
            {
                //�ű�����class��Խ����Ȩ��Խ��ǰ
                Array.Sort( scripts, ( a, b ) =>
                {
                    string ap = AssetDatabase.GUIDToAssetPath( a );
                    string bp = AssetDatabase.GUIDToAssetPath( b );
                    ap = Path.GetFileNameWithoutExtension( ap );
                    bp = Path.GetFileNameWithoutExtension( bp );

                    if ( ap == className )
                    {
                        return -1;
                    }
                    else if ( bp == className )
                    {
                        return 1;
                    }
                    else if ( ap.ToLower( ) == className.ToLower( ) )
                    {
                        return -1;
                    }
                    else if ( bp.ToLower( ) == className.ToLower( ) )
                    {
                        return 1;
                    }
                    else if ( ap.StartsWith( className ) )
                    {
                        return -1;
                    }
                    else if ( bp.StartsWith( className ) )
                    {
                        return 1;
                    }
                    else if ( ap.ToLower( ).StartsWith( className.ToLower( ) ) )
                    {
                        return -1;
                    }
                    else if ( bp.ToLower( ).StartsWith( className.ToLower( ) ) )
                    {
                        return 1;
                    }
                    return 0;
                } );
            }
            var script = scripts.Length > 0 ? scripts[ 0 ] : null;

            Object obj = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>( AssetDatabase.GUIDToAssetPath( string.IsNullOrEmpty( script ) ? clssAssetGuids[ 0 ] : script ) );
            if ( obj is TextAsset textAsset )
            {
                string[] lines = textAsset.text.Split( new string[] { "\r\n", "\n", "\r" }, System.StringSplitOptions.None );
                var listLine = new List<string>( lines );
                string methodName = func.Method.Name;
                List<string> paramNames = new List<string>( );

                //is lambda
                bool islambda = false;
                if ( methodName.Contains( "<" ) && methodName.Contains( ">" ) && methodName.Contains( "_" ) )
                {
                    islambda = true;
                    methodName = methodName.Substring( 1, methodName.IndexOf( '>' ) - 1 );
                    var fileds = func.Target.GetType( ).GetFields( );
                    if ( fileds.Length > 1 )
                    {
                        for ( int j = 1; j < fileds.Length; j++ )
                        {
                            paramNames.Add( fileds[ j ].FieldType.Name );
                        }
                    }
                }

                var totalParams = func.Method.GetParameters( );
                if ( totalParams.Length > 0 )
                {
                    foreach ( var param in totalParams )
                    {
                        paramNames.Add( param.Name );
                    }
                }

                string returnparam = func.Method.ReturnTypeCustomAttributes.ToString( );
                int lineNumber = listLine.FindIndex( line =>
                {
                    if ( string.IsNullOrEmpty( line ) || !line.Contains( "(" ) || line.Contains( "//" ) )
                    {
                        return false;
                    }

                    //overload method filter
                    int methodNameIndex = line.IndexOf( methodName );
                    if ( methodNameIndex < 0 )
                    {
                        return false;
                    }
                    char c = line[ methodNameIndex + methodName.Length ];
                    if ( Char.IsLetter( c ) )
                    {
                        return false;
                    }

                    //return params
                    if ( !islambda && !line.ToLower( ).Contains( returnparam.ToLower( ) ) )
                    {
                        return false;
                    }


                    if ( paramNames.Count > 0 )
                    {
                        int leftIndex = line.IndexOf( "(" );

                        for ( int k = 0; k < paramNames.Count; k++ )
                        {
                            if ( line.IndexOf( paramNames[ k ] ) < leftIndex )
                            {
                                return false;
                            }
                        }

                        //is overload method ?
                        string paramDomain = line.Substring( leftIndex, line.IndexOf( ')' ) + 1 - leftIndex );
                        int paramCount = paramDomain.Split( ',' ).Length;
                        if ( paramCount != paramNames.Count )
                        {
                            return false;
                        }
                    }

                    return true;
                } );
                lineNumber = Mathf.Max( lineNumber, 0 );
                AssetDatabase.OpenAsset( obj, lineNumber + 1 );
            }
        }

    }

}


#endif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2380331.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【iOS】探索消息流程

探索消息流程 Runtime介绍OC三大核心动态特性动态类型动态绑定动态语言 方法的本质代码转换objc_msgSendSELIMPMethod 父类方法在子类中的实现 消息查找流程开始查找快速查找流程慢速查找流程二分查找方法列表父类缓存查找 动态方法解析动态方法决议实例方法类方法优化 消息转发…

413 Payload Too Large 问题定位

源头 一般是服务器或者nginx 配置导致的 nginx http {client_max_body_size 50m; # 调整为所需大小&#xff08;如 50MB&#xff09;# 其他配置... }nginx 不配置&#xff0c;默认是1M 服务器 spring 不配置也是有默认值的好像也是1M 如果出现413 可以试着修改配置来避…

2025年渗透测试面试题总结-360[实习]安全工程师(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 1. 自我介绍 2. WAF及其绕过方式 3. IPS/IDS/HIDS 4. 云安全 5. 绕过安骑士/安全狗 6. Gopher扩展…

Ubuntu16.04升级gcc/g++版本方法

0 前言 gcc与g分别是GNU的c和c编译器&#xff0c;Ubuntu16.04默认的gcc和g的版本是5.4.0&#xff0c;在使用一些交叉编译工具链会提示找不到GLIBC_2.27&#xff0c;而GLIBC_2.27又需要gcc 6.2以上版本&#xff0c;因此本文介绍Ubuntu16.04升级gcc/g版本的方法。 1 Ubuntu16.0…

微信小程序van-dialog确认验证失败时阻止对话框的关闭

使用官方(Vant Weapp - 轻量、可靠的小程序 UI 组件库)的before-close&#xff1a; wxml&#xff1a; <van-dialog use-slot title"名称" show"{{ show }}" show-cancel-button bind:cancel"onClose" bind:confirm"getBackInfo"…

OceanBase 的系统变量、配置项和用户变量有何差异

在继续阅读本文之前&#xff0c;大家不妨先思考一下&#xff0c;数据库中“系统变量”、“用户变量”以及“配置项”这三者之间有何不同。如果感到有些模糊&#xff0c;那么本文将是您理清这些概念的好帮手。 很多用户在使用OceanBase数据库中的“配置项”和“系统变量”&#…

【Python】Jupyter指定具体路径

一、右键Jupyter Notebook 右击Jupyter Notebook点击属性 二、修改以下两个地方

RNope:结合 RoPE 和 NoPE 的长文本建模架构

TL;DR 2025 年 Cohere 提出的一种高效且强大的长上下文建模架构——RNope-SWA。通过系统分析注意力模式、位置编码机制与训练策略&#xff0c;该架构不仅在长上下文任务上取得了当前最优的表现&#xff0c;还在短上下文任务和训练/推理效率方面实现了良好平衡。 Paper name …

virtualbox虚拟机中的ubuntu 20.04.6安装新的linux内核5.4.293 | 并增加一个系统调用 | 证书问题如何解决

参考文章&#xff1a;linux添加系统调用【简单易懂】【含32位系统】【含64位系统】_64位 32位 系统调用-CSDN博客 安装新内核 1. 在火狐下载你需要的版本的linux内核压缩包 这里我因为在windows上面下载过&#xff0c;配置过共享文件夹&#xff0c;所以直接复制粘贴通过共享文…

unity UGUI虚线框shader

Shader "Custom/DottedLineShader" {Properties{_MainTex ("Texture", 2D) "white" {}_Color("Color",COLOR) (1,1,1,1)_LineLength("虚线长度",float) 0.08}SubShader{Tags //设置支持UGUI{ "Queue""Tran…

chirpstack v4版本 全流程部署[ubuntu+docker]

背景介绍 由于chirpstackv3 版本使用的是锐米提供的版本,从网络上寻找的资源大多数都是一样的v3版本,是经过别人编译好发布出来的,原本的chirpsatck项目是运行的linxu环境下的,因此我的想法是在linux服务器上部署chirpsatckv4,暂时使用linux上的chirpstack v4版本,目前编译成e…

数字信号处理-大实验1.1

MATLAB仿真实验目录 验证实验&#xff1a;常见离散信号产生和实现验证实验&#xff1a;离散系统的时域分析应用实验&#xff1a;语音信号的基音周期&#xff08;频率&#xff09;测定 目录 一、常见离散信号产生和实现 1.1 实验目的 1.2 实验要求与内容 1.3 实验…

对抗性机器学习:AI模型安全防护新挑战

随着采用对抗性机器学习&#xff08;Adversarial Machine Learning, AML&#xff09;的AI系统融入关键基础设施、医疗健康和自动驾驶技术领域&#xff0c;一场无声的攻防战正在上演——防御方不断强化模型&#xff0c;而攻击者则持续挖掘漏洞。2025年&#xff0c;对抗性机器学习…

[[春秋云境] Privilege仿真场景

文章目录 靶标介绍&#xff1a;知识点卷影拷贝(VSS) 外网任意文件读取Jenkins管理员后台rdp远程登录Gitlab apiToken 内网搭建代理 Oracle RCESeRestorePrivilege提权mimikatzspn卷影拷贝提取SAM 参考文章 靶标介绍&#xff1a; 在这个靶场中&#xff0c;您将扮演一名资深黑客…

Redis学习打卡-Day3-分布式ID生成策略、分布式锁

分布式 ID 当单机 MySQL 已经无法支撑系统的数据量时&#xff0c;就需要进行分库分表&#xff08;推荐 Sharding-JDBC&#xff09;。在分库之后&#xff0c; 数据遍布在不同服务器上的数据库&#xff0c;数据库的自增主键已经没办法满足生成的主键全局唯一了。这个时候就需要生…

数据库第二次作业--SQL的单表查询与多表查询

单表查询 查询专业信息表中的专业名称和专业类型 SELECT Mname, Mtype FROM MajorP;查询一个学校有多少个专业 SELECT COUNT(Mno) AS 专业数量 FROM MajorP;查询学校的男女学生各有多少位 SELECT Ssex&#xff0c; COUNT(*) AS 人数 FROM StudentP GROUP BY Ssex查询每个专业…

在Cursor中启用WebStorm/IntelliJ风格快捷键

在Cursor中启用WebStorm/IntelliJ风格快捷键 方法一&#xff1a;使用预置快捷键方案 打开快捷键设置 Windows/Linux: Ctrl K → Ctrl SmacOS: ⌘ K → ⌘ S 搜索预设方案 在搜索框中输入keyboard shortcuts&#xff0c;选择Preferences: Open Keyboard Shortcuts (JSON) …

vue3:十三、分类管理-表格--编辑、新增、详情、刷新

一、效果 实现封装表格的新增、编辑、详情查看,表格刷新功能 实现表格组件中表单的封装 1、新增 如下图,新增页面显示空白的下拉,文本框,文本域,并实现提交功能 2、编辑 如下图,点击行数据,可将行数据展示到编辑弹窗,并实现提交功能 3、详情 如下图,点击行数据,…

c#基础01(.Net介绍)

文章目录 .Net平台介绍.Net平台简介跨平台开源.Net Core.Net Framework开发工具安装选项 创建项目 .Net平台介绍 .Net平台简介 .NET是一种用于构建多种应用的免费开源开放平台&#xff0c;例如&#xff1a; Web 应用、Web API 和微服务 云中的无服务器函数 云原生应用 移动…

Logrotate:配置日志轮转、高效管理Linux日志文件

Logrotate 是 Linux 系统中用于自动化管理日志文件的工具&#xff0c;能够定期轮转、压缩、删除日志文件&#xff0c;确保系统日志不会无限制增长&#xff0c;占用过多磁盘空间。 它通常由 Cron 作业定期执行&#xff0c;也可以手动触发。 1. &#x1f527; 核心功能 日志轮转…