How I Spent My Friday Night, or Why Framerate is the Wrong Choice for Managing Time-based Animations, Actions, and Effects

Of the bugs I have written a lot the one that keeps biting me is improperly locking an animation, effect, or action to the framerate of the game rather than to an elapsed time on a clock. You can never depend on your game running at a specific and consistent framerate across every device it’ll get played on. This seems obvious, but is a very easy assumption to make when you’re in the thick of it. If a device slows down, or speeds up, suddenly that tuned animation that elapsed over the course of 60 frames might take 2 seconds to complete.

For a background animation this might be fine and this isn’t necessarily that bad. However, what if it’s a function that the user is relying on for either visual feedback, or to take their next action?

That’s bad.

The Dark Souls of This Bug

There was a bug in Dark Souls 2: Scholar of First Sin in which character equipment was breaking very fast on the PS4. Equipment in that game has a durability rating that goes down as you use it. If you go too long without repairing it it will break.

Turns out, they’d linked that decrementing of durability to the framerate. On the PS4 the game ran at a nice steady 60 frames per second and thereby was resulting in equipment breaking very fast.

Thankfully we have this lovely bit of honesty from the patch notes:

"Fixed issue whereby weapon durability was decreased drastically when used on enemy corpses, friendly characters, etc. (Especially apparent for users running the game at 60 fps as the durability decrease rate was linked to the frame rate)."

Source: IGN

Scope Creep Studios is Not a AAA Studio, but We Have Similar Problems

A common way that I’ve introduced these problems is by doing something like the following:

public IEnumerator moveItOverSome() {   
    while (gameObject.transform.localscale.x >= 0f) {
        gameObject.transform.localscale -= new Vector3(.1f, 0f, 0f);
        yield return null;
    }
}

Every frame the Coroutine is running you shrink the scale of the object a tenth of a unit until it’s below 0, which functionally means that it’s invisible. Please ignore the other four or five problems with that coroutine. I’m going for simple readability right now.

The issue with this is twofold:

  • If your framerate dips or spikes or whatever the movement isn’t going to work as expected
  • Coroutines sometimes don’t behave exactly how you want them to

Give me a specific example, please

We were running into an issue on our upcoming app, Night Lights: A Toddler Toy, where every once in a while the coroutine to shrink our object refused to add the Vector3 how we wanted it to be added. This would result in the coroutine running for seemingly forever as it slowly decremented the size of the object by .0000001f every frame, or it’d shrink part of the way and then get caught somewhere and looked to be about half the size it should have been or looked like a speck, or generally just wasn’t what we wanted.

After a lot of hours debugging—_a lot of hours_—we finally stopped and did what we should have done from the start: wrote the corotuine to shrink from it’s full size to 0 over the course of 1 second using a lerping function.

Lerping interpolates between two values based on a third value between 0 and 1. Roughly: how far along in this process am I? At the start: 0, at the end: 1. Unity includes a Lerp method that returns a Vector3.

For the coroutine then we need to note the values we want to go between and the start time and then every frame the coroutine runs we can determine how far along we are in the shrink.

To whit:

public IEnumerator shrink() {
    Vector3 startSize = new Vector3(2f, 2f, 1f);
    Vector3 endSize = new Vector3(0,0,1f);

    //When does all this start?
    float movementStart = Time.time;

    //While current time is less than when we started
    //Plus how long we want it to go for (1 second)
    while (Time.time <= movementStart + 1.0f) {

        //Calculate our new size using the current time (Time.time)
        Vector3 updatedSize = Vector3.Lerp (startSize, endSize, (Time.time - movementStart));

        //Update the gameObject’s size using the new Vector3
        gameObject.transform.localScale = updatedSize;
        yield return null;
    }
}

To the toddler both approaches—when they work—visually look similar. Our latter example is reliable though and thereby the better solution for our needs.

A Note About Toddlers Tastes for Bugs

We found in testing that toddlers often like it when the app breaks in strange ways, but they also tend to then want it to rebreak in those exact same ways again, which is hard to do.

If you’ve spent any time with toddlers that will not surprise you in the least.

What Did You Learn?

Tozier reminded me what’s really important in life when I subtweeted the very short version of this blog post:

"Imagine how much you're _learning_ though!"

Here’s what we learned:

When it comes to determining how something happens over time in your app or game you need to tie that action to some sort of timer. That timer could be the framerate, system time, game time, a clock in your basement that you have a webcam pointed to, or something else. Unless there’s a strong reason to do so one should reference a clock that isn’t tied to system performance.