引言
在 Unity 开发中,计时器(Timer)是一个基础但不可或缺的功能模块。无论是确保运行时事件按时发生,还是实现循环执行逻辑,计时器都发挥着至关重要的作用。
在成熟的 Unity 项目中,计时器往往作为核心组件被精心设计和封装,使得开发者能够专注于业务逻辑的实现,而无需深究其内部细节。然而,对于追求卓越的程序员而言,重复造轮子不仅是一种挑战自我、提升能力的途径,更是拓展设计思维、深入理解游戏开发底层逻辑的必经之路。
因此,本篇博客旨在分享我在 Unity 中开发计时器的经验和心得。我将从实际需求出发,详细介绍计时器的核心设计思路、实现方法。同时,我也会通过示例代码来展示计时器的实现全过程,让读者能够更直观地理解并掌握相关知识。
业务诉求
延迟执行
在日常业务开发中,"延迟执行"是极为常见的功能需求。举例来说:
- 用户引导:当界面打开后,若用户在接下来的10秒内未进行任何操作,系统将自动播放引导动画,以提示用户执行某项操作。在这个需求中,引导动画的播放被设置为在界面开启后的10秒延迟执行。
- 自动关闭功能:游戏结束后,玩家将进入结算界面。在此界面,系统会启动一个5秒的倒计时。倒计时结束后,结算界面将自动关闭。这一功能确保了结算界面在开启后的5秒自动执行关闭操作。
循环执行
循环触发某项操作也是日常业务开发中的最常见的需求之一。举例来说:
- 循环播放背景音乐:尽管在Unity中,AudioSource组件可以通过设置参数实现背景音乐的循环播放,但这一机制并不支持为每次播放结束添加监听事件。因此,若需要在每次音乐播放完毕后执行某些操作,如更换曲目或触发动画,就需要通过计时器的方式来实现。
- 塔防游戏循环攻击:在塔防游戏中,为了保持游戏的节奏和紧张感,通常需要塔楼每间隔一定时间就进行一次攻击。例如,某一塔楼每间隔2秒就需要发动攻击,并且在整局游戏中循环执行此操作,以确保敌人无法轻易突破防线。这种周期性的行为就需要通过循环触发机制来实现。
技术背景
常见的实现方法
Invoke
scss
Invoke("MethodName", 0.3f);//在0.3秒后执行MethodName方法
Coroutine
arduino
public IEnumerator DelayCall(Action callback, float delayTime)
{
yield return new WaitForSeconds(delayTime);
callback.Invoke();
}
Update
csharp
private float lastCallTime;
private float intervalTime;
public void Update()
{
if(Time.time - lastCallTime >= intervalTime)
{
//Call
lastCallTime = Time.time;
}
}
Tween.InsertCallback
ini
var Sequence = DOTween.Sequence();
Sequence.InsertCallback(Method);
Sequence.Play();
链式调用
什么是链式调用
链式调用是一种编程模式。它允许调用者在同一对象上连续调用方法,而无需在每次调用完后重新指定对象。在 Unity 中,最常见也最著名的链式调用范例就是DoTween插件了,例如:
scss
myTrans.DOAnchorPos(Vector2.zero, 0.3f)
.SetEase(Ease.Linear)
.SetDelay(0.3f)
.onComplete = () =>
{
Debug.Log("链式调用");
}
优点
- 保持代码的简洁和易读性。链式调用的核心优势在于,它允许开发者通过连续调用一系列方法来设置对象的属性或执行相关操作。这种方式避免了传统方法中需要将所有参数一次性传入的情况,使得代码更加简洁。每个方法都专注于一个特定的功能,调用者只需按需调用,无需关注复杂的内部逻辑,从而提高了代码的可读性。
- 提升开发效率。链式调用的设计有助于开发者快速理解并操作对象。通过为每个参数提供一个专门的设置方法,开发者可以直观地了解每个方法的用途,而无需深入查看模块的内部实现。这大大降低了学习和理解成本,从而提高了开发效率。
- 易于拓展和维护。链式调用的结构使得代码更加模块化,每个方法都是独立的单元。因此,在后期需要添加新的参数或功能时,只需新增相应的设置方法,而无需对现有代码进行大规模的修改。这种设计使得代码更加灵活,易于扩展和维护。
缺点
- 调试困难。链式调用的一个潜在问题是,当在链的前端设置了错误的参数时,这可能导致链的后端出现问题。由于所有的操作都是串联在一起的,这种情况会使得定位问题的源头变得尤为困难。开发者需要仔细跟踪整个链式调用的过程,才能准确找到问题的根源,这无疑增加了调试的复杂性和时间成本。
- 可读性问题。虽然链式调用在简单业务场景中能够提高代码的可读性,但在处理复杂业务逻辑时,过度使用链式调用可能会导致代码可读性下降。如果业务逻辑本身就极为复杂,再加上一连串的链式调用,那么代码将变得难以理解和维护。此外,当模块内的参数过多时,采用链式调用也可能导致代码的可读性下降。在这种情况下,直接暴露对象内的成员变量给外部赋值可能是一个更为合理的选择。
如何开发出自己的Timer呢?
本次 Timer 的核心实现思路如下:
- Timer 封装自身行为,如开始(Start)、结束(Stop)、暂停(Pause)、继续(Resume)行为
- Timer 监听 MonoBehavior 的 Update 事件函数,以实现时间更新逻辑
- 为了方便设置参数,Timer 在具体使用上,采用链式调用的方式实现
csharp
public class Timer
{
/// <summary>
/// 开启Timer
/// </summary>
/// <returns>这个Timer本身</returns>
public Timer Start(){}
/// <summary>
/// 停止Timer
/// </summary>
/// <param name="path"></param>
/// <returns>这个Timer本身</returns>
public Timer Stop(){}
/// <summary>
/// 暂停Timer
/// </summary>
/// <returns>这个Timer本身</returns>
public Timer Pause(){}
/// <summary>
/// 继续Timer
/// </summary>
/// <returns>这个Timer本身</returns>
public Timer Resume(){}
/// <summary>
/// 设置时间间隔
/// </summary>
/// <param name="interval">时间间隔,浮点数</param>
/// <returns>这个Timer本身</returns>
public Timer SetInterval(float interval){}
/// <summary>
/// 设置callback的循环执行次数
/// </summary>
/// <param name="repeatTimes">循环次数,整型</param>
/// <returns>这个Timer本身</returns>
public Timer SetRepeatTimes(int repeatTimes){}
/// <summary>
/// 设置回调
/// </summary>
/// <param name="callback">回调</param>
/// <returns>这个Timer本身</returns>
public Timer SetCallback(Action<float> callback){}
/// <summary>
/// 更新时间
/// </summary>
/// <param name="detalTime">上一次调用和本次调用的时间偏移,单位秒</param>
/// <returns>这个Timer本身</returns>
private void UpdateMe(float detalTime){}
}
ini
public class Timer
{
private int _repeatTimes;
private float _interval;
private Action<int> _callback;
private bool _isStarted;
private int _curRepeatTimes;
private float _timeSinceStartup;
private float _lastCallTime;
public Timer Start()
{
if(_isStarted)
{
Debug.Log("[Timer]:timer already started");
return null;
}
_isStarted = true;
}
public Timer Stop()
{
if(!_isStarted)
{
Debug.Log("[Timer]:timer is not start");
return null;
}
_isStarted = false;
}
public Timer Pause()
{
if(!_isStarted)
{
Debug.Log("[Timer]:timer is not start");
return null;
}
_isStarted = false;
}
public Timer Resume()
{
if(_isStarted)
{
Debug.Log("[Timer]:timer already started");
return null;
}
_isStarted = true;
}
public Timer SetInterval(float interval)
{
if(interval < 0)
{
interval = 0;
Debug.LogWarning("[Timer]:interval must be greater than zero!");
}
this._interval = interval;
return this;
}
public Timer SetRepeatTimes(int repeatTimes)
{
if(repeatTimes < 0)
{
repeatTimes = 1;
Debug.LogWarning("[Timer]:repeatTimes must be greater than zero!");
}
this._repeatTimes = repeatTimes;
return this;
}
public Timer SetCallback(Action<float> callback)
{
if(callback == null)
{
Debug.LogError("[Timer]:callback cant be null!");
return null;
}
this._callback = callback;
return this;
}
private void UpdateMe(float detalTime)
{
if(!_isStarted)
{
return;
}
_timeSinceStartup += detalTime;
if(_timeSinceStartup - _lastCallTime >= _interval)
{
_lastCallTime = _timeSinceStartup;
_curRepeatTimes += 1;
_callback.Invoke(_curRepeatTimes);
if(_curRepeatTimes >= _repeatTimes)
{
Stop();
}
}
}
}
csharp
public class UpdateRegister: MonoBehavoir
{
private static List<Action<float>> _invokeUpdateEvents;
private static List<Action<float>> _addUpdateEvents;
private static List<Action<float>> _removeUpdateEvents;
/// <summary>
/// 注册监听Update
/// </summary>
/// <param name="updateEvent">监听事件</param>
/// <returns></returns>
public static void Register(Action<float> updateEvent)
{
if(updateEvent == null)
{
Debug.LogError("[UpdateRegister]:updateEvent is null!");
return;
}
_addUpdateEvents.Add(updateEvent);
}
/// <summary>
/// 取消监听
/// </summary>
/// <param name="updateEvent">取消监听事件</param>
/// <returns></returns>
public static void UnRegister(Action<float> updateEvent)
{
if(updateEvent == null)
{
Debug.LogError("[UpdateRegister]:updateEvent is null!");
return;
}
_removeUpdateEvent.Add(updateEvent);
}
private void Awake()
{
_invokeUpdateEvents = new List<Action<float>>();
_addUpdateEvents = new List<Action<float>>();
_removeUpdateEvents = new List<Action<float>>();
}
private void Update()
{
foreach(var @event in _invokeUpdateEvents)
{
@event.Invoke(Time.detalTime);
}
//不能在_invokeUpdateEvents循环的中途,执行删除、增加操作
foreach(var @event in _removeUpdateEvents)
{
_invokeUpdateEvents.Remove(@event);
}
_removeUpdateEvents.Clear();
foreach(var @event in _addUpdateEvents)
{
_invokeUpdateEvents.Add(@event);
}
_addUpdateEvents.Clear();
}
}
kotlin
public class Timer
{
public Timer Start()
{
if(_isStarted)
{
Debug.Log("[Timer]:timer is already started");
return null;
}
UpdateRegister.Register(this.UpdateMe);//启用时,监听Update监听
_isStarted = true;
}
public Timer Stop()
{
if(!_isStarted)
{
Debug.Log("[Timer]:timer is not start");
return null;
}
_isStarted = false;
UpdateRegister.UnRegister(this.UpdateMe);//停止时,取消监听
}
}
完整代码
csharp
public delegate void TimerCallback(int curRepeatTimes);
csharp
public class Timer
{
private int _repeatTimes;
private float _interval;
private TimerCallback _callback;
private bool _isStarted;
private int _curRepeatTimes;
private float _timeSinceStartup;
private float _lastCallTime;
/// <summary>
/// 开启Timer
/// </summary>
/// <returns>这个Timer本身</returns>
public Timer Start()
{
if(_isStarted)
{
Debug.Log("[Timer]:timer is already started");
return null;
}
UpdateRegister.Register(this.UpdateMe);//启用时,监听Update监听
_isStarted = true;
}
/// <summary>
/// 停止Timer
/// </summary>
/// <param name="path"></param>
/// <returns>这个Timer本身</returns>
public Timer Stop()
{
if(!_isStarted)
{
Debug.Log("[Timer]:timer is not start");
return null;
}
_isStarted = false;
UpdateRegister.UnRegister(this.UpdateMe);//停止时,取消监听
}
/// <summary>
/// 暂停Timer
/// </summary>
/// <returns>这个Timer本身</returns>
public Timer Pause()
{
if(!_isStarted)
{
Debug.Log("[Timer]:timer is not start");
return null;
}
_isStarted = false;
}
/// <summary>
/// 继续Timer
/// </summary>
/// <returns>这个Timer本身</returns>
public Timer Resume()
{
if(_isStarted)
{
Debug.Log("[Timer]:timer already started");
return null;
}
_isStarted = true;
}
/// <summary>
/// 设置时间间隔
/// </summary>
/// <param name="interval">时间间隔,浮点数</param>
/// <returns>这个Timer本身</returns>
public Timer SetInterval(float interval)
{
if(interval < 0)
{
interval = 0;
Debug.LogWarning("[Timer]:interval must be greater than zero!");
}
this._interval = interval;
return this;
}
/// <summary>
/// 设置callback的循环执行次数
/// </summary>
/// <param name="repeatTimes">循环次数,整型</param>
/// <returns>这个Timer本身</returns>
public Timer SetRepeatTimes(int repeatTimes)
{
if(repeatTimes < 0)
{
repeatTimes = 1;
Debug.LogWarning("[Timer]:repeatTimes must be greater than zero!");
}
this._repeatTimes = repeatTimes;
return this;
}
/// <summary>
/// 设置回调
/// </summary>
/// <param name="callback">回调</param>
/// <returns>这个Timer本身</returns>
public Timer SetCallback(TimerCallback callback)
{
if(callback == null)
{
Debug.LogError("[Timer]:callback cant be null!");
return null;
}
this._callback = callback;
return this;
}
/// <summary>
/// 更新时间
/// </summary>
/// <param name="detalTime">上一次调用和本次调用的时间偏移,单位秒</param>
/// <returns>这个Timer本身</returns>
private void UpdateMe(float detalTime)
{
if(!_isStarted)
{
return;
}
_timeSinceStartup += detalTime;
if(_timeSinceStartup - _lastCallTime >= _interval)
{
_lastCallTime = _timeSinceStartup;
_curRepeatTimes += 1;
_callback.Invoke(_curRepeatTimes);
if(_curRepeatTimes >= _repeatTimes)
{
Stop();
}
}
}
}
csharp
public delegate void UpdateDelegate(float detalTime);
less
public class UpdateRegister: MonoBehavoir
{
private static List<UpdateDelegate> _invokeUpdateEvents;
private static List<UpdateDelegate> _addUpdateEvents;
private static List<UpdateDelegate> _removeUpdateEvents;
/// <summary>
/// 注册监听Update
/// </summary>
/// <param name="updateEvent">监听事件</param>
/// <returns></returns>
public static void Register(UpdateDelegate updateEvent)
{
if(updateEvent == null)
{
Debug.LogError("[UpdateRegister]:updateEvent is null!");
return;
}
_addUpdateEvents.Add(updateEvent);
}
/// <summary>
/// 取消监听
/// </summary>
/// <param name="updateEvent">取消监听事件</param>
/// <returns></returns>
public static void UnRegister(UpdateDelegate updateEvent)
{
if(updateEvent == null)
{
Debug.LogError("[UpdateRegister]:updateEvent is null!");
return;
}
_removeUpdateEvent.Add(updateEvent);
}
private void Awake()
{
_invokeUpdateEvents = new List<UpdateDelegate>();
_addUpdateEvents = new List<UpdateDelegate>();
_removeUpdateEvents = new List<UpdateDelegate>();
}
private void Update()
{
foreach(var @event in _invokeUpdateEvents)
{
@event.Invoke(Time.detalTime);
}
foreach(var @event in _removeUpdateEvents)
{
_invokeUpdateEvents.Remove(@event);
}
_removeUpdateEvents.Clear();
foreach(var @event in _addUpdateEvents)
{
_invokeUpdateEvents.Add(@event);
}
_addUpdateEvents.Clear();
}
}