如何实现游戏开发中的计时服务(Unity版)

        在游戏开发中,计时系统是一个基础且核心的组件。从技能冷却、任务倒计时到周期性事件触发,几乎所有游戏都离不开精准可靠的计时服务。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,采用独立线程处理计时逻辑,适用于对性能和精度要求较高的场景。(客户端和服务端均可以使用哦)

        核心设计思路​

        采用 "独立线程 + 最小堆" 的方案实现计时服务:​

  1. 独立线程:使用后台线程处理计时逻辑,避免占用主线程资源​
  2. 最小堆(优先队列):高效管理任务执行顺序,确保最快到期的任务优先处理​
  3. 线程安全队列:实现主线程与计时线程的安全通信​
  4. SynchronizationContext:确保回调在主线程执行,兼容 Unity API​
  5. 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 最佳实践​

        这个计时服务可以满足大多数游戏的计时需求,从简单的技能冷却到复杂的任务系统都能胜任。在实际项目中,可根据具体需求进一步扩展功能,如添加任务优先级、暂停 / 继续功能、时间缩放支持等。​

希望本文能帮助你理解计时系统的设计原理,实现更高效、更可靠的游戏计时功能。

内容概要:该论文探讨了一种基于粒子群优化(PSO)的STAR-RIS辅助NOMA无线通信网络优化方法。STAR-RIS作为一种新型可重构智能表面,能同时反射和传输信号,与传统仅能反射的RIS不同。结合NOMA技术,STAR-RIS可以提升覆盖范围、用户容量和频谱效率。针对STAR-RIS元素众多导致获取完整信道状态信息(CSI)开销大的问题,作者提出一种在不依赖完整CSI的情况下,联合优化功率分配、基站波束成形以及STAR-RIS的传输和反射波束成形向量的方法,以最大化总可实现速率并确保每个用户的最低速率要求。仿真结果显示,该方案优于STAR-RIS辅助的OMA系统。 适合人群:具备一定无线通信理论基础、对智能反射面技术和非正交多址接入技术感兴趣的科研人员和工程师。 使用场景及目标:①适用于希望深入了解STAR-RIS与NOMA结合的研究者;②为解决无线通信中频谱资源紧张、提高系统性能提供新的思路和技术手段;③帮助理解PSO算法在无线通信优化问题中的应用。 其他说明:文中提供了详细的Python代码实现,涵盖系统参数设置、信道建模、速率计算、目标函数定义、约束条件设定、主优化函数设计及结果可视化等环节,便于读者理解和复现实验结果。此外,文章还对比了PSO与其他优化算法(如DDPG)的区别,强调了PSO在不需要显式CSI估计方面的优势。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值