phaser-gamedev について
Phaser 3で2Dブラウザーゲームを作るためのスキルです。シーン、物理演算、アセット、入力の設計と、保守しやすい構成を開発者に案内します。
主な機能
- 01シーンベースのゲーム構成
- 02ArcadeとMatterの物理演算ガイド
- 03キーボードとポインター入力
- 04アセット読み込みとタイルマップ
- 05アニメーションと性能の実践
活用シーン
- 01シーンとゲーム状態の設計
- 022Dの衝突と移動の実装
- 03スプライトシートとTiledマップの読み込み
- 04Phaser実装でよくある問題の回避
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ chongdashu/ phaserjs-tinyswords --skill phaser-gamedevインストール後、Claude Code で/phaser-gamedevを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/arcade-physics.mdperformance.mdspritesheets-nineslice.mdtilemaps.md詳細
Phaser ゲーム開発
Phaser 3 のシーンベースアーキテクチャと物理システムを使って、高速で洗練された 2D ブラウザーゲームを作成します。
哲学:生きたシステムとしてのゲーム
ゲームは静的な UI ではありません。エンティティが相互に作用し、状態が変化し、プレイヤーの入力がすべてを動かす動的なシステムです。コードを書く前に、アーキテクチャについて考えてください。
開発を始める前に、次の点を確認してください:
- このゲームにはどのようなシーンが必要ですか?(Boot、Menu、Game、Pause、GameOver)
- どのようなエンティティがあり、どのように相互作用しますか?
- シーンをまたいで保持する必要がある状態は何ですか?
- どの物理モデルが適していますか?(速度重視なら Arcade、リアリティ重視なら Matter)
- プレイヤーはどのような入力方法を使いますか?
基本原則:
- シーン優先のアーキテクチャ:ゲームをグローバルな状態ではなく、シーンを中心に構成します
- 継承よりコンポジション:ゲームオブジェクトとコンポーネントからエンティティを構築します
- 物理を考慮した設計:衝突判定を実装する前に、物理システムを選択します
- アセットパイプラインの規律:すべてを事前に読み込み、キーで参照します
- フレームレートに依存しない設計:フレーム数ではなく、デルタタイムを使います
ゲーム設定
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 ゲームを一から構築できます。ここに示すガイドラインは道筋を照らすものであり、制約するものではありません。