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

