11.计时器模块
11.1 知识点
计时器模块的作用
在游戏开发中,我们经常需要使用计时功能,例如:
- 延时执行:10秒后执行某个逻辑
- 间隔执行:每隔1秒执行一次某个逻辑
- 倒计时显示:UI上显示剩余时间
- 技能冷却:技能释放后需要等待一段时间才能再次使用
Unity自带的计时功能:
Invoke:延迟执行逻辑InvokeRepeating:间隔一定时间执行逻辑Coroutine:协程实现计时Update中自己实现计时
Unity自带计时功能的问题:
- 只能为继承了MonoBehaviour的类服务:非MonoBehaviour类无法使用
- 代码冗余:每个需要计时的类都要重复编写类似的代码
- 难以统一管理:无法统一控制所有计时器的开启、关闭、暂停等
解决方案:
通过独立的计时器管理模块,统一管理所有计时功能,提供延时执行、间隔执行等功能,让所有类都能使用计时功能。
主要功能:
- 延时执行:指定时间后执行回调函数
- 间隔执行:每隔指定时间执行一次回调函数
- 计时器控制:创建、移除、暂停、恢复、重置计时器
- 真实时间计时:支持不受
Time.timeScale影响的计时器 - 对象池管理:使用对象池减少内存分配
计时器模块的基本原理
设计理念:
- 统一管理:所有计时器由一个管理器统一管理
- 唯一标识:每个计时器有唯一的ID用于控制
- 协同程序:使用协程实现固定间隔的时间检测(每100毫秒检测一次)
- 委托回调:使用UnityAction实现延时回调和间隔回调
- 对象池:使用对象池管理TimerItem对象,减少内存分配
工作流程:
- 创建计时器:调用
CreateTimer创建计时器,返回唯一ID - 时间检测:协程每100毫秒遍历所有计时器,更新剩余时间
- 间隔回调:如果设置了间隔时间,到时间后触发间隔回调
- 完成回调:总时间结束后触发完成回调,并回收计时器
计时器模块基础实现
为什么需要计时器模块
传统方式的问题:
// 传统方式1:使用Invoke(只能用于MonoBehaviour)
public class Skill : MonoBehaviour
{
void UseSkill()
{
// 5秒后执行技能结束逻辑
Invoke("OnSkillEnd", 5f);
}
void OnSkillEnd()
{
// 技能结束逻辑
}
}
存在的问题:
- 只能继承MonoBehaviour:普通C#类无法使用
- 灵活性差:无法动态创建和管理多个计时器
- 难以扩展:无法暂停、恢复、重置计时器
传统方式2:自己写协程(代码冗余)
public class Player : MonoBehaviour
{
private Coroutine timerCoroutine;
void StartCountdown(int seconds)
{
timerCoroutine = StartCoroutine(Countdown(seconds, () =>
{
Debug.Log("倒计时结束");
}));
}
IEnumerator Countdown(int time, UnityAction callBack)
{
int currentTime = time;
while (currentTime > 0)
{
yield return new WaitForSeconds(1);
currentTime--;
Debug.Log($"剩余时间:{currentTime}");
}
callBack?.Invoke();
}
}
存在的问题:
- 代码重复:每个类都需要写类似的代码
- 难以管理:无法统一控制所有计时器
- 资源浪费:频繁创建协程产生内存分配
计时器模块基础实现
实现思路:
- 创建TimerItem类:存储计时器的数据(总时间、间隔时间、回调函数等)
- 创建TimerMgr管理器:统一管理所有计时器
- 使用协程:每100毫秒检测一次所有计时器
- 使用字典:用ID作为键存储所有计时器
- 对象池:使用对象池管理TimerItem对象
TimerItem实现:TimerItem.cs
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 计时器对象
/// 里面存储了计时器的相关数据
/// </summary>
public class TimerItem
{
#region 字段
/// <summary>
/// 唯一ID
/// </summary>
public int keyID;
/// <summary>
/// 计时结束后的委托回调
/// </summary>
public UnityAction overCallBack;
/// <summary>
/// 间隔一定时间去执行的委托回调
/// </summary>
public UnityAction callBack;
/// <summary>
/// 表示计时器总的计时时间,毫秒:1s = 1000ms
/// </summary>
public int allTime;
/// <summary>
/// 记录一开始计时时的总时间,用于时间重置
/// </summary>
public int maxAllTime;
/// <summary>
/// 间隔执行回调的时间,毫秒:1s = 1000ms
/// </summary>
public int intervalTime;
/// <summary>
/// 记录一开始的间隔时间
/// </summary>
public int maxIntervalTime;
/// <summary>
/// 是否在进行计时
/// </summary>
public bool isRuning;
#endregion
#region 方法
/// <summary>
/// 初始化计时器数据
/// </summary>
/// <param name="keyID">唯一ID</param>
/// <param name="allTime">总的时间</param>
/// <param name="overCallBack">总时间计时结束后的回调</param>
/// <param name="intervalTime">间隔执行的时间</param>
/// <param name="callBack">间隔执行时间结束后的回调</param>
public void InitInfo(int keyID, int allTime, UnityAction overCallBack, int intervalTime = 0, UnityAction callBack = null)
{
this.keyID = keyID;
this.maxAllTime = this.allTime = allTime;
this.overCallBack = overCallBack;
this.maxIntervalTime = this.intervalTime = intervalTime;
this.callBack = callBack;
this.isRuning = true;
}
/// <summary>
/// 重置计时器
/// </summary>
public void ResetTimer()
{
this.allTime = this.maxAllTime;
this.intervalTime = this.maxIntervalTime;
this.isRuning = true;
}
/// <summary>
/// 缓存池回收时,清除相关引用数据
/// </summary>
public void ResetInfo()
{
overCallBack = null;
callBack = null;
}
#endregion
}
TimerMgr基础实现:TimerMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 计时器管理器
/// 统一管理所有计时器
/// </summary>
public class TimerMgr : Singleton<TimerMgr>
{
#region 字段
/// <summary>
/// 用于记录当前将要创建的唯一ID
/// </summary>
private int TIMER_KEY = 0;
/// <summary>
/// 用于存储管理所有计时器的字典容器
/// </summary>
private Dictionary<int, TimerItem> timerDic = new Dictionary<int, TimerItem>();
/// <summary>
/// 待移除列表
/// </summary>
private List<TimerItem> delList = new List<TimerItem>();
/// <summary>
/// 计时器协程
/// </summary>
private Coroutine timer;
/// <summary>
/// 计时器管理器中的唯一计时用的协同程序的间隔时间
/// </summary>
private const float intervalTime = 0.1f;
#endregion
#region 构造函数
private TimerMgr()
{
// 注册TimerItem对象池
TimerItemPool timerItemPool = new TimerItemPool();
PoolMgr.Instance.RegisterObjPool<TimerItem>(timerItemPool);
// 默认计时器就是开启的
Start();
}
#endregion
#region 计时器管理
/// <summary>
/// 开启计时器管理器的方法
/// </summary>
public void Start()
{
timer = MonoMgr.Instance.StartCoroutine(StartTiming());
}
/// <summary>
/// 关闭计时器管理器的方法
/// </summary>
public void Stop()
{
MonoMgr.Instance.StopCoroutine(timer);
}
/// <summary>
/// 计时的核心协程
/// </summary>
private IEnumerator StartTiming()
{
while (true)
{
// 100毫秒进行一次计时
yield return new WaitForSeconds(intervalTime);
// 遍历所有的计时器 进行数据更新
foreach (TimerItem item in timerDic.Values)
{
if (!item.isRuning)
continue;
// 判断计时器是否有间隔时间执行的需求
if (item.callBack != null)
{
// 减去100毫秒
item.intervalTime -= (int)(intervalTime * 1000);
// 满足一次间隔时间执行
if (item.intervalTime <= 0)
{
// 间隔一定时间 执行一次回调
item.callBack.Invoke();
// 重置间隔时间
item.intervalTime = item.maxIntervalTime;
}
}
// 总的时间更新
item.allTime -= (int)(intervalTime * 1000);
// 计时时间到 需要执行完成回调函数
if (item.allTime <= 0)
{
item.overCallBack.Invoke();
delList.Add(item);
}
}
// 移除待移除列表中的数据
for (int i = 0; i < delList.Count; i++)
{
// 从字典中移除
timerDic.Remove(delList[i].keyID);
// 放入缓存池中
PoolMgr.Instance.ReturnObj<TimerItem>(delList[i]);
}
// 移除结束后 清空列表
delList.Clear();
}
}
#endregion
#region 计时器控制
/// <summary>
/// 创建单个计时器
/// </summary>
/// <param name="allTime">总的时间,毫秒:1s=1000ms</param>
/// <param name="overCallBack">总时间结束回调</param>
/// <param name="intervalTime">间隔计时时间,毫秒:1s=1000ms</param>
/// <param name="callBack">间隔计时时间结束回调</param>
/// <returns>返回唯一ID 用于外部控制对应计时器</returns>
public int CreateTimer(int allTime, UnityAction overCallBack, int intervalTime = 0, UnityAction callBack = null)
{
// 构建唯一ID
int keyID = ++TIMER_KEY;
// 从缓存池取出对应的计时器
TimerItem timerItem = PoolMgr.Instance.GetObj<TimerItem>();
// 初始化数据
timerItem.InitInfo(keyID, allTime, overCallBack, intervalTime, callBack);
// 记录到字典中 进行数据更新
timerDic.Add(keyID, timerItem);
return keyID;
}
/// <summary>
/// 移除单个计时器
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void RemoveTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
// 移除对应id计时器 放入缓存池
PoolMgr.Instance.ReturnObj<TimerItem>(timerDic[keyID]);
// 从字典中移除
timerDic.Remove(keyID);
}
}
/// <summary>
/// 重置单个计时器
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void ResetTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].ResetTimer();
}
}
/// <summary>
/// 开启单个计时器,主要用于暂停后重新开始
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void StartTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].isRuning = true;
}
}
/// <summary>
/// 停止单个计时器,主要用于暂停
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void StopTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].isRuning = false;
}
}
#endregion
}
基础实现的局限性
存在的问题:
- 受Time.timeScale影响:游戏暂停时计时器也会暂停
- 无法区分计时类型:所有计时器都使用相同的时间系统
- 应用场景受限:某些场景需要真实时间计时(如网络请求超时)
计时器模块支持真实时间计时
为什么需要真实时间计时
应用场景:
- 游戏暂停时UI倒计时继续:暂停菜单上的时间显示不停止
- 网络请求超时:需要真实时间,不受游戏暂停影响
- 广告播放计时:广告倒计时需要真实时间
- 限时活动:活动倒计时不受游戏状态影响
问题分析:
当前计时器使用yield return new WaitForSeconds(0.1f),会受到Time.timeScale的影响。当游戏暂停时(Time.timeScale = 0),所有协程都会停止。
解决方案:
使用yield return new WaitForSecondsRealtime(0.1f)创建不受Time.timeScale影响的协程。
真实时间计时实现
实现思路:
- 添加真实时间字典:
realTimerDic存储不受Time.timeScale影响的计时器 - 添加真实时间协程:单独开一个协程处理真实时间计时
- 修改CreateTimer方法:添加
isRealTime参数用于区分计时类型 - 优化WaitForSeconds:使用成员变量避免重复创建
- 修改所有控制方法:支持在两种字典中查找和操作
TimerMgr进阶版:TimerMgr.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 计时器管理器
/// 统一管理所有计时器
/// 支持普通计时器和真实时间计时器
/// </summary>
public class TimerMgr : Singleton<TimerMgr>
{
#region 字段
/// <summary>
/// 用于记录当前将要创建的唯一ID
/// </summary>
private int TIMER_KEY = 0;
/// <summary>
/// 用于存储管理所有计时器的字典容器(受Time.timeScale影响)
/// </summary>
private Dictionary<int, TimerItem> timerDic = new Dictionary<int, TimerItem>();
/// <summary>
/// 用于存储管理所有计时器的字典容器(不受Time.timeScale影响)
/// </summary>
private Dictionary<int, TimerItem> realTimerDic = new Dictionary<int, TimerItem>();
/// <summary>
/// 待移除列表
/// </summary>
private List<TimerItem> delList = new List<TimerItem>();
/// <summary>
/// 为了避免内存的浪费 每次while都会生成
/// 我们直接将其声明为成员变量
/// </summary>
private WaitForSecondsRealtime waitForSecondsRealtime = new WaitForSecondsRealtime(intervalTime);
private WaitForSeconds waitForSeconds = new WaitForSeconds(intervalTime);
/// <summary>
/// 计时器协程
/// </summary>
private Coroutine timer;
/// <summary>
/// 真实时间计时器协程
/// </summary>
private Coroutine realTimer;
/// <summary>
/// 计时器管理器中的唯一计时用的协同程序的间隔时间
/// </summary>
private const float intervalTime = 0.1f;
#endregion
#region 构造函数
private TimerMgr()
{
// 注册TimerItem对象池
TimerItemPool timerItemPool = new TimerItemPool();
PoolMgr.Instance.RegisterObjPool<TimerItem>(timerItemPool);
// 默认计时器就是开启的
Start();
}
#endregion
#region 计时器管理
/// <summary>
/// 开启计时器管理器的方法
/// </summary>
public void Start()
{
timer = MonoMgr.Instance.StartCoroutine(StartTiming(false, timerDic));
realTimer = MonoMgr.Instance.StartCoroutine(StartTiming(true, realTimerDic));
}
/// <summary>
/// 关闭计时器管理器的方法
/// </summary>
public void Stop()
{
MonoMgr.Instance.StopCoroutine(timer);
MonoMgr.Instance.StopCoroutine(realTimer);
}
/// <summary>
/// 计时的核心协程
/// </summary>
/// <param name="isRealTime">是否真实时间</param>
/// <param name="timerDic">要使用的字典</param>
/// <returns>协程迭代器</returns>
private IEnumerator StartTiming(bool isRealTime, Dictionary<int, TimerItem> timerDic)
{
while (true)
{
// 100毫秒进行一次计时
if (isRealTime)
{
yield return waitForSecondsRealtime;
}
else
{
yield return waitForSeconds;
}
// 遍历所有的计时器 进行数据更新
foreach (TimerItem item in timerDic.Values)
{
if (!item.isRuning)
{
continue;
}
// 判断计时器是否有间隔时间执行的需求
if (item.callBack != null)
{
// 减去100毫秒
item.intervalTime -= (int)(intervalTime * 1000);
// 满足一次间隔时间执行
if (item.intervalTime <= 0)
{
// 间隔一定时间 执行一次回调
item.callBack.Invoke();
// 重置间隔时间
item.intervalTime = item.maxIntervalTime;
}
}
// 总的时间更新
item.allTime -= (int)(intervalTime * 1000);
// 计时时间到 需要执行完成回调函数
if (item.allTime <= 0)
{
item.overCallBack.Invoke();
delList.Add(item);
}
}
// 移除待移除列表中的数据
for (int i = 0; i < delList.Count; i++)
{
// 从字典中移除
timerDic.Remove(delList[i].keyID);
// 放入缓存池中
PoolMgr.Instance.ReturnObj<TimerItem>(delList[i]);
}
// 移除结束后 清空列表
delList.Clear();
}
}
#endregion
#region 计时器控制
/// <summary>
/// 创建单个计时器
/// </summary>
/// <param name="isRealTime">如果是true不受Time.timeScale影响</param>
/// <param name="allTime">总的时间,毫秒:1s=1000ms</param>
/// <param name="overCallBack">总时间结束回调</param>
/// <param name="intervalTime">间隔计时时间,毫秒:1s=1000ms</param>
/// <param name="callBack">间隔计时时间结束回调</param>
/// <returns>返回唯一ID 用于外部控制对应计时器</returns>
public int CreateTimer(bool isRealTime, int allTime, UnityAction overCallBack, int intervalTime = 0, UnityAction callBack = null)
{
// 构建唯一ID
int keyID = ++TIMER_KEY;
// 从缓存池取出对应的计时器
TimerItem timerItem = PoolMgr.Instance.GetObj<TimerItem>();
// 初始化数据
timerItem.InitInfo(keyID, allTime, overCallBack, intervalTime, callBack);
// 记录到字典中 进行数据更新
if (isRealTime)
{
realTimerDic.Add(keyID, timerItem);
}
else
{
timerDic.Add(keyID, timerItem);
}
return keyID;
}
/// <summary>
/// 移除单个计时器
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void RemoveTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
// 移除对应id计时器 放入缓存池
PoolMgr.Instance.ReturnObj<TimerItem>(timerDic[keyID]);
// 从字典中移除
timerDic.Remove(keyID);
}
else if (realTimerDic.ContainsKey(keyID))
{
// 移除对应id计时器 放入缓存池
PoolMgr.Instance.ReturnObj<TimerItem>(realTimerDic[keyID]);
// 从字典中移除
realTimerDic.Remove(keyID);
}
}
/// <summary>
/// 重置单个计时器
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void ResetTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].ResetTimer();
}
else if (realTimerDic.ContainsKey(keyID))
{
realTimerDic[keyID].ResetTimer();
}
}
/// <summary>
/// 开启单个计时器,主要用于暂停后重新开始
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void StartTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].isRuning = true;
}
else if (realTimerDic.ContainsKey(keyID))
{
realTimerDic[keyID].isRuning = true;
}
}
/// <summary>
/// 停止单个计时器,主要用于暂停
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void StopTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].isRuning = false;
}
else if (realTimerDic.ContainsKey(keyID))
{
realTimerDic[keyID].isRuning = false;
}
}
#endregion
}
TimerItemPool实现:TimerItemPool.cs
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// TimerItem对象池
/// 自定义TimerItem的重置逻辑
/// </summary>
public class TimerItemPool : ObjPool<TimerItem>
{
#region 构造函数
/// <summary>
/// 初始化构造函数
/// </summary>
public TimerItemPool() : base()
{
}
#endregion
#region 重写方法
/// <summary>
/// 将对象放回对象池时的清理
/// </summary>
/// <param name="obj">放回的TimerItem对象</param>
protected override void OnReturnObject(TimerItem obj)
{
// 重置TimerItem的数据
obj.ResetInfo();
}
#endregion
}
使用示例:
using UnityEngine;
/// <summary>
/// 计时器模块使用测试
/// 演示计时器的各种功能
/// </summary>
public class TimerModuleTest : MonoBehaviour
{
private int timerID1;
private int timerID2;
private int timerID3;
void Start()
{
Debug.Log("计时器模块测试:开始");
// 测试1:基础延时执行
TestBasicTimer();
// 测试2:间隔执行
TestIntervalTimer();
// 测试3:真实时间计时器
TestRealTimeTimer();
}
/// <summary>
/// 测试基础延时执行
/// </summary>
private void TestBasicTimer()
{
Debug.Log("测试1:基础延时执行");
// 创建一个5秒后执行的计时器
timerID1 = TimerMgr.Instance.CreateTimer(
false, // 受Time.timeScale影响
5000, // 总时间5秒(5000毫秒)
() =>
{
Debug.Log("5秒后执行的回调");
// 输出:5秒后执行的回调
}
);
Debug.Log($"创建计时器,ID: {timerID1}");
// 输出:创建计时器,ID: 1
}
/// <summary>
/// 测试间隔执行
/// </summary>
private void TestIntervalTimer()
{
Debug.Log("测试2:间隔执行");
// 创建一个10秒后完成的计时器,每隔1秒执行一次
timerID2 = TimerMgr.Instance.CreateTimer(
false, // 受Time.timeScale影响
10000, // 总时间10秒
() =>
{
Debug.Log("10秒倒计时结束");
// 输出:10秒倒计时结束
},
1000, // 间隔时间1秒
() =>
{
Debug.Log("间隔1秒执行的回调");
// 输出:间隔1秒执行的回调(会输出10次)
}
);
Debug.Log($"创建计时器,ID: {timerID2}");
// 输出:创建计时器,ID: 2
}
/// <summary>
/// 测试真实时间计时器
/// </summary>
private void TestRealTimeTimer()
{
Debug.Log("测试3:真实时间计时器");
// 创建一个不受Time.timeScale影响的计时器
timerID3 = TimerMgr.Instance.CreateTimer(
true, // 不受Time.timeScale影响
3000, // 总时间3秒
() =>
{
Debug.Log("真实时间计时器3秒后执行");
// 输出:真实时间计时器3秒后执行
// 即使游戏暂停,这个计时器也会继续计时
}
);
Debug.Log($"创建真实时间计时器,ID: {timerID3}");
// 输出:创建真实时间计时器,ID: 3
// 演示暂停和恢复功能
Invoke("PauseAndResume", 1f);
}
/// <summary>
/// 测试暂停和恢复功能
/// </summary>
private void PauseAndResume()
{
Debug.Log("暂停计时器ID2");
TimerMgr.Instance.StopTimer(timerID2);
Invoke("ResumeTimer", 2f);
}
/// <summary>
/// 恢复计时器
/// </summary>
private void ResumeTimer()
{
Debug.Log("恢复计时器ID2");
TimerMgr.Instance.StartTimer(timerID2);
}
/// <summary>
/// 测试场景:倒计时UI
/// </summary>
public void Example_CountdownUI()
{
int countdownTimer = TimerMgr.Instance.CreateTimer(
false,
10000, // 10秒倒计时
() =>
{
Debug.Log("倒计时结束!");
},
1000, // 每隔1秒更新一次
() =>
{
// 更新UI显示
// int currentTime = TimerMgr.Instance.GetRemainingTime(countdownTimer);
// countdownText.text = $"剩余时间:{currentTime / 1000}秒";
}
);
}
/// <summary>
/// 测试场景:技能冷却
/// </summary>
public void Example_SkillCooldown()
{
int cooldownTimer = TimerMgr.Instance.CreateTimer(
false,
5000, // 5秒冷却
() =>
{
Debug.Log("技能冷却完成,可以再次使用");
// skillButton.interactable = true;
}
);
// skillButton.interactable = false;
Debug.Log("技能进入冷却状态");
}
}
测试和使用
实际应用场景示例:
using UnityEngine;
/// <summary>
/// 计时器模块实际应用示例
/// </summary>
public class TimerModuleUsage : MonoBehaviour
{
#region 场景1:游戏倒计时
private int gameTimerID;
private int gameOverTimerID;
/// <summary>
/// 开始游戏倒计时
/// </summary>
public void StartGameTimer(int gameDuration)
{
// 游戏总时长倒计时(受Time.timeScale影响)
gameTimerID = TimerMgr.Instance.CreateTimer(
false,
gameDuration * 1000, // 转换为毫秒
() =>
{
// 游戏时间结束
OnGameTimeOver();
},
1000, // 每秒更新一次
() =>
{
// 更新UI显示剩余时间
UpdateGameTimeUI();
}
);
}
/// <summary>
/// 游戏暂停/继续
/// </summary>
public void PauseGame()
{
Time.timeScale = 0;
TimerMgr.Instance.StopTimer(gameTimerID);
}
public void ResumeGame()
{
Time.timeScale = 1;
TimerMgr.Instance.StartTimer(gameTimerID);
}
/// <summary>
/// 游戏结束倒计时(真实时间)
/// </summary>
private void OnGameTimeOver()
{
Debug.Log("游戏时间结束");
// 游戏结束后3秒退出(不受暂停影响)
gameOverTimerID = TimerMgr.Instance.CreateTimer(
true, // 真实时间
3000, // 3秒
() =>
{
// 退出游戏
Application.Quit();
}
);
}
private void UpdateGameTimeUI()
{
// 更新UI逻辑
// gameTimeText.text = "剩余时间:XX秒";
}
#endregion
#region 场景2:技能冷却系统
private int skillCooldownID;
/// <summary>
/// 使用技能(带冷却)
/// </summary>
public void UseSkill(int cooldownSeconds)
{
if (skillCooldownID != 0)
{
Debug.Log("技能还在冷却中");
return;
}
Debug.Log("使用技能");
// 技能冷却计时
skillCooldownID = TimerMgr.Instance.CreateTimer(
false,
cooldownSeconds * 1000,
() =>
{
Debug.Log("技能冷却完成");
skillCooldownID = 0; // 重置ID,表示可以再次使用
}
);
}
#endregion
#region 场景3:网络请求超时
/// <summary>
/// 发送网络请求(带超时检测)
/// </summary>
public void SendNetworkRequest()
{
int timeoutTimerID = TimerMgr.Instance.CreateTimer(
true, // 真实时间
10000, // 10秒超时
() =>
{
Debug.Log("网络请求超时");
// 取消请求,显示错误信息
}
);
// 发送网络请求
// StartCoroutine(SendRequest(() =>
// {
// // 请求成功,移除超时计时器
// TimerMgr.Instance.RemoveTimer(timeoutTimerID);
// }));
}
#endregion
#region 场景4:UI自动关闭
/// <summary>
/// 显示提示消息(自动关闭)
/// </summary>
public void ShowMessage(string message, int autoCloseSeconds)
{
// 显示消息UI
Debug.Log($"显示消息:{message}");
// 自动关闭计时器(真实时间,不受游戏暂停影响)
TimerMgr.Instance.CreateTimer(
true,
autoCloseSeconds * 1000,
() =>
{
// 关闭消息UI
Debug.Log("消息自动关闭");
}
);
}
#endregion
void OnDestroy()
{
// 清理所有计时器
if (gameTimerID != 0)
TimerMgr.Instance.RemoveTimer(gameTimerID);
if (gameOverTimerID != 0)
TimerMgr.Instance.RemoveTimer(gameOverTimerID);
if (skillCooldownID != 0)
TimerMgr.Instance.RemoveTimer(skillCooldownID);
}
}
进阶和拓展
进阶优化建议:
- 获取剩余时间:提供
GetRemainingTime方法,便于更新UI - 暂停所有计时器:提供一键暂停所有计时器的功能
- 计时器优先级:支持设置计时器优先级,重要计时器优先更新
- 计时器日志:添加计时器日志系统,便于调试
- 秒和毫秒转换:提供便捷的时间单位转换方法
- 循环计时器:支持循环执行的计时器
- 链式调用:支持流畅的链式调用API
11.2 知识点代码
TimerMgr.cs(计时器管理器 - 最终版本)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 计时器管理器
/// 统一管理所有计时器
/// 支持普通计时器和真实时间计时器
/// </summary>
public class TimerMgr : Singleton<TimerMgr>
{
#region 字段
/// <summary>
/// 用于记录当前将要创建的唯一ID
/// </summary>
private int TIMER_KEY = 0;
/// <summary>
/// 用于存储管理所有计时器的字典容器(受Time.timeScale影响)
/// </summary>
private Dictionary<int, TimerItem> timerDic = new Dictionary<int, TimerItem>();
/// <summary>
/// 用于存储管理所有计时器的字典容器(不受Time.timeScale影响)
/// </summary>
private Dictionary<int, TimerItem> realTimerDic = new Dictionary<int, TimerItem>();
/// <summary>
/// 待移除列表
/// </summary>
private List<TimerItem> delList = new List<TimerItem>();
/// <summary>
/// 为了避免内存的浪费 每次while都会生成
/// 我们直接将其声明为成员变量
/// </summary>
private WaitForSecondsRealtime waitForSecondsRealtime = new WaitForSecondsRealtime(intervalTime);
private WaitForSeconds waitForSeconds = new WaitForSeconds(intervalTime);
/// <summary>
/// 计时器协程
/// </summary>
private Coroutine timer;
/// <summary>
/// 真实时间计时器协程
/// </summary>
private Coroutine realTimer;
/// <summary>
/// 计时器管理器中的唯一计时用的协同程序的间隔时间
/// </summary>
private const float intervalTime = 0.1f;
#endregion
#region 构造函数
private TimerMgr()
{
// 注册TimerItem对象池
TimerItemPool timerItemPool = new TimerItemPool();
PoolMgr.Instance.RegisterObjPool<TimerItem>(timerItemPool);
// 默认计时器就是开启的
Start();
}
#endregion
#region 计时器管理
/// <summary>
/// 开启计时器管理器的方法
/// </summary>
public void Start()
{
timer = MonoMgr.Instance.StartCoroutine(StartTiming(false, timerDic));
realTimer = MonoMgr.Instance.StartCoroutine(StartTiming(true, realTimerDic));
}
/// <summary>
/// 关闭计时器管理器的方法
/// </summary>
public void Stop()
{
MonoMgr.Instance.StopCoroutine(timer);
MonoMgr.Instance.StopCoroutine(realTimer);
}
/// <summary>
/// 计时的核心协程
/// </summary>
/// <param name="isRealTime">是否真实时间</param>
/// <param name="timerDic">要使用的字典</param>
/// <returns>协程迭代器</returns>
private IEnumerator StartTiming(bool isRealTime, Dictionary<int, TimerItem> timerDic)
{
while (true)
{
// 100毫秒进行一次计时
if (isRealTime)
{
yield return waitForSecondsRealtime;
}
else
{
yield return waitForSeconds;
}
// 遍历所有的计时器 进行数据更新
foreach (TimerItem item in timerDic.Values)
{
if (!item.isRuning)
{
continue;
}
// 判断计时器是否有间隔时间执行的需求
if (item.callBack != null)
{
// 减去100毫秒
item.intervalTime -= (int)(intervalTime * 1000);
// 满足一次间隔时间执行
if (item.intervalTime <= 0)
{
// 间隔一定时间 执行一次回调
item.callBack.Invoke();
// 重置间隔时间
item.intervalTime = item.maxIntervalTime;
}
}
// 总的时间更新
item.allTime -= (int)(intervalTime * 1000);
// 计时时间到 需要执行完成回调函数
if (item.allTime <= 0)
{
item.overCallBack.Invoke();
delList.Add(item);
}
}
// 移除待移除列表中的数据
for (int i = 0; i < delList.Count; i++)
{
// 从字典中移除
timerDic.Remove(delList[i].keyID);
// 放入缓存池中
PoolMgr.Instance.ReturnObj<TimerItem>(delList[i]);
}
// 移除结束后 清空列表
delList.Clear();
}
}
#endregion
#region 计时器控制
/// <summary>
/// 创建单个计时器
/// </summary>
/// <param name="isRealTime">如果是true不受Time.timeScale影响</param>
/// <param name="allTime">总的时间,毫秒:1s=1000ms</param>
/// <param name="overCallBack">总时间结束回调</param>
/// <param name="intervalTime">间隔计时时间,毫秒:1s=1000ms</param>
/// <param name="callBack">间隔计时时间结束回调</param>
/// <returns>返回唯一ID 用于外部控制对应计时器</returns>
public int CreateTimer(bool isRealTime, int allTime, UnityAction overCallBack, int intervalTime = 0, UnityAction callBack = null)
{
// 构建唯一ID
int keyID = ++TIMER_KEY;
// 从缓存池取出对应的计时器
TimerItem timerItem = PoolMgr.Instance.GetObj<TimerItem>();
// 初始化数据
timerItem.InitInfo(keyID, allTime, overCallBack, intervalTime, callBack);
// 记录到字典中 进行数据更新
if (isRealTime)
{
realTimerDic.Add(keyID, timerItem);
}
else
{
timerDic.Add(keyID, timerItem);
}
return keyID;
}
/// <summary>
/// 移除单个计时器
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void RemoveTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
// 移除对应id计时器 放入缓存池
PoolMgr.Instance.ReturnObj<TimerItem>(timerDic[keyID]);
// 从字典中移除
timerDic.Remove(keyID);
}
else if (realTimerDic.ContainsKey(keyID))
{
// 移除对应id计时器 放入缓存池
PoolMgr.Instance.ReturnObj<TimerItem>(realTimerDic[keyID]);
// 从字典中移除
realTimerDic.Remove(keyID);
}
}
/// <summary>
/// 重置单个计时器
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void ResetTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].ResetTimer();
}
else if (realTimerDic.ContainsKey(keyID))
{
realTimerDic[keyID].ResetTimer();
}
}
/// <summary>
/// 开启单个计时器,主要用于暂停后重新开始
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void StartTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].isRuning = true;
}
else if (realTimerDic.ContainsKey(keyID))
{
realTimerDic[keyID].isRuning = true;
}
}
/// <summary>
/// 停止单个计时器,主要用于暂停
/// </summary>
/// <param name="keyID">计时器唯一ID</param>
public void StopTimer(int keyID)
{
if (timerDic.ContainsKey(keyID))
{
timerDic[keyID].isRuning = false;
}
else if (realTimerDic.ContainsKey(keyID))
{
realTimerDic[keyID].isRuning = false;
}
}
#endregion
}
TimerItem.cs(计时器对象)
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 计时器对象
/// 里面存储了计时器的相关数据
/// </summary>
public class TimerItem
{
#region 字段
/// <summary>
/// 唯一ID
/// </summary>
public int keyID;
/// <summary>
/// 计时结束后的委托回调
/// </summary>
public UnityAction overCallBack;
/// <summary>
/// 间隔一定时间去执行的委托回调
/// </summary>
public UnityAction callBack;
/// <summary>
/// 表示计时器总的计时时间,毫秒:1s = 1000ms
/// </summary>
public int allTime;
/// <summary>
/// 记录一开始计时时的总时间,用于时间重置
/// </summary>
public int maxAllTime;
/// <summary>
/// 间隔执行回调的时间,毫秒:1s = 1000ms
/// </summary>
public int intervalTime;
/// <summary>
/// 记录一开始的间隔时间
/// </summary>
public int maxIntervalTime;
/// <summary>
/// 是否在进行计时
/// </summary>
public bool isRuning;
#endregion
#region 方法
/// <summary>
/// 初始化计时器数据
/// </summary>
/// <param name="keyID">唯一ID</param>
/// <param name="allTime">总的时间</param>
/// <param name="overCallBack">总时间计时结束后的回调</param>
/// <param name="intervalTime">间隔执行的时间</param>
/// <param name="callBack">间隔执行时间结束后的回调</param>
public void InitInfo(int keyID, int allTime, UnityAction overCallBack, int intervalTime = 0, UnityAction callBack = null)
{
this.keyID = keyID;
this.maxAllTime = this.allTime = allTime;
this.overCallBack = overCallBack;
this.maxIntervalTime = this.intervalTime = intervalTime;
this.callBack = callBack;
this.isRuning = true;
}
/// <summary>
/// 重置计时器
/// </summary>
public void ResetTimer()
{
this.allTime = this.maxAllTime;
this.intervalTime = this.maxIntervalTime;
this.isRuning = true;
}
/// <summary>
/// 缓存池回收时,清除相关引用数据
/// </summary>
public void ResetInfo()
{
overCallBack = null;
callBack = null;
}
#endregion
}
TimerItemPool.cs(计时器对象池)
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// TimerItem对象池
/// 自定义TimerItem的重置逻辑
/// </summary>
public class TimerItemPool : ObjPool<TimerItem>
{
#region 构造函数
/// <summary>
/// 初始化构造函数
/// </summary>
public TimerItemPool() : base()
{
}
#endregion
#region 重写方法
/// <summary>
/// 将对象放回对象池时的清理
/// </summary>
/// <param name="obj">放回的TimerItem对象</param>
protected override void OnReturnObject(TimerItem obj)
{
// 重置TimerItem的数据
obj.ResetInfo();
}
#endregion
}
TimerModuleUsageExample.cs(使用示例)
using UnityEngine;
/// <summary>
/// 计时器模块完整使用示例
/// </summary>
public class TimerModuleUsageExample : MonoBehaviour
{
#region 字段
private int skillCooldownTimerID;
private int gameTimerID;
private int networkTimeoutTimerID;
#endregion
#region 生命周期
void Start()
{
Debug.Log("计时器模块使用示例");
// 演示各种使用场景
Example_SkillCooldown();
Example_GameTimer();
Example_NetworkTimeout();
}
#endregion
#region 场景1:技能冷却
/// <summary>
/// 使用技能(带冷却)
/// </summary>
private void Example_SkillCooldown()
{
Debug.Log("场景1:技能冷却");
// 创建技能冷却计时器
skillCooldownTimerID = TimerMgr.Instance.CreateTimer(
false, // 受Time.timeScale影响
5000, // 冷却时间5秒
() =>
{
Debug.Log("技能冷却完成");
skillCooldownTimerID = 0;
}
);
Debug.Log("技能进入冷却状态,ID: " + skillCooldownTimerID);
}
/// <summary>
/// 使用技能
/// </summary>
public void UseSkill()
{
if (skillCooldownTimerID != 0)
{
Debug.Log("技能还在冷却中,无法使用");
return;
}
Debug.Log("使用技能");
// 重新进入冷却
Example_SkillCooldown();
}
#endregion
#region 场景2:游戏计时
/// <summary>
/// 开始游戏计时
/// </summary>
private void Example_GameTimer()
{
Debug.Log("场景2:游戏计时");
// 创建游戏时间计时器(受Time.timeScale影响)
gameTimerID = TimerMgr.Instance.CreateTimer(
false,
60000, // 游戏时间60秒
() =>
{
Debug.Log("游戏时间结束");
OnGameTimeOver();
},
1000, // 每秒更新一次
() =>
{
Debug.Log("更新游戏时间UI");
// 更新UI显示
}
);
}
/// <summary>
/// 游戏时间结束回调
/// </summary>
private void OnGameTimeOver()
{
Debug.Log("游戏结束");
// 游戏结束后3秒显示结果(真实时间)
TimerMgr.Instance.CreateTimer(
true,
3000,
() =>
{
Debug.Log("显示游戏结果界面");
}
);
}
#endregion
#region 场景3:网络请求超时
/// <summary>
/// 发送网络请求(带超时检测)
/// </summary>
private void Example_NetworkTimeout()
{
Debug.Log("场景3:网络请求超时");
networkTimeoutTimerID = TimerMgr.Instance.CreateTimer(
true, // 真实时间,不受游戏暂停影响
10000, // 10秒超时
() =>
{
Debug.Log("网络请求超时");
// 取消请求,显示错误信息
}
);
// 模拟网络请求
// StartCoroutine(SendNetworkRequest(() =>
// {
// // 请求成功,移除超时计时器
// TimerMgr.Instance.RemoveTimer(networkTimeoutTimerID);
// }));
}
#endregion
#region 场景4:UI自动关闭
/// <summary>
/// 显示提示消息(自动关闭)
/// </summary>
public void ShowMessageWithAutoClose(string message, int autoCloseSeconds)
{
Debug.Log($"显示消息:{message}");
// 创建自动关闭计时器(真实时间)
TimerMgr.Instance.CreateTimer(
true,
autoCloseSeconds * 1000,
() =>
{
Debug.Log("消息自动关闭");
}
);
}
#endregion
#region 场景5:暂停和恢复
/// <summary>
/// 暂停游戏
/// </summary>
public void PauseGame()
{
Debug.Log("游戏暂停");
Time.timeScale = 0;
TimerMgr.Instance.StopTimer(gameTimerID);
}
/// <summary>
/// 继续游戏
/// </summary>
public void ResumeGame()
{
Debug.Log("游戏继续");
Time.timeScale = 1;
TimerMgr.Instance.StartTimer(gameTimerID);
}
#endregion
void OnDestroy()
{
// 清理所有计时器
if (skillCooldownTimerID != 0)
TimerMgr.Instance.RemoveTimer(skillCooldownTimerID);
if (gameTimerID != 0)
TimerMgr.Instance.RemoveTimer(gameTimerID);
if (networkTimeoutTimerID != 0)
TimerMgr.Instance.RemoveTimer(networkTimeoutTimerID);
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com