Room Management

Create, join, and manage multiplayer game rooms with Web Engine's Colyseus-powered room system, featuring matchmaking, private rooms, and invite codes.

Rooms are the fundamental building blocks of multiplayer sessions in Web Engine. Each room represents an isolated game session with its own state, players, and lifecycle. Built on Colyseus, the room system provides robust matchmaking, private rooms, and seamless player joining/leaving.

Room Concepts#

Isolated Sessions

Each room maintains its own game state, players, and logic, completely isolated from other rooms.

Private Rooms

Create private rooms with unique invite codes for friends-only games.

Public Matchmaking

Auto-join available rooms or create new ones with join-or-create logic.

A room is a server-side instance that manages:

  • Game State — The authoritative state synchronized to all clients
  • Player Sessions — Connected clients and their session IDs
  • Room Lifecycle — Creation, joining, leaving, and disposal
  • Message Routing — Custom messages between clients and server
  • Capacity Management — Maximum player limits and room availability

Room Types#

Room TypeDescriptionUse Case
Public RoomOpen to all players via matchmakingQuick match, casual multiplayer
Private RoomInvite-only with unique codePlaying with friends, private matches
Persistent RoomLong-lived room that persistsGuild halls, persistent worlds
Temporary RoomAuto-disposed when emptyMatch-based games, quick sessions

Creating Rooms#

Create a new room using the NetworkManager or LobbyManager:

Public Room (Join or Create)#

Join or Create Public Room
typescript
import { NetworkManager } from '@web-engine/core/network';
const network = NetworkManager.getInstance();
// Connect to server and join/create a room
await network.connectToColyseus(
'ws://localhost:2567',
'game_room', // Room type/name
{
name: 'Player1',
avatarId: 'avatar-001'
}
);
// This will:
// 1. Look for available rooms of type 'game_room'
// 2. Join if one exists with space
// 3. Create a new one if none available
console.log('Connected to room:', network.getRoomId());

Private Room with Invite Code#

Create Private Room
typescript
// Create a private room
await network.createPrivateRoom('game_room', {
name: 'Host',
avatarId: 'avatar-001'
});
// Get the invite code
const inviteCode = network.inviteCode;
console.log('Share this code with friends:', inviteCode);
// Display invite code to players
showInviteCodeUI(inviteCode); // e.g., "ABC123"

Advanced Room Creation with Options#

Room Creation with Metadata
typescript
import { LobbyManager } from '@web-engine/core/network';
const lobby = new LobbyManager('ws://localhost:2567');
// Create room with custom options
const room = await lobby.createRoom('game_room', {
name: 'My Epic Game',
maxClients: 8,
metadata: {
gameMode: 'team_deathmatch',
map: 'desert_ruins',
difficulty: 'hard',
isRanked: true,
minLevel: 10
}
});
console.log('Room created:', room.id);
console.log('Room metadata:', room.metadata);

Room Metadata

Use room metadata to store searchable information like game mode, map, difficulty, or custom filters. This enables advanced matchmaking and room filtering in lobbies.

Joining Rooms#

Join by Room ID#

Join Specific Room
typescript
const lobby = new LobbyManager();
// Join a specific room by ID
const room = await lobby.joinRoom('room_abc123', {
name: 'Player2',
avatarId: 'avatar-002'
});
console.log('Joined room:', room.id);

Join by Invite Code#

Join with Invite Code
typescript
// Player receives invite code from friend (e.g., "ABC123")
const inviteCode = prompt('Enter invite code:');
await network.joinPrivateRoom(inviteCode, {
name: 'Player2',
avatarId: 'avatar-002'
});
console.log('Joined private room!');

Join or Create Pattern#

Automatic Matchmaking
typescript
const lobby = new LobbyManager();
// Try to join available room, create if none exists
const room = await lobby.joinOrCreate('game_room', {
name: 'Player1',
avatarId: 'avatar-001',
metadata: {
preferredMap: 'city_streets',
skillLevel: 'intermediate'
}
});
// Check if we joined existing or created new
const isNewRoom = room.clients.length === 1;
if (isNewRoom) {
console.log('Created new room, waiting for players...');
} else {
console.log(`Joined existing room with ${room.clients.length} players`);
}

Room State Management#

Room state is automatically synchronized to all connected clients using Colyseus schemas:

Room State Schema (Server)
typescript
import { Schema, MapSchema, type } from '@colyseus/schema';
export class Player extends Schema {
@type('string') sessionId: string = '';
@type('string') name: string = '';
@type('string') avatarId: string = '';
@type('number') score: number = 0;
@type('boolean') isReady: boolean = false;
// Transform data
@type('number') x = 0;
@type('number') y = 0;
@type('number') z = 0;
}
export class GameRoomState extends Schema {
@type({ map: Player }) players = new MapSchema<Player>();
@type('string') gameMode: string = 'deathmatch';
@type('number') gameTime: number = 0;
@type('boolean') gameStarted: boolean = false;
@type('number') roundNumber: number = 1;
createPlayer(sessionId: string, options: any): Player {
const player = new Player();
player.sessionId = sessionId;
player.name = options.name || 'Player';
player.avatarId = options.avatarId || 'default';
this.players.set(sessionId, player);
return player;
}
removePlayer(sessionId: string): void {
this.players.delete(sessionId);
}
}
Accessing Room State (Client)
typescript
import { ColyseusTransport } from '@web-engine/core/network';
const transport = network.getTransport();
if (transport instanceof ColyseusTransport) {
const room = transport.room;
// Access current state
console.log('Game mode:', room.state.gameMode);
console.log('Players:', room.state.players.size);
// Listen for state changes
room.state.onChange(() => {
console.log('Room state updated');
});
// Listen for specific field changes
room.state.listen('gameStarted', (value) => {
if (value) {
console.log('Game started!');
startGameUI();
}
});
// Listen for player additions
room.state.players.onAdd((player, sessionId) => {
console.log(`Player joined: ${player.name} (${sessionId})`);
spawnPlayerAvatar(player);
});
// Listen for player removals
room.state.players.onRemove((player, sessionId) => {
console.log(`Player left: ${player.name}`);
removePlayerAvatar(sessionId);
});
// Listen for player property changes
room.state.players.forEach((player) => {
player.listen('score', (newScore) => {
console.log(`${player.name} score: ${newScore}`);
updateScoreboard();
});
});
}

Room Lifecycle#

Rooms go through several lifecycle stages:

┌─────────────┐
│ Created │ ← onCreate() called
└──────┬──────┘
┌─────────────┐
│ Active │ ← Players join/leave, game logic runs
│ │ onJoin(), onLeave(), onMessage()
└──────┬──────┘
┌─────────────┐
│ Disposing │ ← Room shutting down
└──────┬──────┘
┌─────────────┐
│ Disposed │ ← onDispose() called, room destroyed
└─────────────┘
Room Lifecycle Hooks (Server)
typescript
import { Room, Client } from 'colyseus';
export class GameRoom extends Room<GameRoomState> {
maxClients = 10;
autoDispose = true; // Dispose when empty
onCreate(options: any) {
console.log('Room created with options:', options);
this.setState(new GameRoomState());
// Initialize game state
this.state.gameMode = options.gameMode || 'deathmatch';
// Set up game loop
this.setSimulationInterval((deltaTime) => {
this.update(deltaTime);
}, 1000 / 60); // 60 FPS
// Set up message handlers
this.setupMessageHandlers();
}
onJoin(client: Client, options: any) {
console.log(`Client ${client.sessionId} joined`);
// Create player in state
const player = this.state.createPlayer(client.sessionId, options);
// Send welcome message
client.send('welcome', {
sessionId: client.sessionId,
roomId: this.roomId,
playerCount: this.clients.length
});
// Broadcast to others
this.broadcast('player_joined', {
sessionId: client.sessionId,
name: player.name
}, { except: client });
// Auto-start game if enough players
if (this.clients.length >= 2 && !this.state.gameStarted) {
this.startGame();
}
}
onLeave(client: Client, consented: boolean) {
console.log(`Client ${client.sessionId} left (consented: ${consented})`);
// Remove player from state
this.state.removePlayer(client.sessionId);
// Broadcast to others
this.broadcast('player_left', {
sessionId: client.sessionId
});
// End game if too few players
if (this.clients.length < 2 && this.state.gameStarted) {
this.endGame('not_enough_players');
}
}
onDispose() {
console.log('Room disposed');
// Clean up resources
this.clearSimulationInterval();
// Save game statistics
this.saveGameStats();
}
async onAuth(client: Client, options: any): Promise<boolean> {
// Validate client can join
if (options.bannedPlayers?.includes(options.playerId)) {
return false;
}
return true;
}
}

Matchmaking Basics#

Implement custom matchmaking logic using room listing and filtering:

Custom Matchmaking
typescript
import { LobbyManager } from '@web-engine/core/network';
async function findBestRoom(
skillLevel: number,
preferredMap?: string
): Promise<string | null> {
const lobby = new LobbyManager();
// List all available rooms
const rooms = await lobby.listRooms('game_room');
console.log(`Found ${rooms.length} available rooms`);
// Filter by criteria
const suitableRooms = rooms.filter(room => {
// Not full
if (room.clients >= room.maxClients) return false;
// Check metadata
const metadata = room.metadata as any;
// Skill level matching (±2 levels)
if (metadata?.skillLevel) {
const levelDiff = Math.abs(metadata.skillLevel - skillLevel);
if (levelDiff > 2) return false;
}
// Preferred map
if (preferredMap && metadata?.map !== preferredMap) {
return false;
}
return true;
});
if (suitableRooms.length === 0) {
console.log('No suitable rooms found, will create new one');
return null;
}
// Sort by player count (prefer fuller rooms)
suitableRooms.sort((a, b) => b.clients - a.clients);
// Return best match
const bestRoom = suitableRooms[0];
console.log(`Found best room: ${bestRoom.roomId} (${bestRoom.clients}/${bestRoom.maxClients} players)`);
return bestRoom.roomId;
}
// Use matchmaking
async function joinMatchmakingQueue() {
const playerSkill = 5; // 1-10 scale
const preferredMap = 'desert_ruins';
const roomId = await findBestRoom(playerSkill, preferredMap);
if (roomId) {
// Join existing room
await network.joinRoom(roomId, {
name: 'Player1',
avatarId: 'avatar-001'
});
} else {
// Create new room
await network.createRoom('game_room', {
name: 'Player1',
avatarId: 'avatar-001',
metadata: {
skillLevel: playerSkill,
map: preferredMap
}
});
}
}

Leaving Rooms#

Leave Room
typescript
// Leave current room gracefully
await network.disconnect();
console.log('Left room');
// Or using LobbyManager
const lobby = new LobbyManager();
await lobby.leaveRoom();
// Handle leave on client
const transport = network.getTransport();
if (transport instanceof ColyseusTransport) {
transport.room.onLeave((code) => {
console.log(`Left room with code: ${code}`);
// Clean up
clearGameState();
returnToMainMenu();
});
}

Graceful Disconnection

Always call disconnect() or leaveRoom() when leaving a room to ensure proper cleanup. Ungraceful disconnections (closing tab, network drop) will trigger timeout-based removal after a few seconds.

Reconnection#

Reconnect to Room
typescript
import { LobbyManager } from '@web-engine/core/network';
const lobby = new LobbyManager();
// Save reconnection token before disconnect
let reconnectionToken: string | null = null;
const transport = network.getTransport();
if (transport instanceof ColyseusTransport) {
reconnectionToken = transport.room.reconnectionToken;
localStorage.setItem('roomToken', reconnectionToken);
}
// Later, reconnect using token
const savedToken = localStorage.getItem('roomToken');
if (savedToken) {
try {
const room = await lobby.reconnect(savedToken);
console.log('Reconnected to room:', room.id);
} catch (error) {
console.error('Reconnection failed:', error);
// Token expired or room disposed
localStorage.removeItem('roomToken');
}
}

Room Management Best Practices#

  • Validate on join — Use onAuth() to validate players before allowing them to join.
  • Set capacity limits — Define maxClients to prevent overcrowding.
  • Use autoDispose — Set autoDispose = true for temporary rooms to clean up automatically.
  • Handle disconnections — Gracefully handle player disconnects and allow reconnection windows.
  • Rich metadata — Use room metadata for matchmaking filters (skill level, map, game mode).
  • Rate limit room creation — Prevent spam by rate-limiting room creation per player.
  • Monitor room count — Track active rooms and dispose stale/empty rooms to prevent resource leaks.
  • Preserve state on disconnect — Save player state for reconnection within a timeout window.
Multiplayer | Web Engine Docs | Web Engine Docs