Structured Payloads
Why Structured Payloads
SendEvent<T>() copies a struct's bytes onto the wire, which is fast and needs no serialization code, but the payload is opaque.
The receiver has to know the exact layout, and nothing on the wire says what the bytes mean, so the two ends have to be built from the same headers with the same compiler settings.
RealtimeValue is the self-describing alternative.
It is a variant over exactly the types the Photon wire protocol understands, so a payload built from it carries its own type information and can be read by clients on other platforms.
The type lives in RealtimeValue.h in the RealtimeCore::Matchmaking namespace.
Use it for anything that crosses a version or platform, for anything with optional or variable-length parts and for payloads that are easier to describe as a map than as a struct. Keep raw bytes for the high-frequency, fixed-layout traffic where every byte counts.
RealtimeValue is also the value type of room and player custom properties, so everything on this page about building and reading values applies there too.
See Custom Properties.
Value Types
Type() reports which variant a value currently holds, as a ValueType.
A default-constructed RealtimeValue is Null.
ValueType |
C++ type |
|---|---|
Null |
std::monostate |
Bool |
bool |
Byte |
uint8_t |
Int16 |
int16_t |
Int32 |
int32_t |
Int64 |
int64_t |
Float |
float |
Double |
double |
String |
StringType |
ByteArray |
std::vector<uint8_t> |
Int16Array |
std::vector<int16_t> |
Int32Array |
std::vector<int32_t> |
Int64Array |
std::vector<int64_t> |
FloatArray |
std::vector<float> |
DoubleArray |
std::vector<double> |
BoolArray |
std::vector<bool> |
StringArray |
std::vector<StringType> |
Array |
RealtimeArray |
Map |
RealtimeMap |
Dictionary |
RealtimeDictionary |
Custom |
RealtimeCustom |
Building Values
RealtimeValue converts implicitly from any of its alternatives, so most values are written inline.
The alternative is picked by exact type, which means an integer literal lands in Int32 and a uint8_t lands in Byte; cast explicitly when the wire size matters.
A braced list of values builds a mixed RealtimeArray.
C++
RealtimeValue flag = true;
RealtimeValue smallNumber = static_cast<uint8_t>(7);
RealtimeValue number = 42; // Int32
RealtimeValue text = PHOTON_STR("hello");
RealtimeValue samples = std::vector<float>{1.0F, 2.0F};
RealtimeValue mixed = {RealtimeValue(1), RealtimeValue(PHOTON_STR("two")), RealtimeValue(3.0)};
Reading Values
Reading is deliberately explicit: nothing converts silently, so a payload built by another client cannot be misread as the wrong type.
| Member | Result |
|---|---|
Type() |
The ValueType currently held. |
IsNull() |
true for a Null value. |
Is<T>() |
true when the value holds exactly T. |
TryGet<T>() |
A const T*, or nullptr when the value does not hold T. |
Get<T>() |
A const T&; asserts when the value does not hold T, and falls back to a default-constructed T when assertions are off. |
TryGetNumber<T>() |
A std::optional<T> holding the number converted to T, empty when the conversion would lose information. |
TryGetCustom<T>() |
A std::optional<T> decoded from a custom value, empty on a code or payload mismatch. |
Visit(visitor) |
Calls the visitor with the alternative currently held. |
Raw() |
The underlying std::variant, for code that wants to work with it directly. |
TryGet<T>() checks and reads in one step and never throws.
C++
if (const RealtimeCore::Common::StringType* text = value.TryGet<RealtimeCore::Common::StringType>())
{
// Use *text.
}
TryGetNumber<T>() exists because a sender may legitimately pick a narrower type than the receiver expects, for example sending Byte where the game logic wants an int32_t.
It only converts when the conversion is lossless for every possible value of the stored type, so widening succeeds and narrowing does not.
Byte to Int32 works, Int32 to Int16 does not, Int32 to float does not because a 32-bit integer does not fit a float mantissa, and bool is never numeric.
Equality is strict about types: RealtimeValue(42) and RealtimeValue(int64_t{42}) are not equal, because they do not travel over the wire as the same type.
Containers
RealtimeArray is a plain std::vector<RealtimeValue> and its elements can each hold a different type.
RealtimeMap is an ordered key-value container whose keys are a RealtimeKey, a variant over uint8_t, int16_t, int32_t, int64_t, float, double and StringType.
Keys of different types may be mixed in one map, insertion order is preserved and Set() upserts.
| Member | Description |
|---|---|
Set(key, value) |
Inserts the entry, or replaces the value of an existing key. |
TryGet(key) |
A const RealtimeValue*, or nullptr when the key is absent. |
Remove(key) |
Removes the entry and reports whether it existed. |
Contains(key) |
Whether the key is present. |
Size(), IsEmpty() |
The entry count. |
KeyAt(index), ValueAt(index) |
Positional access for iterating in insertion order. |
RealtimeDictionary is the homogeneously-keyed variant of the same container.
Its key type is fixed at construction with a RealtimeDictionary::Key (Byte, Int16, Int32, Int64, Float, Double or String), KeyType() reports it and Set() returns false when a key of another type is offered.
The rest of the interface matches RealtimeMap.
All containers can be nested within each other.
C++
RealtimeMap scoreboard;
scoreboard.Set(RealtimeCore::Common::StringType(PHOTON_STR("round")), RealtimeValue(3));
scoreboard.Set(RealtimeCore::Common::StringType(PHOTON_STR("scores")), RealtimeValue(std::vector<int32_t>{10, 7}));
RealtimeDictionary byPlayer(RealtimeDictionary::Key::Int32);
if (!byPlayer.Set(1, RealtimeValue(PHOTON_STR("Alice"))))
{
// Rejected only when the key type does not match Key::Int32.
}
for (size_t i = 0; i < scoreboard.Size(); ++i)
{
const RealtimeKey& key = scoreboard.KeyAt(i);
const RealtimeValue& value = scoreboard.ValueAt(i);
}
Custom Types
A custom type is your own type with a wire code, carried inside a RealtimeValue as a RealtimeCustom (a code plus its serialized bytes).
Photon clients on other platforms register the same code, so a custom type is how a game-specific type such as a vector or a quaternion crosses SDK boundaries.
Any type satisfying the CustomValueType concept converts to a RealtimeValue implicitly.
The concept asks for three members:
| Member | Purpose |
|---|---|
static constexpr uint8_t TypeCode |
The wire code identifying the type. Valid codes are 1 through 254. |
void SerializeValue(std::vector<uint8_t>& out) const |
Appends the value's bytes to out. |
static std::optional<T> DeserializeValue(std::span<const uint8_t> bytes) |
Rebuilds the value, or returns std::nullopt on errors |
C++
struct Vec2
{
static constexpr uint8_t TypeCode = 7;
float X = 0.0F;
float Y = 0.0F;
void SerializeValue(std::vector<uint8_t>& out) const
{
const auto* xBytes = reinterpret_cast<const uint8_t*>(&X);
const auto* yBytes = reinterpret_cast<const uint8_t*>(&Y);
out.insert(out.end(), xBytes, xBytes + sizeof(float));
out.insert(out.end(), yBytes, yBytes + sizeof(float));
}
static std::optional<Vec2> DeserializeValue(std::span<const uint8_t> bytes)
{
if (bytes.size() != (2 * sizeof(float)))
{
return std::nullopt;
}
Vec2 result;
std::memcpy(&result.X, bytes.data(), sizeof(float));
std::memcpy(&result.Y, bytes.data() + sizeof(float), sizeof(float));
return result;
}
};
RealtimeValue value = Vec2{1.5F, -2.0F};
if (std::optional<Vec2> decoded = value.TryGetCustom<Vec2>())
{
// decoded->X and decoded->Y are back.
}
TryGetCustom<T>() checks the stored code against T::TypeCode before calling DeserializeValue(), so a value carrying a different custom type yields std::nullopt instead of garbage.
Custom values nest like any other type, so they can sit inside arrays, maps and dictionaries.
Codes 0 and 255 are reserved and a serialized custom payload is limited to 32767 bytes.
A value that violates either rule cannot be encoded, and sending it fails with ErrorCode::EventEncodeFailed.
Sending and Receiving
SendEvent(code, value, options) takes a RealtimeValue and subscribing with a const RealtimeValue& callback receives one, both are covered on the Custom Events page.
Three ErrorCode values report payload problems on OnError:
| Code | Meaning |
|---|---|
EventEncodeFailed |
An outgoing RealtimeValue could not be encoded, for example a custom code outside 1 to 254 or an oversized custom payload. SendEvent also returns false. |
EventDecodeFailed |
An incoming payload could not be decoded into a RealtimeValue, for example a multi-dimensional array. |
EventPayloadTypeMismatch |
A byte-span subscription received a structured payload. Subscribe with const RealtimeValue& to handle it. |