yakoub-ai/phaser4-gamedev

phaser-animation

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".

First seen Apr 12, 2026

Installation

$ npx skills add yakoub-ai/phaser4-gamedev --skill phaser-animation

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from yakoub-ai/phaser4-gamedev · top by installs.

npx skills add yakoub-ai/phaser4-gamedev

Browse all from yakoub-ai/phaser4-gamedev

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 22
License MIT
Default branch main
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version0.7.0

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 14,814 B
  • docs SUMMARY.md 288 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 145 installs

SKILL.md

Phaser 4 Animations and Tweens

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.

Creating Spritesheet Animations

A spritesheet packs multiple frames into a single image in a regular grid. Define animations in AnimationManager using frame indices.

// 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,
  });
}

generateFrameNumbers Options

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)
});

Atlas-Based Animations

Texture atlases store frames with named keys rather than grid positions. Use generateFrameNames for these:

// 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,
});

Where to Define Animations

Define animations in PreloaderScene.create() — not in each individual scene. Animations registered on the global AnimationManager are available in every scene without re-registering:

// 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');
  }
}

If an animation only makes sense in a single scene (a cutscene animation, for example), define it in that scene's create().

Playing Animations

// 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

Animation Events

Listen for animation lifecycle events on the sprite (not the AnimationManager):

// 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 change

Always remove listeners when the sprite is destroyed to prevent memory leaks:

sprite.on(Phaser.Animations.Events.ANIMATION_COMPLETE, this.onAnimComplete, this);
// In shutdown():
sprite.off(Phaser.Animations.Events.ANIMATION_COMPLETE, this.onAnimComplete, this);

Animation Chaining

Play a sequence of animations one after another:

// 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')
);

Character State Machine Pattern

For characters with idle/walk/jump/attack states, use an explicit state machine in update(). This prevents impossible state transitions and makes animation logic readable.

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')
      );
    }
  }
}

Forced Animations: Cinematic Mode

When a one-shot animation (boss intro, death sequence, dungeon entry) plays for one frame then reverts to idle, the cause is always the entity's update() running its state-machine logic one tick after your forced play() call and overwriting it.

Fix — add a cinematicMode flag as the very first guard in update():

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();          // always stop before play on a 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
  }
}

Clear cinematicMode in the ANIMATIONCOMPLETEKEY handler, not synchronously after play() — the completion event fires after the last frame renders, and update() may run before your handler. Clear it synchronously and your forced animation exits one frame early.

See references/state-machine-patterns.md for the full canonical implementation, worked dungeon-entry example, and the ANIMATION_COMPLETE timing fix.

State Transition Completeness

Adding a new animation state without auditing all other states' transition lists is a silent bug — no error is thrown; the state machine simply fails to reach the new state or gets stuck in the wrong one.

When adding any new state (e.g. 'dodge', 'interact'):

  1. Add it to the CharState union type.
  2. Add a case for it in setState().
  3. Update every other state's "what can interrupt me" logic to include or exclude the new state as appropriate.

The transition table in references/state-machine-patterns.md makes missing transitions obvious on read.

Stopping and Pausing Animations

sprite.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 0

Tweens

Tweens interpolate any numeric property on any object over time. They are Phaser's primary tool for UI animations, cutscenes, and visual feedback.

this.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
});

Common Tween Patterns

Fade In

sprite.setAlpha(0);
this.tweens.add({ targets: sprite, alpha: 1, duration: 400, ease: 'Linear' });

Fade Out and Destroy

this.tweens.add({
  targets:    sprite,
  alpha:      0,
  duration:   300,
  ease:       'Linear',
  onComplete: () => sprite.destroy(),
});

Scale Pulse (hit feedback, collectible)

this.tweens.add({
  targets:  sprite,
  scaleX:   1.3,
  scaleY:   1.3,
  duration: 80,
  ease:     'Quad.Out',
  yoyo:     true,
});

Slide In From Edge

// Slide in from left
sprite.setX(-100);
this.tweens.add({
  targets:  sprite,
  x:        400,
  duration: 500,
  ease:     'Back.Out',
});

Bounce Landing

sprite.setY(targetY - 100);
this.tweens.add({
  targets:  sprite,
  y:        targetY,
  duration: 600,
  ease:     'Bounce.Out',
});

Tween Easing Functions

See references/easing-reference.md for the complete guide with all easing functions and use cases.

Quick reference:

  • 'Linear' — constant speed; mechanical, UI bars
  • 'Quad.Out' — fast start, decelerates; most natural movement
  • 'Quad.In' — accelerates; falling objects, winding up
  • 'Quad.InOut' — symmetric ease; camera moves
  • 'Back.Out' — overshoots target then settles; UI popups, dialog slides
  • 'Bounce.Out' — bounces at destination; objects hitting ground
  • 'Elastic.Out' — spring oscillation; comic, bouncy UI

Tween Timelines

Sequence multiple tweens without nesting onComplete callbacks:

this.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 controls timing relative to the previous tween:

  • '-=200' — overlap by 200ms
  • '+=200' — add 200ms gap
  • absolute number — start at that ms from timeline start

Particle Animations (Brief)

For burst effects (explosions, pickups, impacts), use the built-in particle system:

// 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();

Additional Resources

Reference Files

  • references/animation-api.md — Complete AnimationManager, AnimationConfig, Animation events, TweenManager, and Timeline API reference
  • references/easing-reference.md — All built-in easing functions with descriptions, use cases, and code examples
  • references/state-machine-patterns.md — State-machine discipline for characters: cinematicMode flag, canonical state list, transition table, the stop()/play() ordering rule, and ANIMATION_COMPLETE timing. Read when building any character with more than idle+walk, or when forced animations play for one frame and revert.