Optimisation des RPC dans Unreal Engine : comment arrêter d'inonder votre réseau à chaque Tick
En bref
Ce guide technique détaille comment optimiser les RPC dans Unreal Engine pour éviter la saturation du réseau lors des mises à jour haute fréquence. En implémentant un Accumulator Pattern en C++, les développeurs peuvent découpler les envois réseau du framerate client et réduire massivement la consommation de bande passante. L'article couvre également le batching de données via des structures et les meilleures pratiques de Replication pour garantir une expérience Multiplayer fluide et scalable.
Tout développeur de jeux Multiplayer finit par être confronté au même bottleneck réseau : un client tournant à 144 FPS décide d'envoyer son état de mouvement personnalisé au serveur à chaque Tick. En quelques secondes, la file d'attente réseau du serveur est totalement inondée de Remote Procedure Calls (RPC) redondants, provoquant un lag extrême, des pertes de paquets et une déconnexion inévitable. Votre client est essentiellement en train de lancer une attaque DDoS sur votre propre infrastructure serveur.
Ce scénario représente l'un des pièges les plus courants dans l'architecture des jeux Multiplayer. Lorsque les développeurs doivent envoyer des entrées joueur personnalisées, des états de physique de véhicule complexes ou des mécaniques de tir rapide, placer un RPC à l'intérieur de la fonction Tick() semble être le choix logique pour une réactivité fluide. Cependant, la couche Netcode d'Unreal Engine ne supprime pas automatiquement les RPC intermédiaires. Si votre jeu pousse un RPC à chaque Tick, ils sont tous mis en file d'attente et transmis.
Pour les mises à jour de mouvement et de position, vous ne vous souciez presque jamais des 143 frames intermédiaires ; vous n'avez besoin que du dernier état absolu pour la Replication vers les autres clients. Dans ce guide complet, nous allons plonger dans l'unreal engine rpc optimization, en vous montrant exactement comment réguler ces appels réseau basés sur le Tick, implémenter une accumulation d'état intelligente et réduire considérablement votre consommation de bande passante Multiplayer.
Le danger des événements réseau liés au Tick
Avant d'implémenter une solution, il est crucial de comprendre l'anatomie du problème. Lorsque vous déclarez un RPC dans Unreal Engine, qu'il s'agisse de Server, Client ou NetMulticast, vous demandez au driver réseau du moteur de sérialiser les paramètres de la fonction et de les pousser dans la file d'attente des paquets sortants.
Le problème de la mise en file d'attente
Unreal Engine regroupe les RPC sortants dans des paquets en fonction de la NetUpdateFrequency de la connexion et des limites de bande passante. Si un client appelle un Server RPC à chaque Tick avec un framerate élevé, le moteur tentera de traiter chacun de ces appels.
Si le RPC est marqué comme Reliable, la situation est catastrophique. Les Reliable RPC garantissent la livraison et l'ordre d'exécution. Le canal réseau va rapidement se remplir, et si le buffer déborde, la connexion sera fermée de force par le moteur, entraînant la déconnexion du joueur.
Si le RPC est marqué comme Unreliable, le moteur abandonnera des paquets lorsque la file d'attente sera pleine. Bien que cela évite une déconnexion brutale, cela provoque un rubber-banding massif. Le serveur pourrait recevoir la frame 1, la frame 2, ignorer les frames 3 à 100, puis traiter la frame 101. Le résultat est un mouvement erratique et saccadé qui ruine l'expérience de jeu. C'est une cause fréquente lorsque les équipes cherchent à fixing the Unreal Engine RPC replication issue breaking your states.
Les mathématiques de la bande passante
Regardons quelques chiffres concrets. Imaginez que vous envoyez un simple vecteur (12 octets) et un rotator (12 octets) via un Server RPC. Avec l'overhead de l'en-tête RPC, estimons à 32 octets par appel.
- À 30 FPS :
30 * 32 octets = 960 octets/seconde(environ 1 Ko/s par client). - À 144 FPS :
144 * 32 octets = 4 608 octets/seconde(environ 4,6 Ko/s par client). - À 240 FPS :
240 * 32 octets = 7 680 octets/seconde.
Multipliez cela par 64 joueurs dans un Battle Royale, et votre serveur traite soudainement près d'un demi-mégaoctet d'overhead RPC pur chaque seconde — juste pour le suivi de mouvement de base. Ce n'est pas scalable.
Étape 1 : Rompre la dépendance au Tick avec un Accumulator Pattern
La stratégie la plus efficace pour l'unreal engine rpc optimization est de découpler votre taux d'envoi réseau du taux de rafraîchissement du client. Au lieu de pousser le RPC dans Tick(), vous devriez mettre à jour une variable locale à chaque Tick, puis utiliser un Timer pour envoyer ces données au serveur à un intervalle fixe et prévisible (par exemple, 10 ou 20 fois par seconde).
C'est ce que nous appelons l'Accumulator Pattern. Le client accumule l'état le plus récent en continu mais ne transmet que lorsque la porte réseau s'ouvre.
Identifier la fréquence cible
Vous n'avez pas besoin de 144 mises à jour par seconde pour une expérience Multiplayer fluide. La plupart des shooters compétitifs modernes font tourner leurs serveurs à 30Hz ou 60Hz. Par conséquent, envoyer les mises à jour client 15 à 30 fois par seconde est généralement amplement suffisant, à condition d'utiliser une bonne client-side prediction et une server-side interpolation.
En réduisant le taux d'envoi d'un 144Hz non plafonné à un 20Hz limité, vous réduisez instantanément votre trafic réseau de plus de 85 % pour cette action spécifique.
Étape 2 : Implémentation du Rate-Limiter en C++
Voyons comment implémenter cela efficacement en C++. Nous allons créer un système où le client suit sa position et sa rotation cibles à chaque Tick, mais n'envoie le RPC Server_UpdateTransform qu'en fonction d'un taux d'envoi réseau prédéfini.
Le fichier Header (.h)
Tout d'abord, nous définissons nos variables et fonctions dans notre classe personnalisée APawn ou ACharacter. Nous avons besoin d'un timer handle, d'un taux de mise à jour et de variables pour stocker nos données non envoyées.
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;
// The RPC to send data to the server. Marked as Unreliable for rapid, continuous updates.
UFUNCTION(Server, Unreliable, WithValidation)
void Server_SendTransformUpdate(FVector NewLocation, FRotator NewRotation);
private:
// Timer handle for our network flush
FTimerHandle NetworkUpdateTimerHandle;
// How many times per second we want to send updates to the server
UPROPERTY(EditDefaultsOnly, Category = "Network")
float NetworkSendRate;
// Flag to track if we have new data that hasn't been sent yet
bool bHasPendingNetworkUpdate;
// The accumulated data waiting to be sent
FVector PendingLocation;
FRotator PendingRotation;
// The function called by the timer to flush data
void FlushNetworkUpdate();
};
Le fichier Source (.cpp)
Maintenant, nous implémentons la logique. Nous configurons le Timer dans BeginPlay, mettons à jour nos variables temporaires dans Tick, et laissons le Timer gérer la transmission réseau réelle.
#include "MyCustomPawn.h"
#include "TimerManager.h"
AMyCustomPawn::AMyCustomPawn()
{
PrimaryActorTick.bCanEverTick = true;
// Default to sending 20 updates per second
NetworkSendRate = 20.0f;
bHasPendingNetworkUpdate = false;
}
void AMyCustomPawn::BeginPlay()
{
Super::BeginPlay();
// Only the local controlling client should run the network flush timer
if (IsLocallyControlled())
{
float UpdateInterval = 1.0f / NetworkSendRate; // e.g., 1.0 / 20.0 = 0.05 seconds
GetWorld()->GetTimerManager().SetTimer(
NetworkUpdateTimerHandle,
this,
&AMyCustomPawn::FlushNetworkUpdate,
UpdateInterval,
true // Loop continuously
);
}
}
void AMyCustomPawn::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// Run your custom client-side movement logic here
// e.g., FVector NewLoc = ...; FRotator NewRot = ...;
// SetActorLocationAndRotation(NewLoc, NewRot);
if (IsLocallyControlled())
{
// Instead of calling the RPC here, we just store the latest state
PendingLocation = GetActorLocation();
PendingRotation = GetActorRotation();
// Mark that we have fresh data waiting to be sent
bHasPendingNetworkUpdate = true;
}
}
void AMyCustomPawn::FlushNetworkUpdate()
{
// If there is no new data (e.g., the player is standing still), don't waste bandwidth
if (!bHasPendingNetworkUpdate)
{
return;
}
// Send the latest accumulated state to the server
Server_SendTransformUpdate(PendingLocation, PendingRotation);
// Reset the flag until the next tick modifies the state again
bHasPendingNetworkUpdate = false;
}
bool AMyCustomPawn::Server_SendTransformUpdate_Validate(FVector NewLocation, FRotator NewRotation)
{
// Add anti-cheat validation here. Is the location reasonable?
return true;
}
void AMyCustomPawn::Server_SendTransformUpdate_Implementation(FVector NewLocation, FRotator NewRotation)
{
// The server receives the rate-limited data and applies it
SetActorLocationAndRotation(NewLocation, NewRotation);
// Note: The server would then replicate this to other clients,
// typically via standard Replicated properties, NOT by Multicasting.
}
Pourquoi cette architecture fonctionne
Cette configuration résout élégamment le problème d'inondation réseau. Peu importe si le client tourne à 30 FPS ou 300 FPS, le serveur est garanti de recevoir exactement NetworkSendRate mises à jour par seconde (en supposant qu'il n'y ait pas de perte de paquets).
De plus, nous avons implémenté une vérification de sortie anticipée (!bHasPendingNetworkUpdate). Si le joueur lâche son clavier pour aller chercher un café, le client arrête totalement d'envoyer des RPC, libérant ainsi une bande passante critique pour les joueurs actifs. C'est une victoire massive pour maintenir des performances serveur constantes.
Étape 3 : Gestion de l'interpolation d'état sur les autres clients
Lorsque vous réduisez le taux d'envoi réseau, le mouvement sur le serveur — et par conséquent sur les autres clients connectés — deviendra saccadé. Si vous envoyez des mises à jour à 10Hz, le personnage se téléportera visiblement 10 fois par seconde sur un écran 60 FPS.
Pour corriger cela, vous ne pouvez pas simplement brusquer le personnage vers la nouvelle position. Vous devez utiliser l'interpolation. Lorsque le serveur réplique la NewLocation vers les simulated proxies (les autres clients observant le joueur), ces clients doivent effectuer un FMath::VInterpTo fluide de leur position actuelle vers la position cible répliquée au fil du temps.
Cela garantit que même avec une limite de taux agressive (comme 5 ou 10 mises à jour par seconde), la représentation visuelle reste parfaitement fluide. Si vous rencontrez des problèmes de saccades pendant l'interpolation, vous devriez consulter how to fix player location desync in UEFN and Unreal Engine multiplayer.
Étape 4 : Batching via Structs pour les RPC complexes
Si votre jeu nécessite l'envoi de plusieurs variables différentes, n'envoyez pas plusieurs RPC séparés. Chaque RPC possède un overhead d'en-tête de base (généralement environ 1 à 2 octets minimum, mais pratiquement plus en tenant compte de la sérialisation du payload).
Si vous appelez Server_SendHealth(), Server_SendArmor(), et Server_SendPosition() lors du même flush réseau, vous payez le coût de l'en-tête trois fois.
Créez plutôt une structure dédiée pour vos payloads réseau.
USTRUCT()
struct FPlayerNetworkState
{
GENERATED_BODY()
UPROPERTY()
FVector Location;
UPROPERTY()
FRotator Rotation;
UPROPERTY()
uint8 CurrentWeaponIndex;
UPROPERTY()
bool bIsCrouching;
};
Passez cette structure unique via votre RPC basé sur un Timer. Le système de réflexion d'Unreal Engine packera ces variables efficacement dans un seul payload de paquet, minimisant l'empreinte en octets sur votre connexion.
5 Meilleures pratiques pour l'optimisation des RPC Unreal Engine
Pour garantir que votre jeu passe des tests locaux à des milliers de joueurs simultanés, adoptez ces règles fondamentales pour votre architecture réseau :
- N'envoyez jamais de RPC dans le Tick sans garde : Considérez cela comme une règle absolue. Si un RPC est à l'intérieur de
Tick(), il doit être protégé par une vérification temporelle (ex:if (TimeSinceLastRPC > 0.1f)) ou géré via un Timer en boucle. - Priorisez l'Unreliable par rapport au Reliable : Pour les données mises à jour en continu (mouvement, rotation de vue, armes à rayon continu), utilisez toujours des Unreliable RPC. Si un paquet tombe, le suivant arrivant une fraction de seconde plus tard l'écrasera de toute façon. Les Reliable RPC doivent être strictement réservés aux changements d'état absolus (ex: arme tirée, objet ramassé, mort du joueur).
- Utilisez la Quantization pour les Floats et Vectors : Lors de l'envoi de données
FVector, vous avez rarement besoin d'une précision flottante complète. Unreal Engine vous permet de quantifier les vecteurs dans les RPC (ex:FVector_NetQuantize100), ce qui arrondit les valeurs à deux décimales et réduit drastiquement la bande passante requise. - Privilégiez la Replication standard pour les données descendantes : Alors que les clients doivent utiliser des RPC pour envoyer des données au serveur, le serveur devrait rarement utiliser des Multicast RPC pour renvoyer des données continues. Le serveur devrait mettre à jour une variable
UPROPERTY(Replicated), permettant au gestionnaire de Replication intégré d'Unreal de gérer automatiquement l'optimisation de la bande passante, la priorisation et le tri par pertinence. - Profilez tôt et souvent : Utilisez la commande
net.DumpRelevantActorset l'outil Network Profiler (NetworkProfiler.exesitué dans les binaires de l'Engine) pour visualiser exactement combien d'octets vos RPC consomment par frame. Ne devinez jamais vos gains d'optimisation ; mesurez-les empiriquement.
Gestion de l'infrastructure et scaling du Backend
Maîtriser les subtilités du Netcode d'Unreal Engine est une tâche colossale. Vous passez des heures à ajuster des Timer handles, quantifier des vecteurs et atténuer les desyncs juste pour que vos serveurs dédiés fonctionnent sans dépasser leurs limites de bande passante.
Une fois votre code de jeu optimisé, vous devez encore déployer et scaler ces serveurs à l'échelle mondiale. Construire cela soi-même nécessite de mettre en place des fleet managers, des load balancers, du database sharding et la gestion des certificats SSL — facilement 4 à 6 semaines de travail intensif sur l'infrastructure qui vous éloignent du game design pur.
Avec horizOn, ces services Backend sont pré-configurés spécifiquement pour les développeurs de jeux. Vous bénéficiez d'un hébergement de serveurs dédiés scalable, d'une synchronisation de base de données en temps réel et d'analytics robustes dès le départ, vous permettant de livrer votre jeu plutôt que votre infrastructure.
Réflexions finales
La clé de l'unreal engine rpc optimization est de réaliser que la bande passante réseau est une ressource finie et très volatile. Vous ne pouvez pas traiter la couche réseau comme un frame buffer standard. En vous éloignant de l'exécution pilotée par le Tick et en adoptant l'Accumulator Pattern, vous gagnez un contrôle total sur la sortie des données de votre jeu. Vous réduisez la charge serveur, atténuez les pertes de paquets et créez une expérience nettement plus fluide pour les joueurs disposant de connexions internet instables.
N'oubliez pas que l'optimisation de votre jeu est un processus continu. Arrêtez de compter sur les comportements par défaut du moteur pour vous sauver des inondations réseau. Prenez explicitement le contrôle de votre flux de données. Implémentez ces limites de taux dans votre prototype actuel, surveillez les métriques avant/après à l'aide du Network Profiler, et regardez les performances de votre serveur s'envoler.
Prêt à scaler votre backend Multiplayer fraîchement optimisé ? Essayez horizOn gratuitement ou consultez la documentation API pour voir à quel point une infrastructure de jeu professionnelle peut être simple.