Three.js AnimationMixer Loop Event Tracking

In Three.js, tracking when an animation cycle completes and loops is essential for synchronizing game logic, triggering audio, or chaining character behaviors. This article explains how to listen for the 'loop' event dispatched by the AnimationMixer, identify which specific animation action triggered the event, and implement this logic efficiently in your JavaScript code.

Listening to the Loop Event

The AnimationMixer dispatches a 'loop' event every time an individual animation action finishes a single loop cycle. To track this event, you must add an event listener directly to your AnimationMixer instance.

Here is the basic implementation:

// Initialize your mixer and actions
const mixer = new THREE.AnimationMixer(characterMesh);
const walkAction = mixer.clipAction(walkClip);
const runAction = mixer.clipAction(runClip);

// Play the animations
walkAction.play();
runAction.play();

// Add the event listener to the mixer
mixer.addEventListener('loop', (event) => {
    // Identify which specific animation looped
    if (event.action === walkAction) {
        console.log('Walk animation completed a cycle.');
    } else if (event.action === runAction) {
        console.log('Run animation completed a cycle.');
    }
});

Understanding the Event Object

When the 'loop' event listener is triggered, it passes an event object to the callback function. This object contains crucial properties that allow you to determine exactly what occurred:

Managing Loop Settings

For the 'loop' event to trigger repeatedly, the action’s loop mode must be set to repeat. By default, Three.js actions are configured to loop indefinitely. You can control this behavior using the setLoop method:

// Set action to loop a specific number of times
walkAction.setLoop(THREE.LoopRepeat, 3); // Loops 3 times, then stops

// Set action to loop infinitely (default behavior)
runAction.setLoop(THREE.LoopRepeat, Infinity);

If you need to detect when an animation finishes completely and stops playing (rather than looping), you should listen for the 'finished' event instead of 'loop':

mixer.addEventListener('finished', (event) => {
    console.log('Animation action has finished playing:', event.action);
});