Retour au cours

games / fivem

Synchronisation avancée des véhicules et entités

Leçon 191 exercice

Explication

Ce que vous allez apprendre

  • Comprendre le rôle du "network owner" dans le calcul de la physique d'une entité
  • Éviter le rubber-banding causé par un SetEntityCoords répété sur une entité réseauée
  • Synchroniser un état custom de véhicule (essence, moteur) via les statebags
  • Contrôler explicitement le transfert d'autorité réseau avec SetNetworkIdCanMigrate
  • Identifier pourquoi le serveur ne simule pas lui-même la physique des véhicules

Dans quel contexte ?

Un script de course scénarisée doit garder le contrôle exclusif d'un véhicule piloté automatiquement, sans que FiveM ne transfère automatiquement son autorité réseau au joueur le plus proche. Comprendre le modèle du "owner" réseau, déjà esquissé dans la leçon sur les statebags, devient ici indispensable pour éviter des comportements imprévisibles (téléportations, désynchronisations) sur ce genre de script avancé.

ConceptRôle
Network ownerClient qui calcule la physique et fait autorité sur l'entité
SetEntityCoords répétéÀ éviter sur une entité réseauée : provoque du rubber-banding
Entity().stateSynchronise un état custom sans event manuel
SetNetworkIdCanMigrateFige ou autorise le transfert d'autorité réseau

Piège fréquent

Déplacer une entité synchronisée en réseau avec des appels répétés à SetEntityCoords force les autres clients à "rattraper" brutalement la nouvelle position à chaque appel, provoquant des saccades visibles (rubber-banding). Préfère toujours la physique native du jeu pour un mouvement progressif et fluide.

Une idée reçue à corriger d'abord

On pourrait naturellement penser que le serveur calcule la physique de chaque véhicule et la diffuse à tous les joueurs, un peu comme il fait autorité sur l'économie ou l'inventaire (vus dans d'autres leçons). Ce n'est PAS le cas pour la physique des entités : c'est un client précis, appelé le owner (propriétaire réseau) de l'entité, qui calcule sa position et sa vitesse localement, puis les diffuse aux autres. Le serveur, lui, ne fait généralement que relayer ces mises à jour entre clients.

Pourquoi ce choix de conception

Calculer la physique de centaines de véhicules simultanément sur le serveur, pour ensuite tout retransmettre à chaque client, serait extrêmement coûteux et ajouterait un délai perceptible. Déléguer ce calcul au client le plus concerné (typiquement celui qui conduit le véhicule, ou celui qui en est le plus proche) réduit la latence perçue et répartit la charge de calcul entre toutes les machines connectées plutôt que de tout concentrer sur le serveur.

Le piège du "téléport" qui casse tout

Un réflexe naturel pour déplacer une entité serait d'utiliser SetEntityCoords de façon répétée. Sur une entité synchronisée en réseau, cela force les autres clients à "rattraper" brutalement cette nouvelle position à chaque appel, provoquant un effet visuel de saccades ou de téléportations ("rubber-banding"). Il vaut mieux laisser la physique native du jeu gérer le mouvement progressif, qui se synchronise naturellement de façon fluide.

Les statebags, un pont pour les données custom

Les statebags, vus en leçon 12, trouvent ici une application concrète très utile : synchroniser un état personnalisé propre à une entité (niveau d'essence, état du moteur) sans avoir à écrire un système d'events dédié, en le rattachant directement à l'entité du véhicule.

Contrôler qui a l'autorité

Certains cas (un véhicule piloté par un script, un PNJ transporteur) exigent que l'autorité réseau reste fixe et ne change jamais automatiquement de propriétaire. SetNetworkIdCanMigrate permet de figer explicitement ce comportement, plutôt que de laisser FiveM décider seul du transfert d'autorité selon la proximité des joueurs.

Commandes & code

Synchronisation avancée des véhicules et entités

FiveM utilise une réplication d'état pilotée par un "entity owner" (souvent un joueur, pas le serveur) : comprendre ce modèle évite les téléportations fantômes et la désynchronisation réseau.

lua
-- Chaque entité réseau a UN owner à un instant T, qui calcule sa physique et la broadcast aux autres
-- Le serveur ne simule PAS la physique : il route les mises à jour entre clients (sauf en state-aware)
local vehicle = GetVehiclePedIsIn(PlayerPedId(), false)

if NetworkGetEntityOwner(vehicle) == PlayerId() then
    -- Ce client est propriétaire réseau du véhicule : c'est LUI qui fait autorité sur la position/vitesse
    print("Je suis owner de ce véhicule, ma physique locale fait autorité")
else
    -- Sinon, on reçoit des mises à jour de position depuis l'owner, la physique locale est "prédictive" seulement
    print("Je ne suis pas owner, ma copie locale est une prédiction interpolée")
end
lua
-- Ajuster la fréquence et la précision de sync pour un véhicule qui a un rôle spécial (ex: course, cascade)
local vehicle = CreateVehicle(GetHashKey('adder'), x, y, z, heading, true, false)
SetNetworkIdExistsOnAllMachines(NetworkGetNetworkIdFromEntity(vehicle), true)

-- SetVehicleAlarm/SetEntityDistanceCullingRadius influencent la sync réseau, pas juste le rendu visuel
SetEntityDistanceCullingRadius(vehicle, 500.0)   -- reste synchronisé/répliqué même loin des autres joueurs
SetVehicleDoorsLocked(vehicle, 2)                 -- état répliqué à tous les clients via l'owner

-- Éviter de spammer SetEntityCoords sur une entité réseauée : ça force des corrections brutales (rubber-banding)
-- côté clients distants. Préférer les natives de mouvement natif (physique du jeu) plutôt que la téléportation.
lua
-- Pattern "state bag" pour synchroniser un état de véhicule custom (ex: niveau d'essence) sans event manuel
-- côté serveur ou client owner
local vehicleEntity = GetVehiclePedIsIn(PlayerPedId(), false)
local vehState = Entity(vehicleEntity).state

vehState:set('fuelLevel', 87.5, true)   -- true = replicate : synchronisé automatiquement à TOUS les clients concernés
vehState:set('engineHealth', 950.0, true)

-- Côté n'importe quel client (y compris ceux qui ne sont PAS owner) : lecture réactive
AddStateBagChangeHandler('fuelLevel', nil, function(bagName, key, value, reserved, replicated)
    local entity = GetEntityFromStateBagName(bagName)
    if DoesEntityExist(entity) then
        print(('Véhicule %d: essence = %.1f'):format(entity, value))
    end
end)
lua
-- Gérer le changement d'owner explicitement pour un véhicule qui doit garder une autorité serveur (ex: PNJ transport)
-- SetNetworkIdCanMigrate empêche/autorise le transfert automatique de propriété réseau
local netId = NetworkGetNetworkIdFromEntity(vehicle)
SetNetworkIdCanMigrate(netId, false)   -- empêche la migration automatique d'owner (utile pour un script qui pilote le véhicule)

-- Forcer un changement d'owner manuellement (nécessite d'être exécuté côté client actuellement owner)
-- utile pour transférer l'autorité à un joueur qui monte dans le véhicule
if IsPedInAnyVehicle(PlayerPedId(), false) then
    SetNetworkIdCanMigrate(netId, true)
    SetVehicleAlwaysOwnedByServer(vehicle, false)
end

Résumé

  • La physique d'une entité réseau est calculée par son "owner" (souvent un joueur), pas par le serveur.
  • Éviter SetEntityCoords répété sur une entité synchronisée : privilégier la physique native pour éviter le rubber-banding.
  • Les state bags (Entity().state) répliquent un état custom automatiquement, sans event manuel à écrire.
  • SetNetworkIdCanMigrate contrôle si l'autorité réseau peut changer de propriétaire, utile pour les entités pilotées par script.

Exercices pratiques

1 disponible
1

Mission : réparer une course scénarisée qui saccade

Objectif : Diagnostiquer un bug de rubber-banding causé par un mauvais choix de native, puis figer l'autorité réseau d'un véhicule scénarisé.

Contexte

Un script de course scénarisée pour technologik_rp doit faire avancer un véhicule automatiquement sur un circuit prédéfini. Le développeur fait avancer le véhicule en appelant SetEntityCoords toutes les 100ms pour le déplacer point par point sur le tracé. En test, les autres joueurs présents voient le véhicule saccader violemment, comme s'il se téléportait plusieurs fois par seconde.

Résoudre l’exercice →