You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中用CreateTimerQueueTimer实现10ms高精度定时器遇阻求助

问题

在Unity中需要实现每10ms触发一次C#回调,要求尽可能达到1ms精度。目前用自旋循环+Stopwatch实现了<1ms的误差,但会占满一个CPU核心,不够理想。尝试使用Windows的CreateTimerQueueTimer API解决,但无法实现15ms以下的触发间隔(精度表现没问题),调用timeBeginPeriod(1)也没有改善。想问是代码问题还是方案本身不可行?测试代码如下:

using System.Runtime.InteropServices;
using System;
using UnityEditor;
using UnityEngine;
public static class TestHighResTimer {

    private static readonly string debug_id = "[HighRes Timer]";
    private static System.Diagnostics.Stopwatch sw;
    private static int count = 0;
    private static IntPtr timer = IntPtr.Zero;

    [InitializeOnLoadMethod]
    private static void Init()
    {
        //Setup some callback so that we don't crash when editing code, reloading domain ect...
        AssemblyReloadEvents.beforeAssemblyReload += Destroy;
        EditorApplication.update += StartTimer;
        EditorApplication.update += EditorUpdate;
        Debug.Log($"{debug_id} Init");
    }

    private static void Destroy()
    {
        Debug.Log($"{debug_id} Destroy");
        EditorApplication.update -= EditorUpdate;
    }

    private static void StartTimer()
    {
        EditorApplication.update -= StartTimer;

        Debug.Log($"{debug_id} Start");

        //Prepare the stopwatch and count to measure the callback interval
        sw = new System.Diagnostics.Stopwatch();
        count = 0;

        timeBeginPeriod(1); //It is supposed to improved accuracy of timer event but I didn't notice a change...
        CreateTimerQueueTimer(out timer, IntPtr.Zero, TimerCallback, IntPtr.Zero, 1, 10, 0); //Start a timer to fire the callback every 10ms
    }

    private static void EditorUpdate()
    {
        //We limit the test at 1000 iterations otherwise it will run indefinitely
        if(count > 1000)
        {
            DeleteTimerQueueTimer(IntPtr.Zero, timer, IntPtr.Zero); //Clear the timer
            timeEndPeriod(1);
            count = 0;
        }
    }

    private static void TimerCallback(IntPtr p, bool b)
    {
        Debug.Log($"{debug_id} Tick : {sw.Elapsed.TotalMilliseconds} [ms]");
        sw.Restart();
        count++;
    }

    public delegate void WaitOrTimerDelegate(IntPtr lpParameter, bool TimerOrWaitFired);

    [DllImport("kernel32.dll")]
    static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer, IntPtr TimerQueue, WaitOrTimerDelegate Callback, 
                                                 IntPtr Parameter, uint DueTime, uint Period, uint Flags);

    [DllImport("kernel32.dll")]
    static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, IntPtr CompletionEvent);

    [DllImport("winmm.dll")]
    public static extern uint timeBeginPeriod(int msec);
    [DllImport("winmm.dll")]
    public static extern uint timeEndPeriod(int msec);
}
分析与解决方案

你的代码存在几个关键问题,同时CreateTimerQueueTimer的特性也需要注意,以下是具体说明和优化方案:

  • Debug.Log是间隔不达标的核心原因
    Unity编辑器中的Debug.Log是高开销IO操作,高频调用时会直接拖慢回调执行速度,导致实际触发间隔被拉长到15ms以上。应该将实时打印改为事后统计,用变量记录所有间隔值,最后统一输出结果。

  • 调整CreateTimerQueueTimer的参数
    该API的默认回调是在线程池线程执行,会有额外调度开销。可以给Flags参数传入0x00000020(对应WT_EXECUTEINTIMERTHREAD常量),让回调直接在定时器线程执行,减少调度延迟。另外,确保timeBeginPeriod(1)调用后没有其他程序重置系统时钟分辨率。

  • 修复定时器销毁的安全问题
    当前调用DeleteTimerQueueTimer时传入IntPtr.Zero作为完成事件,可能在回调执行中直接销毁定时器,引发未定义行为。优化时要确保定时器销毁前回调已执行完毕,或者在销毁逻辑中增加状态判断。

  • 优化后的测试代码

using System.Runtime.InteropServices;
using System;
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;

public static class TestHighResTimer {

    private static readonly string debug_id = "[HighRes Timer]";
    private static System.Diagnostics.Stopwatch sw;
    private static int count = 0;
    private static IntPtr timer = IntPtr.Zero;
    private static List<double> intervalRecords = new List<double>();

    [InitializeOnLoadMethod]
    private static void Init()
    {
        AssemblyReloadEvents.beforeAssemblyReload += Destroy;
        EditorApplication.update += StartTimer;
        EditorApplication.update += EditorUpdate;
        Debug.Log($"{debug_id} Init");
    }

    private static void Destroy()
    {
        Debug.Log($"{debug_id} Destroy");
        if (timer != IntPtr.Zero)
        {
            DeleteTimerQueueTimer(IntPtr.Zero, timer, IntPtr.Zero);
            timeEndPeriod(1);
            timer = IntPtr.Zero;
        }
        EditorApplication.update -= EditorUpdate;
    }

    private static void StartTimer()
    {
        EditorApplication.update -= StartTimer;

        Debug.Log($"{debug_id} Start");

        sw = new System.Diagnostics.Stopwatch();
        count = 0;
        intervalRecords.Clear();

        timeBeginPeriod(1);
        // 使用WT_EXECUTEINTIMERTHREAD标志减少线程池调度开销
        CreateTimerQueueTimer(out timer, IntPtr.Zero, TimerCallback, IntPtr.Zero, 1, 10, 0x00000020);
    }

    private static void EditorUpdate()
    {
        if(count > 1000)
        {
            DeleteTimerQueueTimer(IntPtr.Zero, timer, IntPtr.Zero);
            timeEndPeriod(1);
            timer = IntPtr.Zero;
            
            // 统计并输出结果
            double avgInterval = intervalRecords.Average();
            double minInterval = intervalRecords.Min();
            double maxInterval = intervalRecords.Max();
            Debug.Log($"{debug_id} 统计结果 - 平均间隔: {avgInterval:F2}ms, 最小间隔: {minInterval:F2}ms, 最大间隔: {maxInterval:F2}ms");
            
            count = 0;
            intervalRecords.Clear();
        }
    }

    private static void TimerCallback(IntPtr p, bool b)
    {
        if (sw.IsRunning)
        {
            intervalRecords.Add(sw.Elapsed.TotalMilliseconds);
        }
        sw.Restart();
        count++;
    }

    public delegate void WaitOrTimerDelegate(IntPtr lpParameter, bool TimerOrWaitFired);

    [DllImport("kernel32.dll")]
    static extern bool CreateTimerQueueTimer(out IntPtr phNewTimer, IntPtr TimerQueue, WaitOrTimerDelegate Callback, 
                                                 IntPtr Parameter, uint DueTime, uint Period, uint Flags);

    [DllImport("kernel32.dll")]
    static extern bool DeleteTimerQueueTimer(IntPtr TimerQueue, IntPtr Timer, IntPtr CompletionEvent);

    [DllImport("winmm.dll")]
    public static extern uint timeBeginPeriod(int msec);
    [DllImport("winmm.dll")]
    public static extern uint timeEndPeriod(int msec);
}
  • 其他替代方案
    如果上述优化后仍达不到要求,可以尝试:
    • 用QueryPerformanceCounter结合低优先级线程的短休眠+微调,替代自旋循环,在保证精度的同时降低CPU占用;
    • 运行时环境下,修改Time.fixedDeltaTime为0.01s(100Hz),配合FixedUpdate实现近似10ms的回调,该方式依赖Unity主线程调度,精度略逊但CPU占用极低。

内容的提问来源于stack exchange,提问作者Eledwin

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 18:15:15