RPCs
Overview
RPCs in Fusion are fire-and-forget messages sent between clients.
Unlike state synchronization, which continuously replicates property values, RPCs deliver discrete events: damage notifications, chat messages, ability activations and other one-shot actions.
Rpc Structure
C++
class Rpc {
public:
uint64_t Id; // RPC identifier
uint64_t Sequence; // Per-RPC monotonic sequence (assigned by SendUserRpc)
RpcFlags Flags; // Strong-typed delivery / error flags
uint32_t DeliveryAttempts; // Number of attempts so far
PlayerId OriginPlayer; // Sender (uint16_t)
PlayerId TargetPlayer; // Recipient (0 = all)
ObjectId TargetObject; // Target object ((0,0,0) = broadcast)
uint64_t EventHash; // Method/event hash for dispatch
Data Bytes; // Serialized payload
bool IsInternal() const; // True if Id is in [1, 1023]
};
Key Fields
| Field | Purpose |
|---|---|
Id |
Unique RPC identifier. IDs 1-1023 are reserved for SDK internal RPCs. User RPCs use IDs >= 1024. |
Sequence |
Monotonic per-sender sequence number. Assigned in place by Client::SendUserRpc; do not set it manually. |
Flags |
RpcFlags bitset (delivery hints + error indicators). See RpcFlags. |
DeliveryAttempts |
Count of delivery attempts. When the SDK exhausts its retry budget, RpcFlags::MaxDeliveryAttemptsReached is set and the RPC surfaces via OnRpcError. |
TargetPlayer |
Specific player to receive the RPC. 0 for all players, or a PlayerId for targeted delivery. |
TargetObject |
The networked object this RPC is addressed to. ObjectId(0,0,0) indicates a broadcast RPC. |
EventHash |
Hash identifying the event/method. Combined with Id, this routes the RPC to the correct handler. Replaces the older paired DescriptorTypeHash+EventHash scheme — there is now a single hash on the wire. |
Bytes |
Opaque payload containing serialized arguments. |
The older Rpc::TargetComponent (uint16_t) and Rpc::DescriptorTypeHash (uint64_t) fields have been removed; routing now uses the (Id, EventHash) pair, and per-component sub-targeting is handled at the integration layer instead of in the wire format.
Creating and Sending RPCs
CreateUserRpc
C++
Rpc Client::CreateUserRpc(
uint64_t id, // RPC ID (must be >= 1024)
PlayerId targetPlayer, // 0 = all, specific PlayerId = targeted
ObjectId targetObject, // Object target or (0,0,0) for broadcast
uint64_t EventHash, // Method hash for dispatch
const char* data, // Serialized payload bytes
size_t dataLength // Payload length
);
This constructs an Rpc struct with the local player as OriginPlayer. The data parameter should contain pre-serialized arguments. Older SDKs took a separate DescriptorTypeHash argument before EventHash — that parameter has been removed.
SendUserRpc
C++
bool Client::SendUserRpc(Rpc& rpc);
Queues the RPC for delivery. Takes a non-const reference because the SDK assigns Rpc::Sequence in place. RPCs are batched and sent during the next UpdateFrameEnd() cycle. Returns true if the RPC was accepted.
Complete Example
C++
// 1. Serialize payload
FusionCore::WriteBuffer payload;
payload.Int(damageAmount);
payload.UIntVar(targetEntityId);
FusionCore::Data payloadData = payload.Take();
// 2. Create the RPC
auto rpc = client->CreateUserRpc(
1024, // User RPC ID
0, // All players
targetObjectId, // Object-targeted
FusionCore::CRC64(u8"OnDamage", std::strlen("OnDamage")), // Event hash
reinterpret_cast<const char*>(payloadData.Ptr),
payloadData.Length
);
// 3. Send (the SDK fills in Sequence in place)
client->SendUserRpc(rpc);
// 4. Clean up payload
payloadData.Free();
Receiving RPCs
Successful RPCs arrive through the OnRpc broadcaster. Failed deliveries (the RpcFlags::ErrorFlags mask) surface separately via OnRpcError:
C++
PhotonCommon::Broadcaster<void(Rpc&)> OnRpc;
PhotonCommon::Broadcaster<void(Rpc&)> OnRpcError;
Subscribe to receive RPCs:
C++
subs += client->OnRpc.Subscribe([](FusionCore::Rpc& rpc) {
if (rpc.IsInternal()) return; // Skip SDK-internal RPCs
if (rpc.TargetObject.IsSome()) {
dispatch_to_object(rpc);
} else {
dispatch_broadcast(rpc);
}
});
subs += client->OnRpcError.Subscribe([](FusionCore::Rpc& rpc) {
using namespace FusionCore;
if (HasFlag(rpc.Flags, RpcFlags::PlayerMissing)) {
// Target player left before the RPC could be delivered.
}
if (HasFlag(rpc.Flags, RpcFlags::ObjectMissing)) {
// Target object was destroyed.
}
if (HasFlag(rpc.Flags, RpcFlags::MapIncorrect)) {
// Target object's map differs from sender's expectation.
}
if (HasFlag(rpc.Flags, RpcFlags::MaxDeliveryAttemptsReached)) {
// Retry budget exhausted.
}
});
Routing by Hash
The combination of (Id, EventHash) identifies the handler:
Idis the user-visible RPC identifier (must be >= 1024).EventHashis typicallyCRC64(method_name)and lets a singleIdcover multiple events without collisions.
The integration layer computes hashes at registration time and matches them against incoming RPCs.
Reading the Payload
C++
void handle_damage_rpc(FusionCore::Rpc& rpc) {
FusionCore::ReadBuffer reader(rpc.Bytes);
int32_t damage = reader.Int();
uint32_t entityId = reader.UIntVar();
// Apply damage...
}
ID Ranges
| Range | Owner | Purpose |
|---|---|---|
| 1 - 1023 | SDK internal | Map changes, ownership requests, prediction inputs, etc. |
| 1024+ | User | Application-defined RPCs |
Internal RPC Constants
C++
constexpr uint64_t RPC_InternalMinId = 1;
constexpr uint64_t RPC_InternalMaxId = 1023;
constexpr uint64_t RPC_InternalMapChange = 1; // Replaces older RPC_InternalSceneChange
constexpr uint64_t RPC_InternalObjectPriority = 2;
constexpr uint64_t RPC_InternalMapAdd = 3;
constexpr uint64_t RPC_InternalMapRemove = 4;
constexpr uint64_t RPC_InternalOwnershipRequest = 5;
constexpr uint64_t RPC_InternalRejectSubObject = 6;
constexpr uint64_t RPC_InternalDestroyedMapActors = 7;
constexpr uint64_t RPC_InternalForceDestroyObject = 8;
constexpr uint64_t RPC_InternalForceAliveObject = 9;
constexpr uint64_t RPC_InternalInput = 10;
constexpr uint64_t RPC_InternalPlayerInterest = 11;
constexpr uint64_t RPC_InternalOwnershipResponse = 12;
Internal RPCs are processed by the SDK before reaching OnRpc.
The IsInternal() method identifies them.
User code should never send RPCs with IDs in the internal range.
Target Filtering
Player Targeting
| TargetPlayer Value | Behavior |
|---|---|
0 |
Delivered to all players |
Specific PlayerId |
Delivered only to that player |
MasterClientPlayerId (0xFFFF) |
Delivered to the current master client |
ObjectOwnerPlayerId (0xFFFD) |
Delivered to the owner of TargetObject |
Object Targeting
| TargetObject Value | Behavior |
|---|---|
ObjectId(0, 0, 0) (.IsNone()) |
Broadcast RPC -- not object-targeted |
Valid ObjectId (.IsSome()) |
Delivered to the specific networked object |
RpcFlags
C++
enum class RpcFlags : uint32_t {
None = 0,
ReturnResultOnFailure = 1 << 0,
IncorrectTargetForward = 1 << 1,
DontReplyWithResult = 1 << 2,
PlayerMissing = 1 << 5,
ObjectMissing = 1 << 6,
MapIncorrect = 1 << 7,
MaxDeliveryAttemptsReached = 1 << 8,
ErrorFlags = PlayerMissing | ObjectMissing
| MapIncorrect | MaxDeliveryAttemptsReached,
};
bool HasFlag(RpcFlags flags, RpcFlags flag);
RpcFlags is now an enum class (was a wrapper struct in older SDKs). Compose with | and inspect with HasFlag. The ErrorFlags mask groups all delivery-failure indicators surfaced via OnRpcError.
Wire Format
The Rpc struct has built-in serialization for the SDK's packet protocol:
C++
static Rpc Rpc::Read(ReadBuffer& reader);
static void Rpc::Write(WriteBuffer& writer, const Rpc& rpc);
These are used internally by the SDK for packet construction and parsing.
User code does not need to call them directly.
Delivery Guarantees
RPCs are sent through Fusion's Notify protocol, which provides ordered, reliable delivery with acknowledgment tracking.
Lost packets are detected and retransmitted up to the SDK's retry budget; if the budget is exhausted (or the target object/player goes missing), the RPC's Flags are stamped with the appropriate RpcFlags::ErrorFlags bit and surfaced via OnRpcError instead of OnRpc.
This means:
- Successful RPCs are guaranteed to arrive in order from the same sender.
- Failed RPCs are reported (rather than silently lost) — subscribe to
OnRpcErrorto react. - Duplicate delivery does not occur.
Common Mistakes
| Mistake | Symptom |
|---|---|
| Using RPC ID < 1024 | Conflicts with SDK internal RPCs |
Not checking IsInternal() |
Processing SDK-internal RPCs as user RPCs |
Sending RPCs before IsRunning() |
RPCs silently dropped |
Not freeing payload Data after Take() |
Memory leak |
Ignoring OnRpcError |
No visibility when RPCs fail (player left, object destroyed, map mismatch) |
| Mismatched read/write order in payload | Corrupted arguments |
Passing a hand-set Sequence to SendUserRpc |
Overwritten by the SDK at send time — pointless |
Related
- Architecture -- CRC64 for hash generation
- Scene Management -- Internal RPCs drive the multi-map session
- Time -- When RPCs are sent and received in the frame loop