Custom Events

Sending Events

Custom events are the low-latency messaging channel of Realtime Core. An event is an application-defined event code, a uint8_t you choose, plus a payload, fanned out by the server to the other players in the room.

SendEvent(code, data, options) sends raw bytes, either as a std::span<const uint8_t> or as a std::vector<uint8_t>. The template overload SendEvent<T>(code, value, options) accepts any trivially-copyable, non-pointer type and sends its bytes directly, which covers plain structs without any serialization code. A further overload takes a RealtimeValue and sends a self-describing payload, see Structured Payloads. All of them return bool, indicating whether the request could successfully be queued for sending to the server, false otherwise.

Define a plain struct and send it as-is:

C++

struct PositionUpdate
{
    float X;
    float Y;
    float Z;
};

constexpr uint8_t PositionEventCode = 1;

PositionUpdate update{12.5F, 0.0F, -3.2F};

EventOptions options;
options.Reliable = false; // The next update supersedes a lost one anyway.

client.SendEvent(PositionEventCode, update, options);

Event Options

Field Type Default Description
Reliable bool true Resends the event until acknowledged. Unreliable events may be lost but cost less.
Channel uint8_t 0 Sequencing channel. Ordering is guaranteed only within a channel.
TargetGroup ReceiverGroup Others Which players receive the event: Others, All or MasterClient. Only used when TargetPlayers and InterestGroup are not set.
TargetPlayers std::vector<int> empty Explicit recipient player numbers. Overrides TargetGroup and InterestGroup when non-empty.
InterestGroup uint8_t 0 Interest group the event is published to. 0 reaches everyone. Overrides TargetGroup when non-zero, only used when TargetPlayers is not set.
Caching EventCache DoNotCache Whether and how the event is cached for late joiners.
Encrypt bool false Encrypts the event payload.
CacheSliceIndex int 0 Cache slice the event addresses, for the slice-based caching operations.

Reliable = false suits high-frequency data where the next sample supersedes the last, such as positions where losing one packet is cheaper than waiting for its resend. Ordering is guaranteed per channel, so put independent streams on separate channels to keep a large reliable transfer from delaying time-critical events.

Targeting Recipients

The default fan-out is ReceiverGroup::Others, every player in the room except yourself. All includes yourself, which is useful when local and remote handling should share one code path, and MasterClient targets only the master client.

When TargetPlayers is non-empty it overrides the group entirely, and the event goes only to the listed player numbers.

Both targeting styles in use:

C++

constexpr uint8_t StartRequestCode = 2;
constexpr uint8_t WhisperCode      = 3;

// Ask the master client to start the match.
EventOptions toMaster;
toMaster.TargetGroup = ReceiverGroup::MasterClient;
client.SendEvent(StartRequestCode, static_cast<uint8_t>(1), toMaster);

// Reaches only players 2 and 5.
EventOptions toSome;
toSome.TargetPlayers = {2, 5};
client.SendEvent(WhisperCode, static_cast<uint8_t>(0), toSome);

In this example the data is casted to a narrower integer type to make sure it is sent as a single byte. If an integer literal is used without a type cast its type is deduced and could be sent as a 4 byte integer. To avoid this a typed variable can be used for the data.

Receiving Events

Incoming events are delivered to handlers registered with RealtimeClient::SubscribeEvent(). It returns a Subscription exactly like the Broadcaster callbacks do, so see Callbacks and Subscriptions for the lifetime rules and the RAII helpers.

The handler always receives the event code and the sender's player number, plus the payload in one of two forms.

Callback signature Payload
(uint8_t eventCode, int senderId, std::span<const uint8_t> data) The raw bytes of a byte-array payload.
(uint8_t eventCode, int senderId, const RealtimeValue& value) The decoded payload, whatever type it carries.

SubscribeEvent deduces the form from the callback you hand it, so the payload type is never spelled out explicitly unless the callback is generic.

The data span points into a decode buffer owned by the subscription and is only valid during the callback. Copy the payload out, for example with std::memcpy into your struct, before the handler returns if you keep it.

The receiving side mirrors the sending struct:

C++

RealtimeCore::Common::ScopedSubscription events = client.SubscribeEvent(
    [](uint8_t eventCode, int senderId, std::span<const uint8_t> data) {
        switch (eventCode)
        {
            case PositionEventCode:
                if (data.size() == sizeof(PositionUpdate))
                {
                    PositionUpdate update;
                    std::memcpy(&update, data.data(), sizeof(update));
                    // Move senderId's avatar to the received position.
                }
                break;
            default:
                break;
        }
    });

A byte-span subscription only accepts byte-array payloads. When an event carries a structured payload instead, the handler is skipped and the client broadcasts ErrorCode::EventPayloadTypeMismatch on OnError. Subscribe with const RealtimeValue& to receive those. A payload that cannot be decoded at all, a multi-dimensional array for example, raises ErrorCode::EventDecodeFailed.

Disambiguating Generic Callbacks

A generic lambda, or any other callable that accepts both payload forms, is ambiguous and fails to compile with a message that says so. Name the payload form explicitly in that case:

C++

auto bytes  = client.SubscribeEvent<std::span<const uint8_t>>(handler);
auto values = client.SubscribeEvent<const RealtimeValue&>(handler);

Filtering by Event Code

SubscribeEvent has three raw overloads that filter the subscription by event code.

Overload Codes delivered
SubscribeEvent(callback) Every event code, 0 through 255.
SubscribeEvent(eventCode, callback) Exactly eventCode.
SubscribeEvent(firstEventCode, lastEventCode, callback) The inclusive range from firstEventCode to lastEventCode.

Ranges are convenient when one subsystem owns a contiguous block of codes:

C++

constexpr uint8_t FirstInventoryCode = 20;
constexpr uint8_t LastInventoryCode  = 29;

RealtimeCore::Common::ScopedSubscription inventory = client.SubscribeEvent(
    FirstInventoryCode, LastInventoryCode,
    [](uint8_t eventCode, int senderId, std::span<const uint8_t> data) {
        // Only inventory codes arrive here.
    });

Typed Subscriptions

Naming your payload struct as the explicit template argument, SubscribeEvent<T>(eventCode, callback), subscribes by payload type instead of receiving the raw wire form. T has to be trivially copyable and default constructible, which covers the plain structs SendEvent<T>() sends, and the callback takes (int senderId, const T& value).

C++

RealtimeCore::Common::ScopedSubscription positions = client.SubscribeEvent<PositionUpdate>(
    PositionEventCode,
    [](int senderId, const PositionUpdate& update) {
        // Move senderId's avatar to the received position.
    });

A payload whose size differs from sizeof(T) never reaches the callback. The client logs the mismatch and broadcasts ErrorCode::EventSizeMismatch on OnError, once for every typed subscription that rejected the payload.

Typed subscriptions are additive: a raw SubscribeEvent handler on the same code still sees the same event, which is what a logging or fallback path needs.

Layout Compatibility

SendEvent<T>() and SubscribeEvent<T>() copy the object representation of T over the wire, with no padding, alignment or endianness normalization. Both ends have to agree on that layout, so prefer a structured payload for events exchanged between different compilers, architectures or SDKs.

Structured Payloads

Byte payloads are the cheapest option, but they carry no type information, so both ends must agree on a layout out of band. RealtimeValue is the self-describing alternative: a variant over everything the Photon wire protocol can carry, from scalars and typed arrays to nested arrays, maps, dictionaries and your own custom types.

C++

constexpr uint8_t ChatEventCode = 10;

RealtimeMap payload;
payload.Set(RealtimeCore::Common::StringType(PHOTON_STR("text")), RealtimeValue(PHOTON_STR("hello")));
payload.Set(RealtimeCore::Common::StringType(PHOTON_STR("channel")), RealtimeValue(3));

client.SendEvent(ChatEventCode, payload);

RealtimeCore::Common::ScopedSubscription chat = client.SubscribeEvent(
    ChatEventCode,
    [](uint8_t eventCode, int senderId, const RealtimeValue& value) {
        const RealtimeMap* message = value.TryGet<RealtimeMap>();
        if (message == nullptr)
        {
            return;
        }
        // Read the entries with message->TryGet(key).
    });

The Structured Payloads page covers the value types, the containers, the accessors and how to plug your own types in.

Event Caching

Cached events are stored by the server and replayed, in their original order, to every player who joins later. This is the built-in mechanism for late-joiner state: a new player receives the cached events as if it had been present. EventOptions.Caching selects one of the EventCache operations, from adding a single event to the room cache up to slice-based cache management, the dedicated Event Caching page covers the cache structure, removal filters, cache slices and sizing guidance.

Interest Groups

Interest groups partition the event traffic inside a room: every event is published to exactly one group, and players receive only the groups they subscribed to. Group 0 is the always-on broadcast group that every player receives.

ChangeGroups(remove, add) manages the local player's subscriptions, taking the group numbers to leave and to join. On the sending side, EventOptions.InterestGroup selects the group the event is published to.

A zone-based interest scheme keeps events local to a map area:

C++

constexpr uint8_t ForestZone    = 7;
constexpr uint8_t FootstepsCode = 5;

struct Footsteps
{
    float X;
    float Y;
};

// Start receiving events published to the forest zone.
client.ChangeGroups({}, {ForestZone});

// Only players subscribed to the zone receive this.
Footsteps steps{4.0F, 9.5F};

EventOptions options;
options.InterestGroup = ForestZone;
options.Reliable      = false;
client.SendEvent(FootstepsCode, steps, options);
Back to top