Torna al Blog

Ottimizzazione degli RPC in Unreal Engine: come smettere di intasare la rete a ogni Tick

Pubblicato il 4 maggio 2026
Ottimizzazione degli RPC in Unreal Engine: come smettere di intasare la rete a ogni Tick

In breve

Questa guida tecnica esplora le strategie avanzate per l'ottimizzazione degli RPC in Unreal Engine, focalizzandosi sulla mitigazione del network flooding causato dalle chiamate nel Tick(). Viene presentato l'Accumulator Pattern come soluzione per implementare il rate-limiting dei dati, insieme a tecniche di interpolazione e struct batching per ridurre l'overhead di banda. L'obiettivo è fornire agli sviluppatori strumenti pratici per scalare il proprio netcode e garantire un'esperienza multiplayer fluida e professionale.

Ogni sviluppatore di giochi multiplayer prima o poi si scontra con lo stesso bottleneck di rete: un client che gira a 144 FPS decide di inviare il proprio stato di movimento personalizzato al server a ogni singolo tick. In pochi secondi, la coda di rete del server viene completamente inondata da Remote Procedure Calls (RPCs) ridondanti, causando lag estremo, packet loss e l'inevitabile disconnessione. In pratica, il tuo client sta lanciando un attacco Distributed Denial of Service (DDoS) alla tua stessa infrastruttura server.

Questo scenario rappresenta una delle trappole più comuni nell'architettura dei giochi multiplayer. Quando gli sviluppatori devono inviare input personalizzati, stati complessi della fisica dei veicoli o meccaniche di fuoco rapido, inserire un RPC all'interno della funzione Tick() sembra la scelta più logica per garantire reattività. Tuttavia, il networking layer di Unreal Engine non elimina automaticamente gli RPC intermedi. Se il tuo gioco lancia un RPC a ogni tick, ognuno di essi viene messo in coda e trasmesso.

Per gli aggiornamenti di movimento e posizione, quasi mai ti interessano i 143 frame intermedi; hai solo bisogno dell'ultimo stato assoluto da sottoporre a replication verso gli altri client. In questa guida completa, esploreremo a fondo l'unreal engine rpc optimization, mostrandoti esattamente come limitare queste chiamate di rete basate sul tick, implementare uno smart state accumulation e ridurre drasticamente l'overhead di banda nel tuo multiplayer.

Il Pericolo degli Eventi di Rete Legati al Tick

Prima di implementare una soluzione, è fondamentale capire l'anatomia del problema. Quando dichiari un RPC in Unreal Engine, che sia Server, Client o NetMulticast, stai istruendo il network driver dell'engine a serializzare i parametri della funzione e a spingerli nella coda dei pacchetti in uscita.

Il Problema del Queueing

Unreal Engine raggruppa gli RPC in uscita in pacchetti basati sulla NetUpdateFrequency della connessione e sui limiti di banda. Se un client chiama un Server RPC a ogni tick con un frame rate elevato, l'engine tenterà di elaborare ognuna di quelle chiamate.

Se l'RPC è contrassegnato come Reliable, la situazione è catastrofica. Gli RPC Reliable garantiscono la consegna e l'ordine di esecuzione. Il canale di rete si riempirà rapidamente e, se il buffer va in overflow, la connessione verrà chiusa forzatamente dall'engine, con la conseguente disconnessione del giocatore.

Se l'RPC è contrassegnato come Unreliable, l'engine scarterà i pacchetti quando la coda è piena. Sebbene questo eviti una disconnessione netta, porta a un massiccio rubber-banding. Il server potrebbe ricevere il frame 1, il frame 2, scartare i frame 3-100 e poi elaborare il frame 101. Il risultato è un movimento irregolare e a scatti che rovina l'esperienza di gioco. Questa è spesso la causa principale quando i team cercano di risolvere i problemi di replication degli RPC in Unreal Engine che rompono gli stati.

Il Calcolo della Banda

Guardiamo alcuni numeri concreti. Immagina di inviare un semplice vector (12 byte) e un rotator (12 byte) tramite un Server RPC. Con l'overhead dell'header RPC, stimiamo circa 32 byte per chiamata.

  • A 30 FPS: 30 * 32 byte = 960 byte/secondo (circa 1 KB/s per client).
  • A 144 FPS: 144 * 32 byte = 4.608 byte/secondo (circa 4,6 KB/s per client).
  • A 240 FPS: 240 * 32 byte = 7.680 byte/secondo.

Moltiplica questo per 64 giocatori in un battle royale e il tuo server si troverà improvvisamente a elaborare quasi mezzo megabyte di puro overhead RPC ogni secondo, solo per il tracciamento del movimento base. Questo non è scalabile.

Step 1: Rompere la Dipendenza dal Tick con un Accumulator Pattern

La strategia più efficace per l'unreal engine rpc optimization è disaccoppiare la frequenza di invio in rete dal frame rate di rendering del client. Invece di lanciare l'RPC in Tick(), dovresti aggiornare una variabile locale a ogni tick e poi usare un timer per trasmettere quei dati al server a un intervallo fisso e prevedibile (ad esempio, 10 o 20 volte al secondo).

Chiamiamo questo approccio Accumulator Pattern. Il client accumula continuamente l'ultimo stato, ma trasmette solo quando il "gate" di rete si apre.

Identificare la Frequenza Target

Non hai bisogno di 144 aggiornamenti al secondo per un'esperienza multiplayer fluida. La maggior parte degli shooter competitivi moderni fa girare i propri server a 30Hz o 60Hz. Pertanto, inviare aggiornamenti dal client da 15 a 30 volte al secondo è solitamente più che sufficiente, a patto di utilizzare una corretta client-side prediction e server-side interpolation.

Riducendo il send rate da un valore non limitato di 144Hz a un tetto di 20Hz, riduci istantaneamente il traffico di rete di oltre l'85% per quella specifica azione.

Step 2: Implementare il Rate-Limiter in C++

Vediamo come implementare questo sistema efficacemente in C++. Creeremo un sistema in cui il client traccia la posizione e la rotazione target desiderate a ogni tick, ma invia l'RPC Server_UpdateTransform solo in base a un network send rate predefinito.

Il File Header (.h)

Per prima cosa, definiamo le nostre variabili e funzioni nella nostra classe personalizzata APawn o ACharacter. Abbiamo bisogno di un timer handle, di un update rate e delle variabili per contenere i dati non ancora inviati.

UCLASS()
class MYGAME_API AMyCustomPawn : public APawn
{
    GENERATED_BODY()

public:
    AMyCustomPawn();

    virtual void Tick(float DeltaTime) override;
    virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

protected:
    virtual void BeginPlay() override;

    // L'RPC per inviare i dati al server. Marcato come Unreliable per aggiornamenti rapidi e continui.
    UFUNCTION(Server, Unreliable, WithValidation)
    void Server_SendTransformUpdate(FVector NewLocation, FRotator NewRotation);

private:
    // Timer handle per il nostro flush di rete
    FTimerHandle NetworkUpdateTimerHandle;

    // Quante volte al secondo vogliamo inviare aggiornamenti al server
    UPROPERTY(EditDefaultsOnly, Category = "Network")
    float NetworkSendRate;

    // Flag per tracciare se abbiamo nuovi dati non ancora inviati
    bool bHasPendingNetworkUpdate;

    // I dati accumulati in attesa di essere inviati
    FVector PendingLocation;
    FRotator PendingRotation;

    // La funzione chiamata dal timer per trasmettere i dati
    void FlushNetworkUpdate();
};

Il File Sorgente (.cpp)

Ora implementiamo la logica. Impostiamo il timer in BeginPlay, aggiorniamo le nostre variabili pending in Tick e lasciamo che il timer gestisca l'effettiva trasmissione di rete.

#include "MyCustomPawn.h"
#include "TimerManager.h"

AMyCustomPawn::AMyCustomPawn()
{
    PrimaryActorTick.bCanEverTick = true;
    
    // Default: 20 aggiornamenti al secondo
    NetworkSendRate = 20.0f; 
    bHasPendingNetworkUpdate = false;
}

void AMyCustomPawn::BeginPlay()
{
    Super::BeginPlay();

    // Solo il client locale che controlla il pawn deve far girare il timer di flush
    if (IsLocallyControlled())
    {
        float UpdateInterval = 1.0f / NetworkSendRate; // es. 1.0 / 20.0 = 0.05 secondi

        GetWorld()->GetTimerManager().SetTimer(
            NetworkUpdateTimerHandle,
            this,
            &AMyCustomPawn::FlushNetworkUpdate,
            UpdateInterval,
            true // Loop continuo
        );
    }
}

void AMyCustomPawn::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);

    // Esegui qui la tua logica di movimento client-side
    // es. FVector NewLoc = ...; FRotator NewRot = ...;
    // SetActorLocationAndRotation(NewLoc, NewRot);

    if (IsLocallyControlled())
    {
        // Invece di chiamare l'RPC qui, memorizziamo solo l'ultimo stato
        PendingLocation = GetActorLocation();
        PendingRotation = GetActorRotation();
        
        // Segnaliamo che ci sono nuovi dati in attesa di essere inviati
        bHasPendingNetworkUpdate = true;
    }
}

void AMyCustomPawn::FlushNetworkUpdate()
{
    // Se non ci sono nuovi dati (es. il giocatore è fermo), non sprecare banda
    if (!bHasPendingNetworkUpdate)
    {
        return;
    }

    // Invia l'ultimo stato accumulato al server
    Server_SendTransformUpdate(PendingLocation, PendingRotation);

    // Resetta il flag finché il prossimo tick non modificherà di nuovo lo stato
    bHasPendingNetworkUpdate = false;
}

bool AMyCustomPawn::Server_SendTransformUpdate_Validate(FVector NewLocation, FRotator NewRotation)
{
    // Aggiungi qui la validazione anti-cheat. La posizione è ragionevole?
    return true; 
}

void AMyCustomPawn::Server_SendTransformUpdate_Implementation(FVector NewLocation, FRotator NewRotation)
{
    // Il server riceve i dati rate-limited e li applica
    SetActorLocationAndRotation(NewLocation, NewRotation);
    
    // Nota: Il server replicherà poi questo dato agli altri client,
    // tipicamente tramite Replicated properties standard, NON tramite Multicast.
}

Perché Questa Architettura Funziona

Questa configurazione risolve elegantemente il problema del network flood. Non importa se il client gira a 30 FPS o 300 FPS, il server riceverà garantitamente esattamente NetworkSendRate aggiornamenti al secondo (assumendo l'assenza di packet loss).

Inoltre, abbiamo implementato un controllo early-out (!bHasPendingNetworkUpdate). Se il giocatore si allontana dalla tastiera, il client smette completamente di inviare RPC, liberando banda critica per i giocatori attivi. Questa è una vittoria enorme per mantenere prestazioni costanti sul server.

Step 3: Gestire la State Interpolation sugli Altri Client

Quando riduci il network send rate, il movimento sul server — e di conseguenza sugli altri client connessi — diventerà scattoso. Se invii aggiornamenti a 10Hz, il personaggio teletrasporterà visibilmente 10 volte al secondo su un monitor a 60 FPS.

Per risolvere questo, non puoi semplicemente spostare bruscamente il personaggio nella nuova posizione. Devi usare l'interpolazione. Quando il server replica la NewLocation ai simulated proxies (gli altri client che osservano il giocatore), quei client devono eseguire fluidamente un FMath::VInterpTo dalla loro posizione attuale alla posizione target replicata nel tempo.

Questo assicura che, anche con un rate limit molto aggressivo (come 5 o 10 aggiornamenti al secondo), la rappresentazione visiva rimanga fluida. Se riscontri problemi con i personaggi che scattano durante l'interpolazione, potresti consultare come risolvere i desync della posizione del giocatore in UEFN e nel multiplayer di Unreal Engine.

Step 4: Struct Batching per RPC Complessi

Se il tuo gioco richiede l'invio di diverse variabili, non inviare più RPC separati. Ogni RPC ha un overhead di header di base (tipicamente circa 1-2 byte come minimo, ma praticamente di più considerando la serializzazione del payload).

Se chiami Server_SendHealth(), Server_SendArmor() e Server_SendPosition() nello stesso flush di rete, stai pagando il costo dell'header tre volte.

Invece, crea una struct dedicata per i tuoi payload di rete.

USTRUCT()
struct FPlayerNetworkState
{
    GENERATED_BODY()

    UPROPERTY()
    FVector Location;

    UPROPERTY()
    FRotator Rotation;

    UPROPERTY()
    uint8 CurrentWeaponIndex;

    UPROPERTY()
    bool bIsCrouching;
};

Passa questa singola struct attraverso il tuo RPC basato su timer. Il reflection system di Unreal Engine impacchetterà queste variabili in modo efficiente in un singolo payload, riducendo al minimo l'impatto in byte sulla tua connessione.

5 Best Practice per l'Ottimizzazione degli RPC in Unreal Engine

Per garantire che il tuo gioco scali dai test locali a migliaia di giocatori simultanei, adotta queste regole fondamentali per l'architettura di rete:

  1. Mai inviare RPC nel Tick senza un Gate: Considerala una regola ferrea. Se un RPC è dentro Tick(), deve essere protetto da un controllo temporale (es. if (TimeSinceLastRPC > 0.1f)) o gestito tramite un timer.
  2. Privilegia Unreliable rispetto a Reliable: Per i dati che si aggiornano continuamente (movimento, rotazione visuale, armi a raggio continuo), usa sempre RPC Unreliable. Se un pacchetto viene perso, il pacchetto successivo che arriverà una frazione di secondo dopo lo sovrascriverà comunque. Gli RPC Reliable dovrebbero essere riservati esclusivamente a cambiamenti di stato assoluti (es. colpo sparato, oggetto raccolto, morte del giocatore).
  3. Usa la Quantization per Float e Vector: Quando invii dati FVector, raramente hai bisogno della precisione completa in virgola mobile. Unreal Engine ti permette di quantizzare i vettori negli RPC (es. FVector_NetQuantize100), che arrotonda i valori a due cifre decimali e taglia la banda necessaria per inviarli.
  4. Preferisci la Replication Standard per i Dati Downstream: Mentre i client devono usare gli RPC per inviare dati al server, il server dovrebbe raramente usare RPC Multicast per inviare dati continui verso il basso. Il server dovrebbe aggiornare una variabile UPROPERTY(Replicated), permettendo al replication manager integrato di Unreal di gestire automaticamente l'ottimizzazione della banda, la prioritizzazione e la relevancy.
  5. Effettua il Profiling Presto e Spesso: Usa il comando net.DumpRelevantActors e il Network Profiler (NetworkProfiler.exe situato nei binari dell'Engine) per visualizzare esattamente quanti byte stanno consumando i tuoi RPC per frame. Non tirare a indovinare sui guadagni di ottimizzazione; misurali empiricamente.

Gestione dell'Infrastruttura e Scaling del Backend

Padroneggiare le complessità del netcode di Unreal Engine è un'impresa enorme. Passi ore a regolare i timer, quantizzare vettori e mitigare desync solo per far sì che i tuoi Dedicated Servers girino fluidamente senza saturare i limiti di banda.

Una volta che il codice di gioco è finalmente ottimizzato, devi comunque distribuire e scalare quei server a livello globale. Costruire tutto questo da soli richiede la configurazione di fleet manager, load balancer, database sharding e gestione dei certificati SSL — facilmente 4-6 settimane di lavoro intensivo sull'infrastruttura che ti allontana dal game design vero e proprio.

Con horizOn, questi servizi di backend sono pre-configurati specificamente per gli sviluppatori di giochi. Ottieni hosting scalabile per Dedicated Server, sincronizzazione del database in tempo reale e analytics robuste già pronte all'uso, permettendoti di lanciare il tuo gioco invece della tua infrastruttura.

Considerazioni Finali

La chiave per l'unreal engine rpc optimization è capire che la banda di rete è una risorsa finita e altamente volatile. Non puoi trattare il networking layer come un normale frame buffer. Passando dall'esecuzione guidata dal Tick all'adozione dell'Accumulator Pattern, ottieni il controllo totale sull'output dei dati del tuo gioco. Riduci il carico del server, mitighi la perdita di pacchetti e crei un'esperienza drasticamente più fluida per i giocatori con connessioni internet instabili.

Ricorda che ottimizzare il tuo gioco è un processo continuo. Smetti di affidarti ai comportamenti predefiniti dell'engine per salvarti dai network flood. Prendi il controllo esplicito del tuo flusso di dati. Implementa questi rate limit nel tuo prototipo attuale, monitora le metriche prima e dopo usando il Network Profiler e guarda le prestazioni del tuo server decollare.

Pronto a scalare il tuo backend multiplayer appena ottimizzato? Prova horizOn gratuitamente o consulta la documentazione API per vedere quanto può essere semplice un'infrastruttura di gioco professionale.


Fonte: Network: How not to send all PRC every tick?