Claude-skill-registry dev-phaser-sprite-management

Sprites, sprite sheets, texture atlases, and object pooling for Phaser

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/dev-phaser-sprite-management" ~/.claude/skills/majiayu000-claude-skill-registry-dev-phaser-sprite-management && rm -rf "$T"
manifest: skills/data/dev-phaser-sprite-management/SKILL.md
source content

Phaser Sprite Management

"Efficient sprite handling – sheets, atlases, and object pools."

Before/After: Manual Arrays vs Phaser Object Pooling

❌ Before: Manual Array Management

// Manual sprite management without Phaser
interface Bullet {
  x: number;
  y: number;
  vx: number;
  vy: number;
  active: boolean;
}

const bullets: Bullet[] = [];
let lastShotTime = 0;

function shoot(x: number, y: number, dx: number, dy: number) {
  // Create new object every shot
  const bullet: Bullet = {
    x, y,
    vx: dx * 500,
    vy: dy * 500,
    active: true
  };
  bullets.push(bullet);
}

function updateBullets(dt: number) {
  for (let i = bullets.length - 1; i >= 0; i--) {
    const b = bullets[i];
    b.x += b.vx * dt;
    b.y += b.vy * dt;

    // Manual cleanup triggers GC pressure
    if (b.x < 0 || b.x > 800 || b.y < 0 || b.y > 600 || !b.active) {
      bullets.splice(i, 1); // Expensive operation
    }
  }
}

// Problems:
// - New objects every shot (GC pressure)
// - Array splice is expensive
// - No pre-allocation
// - Manual lifecycle management

✅ After: Phaser Object Pooling

// Phaser handles pooling automatically
export class GameScene extends Phaser.Scene {
  private bulletPool!: Phaser.GameObjects.Group;

  create() {
    // Pre-allocate bullet pool
    this.bulletPool = this.add.group({
      defaultKey: 'bullet',
      maxSize: 50, // Never exceeds 50 objects
      createCallback: (bullet: Phaser.GameObjects.Image) => {
        bullet.setActive(false).setVisible(false);
      }
    });

    // Pre-fill pool for performance
    for (let i = 0; i < 20; i++) {
      this.bulletPool.get(0, 0);
    }
  }

  fireBullet(x: number, y: number, vx: number, vy: number) {
    const bullet = this.bulletPool.get(x, y) as Phaser.GameObjects.Image;

    if (bullet) {
      bullet.setActive(true).setVisible(true);

      // Animate and return to pool when done
      this.tweens.add({
        targets: bullet,
        x: x + vx * 2,
        y: y + vy * 2,
        duration: 500,
        onComplete: () => {
          this.bulletPool.killAndHide(bullet);
        }
      });
    }
  }
}

// Benefits:
// - Objects reused (no GC)
// - killAndHide is fast
// - Pre-allocated pool
// - Automatic lifecycle

When to Use This Skill

Use when:

  • Creating and managing game sprites
  • Working with sprite sheets and texture atlases
  • Implementing object pooling for performance
  • Optimizing sprite rendering
  • Managing sprite animations

Quick Start

// Load sprite sheet in preload()
this.load.spritesheet("player", "assets/player.png", {
  frameWidth: 32,
  frameHeight: 32,
  startFrame: 0,
  endFrame: 15,
});

// Create sprite in create()
const player = this.add.sprite(400, 300, "player", 0);
player.play("idle");

Decision Framework

NeedUse
Single image
load.image()
+
add.image()
Animation frames
load.spritesheet()
Multiple sprites
load.atlas()
Many reusable objectsObject pool
Physics body
physics.add.sprite()

Progressive Guide

Level 1: Basic Sprite Loading

export class MainScene extends Phaser.Scene {
  preload() {
    // Single image
    this.load.image("player", "assets/player.png");

    // Sprite sheet (horizontal strip)
    this.load.spritesheet("coin", "assets/coin.png", {
      frameWidth: 16,
      frameHeight: 16,
      endFrame: 7,
    });

    // Multi-row sprite sheet
    this.load.spritesheet("player", "assets/player.png", {
      frameWidth: 32,
      frameHeight: 48,
      startFrame: 0,
      endFrame: 47,
    });
  }

  create() {
    // Create sprite
    const player = this.add.sprite(400, 300, "player");

    // Set frame manually
    player.setFrame(5);

    // Flip sprite
    player.setFlipX(true);
    player.setFlipY(false);
  }
}

Level 2: Texture Atlas Loading

preload() {
  // Load atlas with JSON (from TexturePacker)
  this.load.atlas('game', 'assets/atlas.png', 'assets/atlas.json');

  // Load atlas with XML (from Shoebox)
  this.load.atlasXML('ui', 'assets/ui.png', 'assets/ui.xml');

  // Load atlas with hash array (Unity-style)
  this.load.atlas('items', 'assets/items.png', 'assets/items.json');
}

create() {
  // Create sprite from atlas
  const sword = this.add.image(400, 300, 'game', 'sword.png');
  const shield = this.add.image(400, 350, 'game', 'shield.png');
}

Level 3: Sprite Animations

create() {
  // Create animations from sprite sheet
  this.anims.create({
    key: 'idle',
    frames: this.anims.generateFrameNumbers('player', {
      start: 0,
      end: 3
    }),
    frameRate: 10,
    repeat: -1 // Infinite loop
  });

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

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

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

Level 4: Object Pooling

export class MainScene extends Phaser.Scene {
  private bulletPool!: Phaser.GameObjects.Group;
  private readonly MAX_BULLETS = 50;

  create() {
    // Pre-create bullet pool
    this.bulletPool = this.add.group({
      defaultKey: "bullet",
      maxSize: this.MAX_BULLETS,
      createCallback: (bullet: Phaser.GameObjects.Image) => {
        bullet.setActive(false).setVisible(false);
      },
    });

    // Pre-fill pool
    for (let i = 0; i < 20; i++) {
      this.bulletPool.get(0, 0);
    }

    this.input.on("pointerdown", this.fireBullet, this);
  }

  fireBullet() {
    const bullet = this.bulletPool.get(
      this.player.x,
      this.player.y,
    ) as Phaser.GameObjects.Image;

    if (bullet) {
      bullet.setActive(true).setVisible(true);

      // Animate bullet
      this.tweens.add({
        targets: bullet,
        x: bullet.x + 500,
        duration: 500,
        onComplete: () => {
          // Return to pool
          bullet.setActive(false).setVisible(false);
          this.bulletPool.killAndHide(bullet);
        },
      });
    }
  }
}

Level 5: Advanced Sprite Management

export class MainScene extends Phaser.Scene {
  private spriteManager!: SpriteManager;

  create() {
    this.spriteManager = new SpriteManager(this);

    // Register sprite types
    this.spriteManager.registerType("enemy", {
      poolSize: 30,
      onCreate: (sprite) => this.setupEnemy(sprite),
      onActivate: (sprite, data) => this.spawnEnemy(sprite, data),
      onDeactivate: (sprite) => this.cleanupEnemy(sprite),
    });
  }

  update() {
    // Spawn enemy
    this.spriteManager.spawn("enemy", { x: 400, y: 100, type: "flying" });
  }
}

// Sprite Manager class
class SpriteManager {
  private pools = new Map<string, Phaser.GameObjects.Group>();

  constructor(private scene: Phaser.Scene) {}

  registerType(
    type: string,
    config: {
      poolSize: number;
      onCreate: Function;
      onActivate: Function;
      onDeactivate: Function;
    },
  ) {
    this.pools.set(
      type,
      this.scene.add.group({
        defaultKey: type,
        maxSize: config.poolSize,
        createCallback: config.onCreate,
        removeCallback: config.onDeactivate,
      }),
    );
  }

  spawn(type: string, data: any) {
    const pool = this.pools.get(type);
    if (!pool) return null;

    const sprite = pool.get(data.x, data.y);
    if (sprite) {
      sprite.setActive(true).setVisible(true);
      pool.config.onActivate(sprite, data);
    }
    return sprite;
  }

  despawn(type: string, sprite: Phaser.GameObjects.GameObject) {
    const pool = this.pools.get(type);
    if (pool) {
      pool.killAndHide(sprite);
    }
  }
}

Anti-Patterns

DON'T:

  • Load individual images for sprite frames - use sprite sheets
  • Create/destroy sprites every frame - use object pooling
  • Use large atlases without grouping - organize by scene/use
  • Forget to
    killAndHide()
    pooled objects before reuse
  • Mix pixel densities in sprite sheets
  • Use
    add.sprite()
    when physics needed - use
    physics.add.sprite()

DO:

  • Use TexturePacker or similar for atlas generation
  • Pre-allocate pools during scene creation
  • Group atlas textures by logical usage
  • Set active/visible false when returning to pool
  • Use consistent frame sizes in sprite sheets
  • Cache frequently used sprites

Code Patterns

Object Pool with Custom Class

class Bullet extends Phaser.GameObjects.Image {
  declare body: Phaser.Physics.Arcade.Body;

  constructor(scene: Phaser.Scene, x: number, y: number) {
    super(scene, x, y, 'bullet');
    scene.add.existing(this);
    scene.physics.add.existing(this);
  }

  fire(x: number, y: number, velocity: Phaser.Math.Vector2) {
    this.setPosition(x, y);
    this.body.setVelocity(velocity.x, velocity.y);
    this.setActive(true);
    this.setVisible(true);
  }

  reset() {
    this.body.setVelocity(0, 0);
    this.setActive(false);
    this.setVisible(false);
    this.setPosition(-1000, -1000); // Off-screen
  }
}

// In scene
create() {
  this.bullets = this.add.group({
    classType: Bullet,
    maxSize: 50,
    runChildUpdate: true
  });
}

Sprite Sheet with Multi-Row Animation

// 4x4 sprite sheet (16 frames)
// Row 1: Idle (0-3)
// Row 2: Walk (4-7)
// Row 3: Attack (8-11)
// Row 4: Die (12-15)

this.anims.create({
  key: "idle",
  frames: this.anims.generateFrameNumbers("player", {
    start: 0,
    end: 3,
  }),
  frameRate: 8,
  repeat: -1,
  yoyo: false,
});

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

Checklist

  • Using sprite sheets or atlases (not individual images)
  • Object pools pre-allocated
  • Sprites properly returned to pool
  • Animations defined before use
  • Frame sizes consistent
  • Texture memory optimized
  • Off-screen sprites culled

Reference