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
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
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 #

OptionTypeDefaultDescription
autoRefreshbooleantrueAutomatically poll for room list updates
filterLobbyFilterFilter the room list on the server side

filter #

FieldTypeDescription
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
const { rooms } = useLobby({
  autoRefresh: true,
  filter: { gameMode: "deathmatch", hasPassword: false },
});

Return Value #

PropertyTypeDescription
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

useRoom #

useRoom manages the connection lifecycle for a single room. It handles joining, leaving, reconnection, transport selection, and host migration.

tsx
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 #

OptionTypeDefaultDescription
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 defaultsCustom 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 #

PropertyTypeDescription
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

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
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 #

PropertyTypeDescription
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
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 #

PropertyTypeDescription
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

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
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
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
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
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
interface Player {
  peerId: string;
  displayName: string;
  isHost: boolean;
  isSelf: boolean;
  isReady: boolean;
  isConnected: boolean;
  metadata: Record<string, unknown>;
  latencyMs: number;
  joinedAt: number;
}
FieldTypeDescription
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 connection
metadataRecord<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
interface Room {
  id: string;
  name: string;
  hostId: string;
  playerCount: number;
  maxPlayers: number;
  gameMode?: string;
  isPrivate: boolean;
  metadata: Record<string, unknown>;
  createdAt: number;
  state: RoomState;
}
FieldTypeDescription
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
type RoomState = "waiting" | "starting" | "playing" | "finished";
StateDescription
"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