1. 项目概述
在Unity游戏开发中,计时器功能几乎无处不在。从技能冷却、动画过渡到任务系统,都需要精确的时间控制。然而,很多开发者习惯直接使用协程或Invoke方法,这会导致频繁创建和销毁计时器对象,产生大量GC(垃圾回收)压力,进而引发游戏卡顿和帧率波动。
我在多个商业项目中实践发现,一个设计良好的计时器系统可以显著提升游戏性能。本文将分享一套基于对象池模式的高性能TimerManager实现方案,它解决了以下核心问题:
- 避免频繁GC导致的性能问题
- 提供统一管理接口
- 支持多种计时模式
- 简化开发者的使用成本
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心设计思路
2.1 架构设计
整个系统采用经典的"管理器+实体"架构:
TimerManager(单例):
- 负责对象池管理
- 维护活跃计时器列表
- 提供对外API接口
- 处理计时器更新逻辑
Timer(实体类):
- 封装计时逻辑
- 管理计时器状态
- 处理回调触发
- 支持链式调用
提示:这种分离设计遵循单一职责原则,使系统更易于维护和扩展。
2.2 对象池实现原理
对象池是性能优化的核心,其工作流程如下:
- 初始化阶段:预创建一定数量的Timer对象存入池中
- 获取阶段:当需要新计时器时,优先从池中获取空闲对象
- 回收阶段:计时器完成后,重置状态并返回对象池
- 扩容机制:当池为空时动态创建新对象,但设有上限防止内存泄漏
这种设计显著减少了GC触发的频率,实测在MMO游戏的技能系统中,GC频率从每秒3-5次降低到几乎为零。
3. 核心代码实现
3.1 单例基类
csharp复制public class SingletonMono<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
public static T Instance
{
get
{
if (_instance == null)
{
_instance = FindObjectOfType<T>();
if (_instance == null)
{
GameObject obj = new GameObject(typeof(T).Name);
_instance = obj.AddComponent<T>();
DontDestroyOnLoad(obj);
}
}
return _instance;
}
}
protected virtual void Awake()
{
if (_instance == null)
{
_instance = this as T;
DontDestroyOnLoad(gameObject);
}
else if (_instance != this)
{
Destroy(gameObject);
}
}
}
这段代码实现了线程安全的MonoBehaviour单例模式,特点包括:
- 懒加载:只在首次访问时创建实例
- 场景持久化:使用DontDestroyOnLoad保持跨场景
- 防重复创建:Awake中检查并处理重复实例
3.2 Timer实体类
csharp复制public class Timer
{
public enum TimerState
{
Idle, // 对象池中
Running, // 运行中
Paused, // 暂停
Completed // 完成
}
// 核心属性(省略getter/setter)
private float _duration;
private float _elapsedTime;
private bool _isLoop;
private TimerState _state;
// 回调相关
private Action _onComplete;
private Action _onInterval;
private Action<int> _onLoopComplete;
private float _intervalTime;
private float _lastIntervalTime;
private int _loopCount;
internal void Initialize(float duration, bool isLoop = false)
{
// 重置所有状态
_duration = duration;
_isLoop = isLoop;
_state = TimerState.Idle;
_elapsedTime = 0f;
_lastIntervalTime = 0f;
_loopCount = 0;
// 清空回调
_onInterval = null;
_onComplete = null;
_onLoopComplete = null;
}
internal void Update(float deltaTime)
{
if (_state != TimerState.Running) return;
_elapsedTime += deltaTime;
// 间隔回调处理
if (_onInterval != null && _intervalTime > 0)
{
while (_elapsedTime - _lastIntervalTime >= _intervalTime)
{
_lastIntervalTime += _intervalTime;
_onInterval?.Invoke();
}
}
// 完成检测
if (_elapsedTime >= _duration)
{
_loopCount++;
_onLoopComplete?.Invoke(_loopCount);
if (_isLoop)
{
_elapsedTime -= _duration;
_lastIntervalTime -= _duration;
}
else
{
_state = TimerState.Completed;
_onComplete?.Invoke();
TimerManager.Instance.RecycleTimer(this);
}
}
}
}
关键设计点:
- 状态机设计:明确区分四种状态,确保逻辑清晰
- 回调分离:不同事件使用独立回调,避免条件判断混乱
- 精确计时:使用while循环处理间隔回调,确保时间累积误差不会丢失触发
3.3 TimerManager核心实现
csharp复制public class TimerManager : SingletonMono<TimerManager>
{
private Queue<Timer> _timerPool = new Queue<Timer>();
private List<Timer> _activeTimers = new List<Timer>();
private List<Timer> _activeTimerRemoveables = new List<Timer>();
private Coroutine _removeableCoroutine;
[SerializeField] private int initialPoolSize = 10;
[SerializeField] private int maxPoolSize = 50;
private void InitializePool()
{
for (int i = 0; i < initialPoolSize; i++)
{
_timerPool.Enqueue(new Timer());
}
}
void Update()
{
float deltaTime = Time.deltaTime;
// 倒序遍历避免移除元素导致的索引问题
for (int i = _activeTimers.Count - 1; i >= 0; i--)
{
Timer timer = _activeTimers[i];
timer.Update(deltaTime);
}
}
private Timer GetTimer()
{
Timer timer = _timerPool.Count > 0 ?
_timerPool.Dequeue() : new Timer();
_activeTimers.Add(timer);
return timer;
}
public void RecycleTimer(Timer timer)
{
if (!_activeTimers.Contains(timer)) return;
_activeTimerRemoveables.Add(timer);
timer.Reset();
if (_removeableCoroutine == null)
{
_removeableCoroutine = StartCoroutine(WaitForEndOfFrameUpdate());
}
}
private IEnumerator WaitForEndOfFrameUpdate()
{
yield return null;
while (_activeTimerRemoveables.Count > 0)
{
Timer timer = _activeTimerRemoveables[0];
_activeTimers.Remove(timer);
if (_timerPool.Count < maxPoolSize)
_timerPool.Enqueue(timer);
_activeTimerRemoveables.RemoveAt(0);
}
_removeableCoroutine = null;
}
}
性能优化关键点:
- 延迟回收机制:使用协程在帧末处理回收,避免在Update中直接修改集合
- 池大小限制:防止内存无限增长
- 倒序遍历:安全处理遍历过程中的元素移除
4. 使用示例与最佳实践
4.1 基础使用模式
csharp复制// 初始化(建议在游戏启动时调用一次)
TimerManager.Instance.InternalInit();
// 延迟执行
TimerManager.Instance.Delay(2f, () => {
Debug.Log("2秒后执行");
});
// 间隔执行(每1秒触发,持续5秒)
TimerManager.Instance.Interval(1f, () => {
Debug.Log("间隔回调");
}, 5f);
// 循环执行
Timer loopTimer = TimerManager.Instance.Create(3f, true)
.OnLoopComplete(count => {
Debug.Log($"第{count}次循环");
if(count >= 3) loopTimer.Stop();
});
4.2 批量控制技巧
csharp复制// 游戏暂停时
void OnPauseGame()
{
TimerManager.Instance.PauseAll();
}
// 游戏恢复时
void OnResumeGame()
{
TimerManager.Instance.ResumeAll();
}
// 场景切换时
void OnSceneChange()
{
TimerManager.Instance.StopAll();
}
4.3 性能监控
csharp复制void OnGUI()
{
if(GUILayout.Button("打印计时器状态"))
{
TimerManager.Instance.PrintPoolStatus();
}
}
5. 高级优化与扩展
5.1 时间缩放支持
csharp复制// 在Timer类中添加
private float _timeScale = 1f;
public Timer SetTimeScale(float scale)
{
_timeScale = scale;
return this;
}
// 修改Update方法
internal void Update(float deltaTime)
{
if (_state != TimerState.Running) return;
_elapsedTime += deltaTime * _timeScale;
// ...其余逻辑不变
}
5.2 优先级系统
csharp复制// Timer类添加
private int _priority = 0;
public Timer SetPriority(int priority)
{
_priority = priority;
return this;
}
// TimerManager修改Update
void Update()
{
// 按优先级排序
var sortedTimers = _activeTimers.OrderByDescending(t => t.Priority);
foreach(var timer in sortedTimers)
{
timer.Update(Time.deltaTime);
}
}
5.3 编辑器可视化
csharp复制#if UNITY_EDITOR
[CustomEditor(typeof(TimerManager))]
public class TimerManagerEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
var manager = (TimerManager)target;
GUILayout.Label($"活跃计时器: {manager.ActiveCount}");
GUILayout.Label($"空闲计时器: {manager.IdleCount}");
if(GUILayout.Button("打印详细状态"))
{
manager.PrintDetailedStatus();
}
}
}
#endif
6. 性能对比与实测数据
在以下测试环境下进行性能对比:
- Unity 2021.3.15f1
- 测试平台:iPhone 13 Pro
- 测试场景:连续创建/销毁1000个计时器
| 实现方式 | GC触发次数 | 执行时间(ms) | 内存占用(MB) |
|---|---|---|---|
| 直接new/销毁 | 28 | 45.6 | 12.4 |
| 协程方式 | 15 | 38.2 | 9.8 |
| 对象池方案 | 0 | 22.1 | 5.2 |
关键发现:
- 对象池方案完全避免了GC分配
- 执行效率提升约50%
- 内存占用减少58%
7. 常见问题与解决方案
7.1 计时器不触发
可能原因:
- TimerManager未初始化
- 回调被意外清空
- 游戏对象被禁用
排查步骤:
- 检查是否调用了InternalInit()
- 在回调中添加日志确认是否被调用
- 使用PrintPoolStatus检查计时器状态
7.2 性能突然下降
可能原因:
- 对象池大小设置不合理
- 存在计时器泄漏(未正确回收)
- 间隔回调执行时间过长
优化方案:
- 调整initialPoolSize和maxPoolSize
- 确保所有计时器都有终止条件
- 将耗时操作移到主线程外执行
7.3 多场景切换问题
解决方案:
csharp复制// 在场景加载时清理计时器
void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
TimerManager.Instance.StopAll();
}
8. 工程实践建议
- 初始化时机:在游戏启动场景中尽早初始化TimerManager
- 池大小配置:根据项目需求调整初始值和最大值
- 回调注意事项:
- 避免在回调中执行耗时操作
- 使用?.Invoke()安全调用
- 确保回调中不包含对已销毁对象的引用
- 调试技巧:
- 为Timer添加Name属性便于识别
- 实现编辑器可视化工具
- 添加性能分析标记
我在实际项目中使用这套系统已经超过2年时间,它稳定支撑了包括:
- 技能冷却系统
- UI动画时序控制
- 任务系统计时
- 游戏逻辑事件调度
最复杂的应用场景是在一款MMO游戏中,同时管理超过500个活跃计时器,依然保持60FPS的流畅运行。
