如何正确编写服务器端游戏循环?

问题描述 投票:0回答:1

我正在用 C# 编写一个游戏服务器,我需要一个每秒调用

Update
方法 60 次的循环。

我尝试自己制作并写下了这个:

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 次:

Delta time: 00:00:00.0159905; TPS: 62,53713142178168
Delta time: 00:00:00.0160048; TPS: 62,48125562331301
Delta time: 00:00:00.0162349; TPS: 61,595698156440754
Delta time: 00:00:00.0157741; TPS: 63,395058989102395
Delta time: 00:00:00.0160049; TPS: 62,48086523502178
Delta time: 00:00:00.0160575; TPS: 62,27619492449012
Delta time: 00:00:00.0169454; TPS: 59,013065492700086
Delta time: 00:00:00.0160029; TPS: 62,48867392785058
Delta time: 00:00:00.0165768; TPS: 60,3252738767434
Delta time: 00:00:00.0160028; TPS: 62,4890644137276
Delta time: 00:00:00.0160041; TPS: 62,48398847795252
Delta time: 00:00:00.0169812; TPS: 58,88865333427556
Delta time: 00:00:00.0160020; TPS: 62,49218847644045
Delta time: 00:00:00.0160004; TPS: 62,49843753906152
Delta time: 00:00:00.0162851; TPS: 61,40582495655538
Delta time: 00:00:00.0169158; TPS: 59,11632911242743
Delta time: 00:00:00.0160949; TPS: 62,13148264357033
Delta time: 00:00:00.0159907; TPS: 62,53634925300331
Delta time: 00:00:00.0169858; TPS: 58,87270543630562
Delta time: 00:00:00.0160045; TPS: 62,482426817457586
Delta time: 00:00:00.0162416; TPS: 61,57028864151316
Delta time: 00:00:00.0160519; TPS: 62,29792111837228
Delta time: 00:00:00.0159664; TPS: 62,63152620503057

我尝试用

Task.Delay
替换
Thread.Sleep
并“解决”了问题:

Delta time: 00:00:00.0168171; TPS: 59,46328439505027
Delta time: 00:00:00.0163640; TPS: 61,10975311659741
Delta time: 00:00:00.0167748; TPS: 59,613229367861315
Delta time: 00:00:00.0172399; TPS: 58,00497682701176
Delta time: 00:00:00.0168041; TPS: 59,509286424146495
Delta time: 00:00:00.0169840; TPS: 58,87894488930759
Delta time: 00:00:00.0168015; TPS: 59,51849537243698
Delta time: 00:00:00.0170069; TPS: 58,79966366592384
Delta time: 00:00:00.0170132; TPS: 58,77789010885665
Delta time: 00:00:00.0169979; TPS: 58,83079674548033
Delta time: 00:00:00.0169138; TPS: 59,1233194196455
Delta time: 00:00:00.0169892; TPS: 58,860923410166464
Delta time: 00:00:00.0170012; TPS: 58,819377455709
Delta time: 00:00:00.0170006; TPS: 58,821453360469626
Delta time: 00:00:00.0170160; TPS: 58,76821814762577
Delta time: 00:00:00.0172717; TPS: 57,89818026019442
Delta time: 00:00:00.0161239; TPS: 62,01973467957504
Delta time: 00:00:00.0169921; TPS: 58,850877760841804
Delta time: 00:00:00.0170062; TPS: 58,802083945855045
Delta time: 00:00:00.0171900; TPS: 58,17335660267597
Delta time: 00:00:00.0168114; TPS: 59,483445757045814
Delta time: 00:00:00.0170135; TPS: 58,77685367502277
Delta time: 00:00:00.0170050; TPS: 58,80623346074684

但是,正如您所看到的,有时(并且经常)有些时候 TPS 会超过 60,但有时它会保持在 58,这也不是很好看。

我知道我可以做到:

TimeSpan deltaTime = TimeSpanUtils.Clamp(currentTick - lastTick, targetDeltaTime, maximumDeltaTime);

,但是在这种情况下代码能正常工作吗?
总的来说,有没有可能我不明白某些事情而一切都应该是这样的?

c# server-side game-loop
1个回答
0
投票

我从事过一个类似的项目,但它是 Minecraft 自定义服务器软件,

Thread.Sleep()
Task.Delay
的问题是它们在以毫秒为单位的等待时都不准确,请参见此处:

Stopwatch sw = new();
while (true)
{
   sw.Restart();
   await Task.Delay(3);
   Console.WriteLine("Awaited: " + sw.ElapsedMilliseconds);
}

输出:

Awaited: 16
Awaited: 18
Awaited: 17
Awaited: 8
Awaited: 14
Awaited: 18
Awaited: 11
Awaited: 14
Awaited: 15
Awaited: 15
Awaited: 15
Awaited: 15
Awaited: 15
Awaited: 15
Awaited: 19
Awaited: 11

可以看出,这是一个以毫秒为单位的完全不精确的等待。但这种行为是预期的,当你调用

Thread.Sleep
Task.Sleep
时,你给了系统短暂使用这个线程的可能性,因此这是一个不精确的等待,当不需要毫秒精度时使用
Thread.Sleep/Task.Delay

在我的研究过程中我遇到了

PeriodicTimer
,它的准确度非常高,问题是它是一个周期,这意味着如果刻度超出限制,那么下一个会更隐蔽,但总体来说效果很好,但是这不是一个完整的解决方案。

这意味着如果

Thread.Sleep/Task.Delay
不准确并且
PerioditTimer
不是我们想要的,那么此时我们唯一能做的就是使用我们自己的实现。我的推荐是这样的。

如果剩余的滴答时间(以毫秒为单位)是:

  • 小于 5 毫秒 不执行任何操作,继续下一个刻度。
  • 小于15ms 忙等待(空循环),然后继续下一个tick。
  • 超过15ms拨打电话
    Thread.Sleep/Task.Delay

如果忙等待时间很短(小于20ms),则可以忽略不计。

© www.soinside.com 2019 - 2024. All rights reserved.