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 物理

Phaser 4 使用 Arcade 物理系统——快速的 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);

计时器跟踪与清理

使用 this.time.addEvent() 创建的计时器,用于循环 AI 模式、技能冷却或 Boss 阶段切换,会不断累积,并且不会在场景重启时自动清理。请保存每个计时器的引用,并在 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 属性/方法参考、物理世界配置及高级模式(击退、传送带、移动平台)

评论

支持 Markdown