如何正确编写C#游戏服务器端60TPS的Tick循环?
C#游戏服务器60TPS循环的实现问题与优化
我用C#开发游戏服务器,需要实现每秒调用60次Update方法的循环,自行编写了异步TickLoop代码:
async Task TickLoop() { const int tps = 60; const int maxTPS = 3; Logger.Information("HPET enabled: {Value}", Stopwatch.IsHighResolution); TimeSpan targetDeltaTime = TimeSpan.FromSeconds(1d / tps); TimeSpan maximumDeltaTime = TimeSpan.FromSeconds(1d / maxTPS); Stopwatch stopwatch = Stopwatch.StartNew(); TimeSpan lastTick = stopwatch.Elapsed; while (true) { TimeSpan currentTick = stopwatch.Elapsed; TimeSpan deltaTime = TimeSpanUtils.Min(currentTick - lastTick, maximumDeltaTime); lastTick = currentTick; try { Logger.Information($"Delta time: {deltaTime}; TPS: {1 / deltaTime.TotalSeconds}"); // todo } catch (Exception e) { Logger.Error(e, "Caught an exception in game loop"); } TimeSpan freeTime = targetDeltaTime - (stopwatch.Elapsed - currentTick); if (freeTime > TimeSpan.Zero) await Task.Delay(freeTime); } }
遇到的问题
- 日志显示循环每秒调用约62次,偏离60TPS目标
- 将
Task.Delay替换为Thread.Sleep后,TPS在58-62之间波动,稳定性依然不佳
我的疑问
- 考虑用
TimeSpan deltaTime = TimeSpanUtils.Clamp(currentTick - lastTick, targetDeltaTime, maximumDeltaTime);处理deltaTime是否可行? - 是否我对游戏循环的理解存在偏差?
问题分析与解决方案
1. TPS不稳定的核心原因
Task.Delay和Thread.Sleep的系统定时器精度有限(通常最小粒度为10-15ms),而60TPS对应的目标间隔仅约16.67ms,微小的精度误差就会导致实际TPS偏离。此外,当前逻辑未处理时间累积,单次循环的执行延迟或计时偏差会直接传递到下一次循环,无法修正进度差。
2. 直接Clamp deltaTime不可行
用Clamp强制将deltaTime限制在targetDeltaTime和maximumDeltaTime之间会导致服务器逻辑进度与实际时间脱节。例如,若某次循环因阻塞导致间隔超过目标值,强制拉平deltaTime会让玩家移动、技能冷却等逻辑变慢,引发状态不同步问题。
正确思路是累积未处理的时间差,通过"补帧"追赶进度,但需限制单次循环的最大补帧数,避免服务器过载。
3. 优化后的循环实现
调整逻辑加入时间累积与补帧机制,同时优化CPU占用:
async Task TickLoop() { const int tps = 60; const int maxFramesPerLoop = 5; // 单次循环最多补5帧,防止卡死 Logger.Information("HPET enabled: {Value}", Stopwatch.IsHighResolution); TimeSpan targetDeltaTime = TimeSpan.FromSeconds(1d / tps); Stopwatch stopwatch = Stopwatch.StartNew(); TimeSpan accumulatedTime = TimeSpan.Zero; TimeSpan lastTime = stopwatch.Elapsed; while (true) { TimeSpan currentTime = stopwatch.Elapsed; TimeSpan frameTime = currentTime - lastTime; lastTime = currentTime; // 累积本次循环的时间差 accumulatedTime += frameTime; // 处理所有可执行的逻辑帧 int framesProcessed = 0; while (accumulatedTime >= targetDeltaTime && framesProcessed < maxFramesPerLoop) { try { // 传入固定的targetDeltaTime作为逻辑步长,保证逻辑稳定 // Update(targetDeltaTime); Logger.Information($"TPS: {1 / targetDeltaTime.TotalSeconds:N2}"); } catch (Exception e) { Logger.Error(e, "Caught an exception in game loop"); } accumulatedTime -= targetDeltaTime; framesProcessed++; } // 计算空闲时间并合理让出CPU TimeSpan freeTime = targetDeltaTime - accumulatedTime; if (freeTime > TimeSpan.Zero) { await Task.Delay(freeTime); } else { // 无空闲时间时让出CPU切片,避免占用100%CPU await Task.Yield(); } } }
4. 关键优化点
- 固定逻辑步长:Update使用固定的
targetDeltaTime,确保逻辑计算不受帧时间波动影响,保证速度稳定 - 时间累积与补帧:将超出目标间隔的时间累积,分批处理逻辑帧,既保证进度不脱节,又通过
maxFramesPerLoop防止服务器过载 - CPU友好:无空闲时间时调用
Task.Yield()让出CPU,避免线程持续占用资源
内容的提问来源于stack exchange,提问作者C6OI
相关产品推荐
相关产品推荐

