Rooms and Players

Creating a Room

Creating a room creates it on the server and enters it. On success the client is already inside the room as its first player, which also makes it the master client. In C++ and JavaScript the operation yields a room object that reads and modifies the room, as described below; the C API reads and modifies the current room through functions on the client handle.

A typical creation sets a name, a player limit and the visibility:

C++
C
JavaScript

C++

Task<Result<void>> CreateMatch(RealtimeClient& client)
{
    CreateRoomOptions options;
    options.MaxPlayers = 4;
    options.IsVisible  = false; // Not listed in the lobby; players join by name.

    Result<Room, CreateRoomError> result = co_await client.CreateRoom(PHOTON_STR("match-42"), options);
    if (result.IsErr())
    {
        co_return Result<void>::Err(result.GetError());
    }
    co_return Result<void>::Ok();
}

C

realtime_create_room(client, "match-42",
                     4,        /* maxPlayers                        */
                     0,        /* isVisible: not listed in the lobby */
                     1,        /* isOpen                            */
                     0, 0,     /* playerTtlMs, emptyRoomTtlMs       */
                     NULL,     /* plugins                           */
                     NULL, 0); /* no initial custom properties      */

/* The outcome arrives as CreateRoomResult. */
if (e->type == RealtimeEventType_CreateRoomResult && e->mode != 0)
{
    /* e->mode is a RealtimeCreateRoomError, e.g. RealtimeCreateRoomError_RoomAlreadyExists. */
}

/* realtime_create_room covers eight of the creation options: the name,
   MaxPlayers, IsVisible, IsOpen, PlayerTtlMs, EmptyRoomTtlMs, Plugins as a
   semicolon-separated list and CustomProperties as a map blob. The
   remaining fields are not on the ABI: LobbyName, Lobby,
   SuppressRoomEvents, PublishUserId and DirectMessaging keep their
   defaults for a room a C client creates, while LobbyProperties and
   ExpectedUsers can be applied after the join with
   realtime_room_set_lobby_properties and realtime_room_set_expected_users. */

JavaScript

const room = await client.createRoom('match-42', {
    maxPlayers: 4,
    isVisible: false, // Not listed in the lobby; players join by name.
});

A failed create rejects the promise with a RealtimeError.

Room names are unique within a region and application. Creating a name that already exists fails with the room-already-exists error code (CreateRoomError::RoomAlreadyExists in C++, RealtimeCreateRoomError_RoomAlreadyExists in C, 'roomAlreadyExists' in JavaScript). Use join-or-create when either outcome is acceptable, as described under Joining a Room.

Room Creation Options

C++ JavaScript Default Description
IsVisible isVisible true Lists the room in the lobby and makes it eligible for random matchmaking.
IsOpen isOpen true Allows players to join. A closed room rejects joins but keeps its current players.
MaxPlayers maxPlayers 0 Maximum number of players, from 0 to 255. 0 means no limit.
CustomProperties customProperties empty Initial custom properties of the room. See Custom Properties.
LobbyProperties lobbyProperties empty Keys of the custom properties that are also visible in the lobby.
LobbyName lobbyName empty Name of the lobby the room is listed in.
Lobby lobby the default type Type of that lobby. See Lobbies and Matchmaking.
PlayerTtlMs playerTtlMs 0 How long a leaving player stays inactive in the room and may rejoin, in milliseconds.
EmptyRoomTtlMs emptyRoomTtlMs 0 How long the room survives without any players, in milliseconds.
SuppressRoomEvents suppressRoomEvents false Stops the server from sending join and leave events for this room.
PublishUserId publishUserId false Makes each player's user id visible to the other players in the room.
DirectMessaging directMessaging none Peer-to-peer topology of the room. See Direct Messaging. The web build has no peer-to-peer transport and offers only the relayed modes.
Plugins plugins empty Names of the server plugins to run for this room.
ExpectedUsers expectedUsers empty User ids to reserve slots for.

A few options deserve extra attention because they shape the room's whole life. The player time-to-live decides whether leaving players may rejoin their slot, the empty-room time-to-live lets the room survive being empty for a while, and visibility and openness separate matchmaking visibility from joinability. Suppressing room events and publishing user ids control what the server tells the players about each other.

The lobby options (the lobby type, the lobby name and the lobby properties) control where and how the room appears in matchmaking; they are covered in Lobbies and Matchmaking and SQL Lobby Matchmaking. The direct-messaging option enables peer-to-peer messaging inside the room and is covered in Direct Messaging.

Joining a Room

Joining by name enters a room you already know, for example from a room listing or an invite (JoinRoom in C++, joinRoom() in JavaScript). Join-or-create joins the room if it exists and creates it otherwise, in one atomic operation (JoinOrCreateRoom in C++, joinOrCreateRoom() in JavaScript). Both yield the joined room, the same way creating a room does.

C++ JavaScript Default Description
Rejoin rejoin false Rejoins as a returning, inactive player instead of joining as a new one.
ExpectedUsers expectedUsers empty User ids to reserve slots for.

Join-or-create sends a single expected-users list whether it joins or creates, so the expected users of the join options and the create options are merged and every user named in either gets a reserved slot. It cannot rejoin: setting the rejoin option fails with the invalid-argument error code (JoinRoomError::InvalidArgument in C++, 'invalidArgument' in JavaScript) before anything is sent. To return to a room as an inactive player, join it by name with the rejoin option set, as described under Leaving and Rejoining.

Joining a friend's room works without coordinating who arrives first:

C++
C
JavaScript

C++

Task<Result<Room>> JoinPartyRoom(RealtimeClient& client)
{
    CreateRoomOptions createOptions;
    createOptions.MaxPlayers = 4;

    // Joins "party-of-anna" if it already exists, creates it otherwise.
    co_return co_await client.JoinOrCreateRoom(PHOTON_STR("party-of-anna"), createOptions);
}

C

/* Joins "party-of-anna" if it already exists, creates it otherwise. */
realtime_join_or_create_room(client, "party-of-anna",
                             4,        /* maxPlayers, only used on create */
                             1, 1,     /* isVisible, isOpen               */
                             0, 0,     /* playerTtlMs, emptyRoomTtlMs     */
                             NULL,     /* plugins                         */
                             NULL, 0); /* no initial custom properties    */

/* The creation parameters are only applied when the room does not exist
   yet, exactly as with the C++ overload. The result arrives as
   JoinOrCreateRoomResult, and there is no JoinRoomOptions:
   realtime_join_room takes the rejoin flag directly, and the join-time
   ExpectedUsers has no ABI equivalent. */

JavaScript

// Joins "party-of-anna" if it already exists, creates it otherwise.
const room = await client.joinOrCreateRoom('party-of-anna', { maxPlayers: 4 });

Joining can fail for expected reasons, each with its own error code: no room has that name, the player limit is reached, the room is closed or the client is already in a room. In C++ these are RoomNotFound, RoomFull, RoomClosed and AlreadyJoined in the returned Result. In JavaScript the promise rejects with a RealtimeError whose code is 'roomNotFound', 'roomFull', 'roomClosed' or 'alreadyJoined'. The full error list is in Errors and Disconnects.

The Current Room

The client gives access to the room it is in, and tells when it is in none: in C++ GetCurrentRoom() returns an empty optional, in JavaScript client.room is null and in C realtime_has_current_room() returns 0. Checking for the room replaces any separate state query.

In C++, Room is a copyable value type: its getters read a snapshot of the room, while its mutation methods act on the live room through the client. A Room taken in a previous room becomes inert after leaving: its getters keep returning the old snapshot, but its mutations fail and return false. Fetch a fresh Room from GetCurrentRoom() when you need one, rather than storing it long-term.

In JavaScript, Room is a live view: every getter reads the current state, including from inside an event handler, and the same object is returned for as long as the client stays in the room. After leaving, its isActive turns false and its getters and mutation methods throw an Error.

C++ C JavaScript Description
GetName() realtime_room_name() name The room name.
GetPlayerCount() realtime_room_player_count() playerCount Number of players in the room.
GetMaxPlayers() realtime_room_max_players() maxPlayers Player limit, 0 when unlimited.
IsOpen() realtime_room_is_open() isOpen Whether players can join.
IsVisible() realtime_room_is_visible() isVisible Whether the room is listed in the lobby.
GetCustomProperties() realtime_room_custom_properties() customProperties The room's custom properties.
GetPlayers() realtime_room_players() players All players in the room, including the local one.
GetMasterClientId() realtime_room_master_client_id() masterClientNumber Player number of the current master client.
IsMasterClient() realtime_room_is_master_client() isMasterClient Whether the local player is the master client.
IsMasterClient(int) none none Whether the given player number is the master client.
none none getPlayer(playerNumber) The player with this number, or undefined.
none none isActive Whether the client is still in this room.
GetPlayerTtlMs() realtime_room_player_ttl_ms() playerTtlMs Inactive-player timeout in milliseconds.
GetEmptyRoomTtlMs() realtime_room_empty_room_ttl_ms() emptyRoomTtlMs Empty-room timeout in milliseconds.
GetPublishUserId() realtime_room_publish_user_id() publishUserId Whether user ids are visible to the other players.
GetDirectMode() realtime_room_direct_mode() directMode The room's peer-to-peer topology.
GetExpectedUsers() realtime_room_expected_users() expectedUsers User ids with reserved slots.
GetLobbyProperties() realtime_room_lobby_properties() lobbyProperties Property keys visible in the lobby.
GetSuppressRoomEvents() realtime_room_suppress_room_events() suppressRoomEvents Whether join and leave events are suppressed.
GetPlugins() realtime_room_plugins() plugins Server plugins active for the room.

Modifying the Room

The mutation methods send the change to the server and return whether it was sent: SetOpen, SetVisible, SetMaxPlayers, SetExpectedUsers, SetLobbyProperties, SetMasterClient and the property setters in C++, and the same names in camelCase in JavaScript. In C++ they return false when the Room is no longer attached to a live room, for example after leaving. In JavaScript they throw an Error in that case.

When the match starts, close and hide the room so matchmaking stops routing players into it:

C++
C
JavaScript

C++

std::optional<Room> room = client.GetCurrentRoom();
if (room && room->IsMasterClient())
{
    room->SetOpen(false);
    room->SetVisible(false);
}

C

if (realtime_has_current_room(client) && realtime_room_is_master_client(client))
{
    realtime_room_set_open(client, 0);
    realtime_room_set_visible(client, 0);
}

/* There is no Room object over the C API: every getter and mutation in the
   two tables on this page is a realtime_room_* function on the client
   handle, and realtime_has_current_room replaces the empty std::optional.
   The setters return non-zero when the change was sent and 0 when the
   client is not in a room, mirroring the bool the C++ mutations return. */

JavaScript

const room = client.room;
if (room?.isMasterClient) {
    room.setOpen(false);
    room.setVisible(false);
}

Changing the room's custom properties, including single keys and compare-and-swap updates, is covered in Custom Properties.

Players in the Room

The room lists one entry per player. Each entry carries the player number, the name, the user id, the custom properties and whether the player is inactive or the master client (Number, Name, UserId, CustomProperties, IsInactive and IsMasterClient in C++; number, name, userId, customProperties, isInactive and isMasterClient in JavaScript). Other players' user ids are only filled when the room was created with the publish-user-id option. In C++ a Player is a plain value struct; in JavaScript each entry is a snapshot taken when it is read.

The client also returns your own player (GetLocalPlayer() in C++, localPlayer in JavaScript). Player numbers are positive and unique within the room, which makes the number the standard way to address a player in events and callbacks.

Listing the roster is a straight iteration:

C++
C
JavaScript

C++

std::optional<Room> room = client.GetCurrentRoom();
if (room)
{
    for (const Player& player : room->GetPlayers())
    {
        std::string name(player.Name.begin(), player.Name.end());
        std::printf("#%d %s%s\n", player.Number, name.c_str(), player.IsMasterClient ? " (master)" : "");
    }
}

C

/* The rt_* blob helpers, copied from "Blob Helpers" on the C API page. */
#include "photon_blob.h"

/* Player array blob: [int32 count] then, per player,
   [int32 number][string name][string userId][map props][u8 inactive][u8 master] */
int32_t        length  = 0;
const uint8_t* players = realtime_room_players(client, &length);

rt_reader r     = rt_read(players, length);
int32_t   count = rt_get_i32(&r);

for (int32_t i = 0; i < count; ++i)
{
    int32_t     number    = rt_get_i32(&r);
    int32_t     nameLen   = 0;
    const char* name      = rt_get_str(&r, &nameLen);
    int32_t     userIdLen = 0;
    (void)rt_get_str(&r, &userIdLen);

    rt_skip_map(&r); /* customProperties */

    (void)rt_get_u8(&r);              /* isInactive     */
    uint8_t isMaster = rt_get_u8(&r); /* isMasterClient */

    printf("#%d %.*s%s\n", number, (int)nameLen, name, isMaster ? " (master)" : "");
}

/* Read the Blob Before the Next Call: realtime_room_players writes into
   the client's shared scratch buffer, so calling any other realtime_*
   function on the handle, including realtime_service, invalidates the
   pointer. Finish the iteration, or copy what you keep, before the next
   call. See Return-Pointer Lifetime on the C API page. */

JavaScript

const room = client.room;
if (room) {
    for (const player of room.players) {
        console.log(`#${player.number} ${player.name}${player.isMasterClient ? ' (master)' : ''}`);
    }
}

The Master Client

Every room has exactly one master client. There is no authoritative server logic in a plain Realtime room, so the master client is the designated peer for decisions that must run exactly once per room, starting the match, spawning pickups or scoring a round.

The room tells whether the local player holds the role, returns the master client's player number and transfers the role to another player: IsMasterClient(), GetMasterClientId() and SetMasterClient(playerNumber) in C++, isMasterClient, masterClientNumber and setMasterClient(playerNumber) in JavaScript.

When the master client leaves, the server promotes another player automatically, so the role is never vacant. Every client is notified through the master-client-changed callback (OnMasterClientChanged in C++, the masterClientChanged event in JavaScript); use it to hand over any master-only responsibilities.

Leaving and Rejoining

Leaving the current room takes a flag that says whether the player will come back (LeaveRoom(willComeBack) in C++, leaveRoom(willComeBack) in JavaScript). With the flag set and a room created with a non-zero player time-to-live, the player is not removed but stays in the room as inactive until the time-to-live expires.

To resume, join the room by name with the rejoin option set before the time-to-live expires. The player returns to the same slot with the same player number, and the other players see the inactive flag flip back instead of a new join.

Leaving with the intent to return and rejoining take one option each:

C++
C
JavaScript

C++

Task<Result<void>> StepAway(RealtimeClient& client)
{
    // The room must have been created with a non-zero PlayerTtlMs.
    co_return co_await client.LeaveRoom(/*willComeBack=*/true);
}

Task<Result<Room>> Return(RealtimeClient& client)
{
    JoinRoomOptions options;
    options.Rejoin = true;
    co_return co_await client.JoinRoom(PHOTON_STR("match-42"), options);
}

C

/* Step away: the room must have been created with a non-zero playerTtlMs. */
realtime_leave_room(client, 1); /* willComeBack */

/* Return: the rejoin flag is the second parameter of realtime_join_room. */
realtime_join_room(client, "match-42", 1); /* rejoin */

/* The results arrive as LeaveRoomResult and JoinRoomResult. Rejoining
   after the TTL has expired fails like any other join, with RoomNotFound
   or RoomFull in mode. */

JavaScript

// The room must have been created with a non-zero playerTtlMs.
await client.leaveRoom(true); // willComeBack

// Later, before the time-to-live expires:
const room = await client.joinRoom('match-42', { rejoin: true });

Room and Player Callbacks

C++ C JavaScript Fires When
OnRoomJoined() RealtimeEventType_RoomJoined roomJoined The local client has entered a room, whether by create, join or rejoin.
OnRoomLeft() RealtimeEventType_RoomLeft roomLeft The local client has left the room or was removed from it.
OnPlayerJoined(player) RealtimeEventType_PlayerJoined playerJoined with { player } A remote player joins the current room.
OnPlayerLeft(playerNumber, isInactive) RealtimeEventType_PlayerLeft playerLeft with { playerNumber, isInactive } A player leaves the current room or becomes inactive.
OnMasterClientChanged(newId, oldId) RealtimeEventType_MasterClientChanged masterClientChanged with { newMasterNumber, oldMasterNumber } The master client role moves to another player.
OnRoomPropertiesChanged(changes) RealtimeEventType_RoomPropertiesChanged roomPropertiesChanged with { changed } The room's custom properties change, delivering the changed keys.
OnPlayerPropertiesChanged(playerNumber, changes) RealtimeEventType_PlayerPropertiesChanged playerPropertiesChanged with { playerNumber, changed } A player's custom properties change, delivering the changed keys.

The player-left callback distinguishes a real leave from a time-to-live suspension through its inactive flag. true means the player may still rejoin the reserved slot; false means the player is gone and the slot is released.

Last updated on

Back to top