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 bothAnimationComponent
andAssetComponent
components. - It retrieves animations from the
AssetComponent
and stores them in theAnimationComponent
. - The
processEntity
method handles animation blending and transitions:- It smoothly transitions between the
currentAnimation
andtargetAnimation
using theblendingWeight
andblendSpeed
. - It uses the
loopMap
to determine which animations should loop.
- It smoothly transitions between the
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:
- The
SceneManager
loads thescene.json
and instantiates theAnimationSystem
. - The
EntityManager
calls theAnimationSystem
'sprocessEntity
method for each entity with the required components. - 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.