Examples
Learn by example with a full multiplayer Pac-Man game.
Simple Pac-Man
A self-contained grid-based Pac-Man built entirely with the EasyMultiplayer API. Ghosts, dots, power pellets, scoring, and multiplayer in one HTML file.
Game Setup
const game = new EasyMultiplayer({
room: 'easy-mp-pacman-demo',
tickRate: 10,
canvas: document.getElementById('game'),
state: {
players: {},
ghosts: [],
dots: [],
dotsEaten: 0,
totalDots: 0,
powerTimer: 0,
gameStarted: false,
}
});
State is a plain object. Everything the game needs is right there — player positions, ghost data, dot states, scoring.
Input Definition
game.defineInput('direction', () => {
if (keys['ArrowLeft']) return 'left';
if (keys['ArrowRight']) return 'right';
if (keys['ArrowUp']) return 'up';
if (keys['ArrowDown']) return 'down';
return null;
});
Query-Based Movement
game.on('tick', ({ state, query, random, players }) => {
for (var pid of players) {
var p = state.players[pid];
// Query-based input
var wantLeft = query(pid, i => i.direction === 'left');
var wantRight = query(pid, i => i.direction === 'right');
if (wantLeft) p.x = Math.max(0, p.x - 1);
if (wantRight) p.x = Math.min(COLS - 1, p.x + 1);
// ...
}
});
Graph Pac-Man (Advanced)
The full graph-based Pac-Man game with sprite rendering, tunnel teleportation, ghost AI personalities, level progression, and multiplayer. This is a real production game (~3500 lines) adapted to use EasyMultiplayer.
Migrating an Existing Game
The graph Pac-Man was originally built as a class extending GameBase (which extended RollbackNetcode). Here's how it was migrated:
Step 1: Remove the inheritance
// Before:
export class GraphPacmanGame extends GameBase { ... }
// After:
export class GraphPacmanGame { ... }
Step 2: Add the multiplayer bridge
Replace inherited methods with an mp bridge object that EasyMultiplayer will populate:
class GraphPacmanGame {
// EasyMultiplayer sets these before Init
mp = {
random: () => Math.random(), // default fallback
getInput: () => null,
query: () => false,
getPlayers: () => [],
getPlayerName: (id) => '' + id,
myId: 'local',
lastTickTime: 0,
};
// Stubs for removed inherited methods
PlaySound() {}
PlayBackgroundMusic() {}
StopBackgroundMusic() {}
// ...
}
Step 3: Replace all inherited calls
// Before:
this.Random() // inherited from RollbackNetcode
this.GetInputForPlayer(playerId) // inherited
this.GetActivePlayerIds() // inherited
// After:
this.mp.random() // injected by EasyMultiplayer
this.mp.getInput(playerId) // injected
this.mp.getPlayers() // injected
Step 4: Wire through EasyMultiplayer
var pacmanGame = new GridPacmanGame();
var game = new EasyMultiplayer({
room: 'pacman-room',
tickRate: 20,
canvas: displayCanvas,
state: {}
});
// State: the game handles its own complex serialization
game.manageState(
() => pacmanGame.ExportState(),
(s) => pacmanGame.ImportState(s)
);
// Tick: wire the mp bridge, then call the game's Tick
game.on('tick', (ctx) => {
pacmanGame.mp.random = ctx.random;
pacmanGame.mp.query = ctx.query;
pacmanGame.mp.getPlayers = () => ctx.players;
pacmanGame.mp.lastTickTime = ctx.time;
pacmanGame.Tick(ctx.time);
});
// Draw: let the game render to its canvas
game.on('draw', (ctx) => {
pacmanGame.Draw(ctx.t);
});
game.on('playerJoined', ({playerId}) => pacmanGame.PlayerJoined(playerId));
game.on('playerLeft', ({playerId}) => pacmanGame.PlayerLeft(playerId));
pacmanGame.Init(0);
game.start();
Step 5: Use context-sensitive queries
The graph Pac-Man's movement was rewritten to use queries at decision points:
// At a junction — only query available directions
for (var i in options) {
var dir = options[i].direction;
if (dir == this.movementDirection) continue; // default, no query
var opp = OppositeDirection(dir);
// Single compound query: wants this direction AND NOT opposite
if (this.gameObj.mp.query(this.playerNumber, i => !!i[dir] && !i[opp])) {
chosenOption = options[i];
break;
}
}
// Mid-edge turn-around
var oppDir = OppositeDirection(this.movementDirection);
// Single query: wants opposite AND NOT current direction
var curDir = this.movementDirection;
if (this.gameObj.mp.query(this.playerNumber, i => !!i[oppDir] && !i[curDir])) {
this.TurnAround(t, edgeProgress);
}
This means rollback only happens when a direction change actually affects gameplay — not when the player presses a button that doesn't do anything at that moment.
Live Demos
- Simple Pac-Man — grid-based, self-contained
- Graph Pac-Man — full production game with sprites and ghost AI
- Test Suite — visual debugger with multiple instances