我正在用 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);
,但是在这种情况下代码能正常工作吗?
总的来说,有没有可能我不明白某些事情而一切都应该是这样的?
我从事过一个类似的项目,但它是 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
不是我们想要的,那么此时我们唯一能做的就是使用我们自己的实现。我的推荐是这样的。
如果剩余的滴答时间(以毫秒为单位)是:
Thread.Sleep/Task.Delay
。如果忙等待时间很短(小于20ms),则可以忽略不计。