+12
Pending Review

Bug: deltaTime off by one frame

valler 7 months ago updated by Lazlo Bonin (Lead Developer) 6 months ago 1

Update: I revisited this issue. You can find a big update at the bottom. I keep the old content at the top, to provide context.

/////////////////////////////////////

Original post:

It looks like Chronos has an issue with the way it calculates (unscaled) delta time.

I'll give examples of what one might expect first, and then compare it against the Chronos result.

In Unity Time.deltaTime is the time it took from the beginning of the previous frame to the beginning of the current frame.

// Works with any time scale, in any frame
// Replace any time/deltaTime with unscaledTime/unscaledDeltaTime if you wish,
// Or try both ...

void Update ()
{
    // Very first Update, frameCount == 0 in Awake and Start
    if (Time.frameCount == 1 )
    {
        previousTime = Time.time; // 0f
        return;
    }
    
    Assert.AreEqual(previousTime + Time.deltaTime, Time.time);

    previousTime = Time.time;
}

However Chronos is kinda off by one frame. It adds deltaTime onto the current time, instead of the previous time, and thus is ahead of Unity by deltaTime (or would be, ignoring the fact that Chronos does some weird things for frame 0, 1 and 2)..

I'm not going to show the actual source here. Conceptually Chronos does this:

void Update() => clock.time = Time.time + Time.deltaTime;

Instead of:

void Update() => clock.time = previousTime + Time.deltaTime;

Is there a reason behind this design? I don't see how this improves things compared to what Unity already does.

/////////////////////////////////////

Update:

Given that Unity already does all this, why is Chronos at all concerned with manually handling the time scale (set aside it allows for negative scale)?

Unity does this:

// pseudo code
void BeforeUpdate()
{
    // edit: corrected
    if (Time.frameCount == 0)
    {
        previousTime = Time.unscaledTime;
        return;
    }
    Time.unscaledDeltaTime = Time.unscaledTime - previousTime;
    previousTime = Time.unscaledTime;
    
    Time.deltaTime = Time.unscaledDeltaTime * Time.timeScale;

    Time.time += Time.deltaTime;
}

And now Chronos kicks in and applies the same operation ontop of this result:

// pseudo code
void Update()
{
    // Very first Update, frameCount == 0 happens in Awake and Start
    if (Time.frameCount <= 2 )
    {
        // Don't know why this needs special treatment.
    }

    clock.unscaledTime += Time.unscaledDeltaTime;
    
    clock.deltaTime = Time.unscaledDeltaTime * theChronosTimeScale;

    clock.time += clock.deltaTime;
}

It get's the fixed update "wrong" aswell.

First, what Unity provides us with:

// pseudo code

// Behaves "in reverse" of deltaTime to keep fixed steps constant
BeforeFixedUpdate() =>
    // Shows only the scaling part
    Time.fixedUnscaledDeltaTime = Time.fixedDeltaTime / Time.timeScale; 

But then the docs recommend compensating for this behaviour: manual. This is what Chronos does too, thereby losing fixedUnscaledTime and making the changes available the next frame.

Instead one could:

// pseudo code

const float fixedStep = 0.02f;

void Update()
{
    // Let Unity pick up the changes
    // This would be more complex for multiple objects with different timescales
    Time.fixedDeltaTime = fixedStep * clock.timeScale;

    // Just like Unity's interface
    clock.fixedUnscaledDeltaTime = Time.fixedDeltaTime;
    clock.fixedUnscaledTime += clock.fixedUnscaledDeltaTime;
    clock.fixedDeltaTime = clock.fixedUnscaledDeltaTime * clock.timeScale;
    clock.fixedTime += clock.fixedDeltaTime;
}

I kept out clamping time values for brevity.

Plus, this approach does not need special treatment of magic frames, unless I've overlooked something.

Chronos Version:
2.4.14
Unity Version:
2019.1.4f1
Pending Review

Hi Valler,

Sorry for the late reply on this. Thanks for taking the time to investigate and report this behaviour.

As you guessed, the main reason why Chronos overrides all Unity time measurements is to allow negative time scales, which is a core feature of the plugin. It also allows us to use per-object, group or area time scales, which is not possible in default Unity.

The special 2 frames handling is something I implemented years ago; I must admit I don't remember the exact reason, but I remember it was to compensate for an internal Unity bug.

If I understand correctly, your main suggestion is to change the way time values are incremented in the Clock class' update loop. From your pseudo-code, I don't exactly understand what it is you're proposing. Can you be more specific about the local changes you made? Don't worry about posting parts of the source publicly on the forum, I don't mind.