Agent skill

dev-phaser-animations

Sprite animations, tweens, animation chains, and timeline sequences

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/dev-phaser-animations

SKILL.md

Phaser Animations

"Bring your sprites to life with smooth animations and tweens."

Before/After: Manual Animation vs Phaser Animation System

❌ Before: Manual Frame Animation

typescript
// Manual sprite animation without Phaser
class SpriteAnimator {
  private currentFrame = 0;
  private frameTimer = 0;
  private frameDuration = 83; // ~12 FPS
  private isPlaying = false;
  private frames: HTMLImageElement[] = [];

  constructor(private sprite: HTMLElement) {
    // Load all frames manually
    for (let i = 0; i < 8; i++) {
      const img = document.createElement('img');
      img.src = `assets/walk/frame${i}.png`;
      this.frames.push(img);
    }
  }

  play(animationName: string) {
    this.isPlaying = true;
    this.currentFrame = 0;
  }

  update(dt: number) {
    if (!this.isPlaying) return;

    this.frameTimer += dt;

    if (this.frameTimer >= this.frameDuration) {
      this.frameTimer = 0;
      this.currentFrame = (this.currentFrame + 1) % this.frames.length;
      this.sprite.style.backgroundImage = `url(${this.frames[this.currentFrame].src})`;
    }
  }

  // Manual tween for position
  moveTo(targetX: number, targetY: number, duration: number) {
    const startX = parseFloat(this.sprite.style.left) || 0;
    const startY = parseFloat(this.sprite.style.top) || 0;
    const startTime = performance.now();

    const animate = (currentTime: number) => {
      const elapsed = currentTime - startTime;
      const progress = Math.min(elapsed / duration, 1);

      // Linear interpolation only - no easing options
      this.sprite.style.left = (startX + (targetX - startX) * progress) + 'px';
      this.sprite.style.top = (startY + (targetY - startY) * progress) + 'px';

      if (progress < 1) {
        requestAnimationFrame(animate);
      }
    };

    requestAnimationFrame(animate);
  }
}

// Problems:
// - Manual frame timing is error-prone
// - No built-in easing functions
// - Manual sprite sheet management
// - No animation state management
// - Chained animations require nested callbacks
// - No timeline support

✅ After: Phaser Animation System

typescript
// Phaser handles all animation automatically
export class GameScene extends Phaser.Scene {
  create() {
    const player = this.add.sprite(400, 300, 'player');

    // Create sprite sheet animation - ONE config!
    this.anims.create({
      key: 'walk',
      frames: this.anims.generateFrameNumbers('player', {
        start: 0,
        end: 7
      }),
      frameRate: 12,
      repeat: -1 // Infinite loop
    });

    // Play animation - ONE line!
    player.play('walk');

    // Tween with easing - ONE call!
    this.tweens.add({
      targets: player,
      x: 600,
      y: 400,
      duration: 1000,
      ease: 'Power2', // Built-in easing!
      onComplete: () => {
        console.log('Movement complete!');
      }
    });

    // Timeline for sequences - ONE config!
    this.tweens.timeline({
      targets: player,
      tweens: [
        { alpha: 0, duration: 500, ease: 'Linear' },
        { x: 200, duration: 500, offset: '-=250' }, // Overlap!
        { scale: 2, duration: 300, ease: 'Back.easeOut' }
      ]
    });
  }
}

// Benefits:
// - Automatic frame timing
// - 20+ built-in easing functions
// - Sprite sheet generation helpers
// - Animation state management (isPlaying, currentAnim)
// - Timeline support for complex sequences
// - Event callbacks (onComplete, animationcomplete)

When to Use This Skill

Use when:

  • Creating sprite animations
  • Building tween sequences
  • Implementing timeline-based animations
  • Animating UI elements
  • Creating visual effects and transitions

Quick Start

typescript
create() {
  // Create animation from sprite sheet
  this.anims.create({
    key: 'walk',
    frames: this.anims.generateFrameNumbers('player', { start: 0, end: 7 }),
    frameRate: 12,
    repeat: -1
  });

  // Play animation
  const player = this.add.sprite(400, 300, 'player');
  player.play('walk');
}

Decision Framework

Need Use
Frame-based animation anims.create() + sprite.play()
Property animation tweens.add()
Sequenced animations timeline
Single tween tweens.add() once
Delayed action time.delayedCall()

Progressive Guide

Level 1: Sprite Animations

typescript
export class GameScene extends Phaser.Scene {
  create() {
    // Create animations from sprite sheet
    this.anims.create({
      key: "idle",
      frames: this.anims.generateFrameNumbers("player", {
        start: 0,
        end: 3,
      }),
      frameRate: 8,
      repeat: -1, // Infinite loop
      yoyo: false, // Don't play backwards
    });

    this.anims.create({
      key: "walk",
      frames: this.anims.generateFrameNumbers("player", {
        start: 4,
        end: 11,
      }),
      frameRate: 12,
      repeat: -1,
    });

    this.anims.create({
      key: "attack",
      frames: this.anims.generateFrameNumbers("player", {
        start: 12,
        end: 17,
      }),
      frameRate: 15,
      repeat: 0, // Play once
    });

    // Create sprite and play animation
    const player = this.add.sprite(400, 300, "player");
    player.play("idle");

    // Change animation
    player.play("walk", true); // true = ignore if playing
  }
}

Level 2: Tweening Properties

typescript
create() {
  const box = this.add.rectangle(400, 300, 50, 50, 0xff0000);

  // Basic tween
  this.tweens.add({
    targets: box,
    x: 600,
    duration: 1000,
    ease: 'Power2'
  });

  // Multiple properties
  this.tweens.add({
    targets: box,
    x: 200,
    y: 400,
    alpha: 0.5,
    angle: 180,
    scale: 2,
    duration: 2000,
    ease: 'Elastic.easeOut',
    onComplete: () => {
      console.log('Tween complete!');
    }
  });

  // Yoyo tween (ping-pong)
  this.tweens.add({
    targets: box,
    y: 100,
    duration: 1000,
    yoyo: true,
    repeat: -1
  });
}

Level 3: Tween Chains and Callbacks

typescript
create() {
  const sprite = this.add.sprite(400, 300, 'player');

  // Chain tweens
  this.tweens.add({
    targets: sprite,
    y: 200,
    duration: 500,
    ease: 'Linear',
    onComplete: () => {
      // Second tween
      this.tweens.add({
        targets: sprite,
        x: 600,
        duration: 500,
        ease: 'Linear',
        onComplete: () => {
          // Third tween
          this.tweens.add({
            targets: sprite,
            scale: 2,
            duration: 300,
            ease: 'Back.easeOut'
          });
        }
      });
    }
  });

  // Better approach: Timeline
  const timeline = this.tweens.timeline({
    targets: sprite,
    tweens: [
      {
        y: 200,
        duration: 500,
        ease: 'Linear'
      },
      {
        x: 600,
        duration: 500,
        ease: 'Linear'
      },
      {
        scale: 2,
        duration: 300,
        ease: 'Back.easeOut'
      }
    ]
  });

  // Timeline with delay
  this.tweens.timeline({
    tweens: [
      {
        targets: sprite,
        alpha: 0,
        duration: 500,
        offset: 0 // Start immediately
      },
      {
        targets: otherSprite,
        y: 400,
        duration: 500,
        offset: '-=250' // Start 250ms before previous ends
      }
    ]
  });
}

Level 4: Animation State Management

typescript
class AnimationController {
  private sprite: Phaser.GameObjects.Sprite;
  private currentAnim = '';

  constructor(sprite: Phaser.GameObjects.Sprite) {
    this.sprite = sprite;

    // Listen for animation completion
    this.sprite.on('animationcomplete', this.onAnimComplete, this);
  }

  play(animKey: string, ignoreIfPlaying = false) {
    if (ignoreIfPlaying && this.currentAnim === animKey) {
      return;
    }

    // Don't interrupt non-looping animations
    const current = this.sprite.anims.currentAnim;
    if (current && !current.repeat && current.isPlaying) {
      return;
    }

    this.sprite.play(animKey);
    this.currentAnim = animKey;
  }

  onAnimComplete(event: any) {
    if (event.key === 'attack') {
      this.play('idle');
    } else if (event.key === 'death') {
      this.sprite.setVisible(false);
    }
  }

  isPlaying(animKey: string): boolean {
    return this.sprite.anims.isPlaying && this.sprite.anims.currentAnim?.key === animKey;
  }

  getProgress(): number {
    return this.sprite.anims.getProgress();
  }
}

// In scene
create() {
  this.player = this.add.sprite(400, 300, 'player');
  this.animController = new AnimationController(this.player);
  this.animController.play('idle');
}

update() {
  if (this.cursors.left.isDown || this.cursors.right.isDown) {
    this.animController.play('walk');
  } else {
    this.animController.play('idle');
  }

  if (this.attackKey.isDown && !this.animController.isPlaying('attack')) {
    this.animController.play('attack');
  }
}

Level 5: Advanced Animation Systems

typescript
export class GameScene extends Phaser.Scene {
  private animManager!: AnimationManager;

  create() {
    this.animManager = new AnimationManager(this);

    // Define animation states
    this.animManager.registerState("player", "idle", {
      frames: { start: 0, end: 3 },
      frameRate: 8,
      loop: true,
    });

    this.animManager.registerState("player", "walk", {
      frames: { start: 4, end: 11 },
      frameRate: 12,
      loop: true,
    });

    this.animManager.registerState("player", "jump", {
      frames: { start: 12, end: 15 },
      frameRate: 10,
      loop: false,
      next: "idle",
    });

    this.animManager.registerState("player", "attack", {
      frames: { start: 16, end: 21 },
      frameRate: 15,
      loop: false,
      next: "idle",
      callback: this.onAttackComplete,
    });

    // Set up state transitions
    this.animManager.addTransition("player", "idle", "walk", () =>
      this.isMoving(),
    );
    this.animManager.addTransition(
      "player",
      "walk",
      "idle",
      () => !this.isMoving(),
    );
    this.animManager.addTransition("player", "walk", "jump", () =>
      this.justJumped(),
    );

    // Initialize player
    this.player = this.add.sprite(400, 300, "player");
    this.animManager.attach("player", this.player);
  }

  update() {
    this.animController.update("player");
  }

  isMoving() {
    return this.cursors.left.isDown || this.cursors.right.isDown;
  }

  justJumped() {
    return Phaser.Input.Keyboard.JustDown(this.jumpKey);
  }

  onAttackComplete() {
    console.log("Attack finished");
  }
}

class AnimationManager {
  private states = new Map<string, Map<string, AnimationState>>();
  private sprites = new Map<string, Phaser.GameObjects.Sprite>();
  private transitions: Array<{
    sprite: string;
    from: string;
    to: string;
    condition: () => boolean;
  }> = [];

  constructor(private scene: Phaser.Scene) {}

  registerState(sprite: string, key: string, config: any) {
    if (!this.states.has(sprite)) {
      this.states.set(sprite, new Map());
    }

    const stateConfig = {
      ...config,
      key: `${sprite}_${key}`,
    };

    // Create Phaser animation
    this.scene.anims.create({
      key: stateConfig.key,
      frames: this.scene.anims.generateFrameNumbers(sprite, config.frames),
      frameRate: config.frameRate,
      repeat: config.loop ? -1 : 0,
      yoyo: config.yoyo || false,
    });

    this.states.get(sprite)!.set(key, stateConfig);
  }

  attach(spriteKey: string, sprite: Phaser.GameObjects.Sprite) {
    this.sprites.set(spriteKey, sprite);
    sprite.play(`${spriteKey}_idle`);
  }

  addTransition(
    sprite: string,
    from: string,
    to: string,
    condition: () => boolean,
  ) {
    this.transitions.push({ sprite, from, to, condition });
  }

  update(spriteKey: string) {
    const sprite = this.sprites.get(spriteKey);
    if (!sprite) return;

    const currentAnim = sprite.anims.currentAnim?.key.replace(
      `${spriteKey}_`,
      "",
    );
    const states = this.states.get(spriteKey);

    // Check transitions
    for (const transition of this.transitions) {
      if (
        transition.sprite === spriteKey &&
        transition.from === currentAnim &&
        transition.condition()
      ) {
        this.play(spriteKey, transition.to);
        break;
      }
    }
  }

  play(spriteKey: string, stateKey: string) {
    const sprite = this.sprites.get(spriteKey);
    const state = this.states.get(spriteKey)?.get(stateKey);
    if (sprite && state) {
      sprite.play(state.key);
      if (state.callback) {
        sprite.once("animationcomplete-" + state.key, state.callback);
      }
    }
  }
}

Anti-Patterns

DON'T:

  • Create animations in update() - do it once in create()
  • Override playing animation without checking
  • Use tweens for simple value changes
  • Forget to clean up event listeners
  • Use tweens.chain() for simple sequences - use timeline
  • Ignore animation repeat/yoyo properties

DO:

  • Create animations once in create()
  • Check current animation before changing
  • Use tweens for property animation
  • Clean up listeners on shutdown
  • Use timeline for sequences
  • Configure repeat and yoyo appropriately

Code Patterns

Animation Playback Control

typescript
// Check if animation is playing
if (sprite.anims.isPlaying) {
  console.log("Playing:", sprite.anims.currentAnim?.key);
}

// Get animation progress (0-1)
const progress = sprite.anims.getProgress();

// Pause/resume animation
sprite.anims.pause();
sprite.anims.resume();

// Stop animation
sprite.anims.stop();

// Restart animation
sprite.anims.restart();

Tween Easing Functions

typescript
// Linear
this.tweens.add({ targets: obj, x: 100, ease: "Linear" });

// Power easing
this.tweens.add({ targets: obj, x: 100, ease: "Power0" }); // Linear
this.tweens.add({ targets: obj, x: 100, ease: "Power1" }); // Smooth
this.tweens.add({ targets: obj, x: 100, ease: "Power2" }); // Accelerating
this.tweens.add({ targets: obj, x: 100, ease: "Power3" }); // More accelerating
this.tweens.add({ targets: obj, x: 100, ease: "Power4" }); // Most accelerating

// Special easing
this.tweens.add({ targets: obj, x: 100, ease: "Elastic.easeOut" });
this.tweens.add({ targets: obj, x: 100, ease: "Bounce.easeOut" });
this.tweens.add({ targets: obj, x: 100, ease: "Back.easeOut" });
this.tweens.add({ targets: obj, x: 100, ease: "Cubic.easeOut" });

Tween Controls

typescript
const tween = this.tweens.add({
  targets: sprite,
  alpha: 0,
  duration: 1000,
});

// Pause tween
tween.pause();

// Resume tween
tween.resume();

// Stop tween
tween.stop();

// Complete tween immediately
tween.complete();

Common Easing Functions

Ease Description
Linear Constant speed
Power0 Same as Linear
Power1 Gentle ease
Power2 Medium ease
Power3 Strong ease
Power4 Strongest ease
Elastic Bouncy effect
Bounce Bounce at end
Back Overshoot slightly
Sine Smooth sine curve

Checklist

  • Animations created in create()
  • Frame rates appropriate for smoothness
  • Repeat/yoyo configured correctly
  • Transitions handled gracefully
  • Tweens use proper easing
  • Event listeners cleaned up
  • Animation state managed properly

Reference

Didn't find tool you were looking for?

Be as detailed as possible for better results