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 Type | Description | Use Case |
|---|---|---|
| Public Room | Open to all players via matchmaking | Quick match, casual multiplayer |
| Private Room | Invite-only with unique code | Playing with friends, private matches |
| Persistent Room | Long-lived room that persists | Guild halls, persistent worlds |
| Temporary Room | Auto-disposed when empty | Match-based games, quick sessions |
Creating Rooms#
Create a new room using the NetworkManager or LobbyManager:
Public Room (Join or Create)#
import { NetworkManager } from '@web-engine/core/network'; const network = NetworkManager.getInstance(); // Connect to server and join/create a roomawait 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 availableconsole.log('Connected to room:', network.getRoomId());Private Room with Invite Code#
// Create a private roomawait network.createPrivateRoom('game_room', { name: 'Host', avatarId: 'avatar-001'}); // Get the invite codeconst inviteCode = network.inviteCode;console.log('Share this code with friends:', inviteCode); // Display invite code to playersshowInviteCodeUI(inviteCode); // e.g., "ABC123"Advanced Room Creation with Options#
import { LobbyManager } from '@web-engine/core/network'; const lobby = new LobbyManager('ws://localhost:2567'); // Create room with custom optionsconst 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#
const lobby = new LobbyManager(); // Join a specific room by IDconst room = await lobby.joinRoom('room_abc123', { name: 'Player2', avatarId: 'avatar-002'}); console.log('Joined room:', room.id);Join by Invite Code#
// 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#
const lobby = new LobbyManager(); // Try to join available room, create if none existsconst room = await lobby.joinOrCreate('game_room', { name: 'Player1', avatarId: 'avatar-001', metadata: { preferredMap: 'city_streets', skillLevel: 'intermediate' }}); // Check if we joined existing or created newconst 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:
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); }}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└─────────────┘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:
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 matchmakingasync 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 current room gracefullyawait network.disconnect(); console.log('Left room'); // Or using LobbyManagerconst lobby = new LobbyManager();await lobby.leaveRoom(); // Handle leave on clientconst 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#
import { LobbyManager } from '@web-engine/core/network'; const lobby = new LobbyManager(); // Save reconnection token before disconnectlet reconnectionToken: string | null = null; const transport = network.getTransport();if (transport instanceof ColyseusTransport) { reconnectionToken = transport.room.reconnectionToken; localStorage.setItem('roomToken', reconnectionToken);} // Later, reconnect using tokenconst 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
maxClientsto prevent overcrowding. - Use autoDispose — Set
autoDispose = truefor 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.