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

简介

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.timeUnscaledTime,  // Time.unscaledTimeRealTime       // 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 Timerprivate 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_EDITORusing 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 lambdabool 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 filterint methodNameIndex = line.IndexOf( methodName );if ( methodNameIndex < 0 ){return false;}char c = line[ methodNameIndex + methodName.Length ];if ( Char.IsLetter( c ) ){return false;}//return paramsif ( !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.mzph.cn/web/80919.shtml

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

相关文章

深入理解Docker和K8S

深入理解Docker和K8S Docker 是大型架构的必备技能&#xff0c;也是云原生核心。Docker 容器化作为一种轻量级的虚拟化技术&#xff0c;其核心思想&#xff1a;将应用程序及其所有依赖项打包在一起&#xff0c;形成一个可移植的单元。 容器的本质是进程&#xff1a; 容器是在…

docker中使用openresty

1.为什么要使用openresty 我这边是因为要使用1Panel&#xff0c;第一个最大的原因&#xff0c;就是图方便&#xff0c;比较可以一键安装。但以前一直都是直接安装nginx。所以需要一个过度。 2.如何查看openResty使用了nginx哪个版本 /usr/local/openresty/nginx/sbin/nginx …

CSS【详解】弹性布局 flex

适用场景 一维&#xff08;行或列&#xff09;布局 基本概念 包裹所有被布局元素的父元素为容器 所有被布局的元素为项目 项目的排列方向&#xff08;垂直/水平&#xff09;为主轴 与主轴垂直的方向交交叉轴 容器上启用 flex 布局 将容器的 display 样式设置为 flex 或 i…

全能视频处理工具介绍说明

软件介绍 本文介绍的软件是FFmpeg小白助手&#xff0c;它是一款视频处理工具。 使用便捷性 这款FFmpeg小白助手无需安装&#xff0c;解压出来就能够直接投入使用。 主要功能概述 该工具主要具备格式转换、文件裁剪、文件压缩、文件合并这四大功能。 格式转换能力 软件支持…

Linux中的DNS的安装与配置

DNS简介 DNS&#xff08;DomainNameSystem&#xff09;是互联网上的一项服务&#xff0c;它作为将域名和IP地址相互映射的一个分布式数据库&#xff0c;能够使人更方便的访问互联网。 DNS使用的是53端口 通常DNS是以UDP这个较快速的数据传输协议来查询的&#xff0c;但是没有查…

tshark的使用技巧(wireshark的命令行,类似tcpdump):转换格式,设置filter

tshark的使用技巧&#xff08;wireshark的命令行&#xff0c;类似tcpdump&#xff09;&#xff1a;转换格式&#xff0c;设置filter tshark一般在 C:\Program Files\Wireshark 使用管理员权限 打开cmd tshark -D 列出支持抓包的接口&#xff1a; c:\Program Files\Wiresh…

vscode打开的文件被覆盖/只能打开两个文件(Visual Studio Code)

vscode打开的文件被覆盖/只能打开两个文件&#xff08;Visual Studio Code&#xff09; 单击代码文件&#xff1a;是预览模式&#xff0c;只会显示有限的一两个文件&#xff0c;在一个tab里更新显示 双击代码文件&#xff1a;是编辑模式&#xff0c;可以同时显示多个代码文件…

唯创安全优化纸业车间安全环境:门口盲区预警报警器的应用与成效

一、客户现场 客户主要从事于卷烟纸、成型纸、烟草制造业用纸及其他特定用途纸类制品的加工、生产与销售。在其厂区内&#xff0c;叉车频繁作业&#xff0c;车间环境复杂。经实地查看&#xff0c;发现几大安全隐患&#xff1a; 门口拐角隐患&#xff1a;门口拐角处因卷帘门阻…

Debezium快照事件监听器系统设计

Debezium快照事件监听器系统设计 1. 系统概述 1.1 设计目标 为 Debezium 的快照过程提供可扩展的事件监听机制允许外部系统在快照过程中执行自定义逻辑提供线程安全的事件分发机制确保监听器的异常不会影响主快照流程1.2 核心功能 表快照开始事件监听表快照完成事件监听行数据…

Ubuntu 20.04安装及配置docker

在安装docker的过程中主要参考博客&#xff1a;ubuntu20.04 安装docker (详细版) 但是在测试&#xff1a; sudo docker run hello-world 时报错&#xff1a; docker: Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request ca…

第23天-Python Flet 开发指南

环境准备 pip install flet 示例1:基础计数器应用 import flet as ftdef main(page: ft.Page):page.title = "计数器"page.vertical_alignment = ft.MainAxisAlignment.CENTERtxt_number = ft.TextField(value="0", text_align=ft.TextAlign.RIGHT, wid…

牛客网NC21989:牛牛学取余

牛客网NC21989:牛牛学取余 &#x1f4dd; 题目描述 ⏱️ 限制条件 时间限制&#xff1a;C/C/Rust/Pascal 1秒&#xff0c;其他语言2秒空间限制&#xff1a;C/C/Rust/Pascal 32 M&#xff0c;其他语言64 M输入范围&#xff1a;两个整数&#xff0c;在int范围内 &#x1f4e5;…

unity XCharts插件生成曲线图在UICanvas中

【推荐100个unity插件之22】基于UGUI的功能强大的简单易用的Unity数据可视化图表插件——XCharts3.0插件的使用_unity xcharts-CSDN博客

自动驾驶传感器数据处理:Python 如何让无人车更智能?

自动驾驶传感器数据处理:Python 如何让无人车更智能? 1. 引言:为什么自动驾驶离不开数据处理? 自动驾驶一直被誉为人工智能最具挑战性的应用之一,而其背后的核心技术正是 多传感器融合与数据处理。 一辆智能驾驶汽车,通常搭载: 激光雷达(LiDAR) —— 3D 环境感知,…

MCU 上电不启动的常见原因分析与排查思路

在开发过程中&#xff0c;“MCU 上电不运行”是我们经常遇到的问题之一。但客户对此类问题的描述往往较为模糊&#xff0c;仅简单表示“产品不工作”或“怀疑 MCU 没有运行”&#xff0c;这给我们现场排查带来了较大的挑战。即便工程师到达现场&#xff0c;往往也无法迅速定位问…

React中使用 Ant Design Charts 图表

// 引入 Ant Design Charts 的柱状图组件 Column import { Column } from ant-design/charts;// 定义函数组件 App&#xff0c;用于展示柱状图 function App() {// 数据源&#xff1a;每个对象代表一个柱子&#xff0c;包含类型&#xff08;type&#xff09;和销售额&#xff0…

30、WebAssembly:古代魔法——React 19 性能优化

一、符文编译术&#xff08;编译优化&#xff09; 1. 语言选择与量子精简 // Rust编译优化 cargo build --target wasm32-wasi --release 魔法特性&#xff1a; • 选择低运行时开销语言&#xff08;如Rust/C&#xff09;&#xff0c;编译后文件比Swift小4倍 • --rel…

初识计算机网络。计算机网络基本概念,分类,性能指标

初识计算机网络。计算机网络基本概念&#xff0c;分类&#xff0c;性能指标 本系列博客源自作者在大二期末复习计算机网络时所记录笔记&#xff0c;看的视频资料是B站湖科大教书匠的计算机网络微课堂&#xff0c;祝愿大家期末都能考一个好成绩&#xff01; 视频链接地址 一、…

深入理解 MongoDB 的 _id 和 ObjectId:从原理到实践

在 MongoDB 的世界中&#xff0c;_id 字段和 ObjectId 是每个开发者都必须理解的核心概念。作为 MongoDB 文档的唯一标识符&#xff0c;它们不仅影响着数据库的设计&#xff0c;也直接关系到应用的性能和扩展性。本文将全面剖析 _id 和 ObjectId 的工作原理、实际应用场景以及最…

计算机视觉与深度学习 | Python实现ARIMA-WOA-CNN-LSTM时间序列预测(完整源码和数据

以下是一个结合ARIMA、鲸鱼优化算法(WOA)、CNN和LSTM进行时间序列预测的Python实现框架。由于完整代码和数据量较大,此处提供核心代码结构和示例数据集,您可根据需求扩展。 1. 数据准备(示例数据) 使用airline-passengers.csv(航空乘客数据集): import pandas as pd…