Remote Procedure Calls

Invoke methods on remote machines. GONet provides three RPC types for every direction of communication, with async/await, validation, and late-joiner support built in.

What are RPCs?

Remote Procedure Calls let you execute a method on one machine and have it run on another. Where Auto-Magical Sync handles continuous state (health, position, scores), RPCs handle discrete actions -- picking up an item, firing a weapon, sending a chat message, or triggering a cutscene.

GONet provides three types of RPCs:

ServerRpc

Client calls, server executes. Used for requesting actions that need server authority.

ClientRpc

Server broadcasts to all clients. Used for effects, announcements, and world events.

TargetRpc

Server sends to a specific client or subset. Used for private messages, UI prompts, and targeted effects.

All three types support async/await with return values, server-side validation, and delivery reports.

ServerRpc

A ServerRpc is called on the client and executes on the server. This is the most common RPC type -- any time a player wants to do something that needs server validation (pick up an item, attack an enemy, open a door), it goes through a ServerRpc.

public class Player : GONetParticipantCompanionBehaviour
{
    [ServerRpc]
    void RequestPickup(int itemId)
    {
        if (GONetMain.IsServer)
        {
            // Validate and apply on server
            ApplyPickup(itemId);
        }
    }

    // Client-side usage:
    internal override void UpdateAfterGONetReady()
    {
        if (IsMine && Input.GetKeyDown(KeyCode.E))
        {
            CallRpc(nameof(RequestPickup), nearbyItemId);
        }
    }
}

ServerRpc has two important settings:

  • IsMineRequired = true (default) -- Only the object's owner can call this RPC. Prevents other clients from invoking actions on objects they do not own.
  • IsMineRequired = false -- Any client can call this RPC. Useful for global actions like voting or requesting server information.
  • RunLocally = true (default) -- The RPC also executes on the calling client immediately, so the player sees instant feedback while the server processes the request.

ClientRpc

A ClientRpc is called on the server and executes on all connected clients. Use this for effects, announcements, and anything that every player should see or hear.

[ClientRpc]
void PlayExplosionEffect(Vector3 position, float radius)
{
    // Runs on ALL clients
    Instantiate(explosionPrefab, position, Quaternion.identity);
}

// Server-side usage:
if (GONetMain.IsServer)
{
    CallRpc(nameof(PlayExplosionEffect), explosionPos, 5f);
}

The server calls CallRpc and every client runs the method locally. This keeps effects synchronized across the session without each client needing to independently detect the event.

TargetRpc

A TargetRpc sends from the server to a specific client or subset of clients. This is useful for private messages, player-specific UI prompts, or any communication that should not go to everyone.

// Send to object owner
[TargetRpc(RpcTarget.Owner)]
void ShowPrivateMessage(string message)
{
    ShowNotification(message);
}

// Send to specific authority
[TargetRpc(RpcTarget.SpecificAuthority)]
void NotifyPlayer(ushort targetAuthId, string message)
{
    ShowNotification(message);
}

// Send to everyone except sender
[TargetRpc(RpcTarget.Others)]
void BroadcastToOthers(string message)
{
    ShowChatMessage(message);
}

The available targets are:

  • RpcTarget.Owner -- Sends only to the client that owns this object.
  • RpcTarget.SpecificAuthority -- Sends to a specific client identified by their authority ID. The first parameter of the method must be ushort targetAuthId.
  • RpcTarget.Others -- Sends to every client except the one that initiated the call.

Async RPCs with Return Values

RPCs can return values using async/await. The calling client sends the request, the server processes it, and the result is delivered back to the caller as a Task. This is ideal for request/response patterns like purchase confirmations, matchmaking queries, or permission checks.

[ServerRpc]
async Task<bool> RequestPurchase(int itemId, int price)
{
    // Server validates and processes
    if (playerGold >= price)
    {
        playerGold -= price;
        return true;
    }
    return false;
}

// Client usage:
var success = await CallRpcAsync<bool>(nameof(RequestPurchase), itemId, 100);
if (success)
    Debug.Log("Purchase complete!");

The await suspends the calling coroutine until the server responds. The client can continue rendering and processing input normally while waiting -- only the calling code path is suspended.

Server-Side Validation

GONet supports validation methods that run before an RPC executes. This lets you reject invalid or malicious calls, sanitize parameters, and apply rate limiting -- all before the main RPC logic touches anything.

The validation method follows a naming convention: prefix the RPC method name with Validate. Its parameters are passed by ref, so you can modify them before the RPC executes.

[ServerRpc]
void SendChatMessage(string text)
{
    // Broadcast to everyone
    CallRpc(nameof(ShowChat), text);
}

// Validation runs before the RPC executes
RpcValidationResult ValidateSendChatMessage(ref string text)
{
    if (string.IsNullOrEmpty(text))
    {
        var result = RpcValidationResult.CreatePreAllocated(0);
        result.DenyAll("Empty message");
        return result;
    }
    // Can modify the parameter
    text = text.Trim();
    var ok = RpcValidationResult.CreatePreAllocated(1);
    ok.AllowAll();
    return ok;
}

If the validation method returns a deny result, the RPC is silently dropped and never executes. The caller is not notified by default -- from their perspective, the call simply has no effect. This is intentional: legitimate clients should never trigger validation failures, so there is no need to provide feedback to potential bad actors.

Persistent RPCs

By default, RPCs are transient -- they execute once and are forgotten. But some RPCs represent state that late-joining clients need to know about: a match has started, a door has been opened, a game mode has changed.

Marking an RPC as persistent tells GONet to store it and replay it for any client that joins after the original call:

[ServerRpc(IsPersistent = true)]
void AnnounceMatchStart(string mapName, int maxPlayers)
{
    // Late-joining clients automatically receive this
}

When a new client connects, GONet replays all persistent RPCs in order, bringing the late joiner up to speed with the current game state. This is especially useful for one-time events that have lasting effects on the game world.

Calling RPCs

All RPCs are invoked through the CallRpc and CallRpcAsync methods. You pass the method name using nameof() for compile-time safety, followed by any arguments.

// Fire-and-forget
CallRpc(nameof(MethodName));
CallRpc(nameof(Method), arg1);
CallRpc(nameof(Method), arg1, arg2, arg3);

// Async with return value
var result = await CallRpcAsync<ReturnType>(nameof(Method));
var result2 = await CallRpcAsync<ReturnType>(nameof(Method), arg1, arg2);

Hard limit: RPCs support a maximum of 8 parameters. If you need to send more data, pack it into a [MemoryPackable] struct and pass that as a single parameter.

RPC Parameter Types

RPC parameters support all the same primitive and Unity math types as Auto-Magical Sync: int, float, bool, string, Vector2, Vector3, Quaternion, and all enum types.

For structured data, define a [MemoryPackable] type:

[MemoryPackable]
public partial struct AttackData
{
    public int TargetId;
    public float Damage;
    public Vector3 HitPoint;
}

[ServerRpc]
void Attack(AttackData data) { }

Important: All [MemoryPackable] types must be declared at namespace level. They cannot be nested inside another class. This is a requirement of the MemoryPack serialization library that GONet uses internally.

Deferred RPCs

Sometimes an RPC arrives before the target component is fully initialized -- for example, during scene loading or object spawning. Rather than silently dropping these calls, GONet automatically queues them and retries when the component becomes ready.

This deferral system is transparent. You do not need to write any special code to handle it. The RPC just works, even if the timing is tight.

Two configuration options control deferral behavior:

  • GONetConfig.RpcDeferralTimeoutSeconds -- How long to keep retrying before giving up. Default is 5 seconds. After the timeout, the RPC is discarded and a warning is logged.
  • GONetConfig.MaxDeferredRpcsPerParticipant -- Maximum number of RPCs that can be queued for a single participant. Default is 100. If the queue is full, the oldest deferred RPC is discarded to make room.

Next Steps

With sync and RPCs covered, you have the two main tools for networked communication. Next, learn about the event system for decoupled messaging, or explore how GONet handles networked object spawning.

Remote Procedure Calls | GONet Docs