Animate any Entity with a Data-Driven Animation System

We've built a solid foundation for our game, loading assets, implementing movement, and setting the stage with cameras and lights. Now, it's time to infuse our characters with life by adding animations. We'll use components and systems to manage animations in a flexible and data-driven way.

The AnimationComponent: The Director of Movement

Let's start by defining an AnimationComponent to store animation-related data for our entities:

import { Animation } from "@babylonjs/core";
import { Component } from "../lib";

// AnimationComponent
export class AnimationComponent extends Component {
  animations: Map<String, Animation>;
  currentAnimation: string;
  startingAnimation: string;
  targetAnimation: string;
  blendingWeight: number;
  blendSpeed: number;
  loopMap: any;

  constructor(data) {
    super(data);
    const { startingAnimation, loopMap } = data;
    this.animations = new Map();
    this.currentAnimation = startingAnimation; // Default starting animation
    this.targetAnimation = "";
    this.blendingWeight = 0.4;
    this.blendSpeed = 0.2;
    this.loopMap = loopMap;
  }

  setState(state: string) {
    // console.log(state);
    this.targetAnimation = state;
  }
}

Key Properties:

  • animations: A map to store loaded animations from our 3D models.
  • currentAnimation: The name of the animation currently playing.
  • startingAnimation: The default animation to play when the entity is created.
  • targetAnimation: The animation we want to transition to.
  • blendingWeight: A value controlling the smooth transition between animations (0 to 1).
  • blendSpeed: The speed at which the blending occurs.
  • loopMap: A map specifying which animations should loop.

The AnimationSystem: The Choreographer

Now, let's create the AnimationSystem to handle animation updates and transitions:

import { System, EntityManager } from "../lib";
import { AnimationComponent } from "../components/AnimationComponent";
import { AssetComponent } from "../components/AssetComponent";

export class AnimationSystem extends System {
  constructor(
    entityManager: EntityManager,
    componentClasses = [AnimationComponent, AssetComponent],
  ) {
    super(entityManager, componentClasses);
    this.componentClasses = componentClasses;
    console.log("AnimationSystem initialized");
  }

  processEntity(entity: Entity, deltaTime: number) {
    const animationComponent = entity.getComponent(AnimationComponent);
    const assetComponent = entity.getComponent(AssetComponent);
    let {
      blendingWeight,
      currentAnimation,
      targetAnimation,
      blendSpeed,
      loopMap,
    } = animationComponent;
    if (animationComponent.animations.size < assetComponent.animations.size) {
      animationComponent.animations = assetComponent.animations;
    }
    if (currentAnimation != targetAnimation) {
      animationComponent.targetAnimation = targetAnimation;
    }
    if (animationComponent.animations.size) {
      const idleAnim = animationComponent.animations.get("idle");
      if (targetAnimation == "idle" && currentAnimation == "idle") {
        const currentAnim = idleAnim;
        currentAnim.play(loopMap[currentAnimation]);
        return;
      }
      animationComponent.blendingWeight += blendSpeed;
      animationComponent.blendingWeight = Math.min(
        1,
        animationComponent.blendingWeight,
      );
      const currentAnim = animationComponent.animations.get(currentAnimation);
      const targetAnim = animationComponent.animations.get(targetAnimation);
      if (currentAnim && currentAnim == targetAnim) return;
      if (currentAnim && targetAnim) {
        currentAnim.enableBlending = true;
        targetAnim.enableBlending = true;
        currentAnim.blendingSpeed = blendSpeed;
        targetAnim.blendingSpeed = blendSpeed;
        targetAnim.play(loopMap[targetAnimation]);
        currentAnim.setWeightForAllAnimatables(1 - blendingWeight);
        targetAnim.setWeightForAllAnimatables(blendingWeight);
      }
      if (blendingWeight >= 1) {
        if (currentAnim) currentAnim.stop();
        animationComponent.currentAnimation = targetAnimation;
        animationComponent.blendingWeight = 0;
        if (!loopMap[targetAnimation]) {
          idleAnim.play(true);
        }
      }
    }
  }
}

Explanation:

  • The AnimationSystem processes entities with both AnimationComponent and AssetComponent components.
  • It retrieves animations from the AssetComponent and stores them in the AnimationComponent.
  • The processEntity method handles animation blending and transitions:
    • It smoothly transitions between the currentAnimation and targetAnimation using the blendingWeight and blendSpeed.
    • It uses the loopMap to determine which animations should loop.

Defining Animations in scene.json

We'd define animation-related data in our scene.json like this:

{
  "entities": {
    // ... other entities

    "Player": {
      "components": {
        // ... other components

        "Animation": {
          "startingAnimation": "idle",
          "loopMap": {
            "idle": true,
            "walk": true,
            "run": true
          }
        }
      }
    }
  }
}

Bringing It All Together: Animated Action

When our game runs:

  1. The SceneManager loads the scene.json and instantiates the AnimationSystem.
  2. The EntityManager calls the AnimationSystem's processEntity method for each entity with the required components.
  3. The AnimationSystem retrieves animations, manages blending, and updates the animations of our entities, bringing them to life with smooth movements.

In Sum

We've added another layer of dynamism to our game by implementing a data-driven animation system! By defining animation properties in our scene.json and utilizing components and systems, we've made it easy to manage and update character animations, adding a touch of realism and personality to our game world. There's still work to do to make the character rotate on movement, for instance. And that's next.

But first, think how you would approach the rotation problem. Add a new function to the MovementSystem or create a new Compoennt/System pair called Rotation.