Waitable FixedUpdateAsync inconsistent

I am trying to write a perfect delay function. Not in the sense that if I input 1f it should wait EXACTLY 1 second, 0.999999 is also fine. What’s important is that Given the same input, it would wait the same period every time. I achieved it using this simple set up:

private float _timePassed;
private bool _startDelay;

private void FixedUpdate()
{
    if (!_startDelay) return;
    _timePassed += Time.fixedDeltaTime;
    if (!(_timePassed >= ActivateDelay)) return;
    // Do something
    _startDelay = false;
}

But when I try to do it with an async function (Courutines don’t work as well), it does not wait the same amount of time every run:

private async void Delay(float delay, CancellationToken cancellationToken) 
{
    var startTime = Time.fixedTime; 
    var currentTime = startTime;
    while (currentTime - startTime < delay)
    {
        cancellationToken.ThrowIfCancellationRequested();
        currentTime += Time.fixedDeltaTime;
        await Awaitable.FixedUpdateAsync(cancellationToken); 
    }
    
    // Do something
}

Am I doing something wrong or is this a Unity limitation?

This is nothing to do with 2D physics (the reason I saw the post in the 2D area) so I’ll remove the tag and add the “scripting” tag for you.

FixedUpdate is part of the core player loop, things such as physics, animation etc are registered to run when it runs.

I’ll let someone in scripting answer you.

Thanks.

1 Like

What does that mean specifically and how did you determine this?

Note that FixedUpdate may ran multiple times per “update” if the framerate is low, and it’s “resolution” is only as good as the fixedDeltaTime (default: 0.02). So you may get a precise 1.000 delay or you may get 1.02 in the worst case.

The only way to make accurate timing is to use the Time class and setting a target time to wait for, like this:

float targetTime;
public bool IsTimeElapsed() => Time.time >= targetTime;

Set targetTime from anywhere and IsTimeElapsed will tell you when that time is over. Wrap this in a class or struct to use it anywhere. You do not need awaitables or coroutines for such a simple task.

Also note that accumulating delta times is also ripe with issues since you will always increment the time by either a set amount or by a variable but commonly the same amount (ie 0.01666). So the resolution for doing that is the time it takes to render frames on average.

But perhaps you need the timer to elapse in, say, LateUpdate when 0.01 of that frame time have already passed - this would enable the possibiliy of the IsTimeElapsed being false in FixedUpdate and becoming true in LateUpdate. Whether this is a good thing or not depends on your use case.

If you need really really precise timing you can also use the double times in the Time class. Check the other properties too to see which ones suits your need the best.

2 Likes

I am surprised they’re not working identically :thinking:

But if that is the case, you could always use a FixedUpdate method behind-the-scenes to drive an awaitable API:

using System;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;

public sealed class Delay : MonoBehaviour
{
	static Delay instance;
	readonly List<DelayInProgress> delaysInProgress = new();
	
	public static Awaitable ForSeconds(float delay, CancellationToken cancellationToken) 
	{
		var completionSource = new AwaitableCompletionSource();
		GetOrCreateInstance().delaysInProgress.Add(new(Time.time + delay , completionSource, cancellationToken));
		return completionSource.Awaitable;
	}
	
	void FixedUpdate()
	{
		var time = Time.time;
		for(int i = delaysInProgress.Count - 1; i >= 0; i--)
		{
			var delayInProgress = delaysInProgress[i];
			if(delayInProgress.cancellationToken.IsCancellationRequested)
			{
				delaysInProgress.RemoveAt(i);
				delayInProgress.completionSource.SetCanceled();
			}
			else if(time >= delayInProgress.endTime)
			{
				delaysInProgress.RemoveAt(i);
				delayInProgress.completionSource.SetResult();
			}
		}
	}
	
	static Delay GetOrCreateInstance()
	{
		if(instance)
		{
			return instance;
		}

		if(instance = FindAnyObjectByType<Delay>())
		{
			return instance;
		}
		
		if(!Application.isPlaying)
		{
			throw new NotSupportedException();
		}
		
		var gameObject = new GameObject("Delay");
		DontDestroyOnLoad(gameObject);
		return instance = gameObject.AddComponent<Delay>();
	}
	
	readonly struct DelayInProgress
	{
		public readonly float endTime;
		public readonly AwaitableCompletionSource completionSource;
		public readonly CancellationToken cancellationToken;
		
		public DelayInProgress(float endTime, AwaitableCompletionSource completionSource, CancellationToken cancellationToken)
		{
			this.endTime = endTime;
			this.completionSource = completionSource;
			this.cancellationToken = cancellationToken;
		}
	}
}

Also, always make sure to compare a time stamp against the current time, instead of doing _timePassed += Time.fixedDeltaTime. Because of the inherent inaccuracy of floating-point arithmetic, you can lose some precision every time you add two floats together - which can really add up over time.

Thank you everyone for the replies! I found a solution that works for me, but for anyone else who might have a similar issue, I wanna clarify my use case a bit. I am making a 2D physics-based puzzle game. In my game, the player can configure/plan, before the game starts, different elements to reach the end goal. One of those elements includes a timer that, after the game starts, waits for the set delay and applies a force to the player. The issue was that with the same configuration, the player would end up in different places once the simulation ended. After many hours of isolating the issue, I found out that the cause of this issue was the delayed thrust, which didn’t start after the same time every simulation. The solution is quite simple and obvious. I’m not sure why it took me so long to test it.

public static async Awaitable Delay(double delay, CancellationToken cancellationToken) 
{
    var startTime = Time.fixedTimeAsDouble;
    while (Time.fixedTimeAsDouble <= startTime + delay)
    {
        cancellationToken.ThrowIfCancellationRequested();
        await Awaitable.FixedUpdateAsync(cancellationToken); 
    }
}

Thank you for the quick replies and suggestions, I appreciate it (:

1 Like