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

如何正确编写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之间波动,稳定性依然不佳

我的疑问

  1. 考虑用TimeSpan deltaTime = TimeSpanUtils.Clamp(currentTick - lastTick, targetDeltaTime, maximumDeltaTime);处理deltaTime是否可行?
  2. 是否我对游戏循环的理解存在偏差?

问题分析与解决方案

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 00:32:02