DEV Community

Cover image for Programming "Natural" Idle Character Animations
ArkfulDodger
ArkfulDodger

Posted on

Programming "Natural" Idle Character Animations

When you create animations for a character, one of the key animations you should consider is the idle animation.

Idle Animation - animation that occurs when the character is not performing actions

The purpose of an idle animation is to keep the character feeling "alive" even when they aren't doing anything. Having excellent animation assets can help with creating this illusion, but any animation run on a loop (as our idle animation will be) is bound to feel artificial once the eye catches on to the repetitive pattern.

Below, we are going to explore how to achieve more "natural" idle behavior by introducing purposeful randomness into our code.


Assumptions for This Walkthrough

For this, we are going to assume we have:

  • animation assets (we will use two)
  • a way to invoke the animations in our code

While the method you use for the above may vary drastically based on the game engine/app/extension you use, the logic we will explore in how/when to call these animations is transferrable.

Our code examples below will be in JavaScript.


Examining Idle Animation Structure

A common structure for an idle animation loop is to have a primary looped behavior which may be broken up by intermittent behaviors.

Examine the cat gif below as an example:

  • looped behavior: tail swish
  • intermittent behavior: yawn

pixelated cat gif on a loop of swish, swish, yawn

What you will also notice from this image is that you can very quickly catch on to the fixed pattern (2 swish, 1 yawn, repeat), so while it is more active than a static sprite, there is no illusion of it being "alive" as is our goal.


Creating the Basic Loop

Let's imagine we start with the code below:

let interval = 3000 // interval in milliseconds to run animation

function runAnimation () {
    animateSwish() // invokes our given tail swish animation
}

setInterval( runAnimation, interval ) // invokes animation every interval
Enter fullscreen mode Exit fullscreen mode

setInterval() invokes a callback function every interval in milliseconds. Here, we have set it to run every 3 seconds, at which point it will call runAnimation (which we have told to invoke our given tailSwish animation). The resulting behavior will be to have our cat image swish its tail every three seconds.

But we also want it to yawn, per our gif. So below we add:

let interval = 3000
let count = 1 // creates a counter
let yawnInterval = 3 // sets count on which we will yawn

function runAnimation () {
    // if not yet to our yawn Interval, Swish
    if (count < yawnInterval) {
        count++ // increment our count
        animateSwish()

    // if at/above our yawn interval, Yawn
    } else {
        count = 1 // resets swish counter
        animateYawn() // invokes our given yawn animation
    }
}

setInterval( runAnimation, interval )
Enter fullscreen mode Exit fullscreen mode

This successfully brings us to the point in our gif above where we have our looping swish punctuated by yawns at definite, repeated intervals.


Implementing Randomness

Right now, our cat will always yawn on every 3rd loop. However, we can use Math.random() to randomly drop in a yawn at intervals within a range of our choosing.

First, decide the minimum number of loops after a yawn until our cat should be able to yawn again. This is subjective! Ask yourself: would it look unnatural for our cat to yawn immediately after just yawning? Probably. Let's say 2, so that our yawn will (at earliest) take place on the second loop after the last yawn.

let yawnIntMin = 2

Next, set the maximum loops that should pass between yawns. We know we want the behavior to take place at some point, so what is the longest we want to wait before having the cat yawn? For now, we'll say yawn on the 5th loop at latest.

let yawnIntMax = 5

Now, we can use these to create a function that will return a random number between these two numbers (inclusive).

let yawnIntMin = 2
let yawnIntMax = 5

function getRandomYawnInterval() {
    numberRange = yawnIntMax - yawnIntMin + 1; // the +1 is important because Math.random is not inclusive of our max
    randomRange = Math.random() * numberRange // random decimal between 0 and 4 (not inclusive)
    randomInt = Math.floor(randomRange) // our decimal rounded down to an int (0, 1, 2, or 3 max)
    yawnInt = randomInt + yawnIntMin // add the min back in so we are in the desired range

    return yawnInt // in this example (2, 3, 4, or 5 max)
}
Enter fullscreen mode Exit fullscreen mode

Our entire function can be refactored to be:

let yawnIntMin = 2
let yawnIntMax = 5

function getRandomYawnInterval() {
    return Math.floor(Math.random() * (yawnIntMax - yawnIntMin + 1)) + yawnIntMin;
}
Enter fullscreen mode Exit fullscreen mode

Now let's put this back in our main function so that every time our cat yawns, it will wait a random number of loops (within our defined range) before yawning again!

let interval = 3000
let count = 1
let yawnInterval = getRandomYawnInterval() // set initially

function runAnimation () {
    if (count < yawnInterval) {
        count++
        animateSwish()
    } else {
        count = 1
        yawnInterval = getRandomYawnInterval() // set new
        animateYawn()
    }
}

setInterval( runAnimation, interval )
Enter fullscreen mode Exit fullscreen mode

Now our cat will yawn at unpredictable (more natural-feeling) intervals, while our min and max ensures that they won't yawn either too frequently or not frequently enough.


Building on the Concept

This is the basic idea behind how to use code to create more "natural" behavior.

Decide the bounds within which a behavior feels natural, and allow the behavior to randomly occur within that range.

You can implement this in many ways. So far, we have made it so that our cat will break up its tail swishes with yawns. However, note that our cat is still executing each behavior in unnaturally exact 3-second intervals.

A next step could be to turn that interval at which behavior occurs into a variable itself, which could then be set to a random number of milliseconds within its own predetermined range. See below:

// code governing getting the behavior interval (in milliseconds)
const behaviorIntMin = 2
const behaviorIntMax = 4

function getRandomBehaviorInterval() {
    let intervalSeconds = (Math.random() * (behaviorIntMax - behaviorIntMin)) + behaviorIntMin;
    return intervalSeconds * 1000;
}

// code governing getting the yawn interval (in loops)
const yawnIntMin = 2
const yawnIntMax = 5

function getRandomYawnInterval() {
    return Math.floor(Math.random() * (yawnIntMax - yawnIntMin + 1)) + yawnIntMin;
}

// code to call animations
let count = 1
let yawnInterval = getRandomYawnInterval()

function runAnimation () {
    if (count < yawnInterval) {
        count++
        animateSwish()
    } else {
        count = 1
        yawnInterval = getRandomYawnInterval()
        animateYawn()
    }

    let behaviorInterval = getRandomBehaviorInterval()

    setTimeout(runAnimation, behaviorInterval)
}


// invoking our final function
runAnimation();
Enter fullscreen mode Exit fullscreen mode

View this code in action here!

The link above shows this code being used to animate our cat. You are encouraged to use the developer tools on that page to see the count, yawn interval, behavior interval, and animation calls logged as they happen to see how our randomization is working under the hood.

In addition to the variables and function for getting our random behavior Interval, note that we are now calling setTimeout from within our runAnimation function, feeding in runAnimation recursively as the callback.

We were able use setInterval earlier when the behavior interval was (as the name says) set. Now that the interval needs the freedom to be different every time, each invocation of runAnimation is going to trigger the next animation and then schedule the next call to runAnimation using our setTimeout method.

This is one solution for creating this loop in Javascript, and the language/engine you are using will determine the easiest and most efficient way to do this in your own project.


Closing

Depending on the assets (animations) at your disposal, there are many ways to utilize these ideas. Additionally, you are not bound to truly random sequences of behavior. For instance, a stretching animation might be more likely to occur directly following a yawning animation than it would otherwise, which delves into weighted random choices.

How (and if) you ultimately implement idle animations in your projects is up to you, but understanding how to structure randomness within otherwise static loops is a great tool to have in your belt. Next time you play a game, keep an eye out for idle character animations to see how this has been implemented out in the wild!

Top comments (1)

Collapse
 
svgatorapp profile image
SVGator

So insightful! Thank you for sharing. Never considered how game developers decide what an idle character should do.