Animation
Frame animations from spritesheets and atlases — defining, playing, chaining, and reacting to animation events.
Phaser 4’s animation system plays sequences of texture frames on a Sprite. Animations are defined once at the scene level (or globally) and then played on any number of sprites — the definition and the playback are separate.
Where animations live
Two places:
this.anims— the scene-level animation manager. Animations live here by default.this.game.anims(rare) — a global manager you can use to share definitions across scenes.
Almost always use the scene-level manager. Cross-scene sharing tends to leak state.
Defining an animation
You need a source — a spritesheet (uniform frames) or an atlas (named frames, irregular):
preload() {
// Uniform grid.
this.load.spritesheet('dude', 'assets/dude.png', { frameWidth: 32, frameHeight: 48 });
// Named-frame atlas.
this.load.atlas('hero', 'assets/hero.png', 'assets/hero.json');
}
create() {
this.anims.create({
key: 'walk',
frames: this.anims.generateFrameNumbers('dude', { start: 0, end: 3 }),
frameRate: 10,
repeat: -1, // -1 means loop forever
});
this.anims.create({
key: 'attack',
frames: this.anims.generateFrameNames('hero', { prefix: 'attack-', start: 1, end: 6, zeroPad: 2 }),
frameRate: 18,
repeat: 0, // play once
});
}
frameRate is frames per second, not milliseconds per frame. duration is the alternative — total animation length in ms — and overrides frameRate if both are set.
Playing on a sprite
const player = this.add.sprite(100, 100, 'dude');
player.play('walk'); // start (or restart) immediately
player.play({ key: 'walk', repeat: 3 }); // override config for this playback
player.playAfterDelay('walk', 250); // start in 250 ms
player.chain('attack'); // queue an animation to play when the current one ends
player.stop(); // freeze on the current frame
player.stopAfterRepeat(); // finish the current loop, then stop
A live example
A walking character — defined once, played on a single sprite.
Reacting to playback
Animations emit events on the sprite playing them:
player.on('animationcomplete', (anim) => {
if (anim.key === 'attack') player.play('idle');
});
player.on('animationrepeat', (anim) => {
if (anim.key === 'walk') this.playFootstep();
});
player.on('animationupdate', (anim, frame) => {
// Fires once per frame change.
});
For “play X once then return to Y” the chain API is cleaner than handling animationcomplete manually:
player.play('attack').chain('idle');
Common pitfalls
Defining inside update. anims.create is idempotent (it warns on duplicate keys), but you shouldn’t be calling it every frame. Define in create.
Per-sprite vs. per-key state. The animation definition is shared, but each sprite has its own playback head. Playing the same animation on two sprites runs them independently.
Loading order. generateFrameNumbers and generateFrameNames need the source texture to already exist in the cache, so call them from create() (after preload has resolved), not earlier.
Related
- Loader & Assets — how spritesheets and atlases get into the cache.
- Game Objects —
Spriteis the only built-in type with an animation manager attached.