Web Engine Docs
Preparing documentation
Use the search bar to quickly find any topic
Preparing documentation
Use the search bar to quickly find any topic
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.
Each room maintains its own game state, players, and logic, completely isolated from other rooms.
Create private rooms with unique invite codes for friends-only games.
Auto-join available rooms or create new ones with join-or-create logic.
A room is a server-side instance that manages:
| 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 |
Create a new room using the NetworkManager or LobbyManager:
import { NetworkManager } from '@web-engine-dev/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());
// 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"
import { LobbyManager } from '@web-engine-dev/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.
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);
// 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!');
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 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-dev/core/network';const transport = network.getTransport();if (transport instanceof ColyseusTransport) {const room = transport.room;// Access current stateconsole.log('Game mode:', room.state.gameMode);console.log('Players:', room.state.players.size);// Listen for state changesroom.state.onChange(() => {console.log('Room state updated');});// Listen for specific field changesroom.state.listen('gameStarted', (value) => {if (value) {console.log('Game started!');startGameUI();}});// Listen for player additionsroom.state.players.onAdd((player, sessionId) => {console.log(`Player joined: ${player.name} (${sessionId})`);spawnPlayerAvatar(player);});// Listen for player removalsroom.state.players.onRemove((player, sessionId) => {console.log(`Player left: ${player.name}`);removePlayerAvatar(sessionId);});// Listen for player property changesroom.state.players.forEach((player) => {player.listen('score', (newScore) => {console.log(`${player.name} score: ${newScore}`);updateScoreboard();});});}
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 emptyonCreate(options: any) {console.log('Room created with options:', options);this.setState(new GameRoomState());// Initialize game statethis.state.gameMode = options.gameMode || 'deathmatch';// Set up game loopthis.setSimulationInterval((deltaTime) => {this.update(deltaTime);}, 1000 / 60); // 60 FPS// Set up message handlersthis.setupMessageHandlers();}onJoin(client: Client, options: any) {console.log(`Client ${client.sessionId} joined`);// Create player in stateconst player = this.state.createPlayer(client.sessionId, options);// Send welcome messageclient.send('welcome', {sessionId: client.sessionId,roomId: this.roomId,playerCount: this.clients.length});// Broadcast to othersthis.broadcast('player_joined', {sessionId: client.sessionId,name: player.name}, { except: client });// Auto-start game if enough playersif (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 statethis.state.removePlayer(client.sessionId);// Broadcast to othersthis.broadcast('player_left', {sessionId: client.sessionId});// End game if too few playersif (this.clients.length < 2 && this.state.gameStarted) {this.endGame('not_enough_players');}}onDispose() {console.log('Room disposed');// Clean up resourcesthis.clearSimulationInterval();// Save game statisticsthis.saveGameStats();}async onAuth(client: Client, options: any): Promise<boolean> {// Validate client can joinif (options.bannedPlayers?.includes(options.playerId)) {return false;}return true;}}
Implement custom matchmaking logic using room listing and filtering:
import { LobbyManager } from '@web-engine-dev/core/network';async function findBestRoom(skillLevel: number,preferredMap?: string): Promise<string | null> {const lobby = new LobbyManager();// List all available roomsconst rooms = await lobby.listRooms('game_room');console.log(`Found ${rooms.length} available rooms`);// Filter by criteriaconst suitableRooms = rooms.filter(room => {// Not fullif (room.clients >= room.maxClients) return false;// Check metadataconst 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 mapif (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 matchconst 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 scaleconst preferredMap = 'desert_ruins';const roomId = await findBestRoom(playerSkill, preferredMap);if (roomId) {// Join existing roomawait network.joinRoom(roomId, {name: 'Player1',avatarId: 'avatar-001'});} else {// Create new roomawait network.createRoom('game_room', {name: 'Player1',avatarId: 'avatar-001',metadata: {skillLevel: playerSkill,map: preferredMap}});}}
// 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 upclearGameState();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.
import { LobbyManager } from '@web-engine-dev/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 disposedlocalStorage.removeItem('roomToken');}}
onAuth() to validate players before allowing them to join.maxClients to prevent overcrowding.autoDispose = true for temporary rooms to clean up automatically.