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

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>();

C

/* Writing: a map blob is built key by key, with the type in the tag byte. */
rt_blob properties = {0};

rt_begin(&properties);
rt_put_str(&properties, "mode", "deathmatch");
rt_put_i32(&properties, "round", 3);
rt_end(&properties);

/* Reading: walk the entries and pick out the keys of interest. */
rt_reader r     = rt_read(properties.data, properties.len);
int32_t   count = rt_get_i32(&r);

for (int32_t i = 0; i < count; ++i)
{
    int32_t     keyLen = 0;
    const char* key    = rt_get_str(&r, &keyLen);
    uint8_t     tag    = rt_get_u8(&r);

    if (tag == 3 && keyLen == 5 && memcmp(key, "round", 5) == 0)
    {
        int32_t round = rt_get_i32(&r);
        printf("round %d\n", round);
    }
    else
    {
        rt_skip_value(&r, tag);
    }
}

rt_free(&properties);

/* RealtimeMap and RealtimeValue do not cross the ABI as objects: a
   property set is a map blob, and the type of each value is a one-byte tag
   ahead of its payload. Exact integer widths matter for the same reason
   they do in C++, and the tag is what records them: a value written with
   rt_put_i32 (tag 3) does not read back through the tag-2 or tag-4 branch.
   Nested containers have no tag, so RealtimeArray, RealtimeMap,
   RealtimeDictionary and RealtimeCustom values cannot be used as
   properties from C, and the int16_t, int64_t, double and bool array types
   have no tag either. */

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

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>();
    }
}

C

/* Client A publishes the selected map. */
rt_blob properties = {0};

rt_begin(&properties);
rt_put_str(&properties, "map", "harbor");
rt_end(&properties);

realtime_room_set_properties(clientA, properties.data, properties.len);
rt_free(&properties);

/* Client B, in the same room, reads it once the change has been synchronized. */
int32_t        length = 0;
const uint8_t* props  = realtime_room_custom_properties(clientB, &length);

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

for (int32_t i = 0; i < count; ++i)
{
    int32_t     keyLen = 0;
    const char* key    = rt_get_str(&r, &keyLen);
    uint8_t     tag    = rt_get_u8(&r);

    if (tag == 7 && keyLen == 3 && memcmp(key, "map", 3) == 0)
    {
        int32_t     mapLen = 0;
        const char* map    = rt_get_str(&r, &mapLen);
        printf("map is %.*s\n", (int)mapLen, map);
    }
    else
    {
        rt_skip_value(&r, tag);
    }
}

/* There is no single-key setter over the C API:
   realtime_room_set_properties always takes a whole map, so a one-key
   change is a one-entry blob. realtime_room_remove_properties deletes keys
   and takes a string array blob built with rt_add_str instead of a map.
   Remember that the pointer from realtime_room_custom_properties is only
   valid until the next call on the handle. */

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

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>();
        }
    }
}

C

/* Publish the local player's loadout. */
rt_blob mine = {0};

rt_begin(&mine);
rt_put_i32(&mine, "loadout", 2);
rt_end(&mine);

realtime_set_player_properties(client, mine.data, mine.len);
rt_free(&mine);

/* Read the loadout of every player in the room. */
int32_t        length  = 0;
const uint8_t* players = realtime_room_players(client, &length);

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

for (int32_t i = 0; i < playerCount; ++i)
{
    int32_t number  = rt_get_i32(&r);
    int32_t nameLen = 0;
    (void)rt_get_str(&r, &nameLen);
    int32_t userIdLen = 0;
    (void)rt_get_str(&r, &userIdLen);

    /* customProperties of this player */
    int32_t entries = rt_get_i32(&r);
    for (int32_t j = 0; j < entries; ++j)
    {
        int32_t     keyLen = 0;
        const char* key    = rt_get_str(&r, &keyLen);
        uint8_t     tag    = rt_get_u8(&r);

        if (tag == 3 && keyLen == 7 && memcmp(key, "loadout", 7) == 0)
        {
            printf("#%d uses loadout %d\n", number, rt_get_i32(&r));
        }
        else
        {
            rt_skip_value(&r, tag);
        }
    }

    (void)rt_get_u8(&r); /* isInactive     */
    (void)rt_get_u8(&r); /* isMasterClient */
}

/* realtime_set_player_properties returns void, so it cannot report the
   false the C++ setters return when the client is not in a room. Check
   realtime_is_in_room first, and watch for PropertiesChangeFailed (114)
   for a server-side rejection. Other players' properties are nested inside
   the player array blob, so reading one key means walking every player's
   map; the layout is on the C API page. */

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

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);
}

C

/* Read the current value first: it becomes the expected value of the update. */
int32_t        length = 0;
const uint8_t* props  = realtime_room_custom_properties(client, &length);

rt_reader r       = rt_read(props, length);
int32_t   count   = rt_get_i32(&r);
int32_t   turn    = 0;
int       hasTurn = 0;

for (int32_t i = 0; i < count; ++i)
{
    int32_t     keyLen = 0;
    const char* key    = rt_get_str(&r, &keyLen);
    uint8_t     tag    = rt_get_u8(&r);

    if (tag == 3 && keyLen == 4 && memcmp(key, "turn", 4) == 0)
    {
        turn    = rt_get_i32(&r);
        hasTurn = 1;
    }
    else
    {
        rt_skip_value(&r, tag);
    }
}

if (hasTurn)
{
    rt_blob next     = {0};
    rt_blob expected = {0};

    rt_begin(&next);
    rt_put_i32(&next, "turn", turn + 1);
    rt_end(&next);

    rt_begin(&expected);
    rt_put_i32(&expected, "turn", turn);
    rt_end(&expected);

    /* Applied only if no other client changed "turn" in the meantime. */
    realtime_room_set_properties_expected(client,
                                          next.data, next.len,
                                          expected.data, expected.len);

    rt_free(&next);
    rt_free(&expected);
}

/* realtime_room_set_properties_expected is the compare-and-swap entry
   point and takes two separate map blobs. Build both before the call: the
   read above must finish first, because the expected map is built from a
   pointer into the scratch buffer that the next call on the handle would
   invalidate. A rejected update arrives as PropertiesChangeFailed (114). */

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

C++

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

C

/* RoomPropertiesChanged (102) carries only the keys that changed, as a map. */
if (e->type == RT_RoomPropertiesChanged && e->blobOffset >= 0)
{
    rt_reader r     = rt_read(blob + e->blobOffset, e->blobLength);
    int32_t   count = rt_get_i32(&r);

    for (int32_t i = 0; i < count; ++i)
    {
        int32_t     keyLen = 0;
        const char* key    = rt_get_str(&r, &keyLen);

        rt_skip_value(&r, rt_get_u8(&r));

        if (keyLen == 3 && memcmp(key, "map", 3) == 0)
        {
            /* Load the new map. */
        }
    }
}

/* PlayerPropertiesChanged (107) has the same map payload and puts the
   player number in origin. Both deliver only the changed keys, so a client
   that needs the full set reads it back with
   realtime_room_custom_properties or realtime_room_players after the
   batch. */
Back to top