Play an animation on a character
Goal
An NPC in your scene idles when it is standing still, walks when it moves, and waves once when the player gets close — driven entirely from the character state your game logic already produces.
Files you will edit
assets.jsonsrc/game.ts
Steps
Add the clip assets to
assets.json. They are addressed by id everywhere else; no path ever appears in game code.json{ "assets": [ { "id": "npc.locomotion", "kind": "locomotion", "src": "anim/locomotion.json" }, { "id": "npc.wave", "kind": "clip", "src": "anim/wave.glb" } ] }In
src/game.ts, give the NPC an animator when it spawns.locomotionis the parsedlocomotion.json; the animator sorts it once, here, not per frame.tsimport { createAnimator } from '@aosengine/animation'; const animator = createAnimator({ root: npc.rigRoot, expressionSpace: npc.bundle.expressionSpace, locomotion: assets.get('npc.locomotion'), });Register the gesture clip as ADDITIVE, so the base layer's per-frame weight pass does not stop it while the envelope is running.
tsanimator.addClip('wave', assets.get('npc.wave'), { additive: true });Drive it from the character state each frame. Preallocate the state object outside the system: this runs sixty times a second.
tsconst npcState = { velocity: [0, 0, 0], grounded: true, lookAt: null }; function animateNpc(dt: number): void { npcState.velocity[0] = npc.velocity.x; npcState.velocity[1] = npc.velocity.y; npcState.velocity[2] = npc.velocity.z; npcState.lookAt = playerIsNear ? playerPosition : null; animator.setState(npcState); animator.update(dt, { camera }); npc.setBodyPose(animator.bodyPose); npc.setExpression(animator.expression); }Fire the wave once, from whatever decides the player is close. The gesture channel is one slot: a second
playreplaces whatever was in flight.tsif (playerJustArrived) animator.gesture.play('wave', { durationMs: 1500 });
Verify
Run npm run dev and walk up to the NPC. It should be idling, blend into a walk as it moves, turn its head to follow you without the neck exceeding about 45 degrees, and wave once as you arrive.
Asserting it in a test: with a velocity of [0, 0, 0] the animator's pose matches the idle clip, and at the locomotion index's top speed it matches the run clip, so
ts
animator.setState({ velocity: [0, 0, 0], grounded: true, lookAt: null });
animator.update(1 / 60);
const standing = animator.bodyPose.bones.slice();
animator.setState({ velocity: [4, 0, 0], grounded: true, lookAt: null });
animator.update(1 / 60);
expect(animator.bodyPose.bones).not.toEqual(standing);See also
- Animation
- Characters
packages/animation/README.md— @aosengine/animation- Assets and the manifest