$npx -y skills add Yakoub-ai/phaser4-gamedev --skill phaser-animationThis 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".
| 1 | # Phaser 4 Animations and Tweens |
| 2 | |
| 3 | Phaser 4 has two distinct animation systems: **frame-based sprite animations** (flip through frames in a texture atlas or spritesheet) and **tweens** (interpolate numeric properties over time). Use both together for polished game feel. |
| 4 | |
| 5 | ## Creating Spritesheet Animations |
| 6 | |
| 7 | A spritesheet packs multiple frames into a single image in a regular grid. Define animations in `AnimationManager` using frame indices. |
| 8 | |
| 9 | ```typescript |
| 10 | // preload() |
| 11 | preload(): void { |
| 12 | this.load.spritesheet('player', 'assets/player.png', { |
| 13 | frameWidth: 48, |
| 14 | frameHeight: 48, |
| 15 | }); |
| 16 | } |
| 17 | |
| 18 | // create() — or PreloaderScene.create() for global animations (see below) |
| 19 | create(): void { |
| 20 | this.anims.create({ |
| 21 | key: 'player-idle', |
| 22 | frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }), |
| 23 | frameRate: 8, |
| 24 | repeat: -1, // -1 = loop forever |
| 25 | }); |
| 26 | |
| 27 | this.anims.create({ |
| 28 | key: 'player-walk', |
| 29 | frames: this.anims.generateFrameNumbers('player', { start: 4, end: 11 }), |
| 30 | frameRate: 12, |
| 31 | repeat: -1, |
| 32 | }); |
| 33 | |
| 34 | this.anims.create({ |
| 35 | key: 'player-jump', |
| 36 | frames: this.anims.generateFrameNumbers('player', { start: 12, end: 15 }), |
| 37 | frameRate: 10, |
| 38 | repeat: 0, // 0 = play once |
| 39 | }); |
| 40 | |
| 41 | this.anims.create({ |
| 42 | key: 'player-attack', |
| 43 | frames: this.anims.generateFrameNumbers('player', { start: 16, end: 23 }), |
| 44 | frameRate: 16, |
| 45 | repeat: 0, |
| 46 | }); |
| 47 | } |
| 48 | ``` |
| 49 | |
| 50 | ### generateFrameNumbers Options |
| 51 | |
| 52 | ```typescript |
| 53 | this.anims.generateFrameNumbers('texture', { |
| 54 | start: 0, // first frame index |
| 55 | end: 7, // last frame index (inclusive) |
| 56 | first: 0, // override which frame plays first |
| 57 | frames: [0, 2, 4], // manual frame list (use instead of start/end) |
| 58 | }); |
| 59 | ``` |
| 60 | |
| 61 | ## Atlas-Based Animations |
| 62 | |
| 63 | Texture atlases store frames with named keys rather than grid positions. Use `generateFrameNames` for these: |
| 64 | |
| 65 | ```typescript |
| 66 | // preload() |
| 67 | this.load.atlas('hero', 'assets/hero.png', 'assets/hero.json'); |
| 68 | |
| 69 | // create() |
| 70 | this.anims.create({ |
| 71 | key: 'hero-run', |
| 72 | frames: this.anims.generateFrameNames('hero', { |
| 73 | prefix: 'run_', // frame names are run_01, run_02, ... |
| 74 | start: 1, |
| 75 | end: 8, |
| 76 | zeroPad: 2, // zero-pad the number to 2 digits |
| 77 | suffix: '', // optional suffix after the number |
| 78 | }), |
| 79 | frameRate: 12, |
| 80 | repeat: -1, |
| 81 | }); |
| 82 | |
| 83 | // Manual frame list from atlas |
| 84 | this.anims.create({ |
| 85 | key: 'hero-die', |
| 86 | frames: [ |
| 87 | { key: 'hero', frame: 'die_01' }, |
| 88 | { key: 'hero', frame: 'die_02' }, |
| 89 | { key: 'hero', frame: 'die_03' }, |
| 90 | ], |
| 91 | frameRate: 8, |
| 92 | repeat: 0, |
| 93 | }); |
| 94 | ``` |
| 95 | |
| 96 | ## Where to Define Animations |
| 97 | |
| 98 | **Define animations in `PreloaderScene.create()` — not in each individual scene.** Animations registered on the global `AnimationManager` are available in every scene without re-registering: |
| 99 | |
| 100 | ```typescript |
| 101 | // src/scenes/PreloaderScene.ts |
| 102 | export class PreloaderScene extends Phaser.Scene { |
| 103 | preload(): void { |
| 104 | this.load.spritesheet('player', 'assets/player.png', { frameWidth: 48, frameHeight: 48 }); |
| 105 | this.load.atlas('enemies', 'assets/enemies.png', 'assets/enemies.json'); |
| 106 | } |
| 107 | |
| 108 | create(): void { |
| 109 | // All anims defined here are available in GameScene, UIScene, etc. |
| 110 | this.anims.create({ key: 'player-idle', /* ... */ }); |
| 111 | this.anims.create({ key: 'player-walk', /* ... */ }); |
| 112 | this.anims.create({ key: 'enemy-walk', /* ... */ }); |
| 113 | |
| 114 | this.scene.start('GameScene'); |
| 115 | } |
| 116 | } |
| 117 | ``` |
| 118 | |
| 119 | If an animation only makes sense in a single scene (a cutscene animation, for example), define it in that scene's `create()`. |
| 120 | |
| 121 | ## Playing Animations |
| 122 | |
| 123 | ```typescript |
| 124 | // Basic play |
| 125 | sprite.play('player-walk'); |
| 126 | |
| 127 | // Play but don't restart if already playing this animation |
| 128 | sprite.play('player-walk', true); // ignoreIfPlaying = true |
| 129 | |
| 130 | // Play starting from a specific frame |
| 131 | sprite.playFromFrame('player-walk', 3); |
| 132 | |
| 133 | // Stop on a specific frame number |
| 134 | sprite.stopOnFrame(this.anims.get('player-attack').frames[7]); |
| 135 | |
| 136 | // Reverse playback |
| 137 | sprite.playReverse('player-walk'); |
| 138 | |
| 139 | // Check state |
| 140 | sprite.anims.isPlaying; // boolean |
| 141 | sprite.anims.currentAnim?.key; // string | undefined |
| 142 | sprite.anims.currentFrame?.index; // current frame index |
| 143 | ``` |
| 144 | |
| 145 | ## Animation Events |
| 146 | |
| 147 | Listen for animation lifecycle events on the **sprite** (not the AnimationManager): |
| 148 | |
| 149 | ```typescript |
| 150 | // Fires when any animation completes on this sprite |
| 151 | sprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE, (anim, frame, gameObject) => { |
| 152 | console.log('animation complete:', anim.key); |
| 153 | }); |
| 154 | |
| 155 | // Fires when a SPECIFIC animation completes (preferred — avoids key checks) |
| 156 | sprite.on( |
| 157 | Phaser.Animations.Events.ANIMATION_CO |