Custom Properties

Property Values and Types

Custom properties are typed key/value pairs attached to a room or a player, stored on the server and synchronized to every client in the room. A set of properties is a RealtimeMap, an insertion-ordered container of RealtimeValue entries. RealtimeValue covers every type the Photon wire protocol carries, so a property can hold anything an event payload can.

Type Description
bool Boolean flag.
uint8_t 8-bit unsigned integer, the protocol's byte type.
int16_t, int32_t, int64_t Signed integers of exact width.
float, double Floating-point values.
StringType UTF-8 string. See Strings.
std::vector<uint8_t> Raw byte array.
std::vector<int16_t>, std::vector<int32_t>, std::vector<int64_t> Arrays of signed integers.
std::vector<float>, std::vector<double> Arrays of floating-point values.
std::vector<bool> Array of booleans.
std::vector<StringType> Array of strings.
RealtimeArray Heterogeneous list of nested values.
RealtimeMap, RealtimeDictionary Nested key-value containers.
RealtimeCustom A serialized application type. See Structured Payloads.

A default-constructed RealtimeValue is Null, which is also a value a property may carry. For the full member API of RealtimeValue and its containers, see Data Types.

Integers must be stored with their exact width, so write static_cast<int32_t>(value) rather than relying on a plain literal: a value stored as one width does not read back as another. The 8-bit alternative is uint8_t, covering 0 through 255; there is no signed 8-bit type, so an int8_t needs an explicit cast to one of the listed widths.

RealtimeMap keys are a RealtimeKey and may be numeric, but room and player properties are string-keyed on the wire. SetProperties() and SetPlayerProperties() reject a map containing any non-string key, returning false without sending anything.

Reading Property Values

RealtimeMap::Set(key, value) writes an entry and TryGet(key) reads one back, returning a const RealtimeValue* that is null when the key is absent. Contains(key), Remove(key), Size() and IsEmpty() complete the container, and KeyAt(index)/ValueAt(index) walk the entries in insertion order.

On the value itself, Get<T>() returns a const T& and asserts on a type mismatch, TryGet<T>() returns a pointer that is null on mismatch, Is<T>() tests the stored type and Visit(visitor) dispatches on whatever is held. TryGetNumber<T>() is the tolerant numeric read: a std::optional<T> filled only when the stored number converts to T without loss.

Writing and reading back an integer and a string:

C++

RealtimeMap properties;
properties.Set(PHOTON_STR("mode"), PHOTON_STR("deathmatch"));
properties.Set(PHOTON_STR("round"), static_cast<int32_t>(3));

RealtimeCore::Common::StringType mode  = properties.TryGet(PHOTON_STR("mode"))->Get<RealtimeCore::Common::StringType>();
int32_t                          round = properties.TryGet(PHOTON_STR("round"))->Get<int32_t>();

Both reads dereference TryGet() directly because both keys were just written. For values that may be absent, check the pointer first, as the room and player examples below do.

Room Properties

Seed room properties at creation time via CreateRoomOptions.CustomProperties. Once in the room, change them through the MutableRoomView: SetProperties(map) updates several keys at once, SetProperty<T>(key, value) sets a single key and RemoveProperties(keys) deletes keys.

Read them back with MutableRoomView::GetCustomProperties(). The server synchronizes every change to all clients in the room, so a value written by one client appears in every other client's view once their Service() calls deliver the update.

One client writes, the others read the updated value:

C++

// Client A publishes the selected map.
std::optional<MutableRoomView> room = clientA.GetCurrentRoom();
if (room)
{
    room->SetProperty(PHOTON_STR("map"), RealtimeCore::Common::StringType(PHOTON_STR("harbor")));
}

// Client B, in the same room, reads it once the change has been synchronized.
std::optional<MutableRoomView> view = clientB.GetCurrentRoom();
if (view)
{
    const RealtimeMap&   properties = view->GetCustomProperties();
    const RealtimeValue* value      = properties.TryGet(PHOTON_STR("map"));
    if (value != nullptr)
    {
        RealtimeCore::Common::StringType map = value->Get<RealtimeCore::Common::StringType>();
    }
}

Player Properties

Player properties are set through the client rather than through the room view: SetPlayerProperties(map), SetPlayerProperty<T>(key, value) and RemovePlayerProperties(keys) modify the local player's properties. All three return false when the client is not in a room.

Other players' properties arrive through their PlayerView: fetch the views with MutableRoomView::GetPlayers() and read each player's CustomProperties member.

Publish a per-player choice and read everyone else's:

C++

// Publish the local player's loadout.
client.SetPlayerProperty(PHOTON_STR("loadout"), static_cast<int32_t>(2));

// Read the loadout of every player in the room.
std::optional<MutableRoomView> room = client.GetCurrentRoom();
if (room)
{
    for (const PlayerView& player : room->GetPlayers())
    {
        const RealtimeValue* value = player.CustomProperties.TryGet(PHOTON_STR("loadout"));
        if (value != nullptr)
        {
            int32_t loadout = value->Get<int32_t>();
        }
    }
}

Compare-and-Swap Updates

SetProperties(newProps, expectedProps) is the compare-and-swap overload: the server applies the update only if the current server-side values still match expectedProps. This makes concurrent updates race-free and is the right tool for turn counters, item claims and every other "only one client may win" situation.

An atomic increment reads the current value and makes it the expected value of the update:

C++

std::optional<MutableRoomView> room = client.GetCurrentRoom();
if (room)
{
    const RealtimeMap& properties = room->GetCustomProperties();
    int32_t            turn       = properties.TryGet(PHOTON_STR("turn"))->Get<int32_t>();

    RealtimeMap next;
    RealtimeMap expected;
    next.Set(PHOTON_STR("turn"), static_cast<int32_t>(turn + 1));
    expected.Set(PHOTON_STR("turn"), turn);

    // Applied only if no other client changed "turn" in the meantime.
    room->SetProperties(next, expected);
}

When the expected values no longer match, the server rejects the whole update and OnPropertiesChangeFailed fires on the client that attempted it. Re-read the current values and retry if the update still applies.

Lobby-Visible Properties

By default custom properties are only visible to the clients inside the room. The LobbyProperties list, set at creation via CreateRoomOptions.LobbyProperties or later via MutableRoomView::SetLobbyProperties, selects the keys that are also exposed to the lobby, where they appear in room listings and drive matchmaking filters.

Keep the lobby-visible set small, because these values are broadcast to lobby clients with every room-list update. For query-style filtering on lobby-visible values, see SQL Lobby Matchmaking.

Property Change Callbacks

Property changes are announced to every client in the room: OnRoomPropertiesChanged(const RealtimeMap&) delivers the changed room keys and OnPlayerPropertiesChanged(int playerNumber, const RealtimeMap&) delivers the changed keys of one player. Both carry only the keys that changed, not the full property set.

Reacting to one specific key:

C++

RealtimeCore::Common::ScopedSubscription mapChanged = client.OnRoomPropertiesChanged.Subscribe(
    [](const RealtimeMap& changed) {
        if (changed.Contains(PHOTON_STR("map")))
        {
            // Load the new map.
        }
    });
Back to top