mcpField
P
phaser-gamedev

phaser-gamedev

github.com/chongdashu開発371.1k 回インストール41 週間前に更新
リポジトリ

phaser-gamedev について

Phaser 3で2Dブラウザーゲームを作るためのスキルです。シーン、物理演算、アセット、入力の設計と、保守しやすい構成を開発者に案内します。

主な機能

  1. 01シーンベースのゲーム構成
  2. 02ArcadeとMatterの物理演算ガイド
  3. 03キーボードとポインター入力
  4. 04アセット読み込みとタイルマップ
  5. 05アニメーションと性能の実践

活用シーン

  1. 01シーンとゲーム状態の設計
  2. 022Dの衝突と移動の実装
  3. 03スプライトシートとTiledマップの読み込み
  4. 04Phaser実装でよくある問題の回避

インストール

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

$npx skills add https://github.com/chongdashu/phaserjs-tinyswords --skill phaser-gamedev

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

同梱ファイル

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

references/
arcade-physics.md
performance.md
spritesheets-nineslice.md
tilemaps.md

詳細

Phaser ゲーム開発

Phaser 3 のシーンベースアーキテクチャと物理システムを使って、高速で洗練された 2D ブラウザーゲームを作成します。

哲学:生きたシステムとしてのゲーム

ゲームは静的な UI ではありません。エンティティが相互に作用し、状態が変化し、プレイヤーの入力がすべてを動かす動的なシステムです。コードを書く前に、アーキテクチャについて考えてください。

開発を始める前に、次の点を確認してください:

  • このゲームにはどのようなシーンが必要ですか?(Boot、Menu、Game、Pause、GameOver)
  • どのようなエンティティがあり、どのように相互作用しますか?
  • シーンをまたいで保持する必要がある状態は何ですか?
  • どの物理モデルが適していますか?(速度重視なら Arcade、リアリティ重視なら Matter)
  • プレイヤーはどのような入力方法を使いますか?

基本原則:

  1. シーン優先のアーキテクチャ:ゲームをグローバルな状態ではなく、シーンを中心に構成します
  2. 継承よりコンポジション:ゲームオブジェクトとコンポーネントからエンティティを構築します
  3. 物理を考慮した設計:衝突判定を実装する前に、物理システムを選択します
  4. アセットパイプラインの規律:すべてを事前に読み込み、キーで参照します
  5. フレームレートに依存しない設計:フレーム数ではなく、デルタタイムを使います

ゲーム設定

Phaser のゲームはすべて、設定オブジェクトから始まります。

最小限の設定
const config = {
  type: Phaser.AUTO,           // WebGL with Canvas fallback
  width: 800,
  height: 600,
  scene: [BootScene, GameScene]
};

const game = new Phaser.Game(config);
完全な設定パターン
const config = {
  type: Phaser.AUTO,
  width: 800,
  height: 600,
  parent: 'game-container',    // DOM element ID
  backgroundColor: '#2d2d2d',

  scale: {
    mode: Phaser.Scale.FIT,
    autoCenter: Phaser.Scale.CENTER_BOTH
  },

  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 300 },
      debug: false              // Enable during development
    }
  },

  scene: [BootScene, MenuScene, GameScene, GameOverScene]
};
物理システムの選択
システム使用する場面
Arcadeプラットフォーマー、シューティングゲーム、ほとんどの 2D ゲーム。高速でシンプルな AABB 衝突判定
Matter物理パズル、ラグドール、リアルな衝突判定。低速ですが、より正確です
なしメニューシーン、ビジュアルノベル、カードゲーム

シーンアーキテクチャ

シーンは基本的な構成単位です。各シーンにはライフサイクルがあります。

シーンのライフサイクルメソッド
class GameScene extends Phaser.Scene {
  constructor() {
    super('GameScene');        // Scene key for reference
  }

  init(data) {
    // Called first. Receive data from previous scene
    this.level = data.level || 1;
  }

  preload() {
    // Load assets. Runs before create()
    this.load.image('player', 'assets/player.png');
    this.load.spritesheet('enemy', 'assets/enemy.png', {
      frameWidth: 32, frameHeight: 32
    });
  }

  create() {
    // Set up game objects, physics, input
    this.player = this.physics.add.sprite(100, 100, 'player');
    this.cursors = this.input.keyboard.createCursorKeys();
  }

  update(time, delta) {
    // Game loop. Called every frame
    // delta = milliseconds since last frame
    this.player.x += this.speed * (delta / 1000);
  }
}
シーンの遷移
// Start a new scene (stops current)
this.scene.start('GameOverScene', { score: this.score });

// Launch scene in parallel (both run)
this.scene.launch('UIScene');

// Pause/resume scenes
this.scene.pause('GameScene');
this.scene.resume('GameScene');

// Stop a scene
this.scene.stop('UIScene');
推奨されるシーン構成
scenes/
├── BootScene.js      # Asset loading, progress bar
├── MenuScene.js      # Title screen, options
├── GameScene.js      # Main gameplay
├── UIScene.js        # HUD overlay (launched parallel)
├── PauseScene.js     # Pause menu overlay
└── GameOverScene.js  # End screen, restart option

ゲームオブジェクト

Phaser で表示されるものはすべて、ゲームオブジェクトです。

一般的なゲームオブジェクト
// Images (static)
this.add.image(400, 300, 'background');

// Sprites (can animate, physics-enabled)
const player = this.add.sprite(100, 100, 'player');

// Text
const score = this.add.text(16, 16, 'Score: 0', {
  fontSize: '32px',
  fill: '#fff'
});

// Graphics (draw shapes)
const graphics = this.add.graphics();
graphics.fillStyle(0xff0000);
graphics.fillRect(100, 100, 50, 50);

// Containers (group objects)
const container = this.add.container(400, 300, [sprite1, sprite2]);

// Tilemaps
const map = this.make.tilemap({ key: 'level1' });
スプライトの作成パターン
// Basic sprite
const sprite = this.add.sprite(x, y, 'textureKey');

// Sprite with physics body
const sprite = this.physics.add.sprite(x, y, 'textureKey');

// From spritesheet frame
const sprite = this.add.sprite(x, y, 'sheet', frameIndex);

// From atlas
const sprite = this.add.sprite(x, y, 'atlas', 'frameName');

物理システム

Arcade Physics(推奨されるデフォルト)

ほとんどの 2D ゲームに適した、高速でシンプルな物理演算です。

// Enable physics on sprite
this.physics.add.sprite(x, y, 'player');

// Or add physics to existing sprite
this.physics.add.existing(sprite);

// Configure body
sprite.body.setVelocity(200, 0);
sprite.body.setBounce(0.5);
sprite.body.setCollideWorldBounds(true);
sprite.body.setGravityY(300);

// Collision detection
this.physics.add.collider(player, platforms);
this.physics.add.overlap(player, coins, collectCoin, null, this);

function collectCoin(player, coin) {
  coin.disableBody(true, true);  // Remove from physics and hide
  this.score += 10;
}
物理グループ
// Static group (platforms, walls)
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground').setScale(2).refreshBody();

// Dynamic group (enemies, bullets)
const enemies = this.physics.add.group({
  key: 'enemy',
  repeat: 5,
  setXY: { x: 100, y: 0, stepX: 70 }
});

enemies.children.iterate(enemy => {
  enemy.setBounce(Phaser.Math.FloatBetween(0.4, 0.8));
});
Matter Physics

リアルな物理シミュレーション向けです。

// Config
physics: {
  default: 'matter',
  matter: {
    gravity: { y: 1 },
    debug: true
  }
}

// Create bodies
const ball = this.matter.add.circle(400, 100, 25);
const box = this.matter.add.rectangle(400, 400, 100, 50, { isStatic: true });

// Sprite with Matter body
const player = this.matter.add.sprite(100, 100, 'player');
player.setFriction(0.005);
player.setBounce(0.9);

入力処理

キーボード入力
// Cursor keys
this.cursors = this.input.keyboard.createCursorKeys();

// In update()
if (this.cursors.left.isDown) {
  player.setVelocityX(-160);
} else if (this.cursors.right.isDown) {
  player.setVelocityX(160);
}

if (this.cursors.up.isDown && player.body.touching.down) {
  player.setVelocityY(-330);  // Jump
}

// Custom keys
this.spaceKey = this.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE);

// Key events
this.input.keyboard.on('keydown-SPACE', () => {
  this.fire();
});
ポインター/マウス入力
// Click/tap
this.input.on('pointerdown', (pointer) => {
  console.log(pointer.x, pointer.y);
});

// Make object interactive
sprite.setInteractive();
sprite.on('pointerdown', () => {
  sprite.setTint(0xff0000);
});
sprite.on('pointerup', () => {
  sprite.clearTint();
});

// Drag
this.input.setDraggable(sprite);
this.input.on('drag', (pointer, obj, dragX, dragY) => {
  obj.x = dragX;
  obj.y = dragY;
});

アニメーション

アニメーションの作成
// In create() - define once
this.anims.create({
  key: 'walk',
  frames: this.anims.generateFrameNumbers('player', { start: 0, end: 3 }),
  frameRate: 10,
  repeat: -1  // Loop forever
});

this.anims.create({
  key: 'jump',
  frames: [{ key: 'player', frame: 4 }],
  frameRate: 20
});

// From atlas
this.anims.create({
  key: 'explode',
  frames: this.anims.generateFrameNames('atlas', {
    prefix: 'explosion_',
    start: 1,
    end: 8,
    zeroPad: 2
  }),
  frameRate: 16,
  hideOnComplete: true
});
アニメーションの再生
// Play animation
sprite.anims.play('walk', true);  // true = ignore if already playing

// Play once
sprite.anims.play('jump');

// Stop
sprite.anims.stop();

// Animation events
sprite.on('animationcomplete', (anim, frame) => {
  if (anim.key === 'die') {
    sprite.destroy();
  }
});

アセットの読み込み

事前読み込みのパターン
preload() {
  // Images
  this.load.image('sky', 'assets/sky.png');

  // Spritesheets
  this.load.spritesheet('player', 'assets/player.png', {
    frameWidth: 32,
    frameHeight: 48
  });

  // Atlases (TexturePacker)
  this.load.atlas('sprites', 'assets/sprites.png', 'assets/sprites.json');

  // Tilemaps
  this.load.tilemapTiledJSON('map', 'assets/level1.json');
  this.load.image('tiles', 'assets/tileset.png');

  // Audio
  this.load.audio('bgm', 'assets/music.mp3');
  this.load.audio('sfx', ['assets/sound.ogg', 'assets/sound.mp3']);

  // Progress tracking
  this.load.on('progress', (value) => {
    console.log(`Loading: ${Math.round(value * 100)}%`);
  });
}
Boot シーンのパターン
class BootScene extends Phaser.Scene {
  constructor() {
    super('BootScene');
  }

  preload() {
    // Loading bar
    const width = this.cameras.main.width;
    const height = this.cameras.main.height;

    const progressBar = this.add.graphics();
    const progressBox = this.add.graphics();
    progressBox.fillStyle(0x222222, 0.8);
    progressBox.fillRect(width/2 - 160, height/2 - 25, 320, 50);

    this.load.on('progress', (value) => {
      progressBar.clear();
      progressBar.fillStyle(0xffffff, 1);
      progressBar.fillRect(width/2 - 150, height/2 - 15, 300 * value, 30);
    });

    // Load all game assets here
    this.load.image('player', 'assets/player.png');
    // ... more assets
  }

  create() {
    this.scene.start('MenuScene');
  }
}

タイルマップ(Tiled との連携)

読み込みと作成
preload() {
  this.load.tilemapTiledJSON('map', 'assets/map.json');
  this.load.image('tiles', 'assets/tileset.png');
}

create() {
  const map = this.make.tilemap({ key: 'map' });
  const tileset = map.addTilesetImage('tileset-name-in-tiled', 'tiles');

  // Create layers (match names from Tiled)
  const backgroundLayer = map.createLayer('Background', tileset, 0, 0);
  const groundLayer = map.createLayer('Ground', tileset, 0, 0);

  // Enable collision on specific tiles
  groundLayer.setCollisionByProperty({ collides: true });
  // Or by tile index
  groundLayer.setCollisionBetween(1, 100);

  // Add collision with player
  this.physics.add.collider(this.player, groundLayer);
}
オブジェクトレイヤー
// Spawn points from Tiled object layer
const spawnPoint = map.findObject('Objects', obj => obj.name === 'spawn');
this.player = this.physics.add.sprite(spawnPoint.x, spawnPoint.y, 'player');

// Create objects from layer
const coins = map.createFromObjects('Objects', {
  name: 'coin',
  key: 'coin'
});
this.physics.world.enable(coins);

プロジェクト構成

推奨される構成
game/
├── src/
│   ├── scenes/
│   │   ├── BootScene.js
│   │   ├── MenuScene.js
│   │   ├── GameScene.js
│   │   └── UIScene.js
│   ├── gameObjects/
│   │   ├── Player.js
│   │   ├── Enemy.js
│   │   └── Collectible.js
│   ├── systems/
│   │   ├── InputManager.js
│   │   └── AudioManager.js
│   ├── config/
│   │   └── gameConfig.js
│   └── main.js
├── assets/
│   ├── images/
│   ├── audio/
│   ├── tilemaps/
│   └── fonts/
├── index.html
└── package.json
ES モジュールの設定
// main.js
import Phaser from 'phaser';
import BootScene from './scenes/BootScene';
import GameScene from './scenes/GameScene';
import { gameConfig } from './config/gameConfig';

const config = {
  ...gameConfig,
  scene: [BootScene, GameScene]
};

new Phaser.Game(config);

避けるべきアンチパターン

❌ グローバル状態の寄せ集め:ゲームの状態を window やモジュールのグローバル変数に保存する 問題点:追跡できないバグが発生し、シーン遷移で状態が壊れます 改善策:シーンデータ、レジストリ、専用の状態管理クラスを使用します

❌ Create での読み込み:preload() ではなく create() でアセットを読み込む 問題点:参照時にアセットの読み込みが完了していない可能性があります 改善策:必ず preload() で読み込み、すべてのアセットには Boot シーンを使用します

❌ フレーム依存のロジック:デルタタイムではなくフレーム数を使用する 問題点:ゲーム速度がフレームレートによって変化します 改善策:一貫した動きにするには this.speed * (delta / 1000) を使用します

❌ 過剰な物理演算:単純なプラットフォーマーの衝突判定に Matter を使用する 問題点:パフォーマンスが低下し、不要な複雑さが生じます 改善策:2D ゲームのニーズの 90% は Arcade 物理演算で対応できます

❌ モノリシックなシーン:すべてのゲームロジックを 1 つの巨大なシーンにまとめる 問題点:保守が難しく、機能を追加しにくくなります 改善策:メニュー、ゲームプレイ、UI オーバーレイには個別のシーンを使用します

❌ マジックナンバー:コード内のあちこちに値をハードコードする 問題点:バランス調整が不可能になり、一貫性が失われます 改善策:設定オブジェクトや定数ファイルを使用します

❌ オブジェクトプーリングの無視:毎フレーム、オブジェクトを作成・破棄する 問題点:メモリの頻繁な割り当てと解放が発生し、ガベージコレクションによるカクつきが起きます 改善策:setActive(false) / setVisible(false) を使ったグループを使用します

❌ 同期的なアセットアクセス:アセットが即座に読み込まれると思い込む 問題点:競合状態や未定義のテクスチャが発生します 改善策:シーンの開始を連鎖させ、読み込みイベントを使用します

❌ スプライトシートのフレーム寸法の思い込み:確認せずに推測したフレームサイズを使用する 問題点:寸法が間違っていると、気づかないままフレームが破損します。わずかなピクセルのずれが積み重なると、表示が崩れます 改善策:アセットファイルを開いてフレームを測り、間隔やマージンを考慮して計算し、計算が合っていることを確認します

❌ スプライトシートの間隔の無視:隙間のあるスプライトシートに spacing を指定しない 問題点:フレームが徐々にずれて、後のフレームで誤ったピクセル領域が読み込まれます 改善策:フレーム間に隙間があるかソースアセットを確認し、ローダーの設定で spacing: N を使用します

❌ ナインスライスの色のハードコード:UI パネルのすべてのバリエーションで、背景色を 1 色にする 問題点:フレームの端が透明だと、アセットの配色によっては不適切な色が見えてしまいます 改善策:アセットごとに背景色を設定し、中央のフレーム(フレーム 4)から色を取得します

❌ 余白のあるフレームでのナインスライス:アートが各タイルの中央に配置され、余白がある場合に、フレーム全体をスライス領域として扱う 問題点:端のタイルが内部の塗りつぶし領域に入り込み、パネルの内側に不透明な「サイドバー」が表示されます 改善策:タイルを実際のコンテンツ境界(アルファ境界ボックス)までトリミングしてから、テクスチャを合成・キャッシュします。継ぎ目を防ぐために約 1 ピクセル重ね、スムージングを無効にします

❌ 不連続な UI アートの拡大縮小:内部に透明な隙間がある、切り抜き済みのリボン/バナーの行を引き伸ばす 問題点:透明な隙間も引き伸ばされるため、UI が分断されたように見えたり、フレームの背後に塗りつぶしが隠れたりします。 改善策:アセットを両端と中央に分割し、中央だけを引き伸ばします。描画前にパーツをつなぎ合わせ(約 1 ピクセル重ね、スムージングを無効にする)、ピボットサイズで描画します。


バリエーションに関する指針

重要:ゲームの実装は、次の要素に応じて変える必要があります。

  • ゲームのジャンル:プラットフォーマーと見下ろし型シューティングゲームでは、物理演算が異なります
  • 対象プラットフォーム:モバイルではタッチ入力が必要ですが、デスクトップではキーボードを使用できます
  • アートスタイル:ピクセルアートではニアレストネイバー法による拡大縮小を、HD アートでは線形補間を使用します
  • パフォーマンス要件:スプライトが多い場合はオブジェクトプーリングを、少ない場合は単純な作成を使用します
  • 複雑さ:単純なゲームではインライン化でき、複雑なゲームではクラス階層が必要です

次のような実装に固定されないようにしてください。

  • 常に解像度 800x600 を使用する
  • 常に Arcade 物理演算を使用する
  • 常に同じシーン構成を使用する
  • 適応せずに定型コードをコピー&ペーストする

クイックリファレンス

よく使う物理プロパティ
body.setVelocity(x, y)
body.setVelocityX(x)
body.setBounce(x, y)
body.setGravityY(y)
body.setCollideWorldBounds(true)
body.setImmovable(true)        // For static-like dynamic bodies
body.setDrag(x, y)
body.setMaxVelocity(x, y)
便利なシーンプロパティ
this.cameras.main              // Main camera
this.physics.world             // Physics world
this.input.keyboard            // Keyboard manager
this.sound                     // Audio manager
this.time                      // Time/clock manager
this.tweens                    // Tween manager
this.anims                     // Animation manager
this.registry                  // Cross-scene data store
this.data                      // Scene-specific data store
必須イベント
// Scene events
this.events.on('pause', callback)
this.events.on('resume', callback)
this.events.on('shutdown', callback)

// Physics events
this.physics.world.on('worldbounds', callback)

// Game object events
sprite.on('destroy', callback)
sprite.on('animationcomplete', callback)

関連項目

  • references/arcade-physics.md - Arcade 物理エンジンを詳しく解説
  • references/tilemaps.md - 高度なタイルマップのテクニック
  • references/performance.md - 最適化の方法
  • references/spritesheets-nineslice.md - スプライトシートの読み込み(間隔/余白)、ナインスライス UI パネル、アセットの検査

覚えておきましょう

Phaser はシーン、スプライト、物理演算、入力といった強力なプリミティブを提供しますが、アーキテクチャを決めるのはあなたの責任です。

システムの視点で考えましょう。どのようなシーンが必要ですか?どのようなエンティティが存在しますか?それらはどのように連携しますか?コードを書く前にこれらの問いに答えておけば、ゲームが成長しても保守しやすくなります。

Claude は、完成度の高い Phaser ゲームを一から構築できます。ここに示すガイドラインは道筋を照らすものであり、制約するものではありません。

コメント