在 Godot 中使用 C# 异步延迟
Using C# async delays in Godot
我目前正在尝试使用 Godot C# 制作一个基本的射击游戏,并且为了枪的射速,我一直在尝试不同的延迟系统。尽管我试图使脚本通用,但节点计时器仍然有效,而且计时器调用似乎只调用父脚本中的函数。
我现在正在研究 C# 的 Task.Delay 方法,它似乎也有效,因为它是一个异步操作,它看起来不会受到帧速率的影响或减慢游戏速度。
我的问题是,在游戏应用程序中使用 Task.Delay 是否存在任何已知问题:例如它是否不可靠,或者如果调用该方法的实例过多,它会崩溃吗?
下面是代码,虽然我认为它不重要:
private void shoot() {
//if "canShoot" spawn bullet
ShootCooledDown();
}
private async void ShootCooledDown() {
TimeSpan span = TimeSpan.FromSeconds((double)(new decimal(shotDelay)));
canShoot = false;
await Task.Delay(span);
canShoot = true;
}
我不是 Godot 的专家,但我可以说 Task.Delay()
被认为比 Thread.Sleep()
这样的替代方案更好,例如因为异步我将线程释放到线程池并且时间到了通过它继续执行,与后一个阻塞线程的选项相反。
我看到的问题是每个 Web 服务器可以接受最大限制的并发请求,通过在代码中使用 Task.Delay()
,您可以开始累积由于延迟而“等待”的请求。因此,如果您的应用程序开始收到大量请求并伴有较长的延迟时间,这可能是请求排队(延迟)甚至被拒绝的问题。
如果延迟是数秒(重要时间),那么我可能会考虑将用户存储在缓存中(您也可以存储在字典中 Dictionary<string, bool>
,其中字符串是用户 ID,但此解决方案将不横向扩展,这就是我建议使用分布式缓存的原因),如果允许用户射击,则检查 (TryGetValue()
) 您的缓存。如果延迟是几微秒(可承受的时间)仍然不是理想的解决方案,但它可能会成为一个问题。
我对 Godot 没有任何经验..但我的想法是....
您可以将最后一次拍摄时间存储在 variable/field 中,而不是使用计时器。如果您想在 lastTimeShot+coolDown
范围内射击,请忽略射击命令。
例如:
private DateTime _lastShot = DateTime.MinValue;
private void shoot()
{
TimeSpan span = TimeSpan.FromSeconds((double)(new decimal(shotDelay)));
// if the time when the last shot has fire with the cooldown time
// is greater than the current time. You are still in the cooldown time.
if(_lastShot.Add(span) > DateTime.UtcNow)
return; // within cooldown, do nothing
//if "canShoot" spawn bullet
ShootCooledDown();
_lastShot = DateTime.UtcNow;
}
根据 Theodor 的有效评论,更改系统时间会导致游戏容易出错。
我写了第二个版本
private Stopwatch _shootingCooldownStopwatch = default;
private void shoot()
{
var shotDelayMs = shotDelay * 1000;
// if the _shootingCooldownStopwatch is ever started
// and the ElapsedMilliseconds are in the showDelay
// we're not allowed to fire again. So exit the method.
if (_shootingCooldownStopwatch?.ElapsedMilliseconds < shotDelayMs)
return;
_shootingCooldownStopwatch = Stopwatch.StartNew();
//if "canShoot" spawn bullet
ShootCooledDown();
}
我认为这是一个更好的解决方案。
My question is, is there any known issue for using Task.Delay in game applications: like is it unreliable or can it crash if too many instances of the method are called?
本身不是。 Task.Delay
在游戏中没有什么特别的问题,也没有太多的例子。
但是,您在 Task.Delay
之后所做的事情可能会有问题。如果您执行 await Task.Delay(span);
, 之后的代码可能 运行 在不同的线程中,因此它可能导致竞争条件。 这是因为await
,不是因为Task.Delay
。
例如,如果在 await Task.Delay(span);
之后您将向场景树添加 Node
(例如子弹),这将干扰使用场景树的任何其他线程。 Godot 将在每一帧中使用场景树。快速浏览 Thread-safe APIs 会告诉您场景树不是线程安全的。 顺便说一句,几乎任何小部件 API 都会发生同样的情况。
解决方案是使用call_deferred
(C#中的CallDeferred
)与场景树交互。而且,是的,这可以抵消下一帧发生的那一刻。
我会给你一个非线程的替代方法来做到这一点。
OS
class 上有方法 get_ticks_msec
和 get_ticks_usec
(C# 中的 GetTicksMsec
和 GetTicksUsec
)可用于时间比较的单调时间。
所以,如果你用它应该拍摄的时间制作一个队列(通过使用当前时间加上你需要的任何间隔来计算)。然后在您的流程或物理流程回调中,您可以检查队列。将所有逾期的时间出队,并创建那些项目符号。
如果你不想用 Godot APIs 解决这个问题,那么在游戏开始时启动一个 Stopwatch
,并使用它的运行时间。
但也许这不是您想要的机制。如果你想要一个好的老cool-down,你可以在你需要cool-down的时候开始Stopwatch
,然后将经过的时间与你想知道的cool-down持续时间进行比较,就知道是否结束了。
当您在 Godot 或任何其他游戏引擎中开发游戏时,您不应使用任何基于计算机时钟的计时器,例如 Stopwatch
或 Task.delay
。相反,您必须使用 _Process(float delta)
或 _PhysicsProcess(float delta)
方法接收的前一帧的增量时间来管理自己经过的时间。原因是:
- 在帧率下降的情况下,时间会更准确。
- 如果暂停游戏,计时器也会暂停。
这就是 Godot 为您提供 Timer 组件的主要原因,您必须将其附加到当前场景才能使用它。
如果你不想在场景中添加任何东西,这完全合理,你必须获取增量,将经过的时间存储在一个变量中并检查该变量是否达到某个限制。
在我的游戏中,我使用自己的计时器,非常简单 class:
public class Timer {
public float Elapsed { get; private set; } = 0;
public bool Stopped { get; private set; } = true;
public float Alarm { get; private set; } = float.MaxValue;
public Timer Start() {
Stopped = false;
return this;
}
public Timer Stop() {
Stopped = true;
return this;
}
public Timer Reset() {
Elapsed = 0;
return this;
}
public Timer ClearAlarm() {
Alarm = float.MaxValue;
return this;
}
public Timer SetAlarm(float finish) {
Alarm = finish;
return this;
}
public bool IsAlarm() => Elapsed > Alarm;
public Timer Update(float delta) {
if (!Stopped) {
Elapsed += delta;
}
return this;
}
}
```
You have to Update the timer in every frame
我目前正在尝试使用 Godot C# 制作一个基本的射击游戏,并且为了枪的射速,我一直在尝试不同的延迟系统。尽管我试图使脚本通用,但节点计时器仍然有效,而且计时器调用似乎只调用父脚本中的函数。
我现在正在研究 C# 的 Task.Delay 方法,它似乎也有效,因为它是一个异步操作,它看起来不会受到帧速率的影响或减慢游戏速度。
我的问题是,在游戏应用程序中使用 Task.Delay 是否存在任何已知问题:例如它是否不可靠,或者如果调用该方法的实例过多,它会崩溃吗?
下面是代码,虽然我认为它不重要:
private void shoot() {
//if "canShoot" spawn bullet
ShootCooledDown();
}
private async void ShootCooledDown() {
TimeSpan span = TimeSpan.FromSeconds((double)(new decimal(shotDelay)));
canShoot = false;
await Task.Delay(span);
canShoot = true;
}
我不是 Godot 的专家,但我可以说 Task.Delay()
被认为比 Thread.Sleep()
这样的替代方案更好,例如因为异步我将线程释放到线程池并且时间到了通过它继续执行,与后一个阻塞线程的选项相反。
我看到的问题是每个 Web 服务器可以接受最大限制的并发请求,通过在代码中使用 Task.Delay()
,您可以开始累积由于延迟而“等待”的请求。因此,如果您的应用程序开始收到大量请求并伴有较长的延迟时间,这可能是请求排队(延迟)甚至被拒绝的问题。
如果延迟是数秒(重要时间),那么我可能会考虑将用户存储在缓存中(您也可以存储在字典中 Dictionary<string, bool>
,其中字符串是用户 ID,但此解决方案将不横向扩展,这就是我建议使用分布式缓存的原因),如果允许用户射击,则检查 (TryGetValue()
) 您的缓存。如果延迟是几微秒(可承受的时间)仍然不是理想的解决方案,但它可能会成为一个问题。
我对 Godot 没有任何经验..但我的想法是....
您可以将最后一次拍摄时间存储在 variable/field 中,而不是使用计时器。如果您想在 lastTimeShot+coolDown
范围内射击,请忽略射击命令。
例如:
private DateTime _lastShot = DateTime.MinValue;
private void shoot()
{
TimeSpan span = TimeSpan.FromSeconds((double)(new decimal(shotDelay)));
// if the time when the last shot has fire with the cooldown time
// is greater than the current time. You are still in the cooldown time.
if(_lastShot.Add(span) > DateTime.UtcNow)
return; // within cooldown, do nothing
//if "canShoot" spawn bullet
ShootCooledDown();
_lastShot = DateTime.UtcNow;
}
根据 Theodor 的有效评论,更改系统时间会导致游戏容易出错。
我写了第二个版本
private Stopwatch _shootingCooldownStopwatch = default;
private void shoot()
{
var shotDelayMs = shotDelay * 1000;
// if the _shootingCooldownStopwatch is ever started
// and the ElapsedMilliseconds are in the showDelay
// we're not allowed to fire again. So exit the method.
if (_shootingCooldownStopwatch?.ElapsedMilliseconds < shotDelayMs)
return;
_shootingCooldownStopwatch = Stopwatch.StartNew();
//if "canShoot" spawn bullet
ShootCooledDown();
}
我认为这是一个更好的解决方案。
My question is, is there any known issue for using Task.Delay in game applications: like is it unreliable or can it crash if too many instances of the method are called?
本身不是。 Task.Delay
在游戏中没有什么特别的问题,也没有太多的例子。
但是,您在 Task.Delay
之后所做的事情可能会有问题。如果您执行 await Task.Delay(span);
, 之后的代码可能 运行 在不同的线程中,因此它可能导致竞争条件。 这是因为await
,不是因为Task.Delay
。
例如,如果在 await Task.Delay(span);
之后您将向场景树添加 Node
(例如子弹),这将干扰使用场景树的任何其他线程。 Godot 将在每一帧中使用场景树。快速浏览 Thread-safe APIs 会告诉您场景树不是线程安全的。 顺便说一句,几乎任何小部件 API 都会发生同样的情况。
解决方案是使用call_deferred
(C#中的CallDeferred
)与场景树交互。而且,是的,这可以抵消下一帧发生的那一刻。
我会给你一个非线程的替代方法来做到这一点。
OS
class 上有方法 get_ticks_msec
和 get_ticks_usec
(C# 中的 GetTicksMsec
和 GetTicksUsec
)可用于时间比较的单调时间。
所以,如果你用它应该拍摄的时间制作一个队列(通过使用当前时间加上你需要的任何间隔来计算)。然后在您的流程或物理流程回调中,您可以检查队列。将所有逾期的时间出队,并创建那些项目符号。
如果你不想用 Godot APIs 解决这个问题,那么在游戏开始时启动一个 Stopwatch
,并使用它的运行时间。
但也许这不是您想要的机制。如果你想要一个好的老cool-down,你可以在你需要cool-down的时候开始Stopwatch
,然后将经过的时间与你想知道的cool-down持续时间进行比较,就知道是否结束了。
当您在 Godot 或任何其他游戏引擎中开发游戏时,您不应使用任何基于计算机时钟的计时器,例如 Stopwatch
或 Task.delay
。相反,您必须使用 _Process(float delta)
或 _PhysicsProcess(float delta)
方法接收的前一帧的增量时间来管理自己经过的时间。原因是:
- 在帧率下降的情况下,时间会更准确。
- 如果暂停游戏,计时器也会暂停。
这就是 Godot 为您提供 Timer 组件的主要原因,您必须将其附加到当前场景才能使用它。 如果你不想在场景中添加任何东西,这完全合理,你必须获取增量,将经过的时间存储在一个变量中并检查该变量是否达到某个限制。
在我的游戏中,我使用自己的计时器,非常简单 class:
public class Timer {
public float Elapsed { get; private set; } = 0;
public bool Stopped { get; private set; } = true;
public float Alarm { get; private set; } = float.MaxValue;
public Timer Start() {
Stopped = false;
return this;
}
public Timer Stop() {
Stopped = true;
return this;
}
public Timer Reset() {
Elapsed = 0;
return this;
}
public Timer ClearAlarm() {
Alarm = float.MaxValue;
return this;
}
public Timer SetAlarm(float finish) {
Alarm = finish;
return this;
}
public bool IsAlarm() => Elapsed > Alarm;
public Timer Update(float delta) {
if (!Stopped) {
Elapsed += delta;
}
return this;
}
}
```
You have to Update the timer in every frame