Direct Messaging

Direct messaging opens peer-to-peer UDP connections between the clients in a room, so latency-critical payloads travel directly between players instead of taking the round trip through the server. Typical uses are position streams and voice data.

Advanced Feature

Direct messaging is only available in Realtime Core and is not used by Fusion, Quantum or any other Photon product. It is an advanced option for very specific bespoke applications built directly on Realtime Core. Direct connections expose the IP addresses of players to each other and are therefore considered a security risk; avoid them unless your use case specifically requires them.

How Direct Connections Are Established

The direct links are plain UDP sockets between the peers, established with NAT punch-through. Each client queries STUN servers to discover its own public, server-reflexive address, shares that address with the other clients through the Photon server and then both sides punch a hole through their NATs by sending UDP packets to each other's reflexive address. The client uses a built-in list of public STUN servers to obtain the reflexive addresses; this list is currently not configurable. The mechanism is barebones and UDP only. Punch-through does not succeed for every NAT and firewall combination, which is why the relay fallback exists.

Enabling Direct Messaging

Direct messaging is enabled per room at creation time via CreateRoomOptions.DirectMessaging.

The DirectMode value defines which players establishes a direct connection to which other player:

Value Meaning
AllToOthers Each client establishes a direct connection with every other client inside the room.
AllToAll Each client establishes a direct connection with every client inside the room, including itself.
MasterToOthers The master client establishes a direct connection with every other client inside the room. All other clients only establish a direct connection with the master client but not with each other.
MasterToAll The master client establishes a direct connection with every client inside the room, including itself. All other clients only establish a direct connection with the master client but not with each other.

Everything on this page assumes a room created with a direct mode:

C++

Task<Result<MutableRoomView>> CreateP2PRoom(RealtimeClient& client)
{
    CreateRoomOptions options;
    options.MaxPlayers      = 8;
    options.DirectMessaging = DirectMode::AllToAll;
    co_return co_await client.CreateRoom(PHOTON_STR("p2p-match"), options);
}

Sending Direct Messages

SendDirect(data, options) sends a std::span<const uint8_t> of raw bytes, and the template SendDirect<T>(value, options) sends any trivially-copyable struct byte-for-byte. Both require the client to be in a room with direct messaging enabled.

The return value is the number of target players for which the message could successfully be handed off for sending. A positive count does not guarantee that the message will be received; 0 means there was no eligible target and -1 means the client is not in a room. If FallbackRelay is set to true then this number includes the recipients for which the fallback relay has been used.

Field Type Default Description
TargetPlayers std::vector<int> empty Explicit recipient player numbers. Overrides TargetGroup when non-empty.
TargetGroup ReceiverGroup Others Which players receive the message: Others, All or MasterClient.
FallbackRelay bool false Routes the message through the server when no direct link to a target exists.

Streaming a position struct to every other player each frame:

C++

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

void BroadcastPosition(RealtimeClient& client, const PositionUpdate& position)
{
    DirectMessageOptions options;
    options.FallbackRelay = true; // Use the server if no P2P link exists

    client.SendDirect(position, options);
}

Receiving Direct Messages

Incoming messages arrive through OnDirectMessage(int senderId, std::span<const uint8_t> data, bool isRelay). isRelay reports whether the message arrived over the direct link (false) or took the server relay (true), and the span is only valid during the callback, so copy the payload out if you keep it.

A receive handler that mirrors the position stream above:

C++

RealtimeCore::Common::ScopedSubscription directMessages = client.OnDirectMessage.Subscribe(
    [](int senderId, std::span<const uint8_t> data, bool isRelay) {
        if (data.size() != sizeof(PositionUpdate))
        {
            return;
        }

        PositionUpdate position;
        std::memcpy(&position, data.data(), sizeof(position));

        if (isRelay)
        {
            // Took the server detour: expect higher latency for this sender.
        }
    });

Direct Connection Callbacks

The per-peer link status is reported through two callbacks: OnDirectConnectionEstablished(remotePlayerId) fires when a direct connection to a player comes up, and OnDirectConnectionFailed(remotePlayerId) fires when an attempt does not succeed.

Tracking which peers currently have a live P2P link:

C++

std::set<int> directPeers;

RealtimeCore::Common::SubscriptionBag subscriptions;
subscriptions += client.OnDirectConnectionEstablished.Subscribe(
    [&directPeers](int remotePlayerId) { directPeers.insert(remotePlayerId); });
subscriptions += client.OnDirectConnectionFailed.Subscribe(
    [&directPeers](int remotePlayerId) { directPeers.erase(remotePlayerId); });

Relay Fallback

Pure peer-to-peer is unreliable on the open internet: NAT punch-through fails for many player pairs because symmetric NATs, strict firewalls and mobile carriers block the direct link. DirectMessageOptions::FallbackRelay covers these cases by routing the message through the server whenever the direct link to a target is unavailable; such messages arrive with isRelay = true.

Enable the fallback unless losing the payload entirely is acceptable for the affected peers. Treat OnDirectConnectionFailed as the signal that a peer will stay on relay, and budget that peer's latency accordingly.

Back to top