Techniques d'autorité serveur

*Ce contenu est traduit en utilisant l'IA (Beta) et peut contenir des erreurs. Pour consulter cette page en anglais, clique ici.

Ce guide décrit diverses techniques pour créer des jeux multijoueurs de haute qualité et fluides en utilisant le modèle d'autorité serveur.

Création d'instance prédictive (assemblage d'instances)

L'assemblage d'instances permet aux scripts clients de créer de manière prédictive Instances à l'intérieur des rappels RunService:BindToSimulation(). Le client crée l'Instance immédiatement sans attendre un aller-retour avec le serveur ; lorsque la copie autorisée du serveur arrive, l'instance créée par le client et la copie autorisée du serveur sont fusionnées en une seule. D'un point de vue de votre script, l'Instance existe immédiatement et est cohérente avec le serveur.

L'assemblage d'instances est utile dans les cas où une instance doit être visible et active sur le client le plus tôt possible. Bien que le serveur finisse par répliquer toute instance dont le client a besoin (avec tous les effets qu'elles ont sur le monde), ce processus entraîne au moins un aller-retour de latence en raison de la communication avec le serveur. Des exemples incluent le tir d'un lance-roquettes et la création de contraintes de physique — sans assemblage, le client verra la fusée apparaître loin de lui ou ressentira des tremblements lorsque les nouvelles contraintes seront répliquées.

Comportement technique

L'assemblage d'instances fonctionne en générant le même GUID déterministe à la fois sur le client et le serveur. Le GUID est dérivé de quatre entrées : le type de l'Instance créée, l'identité de la source (voir ci-dessous), le cadre de simulation actuel et un compteur d'appels par script qui se réinitialise à chaque image.

Si le client et le serveur s'accordent sur les entrées, ils produisent des GUID correspondants et l'assemblage réussit.

Mise en œuvre

Pour utiliser l'assemblage d'instances, appelez Instance.new(), Instance:Clone() ou Instance.fromExisting() à l'intérieur d'un rappel RunService:BindToSimulation() d'un ModuleScript requis à la fois sur le client et le serveur. Rien d'autre n'est requis de votre part ; le système gère automatiquement l'attribution et la réconciliation des GUID.

Vous pouvez librement définir des propriétés non-d'accès simulation telles que Name, Size, ou Parent sur une instance avant qu'elle ne soit parentée dans le DataModel.

Simulation (ModuleScript) - Créer une instance dans un rappel BindToSimulation()

local RunService = game:GetService("RunService")
local Simulation = {}
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local part = Instance.new("Part")
part.Name = "PredictedPart"
part.Size = Vector3.new(2, 2, 2)
part.Parent = workspace -- La pièce est maintenant dans le DataModel ; tout changement d'accès non simulation générera une erreur après cela
-- La pièce existe immédiatement sur le client et sera conciliée avec le serveur
end)
end
return Simulation

Instance:Clone() et Instance.fromExisting() s'assemblent correctement lorsque l'instance source a été répliquée à la fois sur le client et le serveur ; les deux côtés clonent depuis des GUID sources correspondants et produisent des GUID prédits correspondants.

Simulation (ModuleScript) - Cloner une instance dans un rappel BindToSimulation()

local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- une instance répliquée
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- La hiérarchie clonée est assemblée avec la copie autorisée du serveur
end)
end
return Simulation

Lissage de position

Vous pouvez visuellement lisser la position des objets synchronisés mal prédits en rendant un objet différent de celui qui est simulé.

  1. Rendez l'objet simulé invisible.
  2. Créez un objet renderer comme un clone non-collidable et sans masse pour suivre l'objet simulé.
  3. Attachez un script à l'objet renderer qui suit en douceur la position de l'objet simulé invisible. Cette séparation entre le rendu et la simulation vous permet de modifier la position de l'objet renderer pour créer une expérience visuellement fluide.

Dans l'exemple suivant de Script, l'objet rendu (parent) suit en douceur l'objet simulé. L'objet rendu est toujours légèrement "en retard" par rapport à l'objet simulé, ce qui est généralement acceptable mais peut être indésirable dans certaines situations.

Suivre en douceur la position de BasePart avec un objet Renderer

local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
-- Objet à suivre en douceur
local smoothTarget:BasePart = Workspace.SimulatedPart
-- Objet visuel qui sera adouci
local renderer:BasePart = script.Parent
-- Temps de lissage ; plus petit signifie plus rapide
local smoothTime = 0.07
-- Stocker les données nécessaires pour calculer la position lissée
local smoothVelocity = Vector3.new()
-- Désactiver la physique de l'objet renderer
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Suivre en douceur l'objet cible
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)

Le jeu d'exemple Soccer utilise une variation de cette technique pour activer et désactiver plus intelligemment le lissage de position pour le ballon de soccer. En particulier, le ballon de soccer ne lisse sa position que lorsque le ballon simulé a "sauté" suffisamment loin du ballon rendu. Cette approche offre le meilleur des deux mondes : le ballon de soccer n'a pas de latence visuelle dans des conditions normales, et le jeu interpole en douceur sa position uniquement après que le ballon simulé a soudainement sauté à un nouvel emplacement, probablement en raison d'un artefact réseau ou d'un changement côté serveur.

Écriture de code d'animation

Sous l'autorité du serveur, la simulation du client peut être annulée et resimulée lorsque le serveur corrige une mauvaise prédiction. Pendant l'annulation, l'état d'animation est rembobiné, ce qui signifie que l'AnimationTrack que vous avez mis en cache dans les images précédentes peut ne plus être valide.

Logique d'animation miroir

Comme pour toute logique de jeu fondamentale, la logique de contrôle des animations doit être synchronisée entre le serveur et le client ou il peut y avoir des prévisions incorrectes et un comportement saccadé. Voir synchronisation de simulation pour un modèle qui lie des fonctions via RunService:BindToSimulation() dans un ModuleScript qui est initialisé à la fois sur le client et le serveur.

Éviter le cache de pistes

Un modèle courant dans les scripts sans autorité serveur est de mettre en cache des objets AnimationTrack au moment du chargement et de les réutiliser indéfiniment. Ce modèle échoue dans un jeu autorisé par le serveur lorsque le serveur corrige une prédiction incorrecte et que le client rembobine/rejoue sa simulation avec des données corrigées. Si votre script conserve une référence à une piste arrêtée ou remplacée, des appels comme AdjustWeight() ou AdjustSpeed() fonctionneront sur une piste qui n'est plus visuellement représentée.

Mettre en cache les pistes sur le client (non fiable)

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
-- Mettre en cache les pistes d'animation
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)

Au lieu de conserver des objets de piste, stockez les ID d'animation (ou instances Animation) et interrogez le Animator pour la piste active lorsque vous devez interagir avec elle. Deux API sont disponibles pour cela :

  • Animator:GetTrackByAnimationId() — Renvoie la piste active actuelle pour un ID d'animation spécifique, ou nil s'il n'y a pas d'animations actives avec cet ID. Utilisez ceci lorsque vous savez quelle animation spécifique vous recherchez.
  • Animator:GetPlayingAnimationTracks() — Renvoie toutes les pistes actives (jouées, en train de s'estomper ou en pause). Utilisez ceci lorsque vous devez parcourir tout ce qui est actif (par exemple, pour arrêter toutes les animations ou trouver des pistes par certains critères).

ModuleScript nommé CustomAnimate dans ReplicatedStorage :

CustomAnimate

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Stocker les références d'animation (pas de pistes chargées)
local animations = {
WalkForward = ReplicatedStorage.Animations.WalkForward,
}
local function getOrLoadTrack(animator: Animator, animation: Animation): AnimationTrack
local track = animator:GetTrackByAnimationId(animation.AnimationId)
if not track then
track = animator:LoadAnimation(animation)
end
return track
end
CustomAnimate.SyncAnimations = function(character)
local humanoid = character:WaitForChild("Humanoid")
local animator = humanoid:WaitForChild("Animator")
RunService:BindToSimulation(function(dt: number)
local walkTrack = getOrLoadTrack(animator, animations.WalkForward)
if not walkTrack.isPlaying then
walkTrack.Looped = true
walkTrack.Priority = Enum.AnimationPriority.Core
walkTrack:Play()
end
walkTrack:AdjustSpeed(1 + math.cos(time()))
end)
end
return CustomAnimate

Jouer des sons et des effets visuels

Dans une simulation prédictive, il est possible de déclencher des effets ou des sons pour des événements que le client prédisait se produire mais qui ne se sont jamais produits sur le serveur. Le système de rendu devrait être prêt à "annuler" tout effet mal prédit. Par exemple, un client pourrait prédire qu'une grenade a explosé et déclencher un effet de particules, mais si un autre joueur désamorçait la grenade, le client devrait cacher l'effet de particules.

Une bonne stratégie pour rendre une simulation prédictive est de synchroniser un modèle de machine d'état dans la boucle de simulation et de rendre les changements d'état dans une fonction de rendu d'étape. L'exemple suivant simule une grenade avec un modèle de machine d'état :

Machine d'État Simple pour Suivre une Grenade (ModuleScript)

local Workspace = game:GetService("Workspace")
local module = {}
module.GrenadeStates = {
Idle = 0,
Lit = 1,
Exploded = 2,
Defused = 3,
}
module.GrenadeExplodeTime = 3.0
module.Initialize = function(grenade)
RunService:BindToSimulation(function(deltaTime)
-- Initialiser l'état vide de la grenade
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Incrémenter le timer de la grenade
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Faire exploser les grenades enflammées
if grenadeState == module.GrenadeStates.Lit then
if timer >= module.GrenadeExplodeTime then
grenadeState = module.GrenadeStates.Exploded
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
end
end)
end
return module

Avec la machine d'état précédente en place, vous pouvez rendre des effets de grenade dans une connexion RunService.RenderStepped dans un script séparé basé sur l'état synchronisé de la grenade :

Rendre des Particules et des Sons basés sur l'État Synchronisé de la Grenade

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Instance de surbrillance pour indiquer l'état de la grenade
local highlight = Instance.new("Highlight")
highlight.Parent = grenade
highlight.FillTransparency = 1
highlight.OutlineTransparency = 1
highlight.DepthMode = Enum.HighlightDepthMode.Occluded
RunService.RenderStepped:Connect(function(deltaTime: number)
local grenadeState = grenade:GetAttribute("State")
local grenadeTimer = grenade:GetAttribute("Timer")
-- Émettre les particules enflammées si la grenade est allumée
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Jouer l'émetteur d'explosion si la grenade vient d'exploser
if previousGrenadeState ~= grenadeState then
if grenadeState == Simulation.GrenadeStates.Exploded and grenadeTimer < 0.2 then
grenade.ExplosionEmitter:Emit(100)
grenade.ExplosionSound:Play()
end
previousGrenadeState = grenadeState
end
-- Changer la couleur de surbrillance de la grenade en fonction de l'état et du temps
if grenadeState == Simulation.GrenadeStates.Lit then
highlight.FillColor = Color3.fromRGB(255, 0, 0)
highlight.FillTransparency = 1 - (grenadeTimer / Simulation.GrenadeExplodeTime)
elseif grenadeState == Simulation.GrenadeStates.Idle then
highlight.FillTransparency = 1
elseif grenadeState == Simulation.GrenadeStates.Exploded then
highlight.FillTransparency = 1
elseif grenadeState == Simulation.GrenadeStates.Defused then
highlight.FillColor = Color3.fromRGB(0, 255, 125)
highlight.FillTransparency = 0.5
end
end)

Concevoir autour de la latence réseau

Certaines mécaniques de jeu se prêtent mieux au multijoueur en réseau que d'autres. Les joueurs auront toujours un certain retard entre le moment où un autre joueur effectue une action et le moment où ils reçoivent l'entrée de ce joueur. Le meilleur moyen de créer un jeu multijoueur très fluide est de concevoir votre jeu en tenant compte de ces limitations.

Par exemple, un jeu avec une accélération plus lente sur le mouvement des joueurs apparaîtra plus fluide qu'un jeu avec une accélération plus élevée, car la différence de position causée par la latence réseau de l'entrée du joueur sera moins importante que dans un jeu avec une accélération plus élevée.

Comme autre exemple, une mécanique de jeu où les joueurs peuvent déclencher immédiatement une grande explosion en appuyant sur une entrée aura plus d'artefacts réseau que si l'explosion est retardée après l'entrée, comme en allumant une mèche. Cela concentre la resimulation sur l'effet de mèche au lieu de l'effet d'explosion, ce qui est un artefact réseau moins perceptible.

Prédire les entrées des autres joueurs

Par défaut, Roblox ne transfère pas les entrées de chaque client à tous les autres clients. Que cela soit approprié pour votre jeu dépend de son design :

  • Pour le mouvement humanoïde de base, le comportement par défaut signifie que les mouvements des autres personnages joueurs ne sont pas extrapolés à partir de l'état autoritaire du serveur et, par conséquent, les autres personnages joueurs ne prévoient pas mal mais s'affichent légèrement dans le passé.
  • Dans un jeu de course, en revanche, le comportement par défaut signifie que les clients ne sauront pas si d'autres joueurs appliquent l'accélération ou d'autres entrées, donc d'autres voitures peuvent apparaître derrière le joueur local même si elles sont en réalité devant. Pour atténuer cela, vous pouvez stocker les entrées des joueurs dans des attributs sur le serveur et opérer sur ces attributs synchronisés côté client à l'aide de RunService:BindToSimulation(), comme démontré dans l'exemple de code suivant et le modèle Racing. Cette approche vous permet d'utiliser des attributs comme entrées pour votre simulation afin d'avoir des entrées de joueur entièrement répliquées.
Stocker l'entrée du joueur dans des attributs (ModuleScript)

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local module = {}
module.storePlayerInput = function(player:Player, humanoidRootPart:BasePart)
local inputContext:InputContext = player.PlayerGui.InputContext
local throttle = inputContext.DefuseAction:GetState()
humanoidRootPart:SetAttribute("Throttle", throttle)
-- Écrire d'autres entrées dans des attributs...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Transférer les entrées du serveur à tous les clients
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- Écrire les entrées du joueur local comme des attributs
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Utiliser les attributs comme entrées pour le jeu
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Appliquer l'accélération au véhicule du joueur
end
end
end)
end)
return module

Débogage

Il existe de nouveaux outils et techniques que vous pouvez utiliser pour déboguer un jeu à autorité serveur.

Visualiseur d'autorité serveur

En appuyant sur CtrlShiftF6 (Windows) ou ShiftF6 (Mac), vous ouvrez le visualiseur d'autorité serveur de Studio qui montre plusieurs informations clés :

DétailsDescription
Taux de succès de prédiction d'instanceLe pourcentage d'instances correctement prédites au cours des 8 dernières secondes.
Taux d'acceptation des entréesLe pourcentage de toutes les entrées des joueurs qui sont arrivées à temps sur le serveur. Les entrées tardives abaisseront ce nombre.
Delta de pas client-serveurLe nombre de frames entre le client et le serveur, y compris le temps d'entrée du client. La stabilité de ce nombre représente la stabilité de votre connexion au serveur.
RCC heartbeat FPSLe taux de frames de la simulation sur le serveur. Si ce nombre tombe en dessous de 59, le serveur ne peut pas suivre la simulation et la qualité du jeu se dégradera.
Nombre d'instances préditesLe nombre d'instances que votre client est en train de prédire.
Comptes de raisons de perte d'entrée

Le nombre de fois que le serveur a perdu une entrée pour chaque raison :

  • [x] trop ancien — Les entrées sont arrivées trop tard, ce qui signifie que votre réseau s'est dégradé ou que le client ne pouvait pas suivre la simulation.
  • [x] hors d'ordre — Un bug réseau s'est produit, ce qui a conduit à un réarrangement et à un rejet de vos entrées.
  • [x] tampon plein — Le serveur n'a pas pu mettre en mémoire tampon votre entrée. Soit votre réseau s'est soudainement amélioré, soit le serveur n'a pas pu suivre la simulation.

Rayon de simulation

Lorsque vous comptez sur une prédiction automatique (Enum.PredictionMode.Automatic), vous pouvez visualiser le rayon de prédiction autour de votre personnage joueur en activant Are Regions Enabled dans les paramètres de Studio (AltS sous Windows ; S sous Mac). Le cylindre vert indique la plage autour de votre personnage dans laquelle les instances sont prédites, et son rayon s'agrandit et se rétrécit en fonction des caractéristiques de performance de l'appareil.

Rayon de simulation autour du personnage joueur avec autorité serveur en cours d'exécution
©2026 Société Roblox. Roblox, le logo Roblox et Powering Imagination font partie de nos marques déposées aux États-Unis et dans d'autres pays.