Loading...
Loading...
This skill should be used when the user asks to "create animation", "animate sprite", "add tweens", "sprite animation not playing", "character animations", "easing", "tween timeline", "idle animation", "walk animation", "fade in", "fade out", or "scale animation".
npx skill4agent add yakoub-ai/phaser4-gamedev phaser-animationAnimationManager// preload()
preload(): void {
this.load.spritesheet('player', 'assets/player.png', {
frameWidth: 48,
frameHeight: 48,
});
}
// create() — or PreloaderScene.create() for global animations (see below)
create(): void {
this.anims.create({
key: 'player-idle',
frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
frameRate: 8,
repeat: -1, // -1 = loop forever
});
this.anims.create({
key: 'player-walk',
frames: this.anims.generateFrameNumbers('player', { start: 4, end: 11 }),
frameRate: 12,
repeat: -1,
});
this.anims.create({
key: 'player-jump',
frames: this.anims.generateFrameNumbers('player', { start: 12, end: 15 }),
frameRate: 10,
repeat: 0, // 0 = play once
});
this.anims.create({
key: 'player-attack',
frames: this.anims.generateFrameNumbers('player', { start: 16, end: 23 }),
frameRate: 16,
repeat: 0,
});
}this.anims.generateFrameNumbers('texture', {
start: 0, // first frame index
end: 7, // last frame index (inclusive)
first: 0, // override which frame plays first
frames: [0, 2, 4], // manual frame list (use instead of start/end)
});generateFrameNames// preload()
this.load.atlas('hero', 'assets/hero.png', 'assets/hero.json');
// create()
this.anims.create({
key: 'hero-run',
frames: this.anims.generateFrameNames('hero', {
prefix: 'run_', // frame names are run_01, run_02, ...
start: 1,
end: 8,
zeroPad: 2, // zero-pad the number to 2 digits
suffix: '', // optional suffix after the number
}),
frameRate: 12,
repeat: -1,
});
// Manual frame list from atlas
this.anims.create({
key: 'hero-die',
frames: [
{ key: 'hero', frame: 'die_01' },
{ key: 'hero', frame: 'die_02' },
{ key: 'hero', frame: 'die_03' },
],
frameRate: 8,
repeat: 0,
});PreloaderScene.create()AnimationManager// src/scenes/PreloaderScene.ts
export class PreloaderScene extends Phaser.Scene {
preload(): void {
this.load.spritesheet('player', 'assets/player.png', { frameWidth: 48, frameHeight: 48 });
this.load.atlas('enemies', 'assets/enemies.png', 'assets/enemies.json');
}
create(): void {
// All anims defined here are available in GameScene, UIScene, etc.
this.anims.create({ key: 'player-idle', /* ... */ });
this.anims.create({ key: 'player-walk', /* ... */ });
this.anims.create({ key: 'enemy-walk', /* ... */ });
this.scene.start('GameScene');
}
}create()// Basic play
sprite.play('player-walk');
// Play but don't restart if already playing this animation
sprite.play('player-walk', true); // ignoreIfPlaying = true
// Play starting from a specific frame
sprite.playFromFrame('player-walk', 3);
// Stop on a specific frame number
sprite.stopOnFrame(this.anims.get('player-attack').frames[7]);
// Reverse playback
sprite.playReverse('player-walk');
// Check state
sprite.anims.isPlaying; // boolean
sprite.anims.currentAnim?.key; // string | undefined
sprite.anims.currentFrame?.index; // current frame index// Fires when any animation completes on this sprite
sprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE, (anim, frame, gameObject) => {
console.log('animation complete:', anim.key);
});
// Fires when a SPECIFIC animation completes (preferred — avoids key checks)
sprite.on(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'player-attack',
(anim, frame, gameObject) => {
this.player.returnToIdle();
}
);
// Other events
sprite.on(Phaser.Animations.Events.ANIMATION_START, cb); // animation started
sprite.on(Phaser.Animations.Events.ANIMATION_REPEAT, cb); // loop restarted
sprite.on(Phaser.Animations.Events.ANIMATION_RESTART, cb); // play() called while already playing
sprite.on(Phaser.Animations.Events.ANIMATION_STOP, cb); // stop() called
sprite.on(Phaser.Animations.Events.ANIMATION_UPDATE, cb); // every frame changesprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE, this.onAnimComplete, this);
// In shutdown():
sprite.off(Phaser.Animations.Events.ANIMATION_COMPLETE, this.onAnimComplete, this);// Chain via array — plays 'attack', then 'idle' automatically
sprite.chain(['player-attack', 'player-idle']);
sprite.play('player-attack');
// Chain via ANIMATION_COMPLETE event
sprite.play('player-jump');
sprite.once(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'player-jump',
() => sprite.play('player-fall')
);update()type CharState = 'idle' | 'walk' | 'jump' | 'attack' | 'hurt';
export class Player extends Phaser.Physics.Arcade.Sprite {
private state: CharState = 'idle';
setState(newState: CharState): void {
if (this.state === newState) return;
this.state = newState;
switch (newState) {
case 'idle': this.play('player-idle', true); break;
case 'walk': this.play('player-walk', true); break;
case 'jump': this.play('player-jump', true); break;
case 'attack': this.play('player-attack', true); break;
case 'hurt':
this.play('player-hurt', true);
this.once(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'player-hurt',
() => this.setState('idle')
);
break;
}
}
update(cursors: Phaser.Types.Input.Keyboard.CursorKeys): void {
const body = this.body as Phaser.Physics.Arcade.Body;
if (this.state === 'attack' || this.state === 'hurt') return; // locked states
if (!body.blocked.down) {
this.setState('jump');
} else if (cursors.left.isDown || cursors.right.isDown) {
this.setState('walk');
} else {
this.setState('idle');
}
if (Phaser.Input.Keyboard.JustDown(cursors.space)) {
this.setState('attack');
this.once(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + 'player-attack',
() => this.setState('idle')
);
}
}
}update()play()cinematicModeupdate()export class Player extends Phaser.Physics.Arcade.Sprite {
private state: CharState = 'idle';
private cinematicMode = false;
setCinematicMode(active: boolean, forcedAnimKey?: string): void {
this.cinematicMode = active;
if (active && forcedAnimKey) {
this.anims.stop(); // RC7: always stop before play on state switch
this.play(forcedAnimKey, true);
this.once(
Phaser.Animations.Events.ANIMATION_COMPLETE_KEY + forcedAnimKey,
() => { this.cinematicMode = false; }
);
}
}
update(cursors: Phaser.Types.Input.Keyboard.CursorKeys): void {
if (this.cinematicMode) return; // MUST be first line — blocks state logic
// ... rest of state machine
}
}cinematicModeANIMATION_COMPLETE_KEYplay()references/state-machine-patterns.mdANIMATION_COMPLETE'dodge''interact'CharStatecasesetState()references/state-machine-patterns.mdsprite.stop(); // stop and stay on current frame
sprite.anims.pause(); // pause on current frame (resumable)
sprite.anims.resume(); // resume paused animation
sprite.anims.restart(); // restart from frame 0this.tweens.add({
targets: sprite, // one object, an array, or a group
x: 400, // tween x to 400
y: 300,
alpha: 1,
duration: 800, // milliseconds
ease: 'Quad.Out', // easing function
delay: 0, // ms before starting
repeat: 0, // 0 = once; -1 = infinite
yoyo: false, // reverse back to start after completing
hold: 0, // ms to hold at end before yoyo
onStart: () => {}, // fires when tween starts
onUpdate: () => {}, // fires every frame
onComplete: () => {}, // fires on completion
});sprite.setAlpha(0);
this.tweens.add({ targets: sprite, alpha: 1, duration: 400, ease: 'Linear' });this.tweens.add({
targets: sprite,
alpha: 0,
duration: 300,
ease: 'Linear',
onComplete: () => sprite.destroy(),
});this.tweens.add({
targets: sprite,
scaleX: 1.3,
scaleY: 1.3,
duration: 80,
ease: 'Quad.Out',
yoyo: true,
});// Slide in from left
sprite.setX(-100);
this.tweens.add({
targets: sprite,
x: 400,
duration: 500,
ease: 'Back.Out',
});sprite.setY(targetY - 100);
this.tweens.add({
targets: sprite,
y: targetY,
duration: 600,
ease: 'Bounce.Out',
});references/easing-reference.md'Linear''Quad.Out''Quad.In''Quad.InOut''Back.Out''Bounce.Out''Elastic.Out'onCompletethis.tweens.timeline({
tweens: [
{
targets: panel,
alpha: 1,
duration: 200,
},
{
targets: panel,
y: 300,
duration: 400,
ease: 'Back.Out',
},
{
targets: title,
alpha: 1,
duration: 300,
offset: '-=100', // start 100ms before previous tween ends (overlap)
},
{
targets: button,
alpha: 1,
duration: 200,
// no offset = starts after previous completes
},
],
});offset'-=200''+=200'// One-shot burst
this.add.particles(x, y, 'spark', {
speed: { min: 50, max: 200 },
angle: { min: 0, max: 360 },
scale: { start: 1, end: 0 },
lifespan: 600,
quantity: 12,
emitting: false, // don't start automatically
}).explode(12); // emit 12 particles immediately then stop
// Persistent emitter (fire, rain)
const emitter = this.add.particles(x, y, 'flame', {
speed: 30,
lifespan: 1200,
scale: { start: 0.8, end: 0 },
alpha: { start: 1, end: 0 },
frequency: 80, // ms between emissions
});
// Stop later:
emitter.stop();references/animation-api.mdreferences/easing-reference.mdreferences/state-machine-patterns.mdcinematicModestop()play()ANIMATION_COMPLETE