| name | phaser-physics |
| description | This skill should be used when the user asks to "add physics", "set up collisions", "implement gravity", "create a platformer", "add physics to sprite", "detect overlaps", "create collision groups", "make objects bounce", "top-down movement", "set velocity", "apply forces", "enable arcade physics", or needs any Arcade Physics configuration in Phaser 4. |
| version | 0.5.0 |
Phaser 4 Arcade Physics
Phaser 4 uses Arcade Physics — fast AABB (axis-aligned bounding box) simulation. No real curves or rotation-based collisions, but excellent for most 2D games.
Enable Physics
In main.ts GameConfig:
const config: Phaser.Types.Core.GameConfig = {
physics: {
default: 'arcade',
arcade: {
gravity: { x: 0, y: 300 },
debug: true,
},
},
};
Gravity values by genre:
- Platformer:
{ x: 0, y: 500 } to { x: 0, y: 800 }
- Top-down:
{ x: 0, y: 0 } (no gravity)
- Space shooter:
{ x: 0, y: 0 } (no gravity)
- Pinball:
{ x: 0, y: 600 } with high bounce
Creating Physics Bodies
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');
const ground = this.physics.add.staticSprite(x, y, 'platform');
const wall = this.physics.add.staticImage(x, y, 'wall');
const platforms = this.physics.add.staticGroup();
platforms.create(400, 568, 'ground');
platforms.create(100, 400, 'platform');
platforms.create(600, 300, 'platform');
const enemies = this.physics.add.group({
classType: Enemy,
runChildUpdate: true,
});
Body Configuration
const body = player.body as Phaser.Physics.Arcade.Body;
player.setVelocity(vx, vy);
player.setVelocityX(200);
player.setVelocityY(-400);
body.setAccelerationX(100);
body.setAccelerationY(0);
body.setMaxVelocity(300, 600);
body.setMaxVelocityX(300);
body.setDrag(100, 0);
body.setDragX(200);
player.setBounce(0.2);
player.setBounceX(0);
player.setBounceY(0.4);
body.setSize(24, 40);
body.setOffset(4, 8);
player.setCollideWorldBounds(true);
this.physics.world.setBounds(0, 0, worldWidth, worldHeight);
body.setGravityY(-300);
body.setAllowGravity(false);
body.setImmovable(true);
Colliders and Overlaps
this.physics.add.collider(player, platforms);
this.physics.add.collider(player, enemies, this.hitEnemy, undefined, this);
this.physics.add.collider(enemies, platforms);
this.physics.add.overlap(player, coins, this.collectCoin, undefined, this);
this.physics.add.overlap(player, powerups, this.collectPowerup, undefined, this);
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);
}
this.physics.add.collider(
player,
onewayPlatforms,
undefined,
(playerObj, platformObj) => {
const body = (playerObj as Phaser.Physics.Arcade.Sprite)
.body as Phaser.Physics.Arcade.Body;
return body.velocity.y > 0;
},
this
);
Genre Physics Recipes
Platformer
physics: { default: 'arcade', arcade: { gravity: { y: 500 }, debug: true } }
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);
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 {
this.player.setVelocityX(this.player.body.velocity.x * 0.85);
}
if (this.cursors.up.isDown && body.blocked.down) {
this.player.setVelocityY(-550);
}
if (!this.cursors.up.isDown && body.velocity.y < -200) {
this.player.setVelocityY(this.player.body.velocity.y * 0.85);
}
Top-Down (RPG/Shooter)
physics: { default: 'arcade', arcade: { gravity: { y: 0 }, debug: true } }
this.player = this.physics.add.sprite(400, 300, 'player');
this.player.setCollideWorldBounds(true);
(this.player.body as Phaser.Physics.Arcade.Body).setDrag(1000, 1000);
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;
if (vx !== 0 && vy !== 0) {
vx *= 0.707;
vy *= 0.707;
}
this.player.setVelocity(vx, vy);
Space Shooter / Bullet Hell
physics: { default: 'arcade', arcade: { gravity: { y: 0 } } }
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);
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;
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);
}
Object Pooling for Bullets/Projectiles
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);
if (this.y < -50 || this.y > this.scene.scale.height + 50) {
this.setActive(false).setVisible(false);
}
}
}
this.bullets = this.physics.add.group({
classType: Bullet,
maxSize: 30,
runChildUpdate: true,
});
this.physics.add.overlap(this.bullets, this.enemies, this.bulletHitEnemy, undefined, this);
const b = this.bullets.get(this.player.x, this.player.y) as Bullet;
if (b) b.fire(this.player.x, this.player.y);
Utility Methods
this.physics.moveTo(enemy, target.x, target.y, 150);
this.physics.velocityFromAngle(angle, speed, body.velocity);
this.physics.velocityFromRotation(rotation, speed, body.velocity);
const dist = Phaser.Math.Distance.Between(a.x, a.y, b.x, b.y);
const angle = Phaser.Math.Angle.Between(a.x, a.y, b.x, b.y) * (180 / Math.PI);
Timer Tracking and Cleanup
Timers created with this.time.addEvent() for recurring AI patterns, ability cooldowns, or boss phase transitions accumulate and never self-clean across scene restarts. Store every timer reference and remove them in shutdown() (or the equivalent entity cleanup).
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);
}
Rule: Every time.addEvent call in an entity must have a corresponding t.remove() in destroy() or shutdown(). Anonymous fire-and-forget timers on looping entities are the fastest path to a timer leak.
Physics Group Lifecycle
Physics groups created for weapon evolutions, temporary spawn waves, or ability effects must be explicitly destroyed when the context ends. Stopping the parent scene does NOT automatically destroy child groups that were created dynamically.
const projectileGroup = this.physics.add.group({ classType: MyProjectile, maxSize: 20 });
projectileGroup.clear(true, true);
projectileGroup.destroy();
Call clear(true, true) before destroy(). Without it, the children remain in the scene's display list as orphaned objects.
Debug Mode
Enable during development, disable for release:
arcade: { gravity: { y: 300 }, debug: true }
(this.physics.world as Phaser.Physics.Arcade.World).drawDebug = false;
Additional Resources
Reference Files
references/arcade-physics-api.md — Complete Arcade Body property/method reference, physics world config, advanced patterns (knockback, conveyor belts, moving platforms)