Retrofit a Phaser game
Read this first. There is no Phaser example in the irtio repository, so unlike the canvas and Three.js recipes, the diff below has not been compiled or run against a real Phaser build. The irtio half of it is the same API those two pages use and is covered by tests; the Phaser half is written against Phaser 3’s documented
Scenelifecycle andArcade.SpriteAPI. Treat it as a shape to follow, not a snippet to paste.
Phaser is a good fit for the owned-write retrofit, because a Phaser scene already separates the sprite from the thing the sprite represents. The retrofit puts irtio in between.
The one Phaser-specific decision
Do not sync a Phaser.GameObjects.Sprite. Sync the numbers. A sprite carries a texture, a
body, a tween state and a scene reference, none of which belong on a wire. The schema holds the
position, the facing and whatever else the other player must agree with, and the sprite reads from
it in update.
The second consequence is where your input goes. Phaser’s Arcade physics moves sprites by setting
velocity, and if the sprite is the source of truth, two clients disagree the moment either one
lags. So the local sprite stays the thing you move, and its position is written to your entity
once per frame; the room’s validate is what decides whether that write is plausible.
1. Scaffold
npx irtio init Answer the one question with the mode your game is in. A Phaser game with a update() that moves
things on its own is tick mode; a turn-based board is event mode.
2. The schema is the sprite’s numbers, not the sprite
// irtio/schema.ts
players: entity({ x: f32, y: f32, vx: f32, vy: f32, facing: u8, name: str(24) }), 3. The room validates a move the way your game already bounds one
// irtio/room.ts
const WORLD_WIDTH = 1600;
const WORLD_HEIGHT = 900;
/** Arcade physics top speed times the longest plausible gap between two writes. */
const MAX_STEP = 400 * 0.25;
validate: {
players(prev, next) {
if (Math.hypot(next.x - prev.x, next.y - prev.y) > MAX_STEP) return prev;
return {
...next,
x: Math.min(WORLD_WIDTH, Math.max(0, next.x)),
y: Math.min(WORLD_HEIGHT, Math.max(0, next.y)),
};
},
}, 4. Join in create, and write your own sprite in update
+import { joinRoom } from '@irtio/client';
+
+import { schema } from './irtio/schema.js';
+
class Play extends Phaser.Scene {
+ private room!: Awaited<ReturnType<typeof joinRoom<typeof schema>>>;
+ private others = new Map<string, Phaser.GameObjects.Sprite>();
+
- create() {
+ async create() {
+ this.room = await joinRoom(schema, { name: 'you' });
this.player = this.physics.add.sprite(400, 300, 'hero');
this.cursors = this.input.keyboard.createCursorKeys();
}
update() {
this.player.setVelocityX(this.cursors.left.isDown ? -400 : this.cursors.right.isDown ? 400 : 0);
+
+ const me = this.room.state.players[this.room.me];
+ if (!me) return;
+ me.x = this.player.x;
+ me.y = this.player.y;
+ me.facing = this.player.flipX ? 1 : 0;
}
} create returning a promise is supported by Phaser’s scene lifecycle, and the if (!me) return; covers the frames between the scene starting and the entity existing.
5. Reconcile the other players’ sprites
update() {
// ...your own movement, as above
+
+ for (const [id, p] of this.room.render.players) {
+ if (id === this.room.me) continue;
+ let sprite = this.others.get(id);
+ if (!sprite) {
+ sprite = this.add.sprite(p.x, p.y, 'hero');
+ this.others.set(id, sprite);
+ }
+ sprite.setPosition(p.x, p.y);
+ sprite.setFlipX(p.facing === 1);
+ }
+ for (const [id, sprite] of this.others) {
+ if (!this.room.state.players.has(id)) {
+ sprite.destroy();
+ this.others.delete(id);
+ }
+ }
} That reconcile pass is the whole Phaser-specific part of the retrofit, and it is the same pass the
Three.js recipe writes against meshes. Read remote players from room.render so they interpolate
between updates instead of stepping at the tick rate; read room.state when you need the
authoritative value, as the removal check above does.
Remote sprites are display objects, not physics bodies. Adding them with this.add.sprite rather
than this.physics.add.sprite keeps Arcade physics from fighting the positions the server sent.
What did not change
The scene list, the tilemap, the animations, the camera, the sound, the particle emitters, the
Arcade collider for your own player. The retrofit touches create, update, and nothing else.
Verify it
npx irtio dev
npx irtio simulate --bots 5 --seconds 10 --cheat --cheat is the one worth running against a Phaser retrofit, because writing your sprite’s
position straight to the wire is exactly the shape a cheating client abuses. If the run prints HOLE bot 0 cheated and drew 0 corrections, your MAX_STEP is too generous or missing.
Next steps
- Retrofit an existing game, the canvas original
- Server authority and validation
- Simulated players