Home > database >  Using C# async delays in Godot
Using C# async delays in Godot

Time:11-19

I am currently experimenting with Godot C# making a basic shooter and for the gun's fire rate I have been experimenting with different delay systems. Node Timers work although I'm trying to make the script generic, and the Timer calls seem to only call functions in the parent script.

I'm now looking at C#'s Task.Delay method and it also seems to work, with it being an async action it does not look to be affected by the frame rate or slow down the game.

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?

Here's the code below although I don't think it’s important:

 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;
}  

CodePudding user response:

I don't have any experience with Godot.. but my idea would be....

instead of using a timer, you could store the last shoottime in a variable/field. If you're trying to shoot within the lastTimeShot coolDown, just ignore the shoot command.

For example:

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;
}

Due to a valid comment of Theodor, about changing the system time would lead bug-prone gameplay.

I wrote a second version.

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();
}

I think this would be a better solution.

CodePudding user response:

I am no expert in Godot but I can tell that Task.Delay() is considered better than alternatives like Thread.Sleep() for example because being asynchronous i releases the thread to the thread pool and when the time has passed it continues execution, in contrast to the latter option that blocks the thread instead.

The problem I can see is that each web server can accept a max limit of concurrent requests, by using Task.Delay() in your code you can start accumulating requests "just waiting" due to the delay. So if your app starts receiving a big amount of requests coupled with a long Delay time that might be an issue with requests queued up (delay) or even denied.

If the delay is a number of seconds (significant time) then I would probably think about storing user in a cache (you can also store in a dictionary Dictionary<string, bool> where string is the userId but this solution will not scale out, that is why I suggest a distributed cache), and check (TryGetValue()) your cache if user is allowed to shoot. If delay is a couple of microseconds (affordable time) still not an ideal solution but it will probably be a problem.

CodePudding user response:

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?

Not per se. There is nothing in particular wrong with Task.Delay in games, nor too many instances of it.

However, what you are doing after Task.Delay can be a problem. If you execute await Task.Delay(span);, the code that comes after might run in a different thread, and thus it could cause a race condition. This is because of await, not because of Task.Delay.

For example, if after await Task.Delay(span); you will be adding a Node to the scene tree (e.g. a bullet), that will interfere with any other thread using the scene tree. And Godot will be using the scene tree every frame. A quick look at Thread-safe APIs will tell you that the scene tree is not thread-safe. By the way, the same happen with virtually any widget API out there.

The solution is use call_deferred (CallDeferred in C#) to interact with the scene tree. And, yes, that could offset the moment it happens to the next frame.


I'll give you a non threading alternative to do that.

There are method get_ticks_msec and get_ticks_usec (GetTicksMsec and GetTicksUsec in C#) on the OS class, that give you monotone time which you can use for time comparison.

So, if you make a queue with the times it should shoot (computed by taking the current time plus whatever interval you need). Then in your process or physics process callback, you can check the queue. Dequeue all the times that are overdue, and create those bullets.

If you don't want to solve this with Godot APIs, then start a Stopwatch at the start of the game, and use its elapsed time.


But perhaps that is not the mechanic you want anyway. If you want a good old cool-down, you can start the Stopwatch when you need the cool-down, and then compare the elapsed time with the cool-down duration you want to know if it is over.

  • Related