11.计时器模块

11.计时器模块


11.1 知识点

计时器模块的作用

在游戏开发中,我们经常需要使用计时功能,例如:

  • 延时执行:10秒后执行某个逻辑
  • 间隔执行:每隔1秒执行一次某个逻辑
  • 倒计时显示:UI上显示剩余时间
  • 技能冷却:技能释放后需要等待一段时间才能再次使用

Unity自带的计时功能:

  • Invoke:延迟执行逻辑
  • InvokeRepeating:间隔一定时间执行逻辑
  • Coroutine:协程实现计时
  • Update中自己实现计时

Unity自带计时功能的问题:

  1. 只能为继承了MonoBehaviour的类服务:非MonoBehaviour类无法使用
  2. 代码冗余:每个需要计时的类都要重复编写类似的代码
  3. 难以统一管理:无法统一控制所有计时器的开启、关闭、暂停等

解决方案:
通过独立的计时器管理模块,统一管理所有计时功能,提供延时执行、间隔执行等功能,让所有类都能使用计时功能。

主要功能:

  1. 延时执行:指定时间后执行回调函数
  2. 间隔执行:每隔指定时间执行一次回调函数
  3. 计时器控制:创建、移除、暂停、恢复、重置计时器
  4. 真实时间计时:支持不受Time.timeScale影响的计时器
  5. 对象池管理:使用对象池减少内存分配

计时器模块的基本原理

设计理念:

  • 统一管理:所有计时器由一个管理器统一管理
  • 唯一标识:每个计时器有唯一的ID用于控制
  • 协同程序:使用协程实现固定间隔的时间检测(每100毫秒检测一次)
  • 委托回调:使用UnityAction实现延时回调和间隔回调
  • 对象池:使用对象池管理TimerItem对象,减少内存分配

工作流程:

  1. 创建计时器:调用CreateTimer创建计时器,返回唯一ID
  2. 时间检测:协程每100毫秒遍历所有计时器,更新剩余时间
  3. 间隔回调:如果设置了间隔时间,到时间后触发间隔回调
  4. 完成回调:总时间结束后触发完成回调,并回收计时器

计时器模块基础实现

为什么需要计时器模块

传统方式的问题:

// 传统方式1:使用Invoke(只能用于MonoBehaviour)
public class Skill : MonoBehaviour
{
    void UseSkill()
    {
        // 5秒后执行技能结束逻辑
        Invoke("OnSkillEnd", 5f);
    }
    
    void OnSkillEnd()
    {
        // 技能结束逻辑
    }
}

存在的问题:

  1. 只能继承MonoBehaviour:普通C#类无法使用
  2. 灵活性差:无法动态创建和管理多个计时器
  3. 难以扩展:无法暂停、恢复、重置计时器

传统方式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();
    }
}

存在的问题:

  1. 代码重复:每个类都需要写类似的代码
  2. 难以管理:无法统一控制所有计时器
  3. 资源浪费:频繁创建协程产生内存分配

计时器模块基础实现

实现思路:

  1. 创建TimerItem类:存储计时器的数据(总时间、间隔时间、回调函数等)
  2. 创建TimerMgr管理器:统一管理所有计时器
  3. 使用协程:每100毫秒检测一次所有计时器
  4. 使用字典:用ID作为键存储所有计时器
  5. 对象池:使用对象池管理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
}

基础实现的局限性

存在的问题:

  1. 受Time.timeScale影响:游戏暂停时计时器也会暂停
  2. 无法区分计时类型:所有计时器都使用相同的时间系统
  3. 应用场景受限:某些场景需要真实时间计时(如网络请求超时)

计时器模块支持真实时间计时

为什么需要真实时间计时

应用场景:

  • 游戏暂停时UI倒计时继续:暂停菜单上的时间显示不停止
  • 网络请求超时:需要真实时间,不受游戏暂停影响
  • 广告播放计时:广告倒计时需要真实时间
  • 限时活动:活动倒计时不受游戏状态影响

问题分析:
当前计时器使用yield return new WaitForSeconds(0.1f),会受到Time.timeScale的影响。当游戏暂停时(Time.timeScale = 0),所有协程都会停止。

解决方案:
使用yield return new WaitForSecondsRealtime(0.1f)创建不受Time.timeScale影响的协程。

真实时间计时实现

实现思路:

  1. 添加真实时间字典realTimerDic存储不受Time.timeScale影响的计时器
  2. 添加真实时间协程:单独开一个协程处理真实时间计时
  3. 修改CreateTimer方法:添加isRealTime参数用于区分计时类型
  4. 优化WaitForSeconds:使用成员变量避免重复创建
  5. 修改所有控制方法:支持在两种字典中查找和操作

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

进阶和拓展

进阶优化建议:

  1. 获取剩余时间:提供GetRemainingTime方法,便于更新UI
  2. 暂停所有计时器:提供一键暂停所有计时器的功能
  3. 计时器优先级:支持设置计时器优先级,重要计时器优先更新
  4. 计时器日志:添加计时器日志系统,便于调试
  5. 秒和毫秒转换:提供便捷的时间单位转换方法
  6. 循环计时器:支持循环执行的计时器
  7. 链式调用:支持流畅的链式调用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

×

喜欢就点赞,疼爱就打赏