在游戏开发中,计时系统是一个基础且核心的组件。从技能冷却、任务倒计时到周期性事件触发,几乎所有游戏都离不开精准可靠的计时服务。Unity 虽然提供了Invoke、Coroutine等基础计时工具,但在面对复杂场景(如大量并行计时任务、高精度计时需求)时,这些原生工具往往显得力不从心。本文将详细介绍如何实现一个高性能、线程安全的通用计时服务,适用于各类 Unity 游戏项目。
一、为什么需要自定义计时服务?
Unity 原生计时方案的局限性:Invoke和InvokeRepeating缺乏灵活的取消机制,无法传递复杂参数,且在对象失活时会失效;协程依赖MonoBehaviour生命周期,大量协程会导致性能问题,管理困难;而使用Time.deltaTime手动累加,不仅需要在Update中轮询,代码侵入性强,时间精度还受帧率影响。
- Invoke和InvokeRepeating:缺乏灵活的取消机制,无法传递复杂参数,且在对象失活时会失效
- 协程(Coroutine):依赖 MonoBehaviour 生命周期,大量协程会导致性能问题,管理困难
- Time.deltaTime手动累加:需要在 Update 中轮询,代码侵入性强,时间精度受帧率影响
一个专业的计时服务应当具备:
- 不依赖 MonoBehaviour,可全局使用
- 支持一次性和周期性任务
- 精准的时间控制,不受帧率波动影响
- 线程安全的任务添加 / 取消操作
- 异常隔离,单个任务错误不影响整体
- 资源自动释放,无内存泄漏风险
以下介绍两种不同版本(基于MonoBehaviour版本 、独立线程版本)的计时服务,均使用最小堆(优先队列)来实现。
二、基于 MonoBehaviour 的计时服务实现
基于 MonoBehaviour 的计时服务是 Unity 开发者最常用的方式,它依托于 Unity 的生命周期函数,实现简单但有一定局限性。这里不过多介绍代码有注释。
1.基础实现代码
using System;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 计时任务管理器(最小堆实现高效的任务调度)
/// 添加、取消任务的时间复杂度均为 O(log n)
/// </summary>
public class TimerManager : MonoBehaviour
{
private static readonly object lockObject = new object();
private static TimerManager instance;
public static TimerManager Instance
{
get
{
if (instance == null)
{
lock (lockObject)
{
instance = FindObjectOfType<TimerManager>();
if (instance == null)
{
GameObject go = new GameObject("TimerManager");
instance = go.AddComponent<TimerManager>();
DontDestroyOnLoad(go);
}
}
}
return instance;
}
}
/// <summary>
/// 计时任务类
/// </summary>
private class TimerTask : IComparable<TimerTask>
{
public int Id { get; set; }
public float ExecuteTime { get; set; } // 绝对执行时间点(而非剩余时间),避免每帧遍历所有任务更新剩余时间
public float Interval { get; set; } // 重复间隔(<=0表示不重复)
public Action Callback { get; set; }
public bool IsCancelled { get; set; }
// 用于最小堆比较,时间小的任务排在前面
public int CompareTo(TimerTask other)
{
return ExecuteTime.CompareTo(other.ExecuteTime);
}
}
private List<TimerTask> taskHeap = new List<TimerTask>(); // 最小堆
private Dictionary<int, TimerTask> taskDict = new Dictionary<int, TimerTask>(); // 快速查找
private int nextTaskId = 1; // 下一个可用的任务ID
private void Update()
{
float currentTime = Time.time;
// 处理所有到期的任务
while (this.taskHeap.Count > 0 && this.taskHeap[0].ExecuteTime <= currentTime)
{
TimerTask task = this.taskHeap[0];
RemoveTaskFromHeap(task);
if (!task.IsCancelled)
{
try
{
task.Callback?.Invoke();
}
catch (Exception e)
{
Debug.LogError($"执行计时任务 {task.Id} 时出错: {e.Message}");
}
// 处理重复任务
if (task.Interval > 0)
{
task.ExecuteTime = currentTime + task.Interval;
AddTaskToHeap(task);
}
else
{
// 非重复任务执行后从字典中移除
this.taskDict.Remove(task.Id);
}
}
else
{
// 已取消的任务从字典中移除
this.taskDict.Remove(task.Id);
}
}
}
/// <summary>
/// 添加一个计时任务
/// </summary>
/// <param name="delay">延迟时间(秒)</param>
/// <param name="callback">回调方法</param>
/// <param name="isRepeating">是否重复执行</param>
/// <param name="interval">重复间隔(秒),仅在isRepeating为true时有效</param>
/// <returns>任务ID,用于取消任务</returns>
public int AddTimer(float delay, Action callback, bool isRepeating = false, float interval = 0f)
{
if (callback == null)
{
Debug.LogError("回调方法不能为空");
return -1;
}
if (delay < 0)
{
delay = 0;
}
if (isRepeating && interval <= 0)
{
interval = delay; // 默认使用与延迟相同的间隔
}
int taskId = this.nextTaskId++;
TimerTask task = new TimerTask
{
Id = taskId,
ExecuteTime = Time.time + delay,
Interval = isRepeating ? interval : 0,
Callback = callback,
IsCancelled = false
};
this.taskDict[taskId] = task;
AddTaskToHeap(task);
return taskId;
}
public bool CancelTimer(int taskId)
{
if (this.taskDict.TryGetValue(taskId, out TimerTask task))
{
task.IsCancelled = true;
return true;
}
return false;
}
public bool HasTimer(int taskId)
{
return this.taskDict.ContainsKey(taskId);
}
public void ClearAllTimers()
{
this.taskHeap.Clear();
this.taskDict.Clear();
}
// 最小堆操作 - 添加任务
private void AddTaskToHeap(TimerTask task)
{
this.taskHeap.Add(task);
int currentIndex = this.taskHeap.Count - 1;
// 向上调整堆
while (currentIndex > 0)
{
int parentIndex = (currentIndex - 1) / 2;
if (this.taskHeap[currentIndex].CompareTo(this.taskHeap[parentIndex]) >= 0)
break;
SwapHeapElements(currentIndex, parentIndex);
currentIndex = parentIndex;
}
}
// 最小堆操作 - 移除任务
private void RemoveTaskFromHeap(TimerTask task)
{
int index = this.taskHeap.IndexOf(task);
if (index == -1) return;
int lastIndex = this.taskHeap.Count - 1;
this.taskHeap[index] = this.taskHeap[lastIndex];
this.taskHeap.RemoveAt(lastIndex);
// 向下调整堆
lastIndex--;
int currentIndex = index;
while (true)
{
int leftChild = currentIndex * 2 + 1;
int rightChild = currentIndex * 2 + 2;
int smallest = currentIndex;
if (leftChild <= lastIndex && this.taskHeap[leftChild].CompareTo(this.taskHeap[smallest]) < 0)
smallest = leftChild;
if (rightChild <= lastIndex && this.taskHeap[rightChild].CompareTo(this.taskHeap[smallest]) < 0)
smallest = rightChild;
if (smallest == currentIndex)
break;
SwapHeapElements(currentIndex, smallest);
currentIndex = smallest;
}
}
private void SwapHeapElements(int index1, int index2)
{
TimerTask temp = this.taskHeap[index1];
this.taskHeap[index1] = this.taskHeap[index2];
this.taskHeap[index2] = temp;
}
}
2.使用示例
using UnityEngine;
public class Test : MonoBehaviour
{
private int taskId1;
private int taskId2;
void Start()
{
// 添加一个1秒后执行一次的任务
taskId1 = TimerManager.Instance.AddTimer(1f, () =>
{
Debug.Log("1秒后的一次性任务执行");
});
// 添加一个每2秒重复执行的任务
taskId2 = TimerManager.Instance.AddTimer(2f, () =>
{
Debug.Log("每2秒重复执行的任务");
}, true, 2f);
}
void OnDestroy()
{
TimerManager.Instance.ClearAllTimers();
}
}
三、基于独立线程的计时服务实现
这种实现方式不依赖于 MonoBehaviour,采用独立线程处理计时逻辑,适用于对性能和精度要求较高的场景。(客户端和服务端均可以使用哦)
核心设计思路
采用 "独立线程 + 最小堆" 的方案实现计时服务:
- 独立线程:使用后台线程处理计时逻辑,避免占用主线程资源
- 最小堆(优先队列):高效管理任务执行顺序,确保最快到期的任务优先处理
- 线程安全队列:实现主线程与计时线程的安全通信
- SynchronizationContext:确保回调在主线程执行,兼容 Unity API
- IDisposable 模式:优雅释放资源,避免内存泄漏
1. 计时任务类
首先定义计时任务的数据结构,实现IComparable<T>接口用于堆排序:
// 计时任务类
private class TimerTask_SingleThreaded : IComparable<TimerTask_SingleThreaded>
{
public int Id { get; set; }
public DateTime ExecuteTime { get; set; } // 使用DateTime替代float, 绝对执行时间点(而非剩余时间),避免每帧遍历所有任务更新剩余时间
public TimeSpan Interval { get; set; } // 使用TimeSpan替代float
public Action Callback { get; set; }
public bool IsCancelled { get; set; }
public int CompareTo(TimerTask_SingleThreaded other)
{
return ExecuteTime.CompareTo(other.ExecuteTime);
}
}
2. 计时管理器核心类
实现单例模式的计时服务核心逻辑:
using System;
using System.Collections.Generic;
using System.Threading;
/// <summary>
/// 独立线程的计时任务管理器 - 使用最小堆实现高效的任务调度
/// </summary>
public sealed class TimerManager_SingleThreaded: IDisposable
{
// 单例实现 Lazy<T>这是一个内置类型,用于延迟初始化对象(即直到第一次使用时才创建实例)
private static readonly Lazy<TimerManager_SingleThreaded> lazy = new Lazy<TimerManager_SingleThreaded>(() => new TimerManager_SingleThreaded());
public static TimerManager_SingleThreaded Instance => lazy.Value;
private readonly List<TimerTask_SingleThreaded> taskHeap = new List<TimerTask_SingleThreaded>(); // 最小堆
private readonly Dictionary<int, TimerTask_SingleThreaded> taskDict = new Dictionary<int, TimerTask_SingleThreaded>(); // 快速查找
private readonly object lockObj = new object(); // 线程锁
//ManualResetEvent是一个线程同步原语,用于控制多个线程的执行顺序。
private readonly ManualResetEvent stopEvent = new ManualResetEvent(false); // 停止事件(门关闭)
private readonly Thread timerThread; // 计时线程
private int nextTaskId = 1; // 下一个可用的任务ID
private bool isDisposed = false;
// 主线程同步上下文(用于回调执行)
private readonly SynchronizationContext mainThreadContext;
private TimerManager_SingleThreaded()
{
// 捕获主线程同步上下文
mainThreadContext = SynchronizationContext.Current ?? new SynchronizationContext();
// 创建并启动计时线程
timerThread = new Thread(TimerLoop)
{
IsBackground = true,
Name = "TimerManager_SingleThreadedThread"
};
timerThread.Start();
}
// 计时线程主循环
private void TimerLoop()
{
while (!stopEvent.WaitOne(10)) // 每10ms检查一次
{
ProcessExpiredTasks();
}
}
// 处理到期的任务
private void ProcessExpiredTasks()
{
List<TimerTask_SingleThreaded> tasksToExecute = new List<TimerTask_SingleThreaded>();
DateTime currentTime = DateTime.UtcNow;
lock (lockObj)
{
// 提取所有到期的任务
while (taskHeap.Count > 0 && taskHeap[0].ExecuteTime <= currentTime)
{
TimerTask_SingleThreaded task = taskHeap[0];
RemoveTaskFromHeap(task);
if (!task.IsCancelled)
{
tasksToExecute.Add(task);
// 处理重复任务
if (task.Interval > TimeSpan.Zero)
{
task.ExecuteTime = currentTime + task.Interval;
AddTaskToHeap(task);
}
else
{
// 非重复任务执行后从字典中移除
taskDict.Remove(task.Id);
}
}
else
{
// 已取消的任务从字典中移除
taskDict.Remove(task.Id);
}
}
}
// 在主线程上执行回调
foreach (var task in tasksToExecute)
{
try
{
mainThreadContext.Post(_=> {
try
{
task.Callback?.Invoke();
}
catch (Exception ex)
{
Console.WriteLine($"执行计时任务 {task.Id} 时出错: {ex.Message}");
}
}, null);
}
catch (Exception ex)
{
Console.WriteLine($"调度计时任务 {task.Id} 时出错: {ex.Message}");
}
}
}
/// <summary>
/// 添加一个计时任务
/// </summary>
/// <param name="delay">延迟时间(毫秒)</param>
/// <param name="callback">回调方法</param>
/// <param name="isRepeating">是否重复执行</param>
/// <param name="interval">重复间隔(毫秒),仅在isRepeating为true时有效</param>
/// <returns>任务ID,用于取消任务</returns>
public int AddTimer(int delay, Action callback, bool isRepeating = false, int interval = 0)
{
if (callback == null)
{
Console.WriteLine("回调方法不能为空");
return -1;
}
if (delay < 0)
{
delay = 0;
}
if (isRepeating && interval <= 0)
{
interval = delay; // 默认使用与延迟相同的间隔
}
int taskId;
TimerTask_SingleThreaded task;
lock (lockObj)
{
taskId = nextTaskId++;
task = new TimerTask_SingleThreaded
{
Id = taskId,
ExecuteTime = DateTime.UtcNow.AddMilliseconds(delay),
Interval = isRepeating ? TimeSpan.FromMilliseconds(interval) : TimeSpan.Zero,
Callback = callback,
IsCancelled = false
};
taskDict[taskId] = task;
AddTaskToHeap(task);
}
return taskId;
}
/// <summary>
/// 取消一个计时任务
/// </summary>
/// <param name="taskId">任务ID</param>
/// <returns>是否成功取消</returns>
public bool CancelTimer(int taskId)
{
lock (lockObj)
{
if (taskDict.TryGetValue(taskId, out TimerTask_SingleThreaded task))
{
task.IsCancelled = true;
return true;
}
return false;
}
}
/// <summary>
/// 检查一个计时任务是否存在
/// </summary>
/// <param name="taskId">任务ID</param>
/// <returns>任务是否存在</returns>
public bool HasTimer(int taskId)
{
lock (lockObj)
{
return taskDict.ContainsKey(taskId);
}
}
/// <summary>
/// 清除所有计时任务
/// </summary>
public void ClearAllTimers()
{
lock (lockObj)
{
taskHeap.Clear();
taskDict.Clear();
}
}
// 最小堆操作 - 添加任务
private void AddTaskToHeap(TimerTask_SingleThreaded task)
{
taskHeap.Add(task);
int currentIndex = taskHeap.Count - 1;
// 向上调整堆
while (currentIndex > 0)
{
int parentIndex = (currentIndex - 1) / 2;
if (taskHeap[currentIndex].CompareTo(taskHeap[parentIndex]) >= 0)
break;
SwapHeapElements(currentIndex, parentIndex);
currentIndex = parentIndex;
}
}
// 最小堆操作 - 移除任务
private void RemoveTaskFromHeap(TimerTask_SingleThreaded task)
{
int index = taskHeap.IndexOf(task);
if (index == -1) return;
int lastIndex = taskHeap.Count - 1;
taskHeap[index] = taskHeap[lastIndex];
taskHeap.RemoveAt(lastIndex);
// 向下调整堆
lastIndex--;
int currentIndex = index;
while (true)
{
int leftChild = currentIndex * 2 + 1;
int rightChild = currentIndex * 2 + 2;
int smallest = currentIndex;
if (leftChild <= lastIndex && taskHeap[leftChild].CompareTo(taskHeap[smallest]) < 0)
smallest = leftChild;
if (rightChild <= lastIndex && taskHeap[rightChild].CompareTo(taskHeap[smallest]) < 0)
smallest = rightChild;
if (smallest == currentIndex)
break;
SwapHeapElements(currentIndex, smallest);
currentIndex = smallest;
}
}
// 交换堆中两个元素的位置
private void SwapHeapElements(int index1, int index2)
{
TimerTask_SingleThreaded temp = taskHeap[index1];
taskHeap[index1] = taskHeap[index2];
taskHeap[index2] = temp;
}
// 释放资源(游戏结束时一定要释放)
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!isDisposed)
{
if (disposing)
{
// 释放托管资源
stopEvent.Set();
if (timerThread != null && timerThread.IsAlive)
{
timerThread.Join(500); // 等待线程结束,最多500ms
}
stopEvent.Dispose();
lock (lockObj)
{
taskHeap.Clear();
taskDict.Clear();
}
}
isDisposed = true;
}
}
~TimerManager_SingleThreaded()
{
Dispose(false);
}
}
3.使用方法示例
(1)在 Unity 中使用
一定要及时释放资源:TimerManager_SingleThreaded.Instance.Dispose();
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
void Start()
{
// 添加一个2秒后执行的一次性任务
int taskId = TimerManager_SingleThreaded.Instance.AddTimer(2000, () =>
{
Debug.Log("2秒后执行的任务");
});
// 添加一个每3秒重复执行的任务
TimerManager_SingleThreaded.Instance.AddTimer(3000, () =>
{
Debug.Log("每3秒执行一次的任务");
}, true, 3000);
// 5秒后取消第一个任务
TimerManager_SingleThreaded.Instance.AddTimer(5000, () =>
{
TimerManager_SingleThreaded.Instance.CancelTimer(taskId);
Debug.Log("已取消任务 " + taskId);
});
}
void OnDestroy()
{
TimerManager_SingleThreaded.Instance.Dispose();
}
}
4.性能优化建议
(1)调整检查间隔:TimerLoop中的WaitOne(10)可以根据项目需求调整,精度要求不高的场景可增大到 50ms 或 100ms,减少 CPU 占用。
(2)批量处理任务:当存在大量任务时,可在ProcessExpiredTasks中一次性提取多个到期任务,减少锁竞争时间。
(3)任务合并:对于高频小任务(如每帧更新),可合并为单个任务批量处理,降低回调开销。
(4)对象池复用:对于频繁创建和销毁的任务,可使用对象池复用TimerTask对象,减少 GC 压力。
(5)分级计时:对不同精度要求的任务进行分级管理,高精度任务用短间隔检查,低精度任务用长间隔检查。
5.注意事项
(1)Unity API 限制:计时回调中可以安全调用 Unity API,因为我们已通过SynchronizationContext确保回调在主线程执行。
(2)场景切换处理:在 Unity 场景切换时,建议取消与当前场景相关的计时任务,避免回调到已销毁的对象。
(3)长时间运行:对于运行时间超过几天的游戏(如 MMO),需考虑 UTC 时间精度问题,可定期同步系统时间。
(4)异常处理:示例中仅做了简单的异常捕获,实际项目中应结合日志系统,记录详细的错误信息便于调试。
(5)线程安全:所有对任务的操作(添加 / 取消)都已通过锁机制保证线程安全,可以在任意线程调用。
四、总结
本文实现的计时服务具有以下特点:
- 完全独立于 MonoBehaviour,可在项目任何地方使用
- 采用独立线程处理计时逻辑,不占用主线程资源
- 使用最小堆管理任务,确保高效的任务调度
- 线程安全设计,支持多线程环境下的任务操作
- 完善的资源释放机制,符合.NET 最佳实践
这个计时服务可以满足大多数游戏的计时需求,从简单的技能冷却到复杂的任务系统都能胜任。在实际项目中,可根据具体需求进一步扩展功能,如添加任务优先级、暂停 / 继续功能、时间缩放支持等。
希望本文能帮助你理解计时系统的设计原理,实现更高效、更可靠的游戏计时功能。