Docs
Multiplayer
Lobby & Room Management
Lobby & Room Management
CarverJS multiplayer ships four hooks that cover the entire lobby-to-gameplay lifecycle: discovering rooms, joining one, reading player state, and managing the session as a host.
tsx Copy
import { useLobby, useRoom, usePlayers, useHost } from "@carverjs/multiplayer" ;
useLobby #
useLobby subscribes to room announcements on the signaling network and returns a live list of available rooms. Use it to build a lobby screen where players can browse, filter, and create rooms.
tsx Copy
import { useLobby } from "@carverjs/multiplayer" ;
function LobbyScreen ( ) {
const { rooms, createRoom, refresh, isLoading } = useLobby ();
return (
<div >
<button onClick ={refresh} disabled ={isLoading} > Refresh</button >
<button onClick ={() => createRoom({ name: "My Room", maxPlayers: 4 })}>
Create Room
</button >
<ul >
{rooms.map((room) => (
<li key ={room.id} >
{room.name} — {room.playerCount}/{room.maxPlayers}
</li >
))}
</ul >
</div >
);
}
Options #
Option Type Default Description autoRefreshbooleantrueAutomatically poll for room list updates filterLobbyFilter— Filter the room list on the server side
filter #
Field Type Description maxPlayersnumberOnly show rooms with this max player count gameModestringOnly show rooms matching this game mode hasPasswordbooleanOnly show rooms that are password-protected (or not)
tsx Copy
const { rooms } = useLobby ({
autoRefresh : true ,
filter : { gameMode : "deathmatch" , hasPassword : false },
});
Return Value #
Property Type Description roomsRoom[]Live list of available rooms matching the current filter createRoom(config: RoomConfig) => Promise<Room>Create a new room and return it. The caller automatically becomes the host refresh() => voidManually trigger a room list refresh isLoadingbooleantrue while a fetch or create is in progress
When `autoRefresh` is enabled, the room list updates on a regular interval. Call `refresh()` manually after a player action (e.g. creating a room) for an immediate update.
useRoom #
useRoom manages the connection lifecycle for a single room. It handles joining, leaving, reconnection, transport selection, and host migration.
tsx Copy
import { useRoom } from "@carverjs/multiplayer" ;
function RoomScreen ({ roomId }: { roomId: string } ) {
const { join, leave, connectionState, room, error, isConnected, isHost } =
useRoom (roomId, {
displayName : "Player1" ,
transport : "webrtc" ,
});
if (error) return <div > Error: {error.message}</div > ;
if (!isConnected) return <div > Connecting... ({connectionState})</div > ;
return (
<div >
<h2 > {room?.name}</h2 >
<p > You are {isHost ? "the host" : "a client"}</p >
<button onClick ={leave} > Leave Room</button >
</div >
);
}
Options #
Option Type Default Description transportCarverTransport-- Supply a custom CarverTransport instance to bypass the built-in WebRTCTransport passwordstring-- Room password (for private rooms) displayNamestring-- Display name shown to other players playerMetadataRecord<string, unknown>-- Arbitrary metadata attached to this player (avatar, team, etc.) iceServersRTCIceServer[]Provider defaults Custom STUN/TURN servers for this room hostMigrationbooleantrueAutomatically elect a new host if the current host disconnects reconnectAttemptsnumber3Number of automatic reconnection attempts on disconnect privacy'all' | 'relay''all'Set to 'relay' to force TURN relay (hides player IPs) onConnected() => void-- Callback fired when the connection is established onDisconnected(reason: string) => void-- Callback fired when the connection is lost onHostMigration(newHostId: string) => void-- Callback fired when host migration occurs onError(error: CarverMultiplayerError) => void-- Callback fired on connection or room errors
Return Value #
Property Type Description join() => Promise<void>Connect to the room. Called automatically on mount unless manually controlled leave() => voidDisconnect from the room and clean up connectionStatestringCurrent state: "disconnected", "connecting", "connected", "reconnecting" roomRoom | nullThe current room object, or null if not yet connected errorError | nullThe latest error, or null isConnectedbooleanShorthand for connectionState === "connected" isHostbooleanWhether the local player is the room host
Setting `hostMigration: false` means the room is destroyed when the host disconnects. This is useful for games where the host runs authoritative game logic that cannot be transferred.
usePlayers #
usePlayers provides a reactive view of every player in the room. It re-renders whenever a player joins, leaves, changes ready state, or updates metadata.
tsx Copy
import { usePlayers } from "@carverjs/multiplayer" ;
function PlayerList ( ) {
const { players, self, host, allReady } = usePlayers ();
return (
<div >
<h3 > Players ({players.length})</h3 >
<ul >
{players.map((p) => (
<li key ={p.peerId} >
{p.displayName}
{p.isHost && " (Host)"}
{p.isSelf && " (You)"}
{p.isReady ? " Ready" : " Not Ready"}
<span > — {p.latencyMs}ms</span >
</li >
))}
</ul >
{self && <p > Your ID: {self.peerId}</p > }
{host && <p > Host: {host.displayName}</p > }
{allReady && <p > All players are ready!</p > }
</div >
);
}
Return Value #
Property Type Description playersPlayer[]All players currently in the room (including self) selfPlayer | nullThe local player, or null if not yet connected hostPlayer | nullThe current room host, or null getPlayer(peerId: string) => Player | undefinedLook up a player by their peer ID allReadybooleantrue when every player in the room has isReady: true
useHost #
useHost exposes host-only room management actions. Every function checks host status before executing — if called by a non-host client, it logs a warning and no-ops.
tsx Copy
import { useHost } from "@carverjs/multiplayer" ;
function HostControls ( ) {
const { kick, transferHost, setRoomState, setMaxPlayers, lockRoom, unlockRoom, isHost } =
useHost ();
if (!isHost) return null ;
return (
<div >
<button onClick ={() => setMaxPlayers(8)}>Set Max Players: 8</button >
<button onClick ={lockRoom} > Lock Room</button >
<button onClick ={unlockRoom} > Unlock Room</button >
</div >
);
}
Return Value #
Property Type Description kick(peerId: string) => voidRemove a player from the room transferHost(peerId: string) => voidTransfer host privileges to another player setRoomState(state: RoomState) => voidSet the room state ("waiting", "starting", "playing", "finished") setMaxPlayers(max: number) => voidUpdate the maximum player count lockRoom() => voidPrevent new players from joining unlockRoom() => voidAllow new players to join again isHostbooleanWhether the local player is the room host
All `useHost` functions are safe to call from any player. Non-host calls are silently ignored with a console warning, so you do not need to guard every call with an `isHost` check — but hiding host-only UI behind `isHost` provides a better user experience.
Complete Flow Example #
The typical multiplayer journey: lobby screen (browse and create rooms) → room screen (wait for players, ready up) → game screen (gameplay with synced state).
tsx Copy
import { useState } from "react" ;
import { useLobby, useRoom, usePlayers, useHost } from "@carverjs/multiplayer" ;
import { Game , World , Actor } from "@carverjs/core/components" ;
type Screen = "lobby" | "room" | "game" ;
function App ( ) {
const [screen, setScreen] = useState<Screen >("lobby" );
const [roomId, setRoomId] = useState<string | null >(null );
if (screen === "lobby" ) {
return (
<LobbyScreen
onJoin ={(id) => { setRoomId(id); setScreen("room"); }}
/>
);
}
if (screen === "room" && roomId) {
return (
<RoomScreen
roomId ={roomId}
onStart ={() => setScreen("game")}
onLeave={() => { setRoomId(null); setScreen("lobby"); }}
/>
);
}
if (screen === "game" && roomId) {
return <GameScreen roomId ={roomId} onExit ={() => setScreen("lobby")} /> ;
}
return null ;
}
Lobby Screen #
tsx Copy
function LobbyScreen ({ onJoin }: { onJoin: (id: string ) => void } ) {
const { rooms, createRoom, refresh, isLoading } = useLobby ({
autoRefresh : true ,
filter : { hasPassword : false },
});
const handleCreate = async ( ) => {
const room = await createRoom ({
name : "My Game" ,
maxPlayers : 4 ,
gameMode : "coop" ,
});
onJoin (room.id );
};
return (
<div >
<h1 > Lobby</h1 >
<button onClick ={handleCreate} > Create Room</button >
<button onClick ={refresh} disabled ={isLoading} > Refresh</button >
<ul >
{rooms.map((room) => (
<li key ={room.id} >
{room.name} ({room.playerCount}/{room.maxPlayers})
<button onClick ={() => onJoin(room.id)}>Join</button >
</li >
))}
</ul >
</div >
);
}
Room Screen #
tsx Copy
function RoomScreen ({
roomId,
onStart,
onLeave,
}: {
roomId: string ;
onStart: () => void ;
onLeave: () => void ;
} ) {
const { leave, isConnected } = useRoom (roomId, {
displayName : "Player1" ,
onDisconnected : () => onLeave (),
});
const { players, allReady } = usePlayers ();
const { setRoomState, kick, isHost } = useHost ();
if (!isConnected) return <div > Connecting...</div > ;
const handleStart = ( ) => {
setRoomState ("playing" );
onStart ();
};
return (
<div >
<h2 > Room</h2 >
<ul >
{players.map((p) => (
<li key ={p.peerId} >
{p.displayName} {p.isHost && "(Host)"} {p.isReady ? "Ready" : "..."}
{isHost && !p.isSelf && (
<button onClick ={() => kick(p.peerId)}>Kick</button >
)}
</li >
))}
</ul >
{isHost && (
<button onClick ={handleStart} disabled ={!allReady} >
Start Game
</button >
)}
<button onClick ={() => { leave(); onLeave(); }}>Leave</button >
</div >
);
}
Game Screen #
tsx Copy
function GameScreen ({ roomId, onExit }: { roomId: string ; onExit: () => void } ) {
const { players, self } = usePlayers ();
return (
<Game >
<World >
{players.map((p, i) => (
<Actor
key ={p.peerId}
type ="primitive"
shape ="box"
color ={p.isSelf ? "blue " : "red "}
position ={[i * 2 , 1 , 0 ]}
/>
))}
<Actor
type ="primitive"
shape ="plane"
color ="#eee"
size ={20}
rotation ={[-Math.PI / 2 , 0 , 0 ]}
receiveShadow
/>
</World >
</Game >
);
}
Type Definitions #
Player #
ts Copy
interface Player {
peerId : string ;
displayName : string ;
isHost : boolean ;
isSelf : boolean ;
isReady : boolean ;
isConnected : boolean ;
metadata : Record <string , unknown >;
latencyMs : number ;
joinedAt : number ;
}
Field Type Description peerIdstringUnique identifier assigned by the transport layer displayNamestringHuman-readable name set via useRoom options isHostbooleanWhether this player is the current room host isSelfbooleanWhether this player is the local client isReadybooleanReady state toggled by the player isConnectedbooleantrue while the player has an active connectionmetadataRecord<string, unknown>Arbitrary data set via playerMetadata in useRoom latencyMsnumberRound-trip latency in milliseconds joinedAtnumberUnix timestamp (ms) when the player joined the room
Room #
ts Copy
interface Room {
id : string ;
name : string ;
hostId : string ;
playerCount : number ;
maxPlayers : number ;
gameMode ?: string ;
isPrivate : boolean ;
metadata : Record <string , unknown >;
createdAt : number ;
state : RoomState ;
}
Field Type Description idstringUnique room identifier namestringDisplay name set at creation hostIdstringPeer ID of the current host playerCountnumberCurrent number of connected players maxPlayersnumberMaximum allowed players gameModestring | undefinedOptional game mode label isPrivatebooleanWhether the room requires a password metadataRecord<string, unknown>Arbitrary room-level data createdAtnumberUnix timestamp (ms) when the room was created stateRoomState"waiting", "starting", "playing", or "finished"
RoomState #
ts Copy
type RoomState = "waiting" | "starting" | "playing" | "finished" ;
State Description "waiting"Room is open, players are joining and readying up "starting"Countdown or loading phase before gameplay begins "playing"Game is in progress "finished"Game has ended, room may be cleaned up or returned to waiting
Previous
Core Concepts
Next
Sync Modes