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 );
}
}
}
}