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