Examples

Learn by example with a full multiplayer Pac-Man game.

Simple Pac-Man

Open the live demo →

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)

Open the live demo →

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