phaser-physics について
Phaser 4 開発者向けの Arcade Physics 実践リファレンスです。物理ボディ、衝突、ジャンル別設定、ライフサイクルの後片付けを扱います。
主な機能
- 01重力と Arcade Physics の設定
- 02動的・静的ボディの作成
- 03衝突とオーバーラップのコールバック設定
- 04プラットフォーマーやトップダウン向けレシピ
- 05プール、タイマー、物理グループの管理
- 06物理デバッグ表示の有効化
活用シーン
- 01プラットフォーマーやトップダウンゲームの物理設定
- 02プレイヤー、敵、足場の衝突処理
- 03弾の再利用と一時的な物理グループの破棄
- 04シーン再起動後のタイマーリーク防止
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ yakoub-ai/ phaser4-gamedev --skill phaser-physicsインストール後、Claude Code で/phaser-physicsを入力して読み込まれたか確認します。
同梱ファイルreferences/arcade-physics-api.mdreferences/multiplayer-patterns.md
詳細
Phaser 4 Arcade Physics
Phaser 4 では Arcade Physics を使用します — 高速な AABB (軸並行境界ボックス) シミュレーションです。実際の曲線や回転ベースの衝突はありませんが、ほとんどの 2D ゲームには最適です。
物理を有効にする
main.ts の GameConfig で:
const config: Phaser.Types.Core.GameConfig = {
// ...
physics: {
default: 'arcade',
arcade: {
gravity: { x: 0, y: 300 }, // world gravity (adjust per genre)
debug: true, // set false for production
},
},
};
ジャンル別の重力値:
- プラットフォーマー:
{ x: 0, y: 500 }から{ x: 0, y: 800 } - トップダウン:
{ x: 0, y: 0 }(重力なし) - スペースシューター:
{ x: 0, y: 0 }(重力なし) - ピンボール:
{ x: 0, y: 600 }で高い反発力
物理ボディの作成
// Dynamic body — affected by gravity, velocity, collisions
const player = this.physics.add.sprite(x, y, 'player');
const enemy = this.physics.add.sprite(x, y, 'enemy');
const coin = this.physics.add.sprite(x, y, 'coin');
// Static body — never moves, others bounce off it
const ground = this.physics.add.staticSprite(x, y, 'platform');
const wall = this.physics.add.staticImage(x, y, 'wall');
// Static group — collection of immovable objects
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground');
platforms.create(100, 400, 'platform');
platforms.create(600, 300, 'platform');
// NOTE: After moving a static body, call staticGroup.refresh() to update physics
// Dynamic group with pooling
const enemies = this.physics.add.group({
classType: Enemy,
runChildUpdate: true,
});
ボディの設定
const body = player.body as Phaser.Physics.Arcade.Body;
// Velocity — pixels per second
player.setVelocity(vx, vy);
player.setVelocityX(200);
player.setVelocityY(-400); // negative = up
// Acceleration — adds to velocity each second
body.setAccelerationX(100);
body.setAccelerationY(0);
// Maximum velocity cap
body.setMaxVelocity(300, 600); // max x and y
body.setMaxVelocityX(300);
// Deceleration (friction when no input)
body.setDrag(100, 0); // x drag, y drag
body.setDragX(200); // horizontal friction only
// Bounce (0 = no bounce, 1 = perfect bounce)
player.setBounce(0.2);
player.setBounceX(0);
player.setBounceY(0.4);
// Hitbox size and offset (often smaller than visual sprite)
body.setSize(24, 40); // hitbox width, height
body.setOffset(4, 8); // offset from top-left of sprite
// World boundary collision
player.setCollideWorldBounds(true);
// Note: world bounds default to canvas size; expand for big levels:
this.physics.world.setBounds(0, 0, worldWidth, worldHeight);
// Gravity override for specific body
body.setGravityY(-300); // counteract world gravity
body.setAllowGravity(false); // disable gravity for this body (float)
// Immovable (static-like but in a dynamic group)
body.setImmovable(true); // won't move when hit, but still detects
コライダーとオーバーラップ
// Collider — objects physically collide (physics response)
this.physics.add.collider(player, platforms);
this.physics.add.collider(player, enemies, this.hitEnemy, undefined, this);
this.physics.add.collider(enemies, platforms);
// Overlap — trigger callback without physics bounce
this.physics.add.overlap(player, coins, this.collectCoin, undefined, this);
this.physics.add.overlap(player, powerups, this.collectPowerup, undefined, this);
// Collider/Overlap callback signature
private hitEnemy(
playerObj: Phaser.Types.Physics.Arcade.GameObjectWithBody,
enemyObj: Phaser.Types.Physics.Arcade.GameObjectWithBody
): void {
const player = playerObj as Phaser.Physics.Arcade.Sprite;
const enemy = enemyObj as Phaser.Physics.Arcade.Sprite;
this.playerHit(player, enemy);
}
// Conditional collider (process callback — return true to proceed)
this.physics.add.collider(
player,
onewayPlatforms,
undefined, // no callback
(playerObj, platformObj) => {
// Only collide if player is falling down through platform
const body = (playerObj as Phaser.Physics.Arcade.Sprite)
.body as Phaser.Physics.Arcade.Body;
return body.velocity.y > 0;
},
this
);
ジャンル別の物理レシピ
プラットフォーマー
// GameConfig
physics: { default: 'arcade', arcade: { gravity: { y: 500 }, debug: true } }
// Player setup
this.player = this.physics.add.sprite(100, 400, 'player');
this.player.setBounce(0.1);
this.player.setCollideWorldBounds(true);
(this.player.body as Phaser.Physics.Arcade.Body)
.setSize(24, 40).setOffset(4, 8);
// In update():
const body = this.player.body as Phaser.Physics.Arcade.Body;
if (this.cursors.left.isDown) {
this.player.setVelocityX(-200);
this.player.flipX = true;
} else if (this.cursors.right.isDown) {
this.player.setVelocityX(200);
this.player.flipX = false;
} else {
// Decelerate
this.player.setVelocityX(this.player.body.velocity.x * 0.85);
}
// Jump — only when on the ground
if (this.cursors.up.isDown && body.blocked.down) {
this.player.setVelocityY(-550);
}
// Variable jump height — release early for lower jump
if (!this.cursors.up.isDown && body.velocity.y < -200) {
this.player.setVelocityY(this.player.body.velocity.y * 0.85);
}
// Coyote time (jump grace period after walking off ledge)
// Track lastGroundedTime and allow jump within 100ms window
トップダウン (RPG/シューター)
// GameConfig — no gravity
physics: { default: 'arcade', arcade: { gravity: { y: 0 }, debug: true } }
// Player setup
this.player = this.physics.add.sprite(400, 300, 'player');
this.player.setCollideWorldBounds(true);
(this.player.body as Phaser.Physics.Arcade.Body).setDrag(1000, 1000);
// In update():
const speed = 200;
let vx = 0;
let vy = 0;
if (this.cursors.left.isDown) vx = -speed;
if (this.cursors.right.isDown) vx = speed;
if (this.cursors.up.isDown) vy = -speed;
if (this.cursors.down.isDown) vy = speed;
// Normalize diagonal movement
if (vx !== 0 && vy !== 0) {
vx *= 0.707;
vy *= 0.707;
}
this.player.setVelocity(vx, vy);
スペースシューター / 弾幕
// GameConfig — no gravity
physics: { default: 'arcade', arcade: { gravity: { y: 0 } } }
// Velocity-based movement (ship feels floaty)
this.ship = this.physics.add.sprite(400, 500, 'ship');
this.ship.setMaxVelocity(300, 300);
(this.ship.body as Phaser.Physics.Arcade.Body).setDrag(300, 300);
// 8-directional movement with velocity
if (this.cursors.up.isDown) this.ship.body.velocity.y -= 400 * delta / 1000;
if (this.cursors.down.isDown) this.ship.body.velocity.y += 400 * delta / 1000;
// Angle-based bullet firing
private fireBullet(): void {
const bullet = this.bullets.get() as Phaser.Physics.Arcade.Sprite;
if (!bullet) return;
bullet.setActive(true).setVisible(true).setPosition(this.ship.x, this.ship.y - 20);
this.physics.velocityFromAngle(this.ship.angle - 90, 500, bullet.body.velocity);
}
弾・発射物のオブジェクトプーリング
// Define bullet class
export class Bullet extends Phaser.Physics.Arcade.Sprite {
constructor(scene: Phaser.Scene, x: number, y: number) {
super(scene, x, y, 'bullet');
}
fire(x: number, y: number, velocityY: number = -500): void {
this.setActive(true).setVisible(true).setPosition(x, y);
this.setVelocityY(velocityY);
}
preUpdate(time: number, delta: number): void {
super.preUpdate(time, delta);
// Recycle off-screen
if (this.y < -50 || this.y > this.scene.scale.height + 50) {
this.setActive(false).setVisible(false);
}
}
}
// In GameScene.create():
this.bullets = this.physics.add.group({
classType: Bullet,
maxSize: 30,
runChildUpdate: true,
});
this.physics.add.overlap(this.bullets, this.enemies, this.bulletHitEnemy, undefined, this);
// To shoot:
const b = this.bullets.get(this.player.x, this.player.y) as Bullet;
if (b) b.fire(this.player.x, this.player.y);
ユーティリティメソッド
// Move body toward a point at speed
this.physics.moveTo(enemy, target.x, target.y, 150); // 150 px/s
// Velocity from angle (angle in degrees, 0=right, 90=down)
this.physics.velocityFromAngle(angle, speed, body.velocity);
// Velocity from rotation (radians)
this.physics.velocityFromRotation(rotation, speed, body.velocity);
// Distance between two game objects
const dist = Phaser.Math.Distance.Between(a.x, a.y, b.x, b.y);
// Angle from one object to another (degrees)
const angle = Phaser.Math.Angle.Between(a.x, a.y, b.x, b.y) * (180 / Math.PI);
タイマーの追跡とクリーンアップ
繰り返し実行される AI パターン、アビリティのクールダウン、ボスのフェーズ遷移のために this.time.addEvent() で作成されたタイマーは蓄積され、シーンの再起動をまたいで自動的にクリーンアップされることはありません。すべてのタイマー参照を保存し、shutdown() (または同等のエンティティのクリーンアップ) でそれらを削除してください。
// In your entity class:
private activeTimers: Phaser.Time.TimerEvent[] = [];
startBossPhase(): void {
const t = this.scene.time.addEvent({
delay: 3000,
loop: true,
callback: this.fireVolley,
callbackScope: this,
});
this.activeTimers.push(t);
}
destroy(fromScene?: boolean): void {
for (const t of this.activeTimers) t.remove(false);
this.activeTimers = [];
super.destroy(fromScene);
}
ルール: エンティティ内のすべての time.addEvent 呼び出しには、destroy() または shutdown() 内に対応する t.remove() が必要です。ループするエンティティにおける匿名の投げっぱなしタイマーは、タイマーリークへの最短経路です。
物理グループのライフサイクル
武器の進化、一時的なスポーンウェーブ、アビリティ効果のために作成された物理グループは、コンテキストが終了したときに明示的に破棄する必要があります。親シーンを停止しても、動的に作成された子グループが自動的に破棄されるわけではありません。
// When creating a group for a weapon or phase effect:
const projectileGroup = this.physics.add.group({ classType: MyProjectile, maxSize: 20 });
// When that weapon is replaced or the phase ends:
projectileGroup.clear(true, true); // destroyChild=true, removeFromScene=true
projectileGroup.destroy();
destroy() の前に clear(true, true) を呼び出してください。これがないと、子は孤立したオブジェクトとしてシーンの表示リストに残ります。
デバッグモード
開発中は有効にし、リリース時は無効にします:
// In GameConfig:
arcade: { gravity: { y: 300 }, debug: true }
// Shows: body outlines (green=normal, red=blocked), velocity arrows, sleep states
// Toggle at runtime:
(this.physics.world as Phaser.Physics.Arcade.World).drawDebug = false;
追加リソース
参照ファイル
references/arcade-physics-api.md— 完全な Arcade Body のプロパティ/メソッドリファレンス、物理ワールドの設定、高度なパターン (ノックバック、コンベアベルト、移動プラットフォーム)