mcpField
P
phaser-physics

phaser-physics

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

phaser-physics について

Phaser 4 開発者向けの Arcade Physics 実践リファレンスです。物理ボディ、衝突、ジャンル別設定、ライフサイクルの後片付けを扱います。

主な機能

  1. 01重力と Arcade Physics の設定
  2. 02動的・静的ボディの作成
  3. 03衝突とオーバーラップのコールバック設定
  4. 04プラットフォーマーやトップダウン向けレシピ
  5. 05プール、タイマー、物理グループの管理
  6. 06物理デバッグ表示の有効化

活用シーン

  1. 01プラットフォーマーやトップダウンゲームの物理設定
  2. 02プレイヤー、敵、足場の衝突処理
  3. 03弾の再利用と一時的な物理グループの破棄
  4. 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 のプロパティ/メソッドリファレンス、物理ワールドの設定、高度なパターン (ノックバック、コンベアベルト、移動プラットフォーム)

コメント