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