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
AnimationSystemprocesses entities with bothAnimationComponentandAssetComponentcomponents. - It retrieves animations from the
AssetComponentand stores them in theAnimationComponent. - The
processEntitymethod handles animation blending and transitions:- It smoothly transitions between the
currentAnimationandtargetAnimationusing theblendingWeightandblendSpeed. - It uses the
loopMapto 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
SceneManagerloads thescene.jsonand instantiates theAnimationSystem. - The
EntityManagercalls theAnimationSystem'sprocessEntitymethod for each entity with the required components. - The
AnimationSystemretrieves 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.