mcpField
P
phaser-animation

phaser-animation

github.com/yakoub-ai開発29202 回インストール9 週間前に更新
リポジトリ

phaser-animation について

Phaser 4 でゲームを開発する人向けのアニメーションスキルです。スプライトフレーム、状態遷移、トゥイーン、パーティクルを扱い、状態マシンの安定した実装と詳細なリファレンスを重視します。

主な機能

  1. 01スプライトシートとアトラスの設定
  2. 02アニメーションイベントと連結再生
  3. 03キャラクター状態マシンのパターン
  4. 04トゥイーン効果とタイムライン
  5. 05パーティクルのバースト例

活用シーン

  1. 01キャラクターの移動や攻撃アニメーションの作成
  2. 02強制アニメーションの早期終了の防止
  3. 03UI要素やカットシーンの演出
  4. 04衝撃やアイテム取得のバースト表現

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yakoub-ai/phaser4-gamedev --skill phaser-animation

インストール後、Claude Code で/phaser-animationを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
animation-api.md
easing-reference.md
state-machine-patterns.md

詳細

Phaser 4 のアニメーションとトゥイーン

Phaser 4 には、異なるアニメーションシステムが 2 つあります。フレームベースのスプライトアニメーション(テクスチャアトラスまたはスプライトシートのフレームを切り替える)と、トゥイーン(時間経過に応じて数値プロパティを補間する)です。洗練されたゲームの操作感を実現するには、両方を組み合わせて使いましょう。

スプライトシートアニメーションの作成

スプライトシートには、規則的なグリッド状に複数のフレームが 1 枚の画像にまとめられています。AnimationManager でフレームインデックスを指定してアニメーションを定義します。

// 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 のオプション
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

アニメーションイベント

アニメーションのライフサイクルイベントは、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

メモリリークを防ぐため、スプライトが破棄される際は必ずリスナーを削除してください。

sprite.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')
      );
    }
  }
}
強制アニメーション:シネマティックモード

ワンショットアニメーション(ボスの登場、死亡シーケンス、ダンジョンへの進入など)が1 フレームだけ再生されてから待機状態に戻る場合、原因は常に、強制的に play() を呼び出した 1 ティック後にエンティティの update() がステートマシンのロジックを実行し、アニメーションを上書きしていることです。

対処法 — update() の最初のガードとして cinematicMode フラグを追加します。

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

cinematicMode は play() の直後に同期的に解除するのではなく、ANIMATION_COMPLETE_KEY ハンドラー内で解除してください。完了イベントは最後のフレームが描画された後に発火し、ハンドラーが実行される前に update() が呼び出される場合があります。同期的に解除すると、強制アニメーションが 1 フレーム早く終了してしまいます。

完全な標準実装、ダンジョンへの進入例、ANIMATION_COMPLETE のタイミングに関する修正については、references/state-machine-patterns.md を参照してください。

状態遷移の網羅性

新しいアニメーション状態を追加する際に、他のすべての状態の遷移リストを確認しないと、気付きにくいバグが発生します。エラーは表示されず、ステートマシンが新しい状態に到達できなかったり、誤った状態から抜け出せなくなったりします。

新しい状態(例:'dodge'、'interact')を追加する際は、次の作業を行ってください。

  1. CharState 共用体型に追加します。
  2. setState() に case を追加します。
  3. 新しい状態を適切に含めるか除外するよう、他のすべての状態の「何によって中断されるか」のロジックを更新します。

references/state-machine-patterns.md の遷移表を確認すれば、遷移の抜けを簡単に見つけられます。

アニメーションの停止と一時停止

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

トゥイーン

トゥイーンは、時間経過に応じてあらゆるオブジェクトの数値プロパティを補間します。Phaser では、UI アニメーション、カットシーン、視覚的なフィードバックに主に使用します。

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

よく使うトゥイーンパターン

フェードイン
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' — 速度一定。機械的な動き、UI バー向け
  • 'Quad.Out' — 速く始まり、減速します。最も自然な動き
  • 'Quad.In' — 加速します。落下するオブジェクト、巻き上げる動き向け
  • 'Quad.InOut' — 対称的に緩急がつきます。カメラの移動向け
  • 'Back.Out' — 目標を行き過ぎてから戻ります。UI ポップアップ、ダイアログのスライド向け
  • 'Bounce.Out' — 終点でバウンドします。地面にぶつかるオブジェクト向け
  • 'Elastic.Out' — バネのように振動します。コミカルな動き、弾むような UI 向け

トゥイーンタイムライン

onComplete コールバックをネストせずに複数のトゥイーンを連続実行できます:

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 は前のトゥイーンに対するタイミングを制御します:

  • '-=200' — 200ms 分オーバーラップします
  • '+=200' — 200ms の間隔を追加します
  • 絶対数値 — タイムライン開始からその ms の位置で開始します

パーティクルアニメーション (概要)

バーストエフェクト (爆発、アイテム取得、衝撃) には、組み込みのパーティクルシステムを使用します:

// 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.md — AnimationManager、AnimationConfig、Animation イベント、TweenManager、Timeline API の完全なリファレンス
  • references/easing-reference.md — すべての組み込みイージング関数と、その説明、ユースケース、コード例
  • references/state-machine-patterns.md — キャラクターのステートマシン規律: cinematicMode フラグ、正規のステート一覧、遷移テーブル、stop()/play() の順序ルール、ANIMATION_COMPLETE のタイミング。アイドル+歩行以上のステートを持つキャラクターを構築するとき、または強制アニメーションが 1 フレームだけ再生されて元に戻ってしまう場合に読んでください。

コメント