Unity 史上用最舒服的Timer定时器插件 + 编辑器扩展 实时显示定时器调度的方法, 可跳转脚本执行方案

Unity 有多个线上项目打磨下来的定时器,集好用,安全,高效为一体

Class Timer

扩展功能展示

在这里插入图片描述

  • 点击 Jump 即可跳转到脚本定时器注册对应位置
    在这里插入图片描述

原理

我实现了一套反射查找机制,说白了就是纯字符串匹配 加上一堆正则检测 配合 AssetDatabase.OpenAsset( $文件命, $多少行 ) Api来实现的

下面介绍该定时器插件的使用方法

方法 参数 作用
Timer.Delay float delay, Action func 延迟 delay秒后 调用 func方法
Timer.Loop float interval, Action func, bool immediate = false, int times = 0 循环调用func方法, 间隔interval秒, immediate 是否立即执行一次, times <=0 永久循环,>0 执行对应次数
Timer.CallerLate Action func 在帧结束的时候执行一次 func方法, 此监听具有唯一性,即:一帧之内只作用一次,多次监听不会调用多次
Timer.Find ulong ID 通过ID来查找定时器
Timer.Find Action func 通过注册的方法来查找
Timer.Find object target 通过类对象的实例来查找其内部注册的所有定时器
Timer.Kill ulong ID 通过ID来删除定时器
Timer.Kill Action func 通过注册的方法来删除
Timer.Kill object target 传入this, 在哪个类注册的 移除的时候传这个类的this指针就可以了, 此方法会移除所有当前类监听的定时器
Timer.Kill 通过反射类的Type移除,会移除所有此Type的定时器回调,比如 Timer.Kill< Monster >() 移除所有Monster类实例的注册的定时器
Timer.KillAll 移除游戏内所有的定时器

源码仓库

github: https://github.com/badApple001/Timer

Timer.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
///
/// Timer 轻量高效定时器
/// 
/// Anchor: ChenJC
/// Time: 2022/10/09
/// Feedback: [email protected]
/// Example: https://blog.csdn.net/qq_39162566/article/details/113105351
/// </summary>
public class Timer : MonoBehaviour
{
    
    

    //定时器数据类
    public class TimerTask
    {
    
    
        public ulong ID;
        public float lifeCycle;
        public float expirationTime;
        public long times;
        public Action func;

        //你可以通过此方法来获取定时器的运行进度  0 ~ 1  1.0表示即将要调用func
        //你可以通过 Find( id ) 获取当前Task的Clone体 
        public float progress
        {
    
    
            get
            {
    
    
                return 1.0f - Mathf.Clamp01( ( expirationTime - Time.time ) / lifeCycle );
            }
        }

        //获取一个当前TimerTask的副本出来
        public TimerTask Clone( )
        {
    
    
            var timerTask = new TimerTask( );
            timerTask.ID = ID;
            timerTask.expirationTime = expirationTime;
            timerTask.lifeCycle = lifeCycle;
            timerTask.times = times;
            timerTask.func = func;
            return timerTask;
        }

        //释放回收当前定时器
        public void Recycle( )
        {
    
    
            freeTaskCls.Enqueue( this );
        }

        //刷新
        public void Refresh( )
        {
    
    
            expirationTime = Time.time + lifeCycle;
        }
    }


    #region Member property

    protected static List<TimerTask> activeTaskCls = new List<TimerTask>( );//激活中的TimerTask对象
    protected static Queue<TimerTask> freeTaskCls = new Queue<TimerTask>( );//闲置TimerTask对象
    protected static HashSet<Action> lateChannel = new HashSet<Action>( );//确保callLate调用的唯一性
    protected static ulong timerID = 1000; //timer的唯一标识

    #endregion


    #region Enable timer methods

    //每帧结束时执行回调 : 当前帧内的多次调用仅在当前帧结束的时候执行一次
    public static void CallerLate( Action func )
    {
    
    
        if ( !lateChannel.Contains( func ) )
        {
    
    
            lateChannel.Add( func );
            Delay( 0f, func );
        }
    }


    //delay秒后 执行一次回调
    public static ulong Delay( float delay, Action func )
    {
    
    
        return Loop( delay, func, false, 1 );
    }

    /// <summary>
    /// 周期性定时器 间隔一段时间调用一次
    /// </summary>
    /// <param name="interval"> 间隔时长: 秒</param>
    /// <param name="func"> 调用的方法回调 </param>
    /// <param name="immediate"> 是否立即执行一次 </param>
    /// <param name="times"> 调用的次数: 默认永久循环 当值<=0时会一直更新调用 当值>0时 循环指定次数后 停止调用 </param>
    /// <returns></returns>
    public static ulong Loop ( float interval, Action func, bool immediate = false, int times = 0 )
    {
    
    
        //从free池中 获取一个闲置的TimerTask对象
        var timer = GetFreeTimerTask( );
        timer.lifeCycle = interval;
        timer.Refresh( );
        timer.func = func;
        timer.times = times;
        timer.ID = ++timerID;

        //立即执行一次
        if ( immediate )
        {
    
    
            --timer.times;
            func?.Invoke( );
            if ( timer.times == 0 )
            {
    
    

                timer.Recycle( );
            }
            else
            {
    
    
                //添加到激活池中
                activeTaskCls.Add( timer );
            }
        }
        else
        {
    
    
            //添加到激活池中
            activeTaskCls.Add( timer );
        }

        return timerID;
    }

    #endregion


    #region Get Timer methods

    /// <summary>
    /// 通过Tag获取定时器对象
    /// </summary>
    /// <param name="ID"></param>
    /// <returns></returns>
    public static TimerTask Find( ulong ID )
    {
    
    
        return activeTaskCls.Find( ( TimerTask t ) =>
        {
    
    
            return t.ID == ID;
        } )?.Clone( );
    }

    /// <summary>
    /// 通过方法获取定时器对象
    /// </summary>
    /// <param name="tag"></param>
    /// <returns></returns>
    public static List<TimerTask> Find( Action func )
    {
    
    
        return activeTaskCls.FindAll( t =>
        {
    
    
            return t.func == func;
        } );
    }

    /// <summary>
    /// 通过对用对象获取定时器对象
    /// </summary>
    /// <param name="tag"></param>
    /// <returns></returns>
    public static List<TimerTask> Find( object target )
    {
    
    
        return activeTaskCls.FindAll( t =>
        {
    
    
            return t.func.Target == target;
        } );
    }
    #endregion


    #region Clean Timer methods

    /// <summary>
    /// 通过ID 清理定时器
    /// </summary>
    /// <param name="ID">定时器标签</param>
    /// <returns></returns>
    public static void Kill( ulong ID )
    {
    
    
        int index = activeTaskCls.FindIndex( ( TimerTask t ) =>
        {
    
    
            return t.ID == ID;
        } );

        if ( index != -1 )
        {
    
    
            var timerTask = activeTaskCls[ index ];
            KillTimers( new List<TimerTask>( ) {
    
     timerTask } );
        }
    }


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

        var allMatchTask = activeTaskCls.FindAll( t =>
        {
    
    
            if ( null != t.func && null != t.func.Target )
            {
    
    
                var fullname = t.func.Target.GetType( ).FullName;
                var currentClsNameClip = fullname.Split( '+' );
                if ( currentClsNameClip.Length > 0 )
                {
    
    
                    if ( currentClsNameClip[ 0 ] == clsName )
                    {
    
    
                        return true;
                    }
                }
            }
            return false;
        } );

        KillTimers( allMatchTask );
    }

    /// <summary>
    /// 通过方法 清理定时器
    /// </summary>
    /// <param name="func">处理方法</param>
    /// <returns></returns>
    public static void Kill( Action func )
    {
    
    
        var allMatchTask = activeTaskCls.FindAll( t => t.func == func );
        KillTimers( allMatchTask );
    }


    /// <summary>
    /// 清理当前类的所有方法
    /// 
    /// 支持清理类的成员方法相关定时器清理 也包含有限lambda的释放
    /// 
    /// 支持的lambda:
    /// 
    /// 
    /// 
    //class Mtest
    //{
    
    
    //    string str_n = "123";
    //    public Mtest()
    //    {
    
    
    //        test(); //此方法内的闭包调用支持
    //        test2(); //此方法内的闭包调用不支持
    //        Timer.Loop( 1f,UpdatePerSecond ); //成员方法支持
    //    }
    //    //这个方法内的闭包支持
    //    private void test()
    //    {
    
    
    //        Timer.Delay( 1.0f, () =>
    //        {
    
    
    //            //在lambda内部定义的变量
    //            string t = "12313213";
    //            //在lambda内部访问和修改类成员变量行为
    //            str_n = t;
    //        } );
    //    }
    //    //类成员支持
    //    private void UpdatePerSecond()
    //    {
    
    
    //        Debug.Log( Time.realtimeSinceStartup );
    //    }
    //    //这个方法内的闭包不支持
    //    private void test2()
    //    {
    
    
    //        //在lambda外定义的变量
    //        string t = "12313213";
    //        Timer.Delay( 1.0f, () =>
    //        {
    
    
    //            //在lambda内部访问lambda外部的变量行为会让当前闭包的 Target变成一个新的类
    //            t = "1231";
    //            //在lambda内部访问和修改类成员变量行为
    //            str_n = t;
    //        } );
    //    }
    //    //清理这个类的所有定时器调度
    //    private void clearTime()
    //    {
    
    
    //        Timer.Kill( this );
    //    }
    //}
    /// 
    /// 
    /// </summary>
    /// <param name="func">处理方法</param>
    /// <returns></returns>
    public static void Kill( object target )
    {
    
    
        var allMatchTask = activeTaskCls.FindAll( t => t.func.Target == target );
        KillTimers( allMatchTask );
    }


    /// <summary>
    /// 清理所有定时器
    /// </summary>
    public static void KillAll( )
    {
    
    
        lateChannel.Clear( );
        activeTaskCls.ForEach( timer => freeTaskCls.Enqueue( timer ) );
        activeTaskCls.Clear( );
    }


    /// <summary>
    /// 批量清理定时器
    /// </summary>
    /// <param name="allMatchTask"></param>
    public static void KillTimers( List<TimerTask> allMatchTask )
    {
    
    
        allMatchTask?.ForEach( task =>
        {
    
    
            if ( lateChannel.Count != 0 && lateChannel.Contains( task.func ) )
            {
    
    
                lateChannel.Remove( task.func );
            }
            activeTaskCls.Remove( task );
            freeTaskCls.Enqueue( task );
        } );
    }

    #endregion


    #region System methods

    [RuntimeInitializeOnLoadMethod( RuntimeInitializeLoadType.BeforeSceneLoad )]
    static void Init( )
    {
    
    
        activeTaskCls.Clear( );
        freeTaskCls.Clear( );
        lateChannel.Clear( );
        timerID = 1000;
        DontDestroyOnLoad( new GameObject( "Timer", typeof( Timer ) ) );
    }

    private void Awake( )
    {
    
    
        StartCoroutine( TimeElapse( ) );
    }

   
    private IEnumerator TimeElapse( )
    {
    
    
        TimerTask t = null;
        while ( true )
        {
    
    
            if ( activeTaskCls.Count > 0 )
            {
    
    
                float time = Time.time;
                for ( int i = 0; i < activeTaskCls.Count; ++i )
                {
    
    
                    t = activeTaskCls[ i ];

                    if ( t.expirationTime <= time )
                    {
    
    
                        if ( t.times == 1 )
                        {
    
    
                            activeTaskCls.RemoveAt( i-- );
                            t.Recycle( );

                            if ( lateChannel.Count != 0 && lateChannel.Contains( t.func ) )
                            {
    
    
                                lateChannel.Remove( t.func );
                            }
                        }

                        t.Refresh( );
                        --t.times;
                        t.func( );
                    }
                }
            }
            yield return 0;
        }
    }

  
    protected static TimerTask GetFreeTimerTask( )
    {
    
    
        if ( freeTaskCls.Count > 0 )
        {
    
    
            return freeTaskCls.Dequeue( );
        }
        return new TimerTask( );
    }

    #endregion

}






TimerInspector.cs

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

[CanEditMultipleObjects, CustomEditor( typeof( Timer ) )]
public class TimerInspector : Editor
{
    
    
    FieldInfo activeTaskClsInfo;
    private void OnEnable( )
    {
    
    
        activeTaskClsInfo = typeof( Timer ).GetField( "activeTaskCls", 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 );
            }
        }

    }

}