Técnicas de autoridad del servidor

*Este contenido se traduce usando la IA (Beta) y puede contener errores. Para ver esta página en inglés, haz clic en aquí.

Esta guía describe varias técnicas para crear juegos multijugador de alta calidad y suaves utilizando el modelo de autoridad del servidor.

Creación predictiva de instancias (costura de instancias)

La costura de instancias permite que los scripts del cliente creen predictivamente Instances dentro de los callbacks de RunService:BindToSimulation(). El cliente crea la Instance inmediatamente sin esperar un viaje de ida y vuelta al servidor; cuando llega la copia autoritativa del servidor, la instancia creada por el cliente y la copia autoritativa del servidor se fusionan en una sola. Desde la perspectiva de tu script, la Instance existe de inmediato y es consistente con el servidor.

La costura de instancias es útil en casos donde una instancia debe ser visible y activa en el cliente lo antes posible. Aunque el servidor eventualmente replicará cualquier instancia que necesite el cliente (junto con cualquier efecto que hayan tenido en el mundo), este proceso incurre en al menos un viaje de ida y vuelta de latencia debido a la comunicación con el servidor. Ejemplos incluyen disparar un lanza cohetes y crear restricciones físicas — sin costura, el cliente verá que el cohete aparece muy lejos de ellos, o algún temblor cuando se repitan las nuevas restricciones.

Comportamiento técnico

La costura de instancias funciona generando el mismo GUID determinista tanto en el cliente como en el servidor. El GUID se deriva de cuatro entradas: el tipo de Instance que se está creando, la identidad de la fuente (ver más abajo), el fotograma de simulación actual y un contador de llamadas por script que se reinicia en cada fotograma.

Si el cliente y el servidor están de acuerdo en las entradas, producen GUIDs coincidentes y la costura tiene éxito.

Implementación

Para utilizar la costura de instancias, llama a Instance.new(), Instance:Clone() o Instance.fromExisting() dentro de un callback de RunService:BindToSimulation() desde un ModuleScript que se requiera tanto en el cliente como en el servidor. No se requiere nada más de tu parte; el sistema maneja automáticamente la asignación y conciliación de GUID.

Puedes establecer libremente propiedades no accesibles por simulación como Name, Size, o Parent en una instancia antes de que se parenten en el DataModel.

Simulación (ModuleScript) - Crear instancia en un callback de 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 parte ahora está en el DataModel; cualquier cambio de acceso no simulado generará un error después de esto
-- La parte existe inmediatamente en el cliente y será reconciliada con el servidor
end)
end
return Simulation

Instance:Clone() y Instance.fromExisting() se costuran correctamente cuando la instancia fuente fue replicada tanto en el cliente como en el servidor; ambos lados clonan de GUIDs fuente coincidentes y producen GUIDs predictivos coincidentes.

Simulación (ModuleScript) - Clonar instancia en un callback de BindToSimulation()

local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- una instancia replicada
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- La jerarquía clonada se cose con la copia autoritativa del servidor
end)
end
return Simulation

Suavizado de posiciones

Puedes suavizar visualmente la posición de objetos sincronizados mal predichos renderizando un objeto diferente al que se está simulando.

  1. Haz que el objeto simulado sea invisible.
  2. Crea un objeto renderer como un clon sin masa, no colisionable y solo visual para seguir el objeto simulado.
  3. Adjunta un script al objeto renderer que siga suavemente la posición del objeto simulado e invisible. Esta separación entre renderizado y simulación te permite alterar la posición del objeto renderer para crear una experiencia visualmente suave.

En el siguiente Script de ejemplo, el objeto renderizado (padre) sigue suavemente el objeto simulado. El objeto renderizado siempre está ligeramente "detrás" del objeto simulado, lo cual es típicamente aceptable pero puede no ser deseable en ciertas situaciones.

Seguir suavemente la posición de BasePart con un renderer

local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
-- Objeto a seguir suavemente
local smoothTarget:BasePart = Workspace.SimulatedPart
-- Objeto visual que será suavizado
local renderer:BasePart = script.Parent
-- Tiempo para suavizar; menor significa más rápido
local smoothTime = 0.07
-- Almacena datos necesarios para calcular la posición suave
local smoothVelocity = Vector3.new()
-- Desactivar la física del objeto renderer
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Seguir suavemente el objeto objetivo
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)

El juego de ejemplo Soccer utiliza una variación de esta técnica para encender y apagar más inteligentemente el suavizado de posición para el balón de fútbol. Específicamente, el balón de fútbol solo suaviza su posición cuando el balón simulado se ha "saltado" lo suficientemente lejos del balón renderizado. Este enfoque proporciona lo mejor de ambos mundos: el balón de fútbol no tiene latencia visual en condiciones normales, y el juego suaviza su posición solo después de que el balón simulado ha saltado inesperadamente a una nueva ubicación, probablemente debido a un artefacto de red o un cambio del lado del servidor.

Escribir código de animación

Bajo la autoridad del servidor, la simulación del cliente puede ser retrocedida y resimulada cuando el servidor corrige una mala predicción. Durante el retroceso, el estado de la animación se retrocede, lo que significa que AnimationTrack que manejaste en fotogramas anteriores puede ya no ser válido.

Lógica de animación de espejo

Al igual que con cualquier lógica central de juego, la lógica para controlar las animaciones debe estar en sincronía entre el servidor y el cliente o puede haber malas predicciones y comportamientos entrecortados. Consulta sincronización de simulación para un patrón que vincula funciones a través de RunService:BindToSimulation() en un ModuleScript que se inicializa tanto en el cliente como en el servidor.

Evita la caché de pistas

Un patrón común en scripts que no se basan en la autoridad del servidor es almacenar objetos AnimationTrack en el tiempo de carga y reutilizarlos indefinidamente. Este patrón falla en un juego bajo autoridad del servidor cuando el servidor corrige una mala predicción y el cliente rebobina/reproduce su simulación con datos corregidos. Si tu script aún tiene una referencia a una pista detenida o reemplazada, llamadas como AdjustWeight() o AdjustSpeed() operarán en una pista que ya no se representa visualmente.

Caché de pistas en el cliente (no confiable)

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")
-- Caché de pistas de animación
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)

En lugar de mantener objetos de pista, almacena los IDs de animación (o instancias Animation) y consulta al Animator por la pista activa en vivo siempre que necesites interactuar con ella. Dos API están disponibles para esto:

  • Animator:GetTrackByAnimationId() — Devuelve la pista actualmente activa para un ID de animación específico, o nil si no hay animaciones activas con ese ID. Utiliza esto cuando sepas qué animación específica estás buscando.
  • Animator:GetPlayingAnimationTracks() — Devuelve todas las pistas activas (jugando, desvaneciéndose o pausadas). Utiliza esto cuando necesites iterar sobre todo lo que está activo (por ejemplo, para detener todas las animaciones o encontrar pistas por algún criterio).

ModuleScript llamado CustomAnimate en ReplicatedStorage:

CustomAnimate

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Almacenar referencias de animación (pistas no cargadas)
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

Reproduciendo sonidos y efectos visuales

En una simulación predictiva, es posible activar efectos o sonidos para eventos que el cliente predijo que sucederían pero que nunca ocurrieron en el servidor. El sistema de renderizado debe estar preparado para "deshacer" cualquier efecto mal predicho. Por ejemplo, un cliente puede predecir que una granada explotó y activar un efecto de partículas, pero si otro jugador desactiva la granada, el cliente debería ocultar el efecto de partículas.

Una buena estrategia para renderizar una simulación predictiva es sincronizar un patrón de máquina de estados dentro del ciclo de simulación y renderizar cambios en el estado en una función de paso de renderizado. El siguiente ejemplo simula una granada con un patrón de máquina de estados:

Máquina de Estados Simple para Rastrear una Granada (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)
-- Inicializar el estado vacío de la granada
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Incrementar el temporizador de la granada
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Explotar granadas encendidas
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

Con la máquina de estados anterior en su lugar, puedes renderizar los efectos de granada en una conexión de RunService.RenderStepped dentro de un script separado basado en el estado sincronizado de la granada:

Renderizar partículas y sonidos en base al estado sincronizado de la granada

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Instancia de resaltado para indicar el estado de la granada
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")
-- Emitir las partículas si la granada está encendida
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Reproducir el emisor de explosión si la granada acaba de explotar
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
-- Cambiar el color del resaltado de la granada según el estado y el tiempo
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)

Diseñando en torno a la latencia de la red

Ciertos mecanismos de juego se prestan mejor a multijugador en red que otros. Los jugadores siempre tendrán algún retraso entre cuando otro jugador realiza una acción y cuando reciben la entrada de ese jugador. La mejor manera de crear un juego multijugador super suave es diseñar tu juego teniendo en cuenta estas limitaciones.

Por ejemplo, un juego con una aceleración más lenta en el movimiento del jugador parecerá más suave que uno con una aceleración más alta porque la diferencia en posición causada por la latencia de red de la entrada del jugador será menor que en un juego con una aceleración más alta.

Como otro ejemplo, un mecanismo de juego donde los jugadores pueden activar instantáneamente una gran explosión al presionar una entrada tendrá más artefactos de red que si la explosión se retrasa después de la entrada, como si se estuviera encendiendo una mecha. Esto coloca la resimulación en el efecto de la mecha en lugar de en el efecto de explosión, lo cual es un artefacto de red menos notable.

Prediciendo las entradas de otros jugadores

Por defecto, Roblox no reenvía las entradas de cada cliente a cada otro cliente. Si esto es adecuado para tu juego depende de su diseño:

  • Para el movimiento básico del humanoide, el comportamiento predeterminado significa que los movimientos de los personajes de otros jugadores no se extrapolan del estado autoritativo del servidor y, como resultado, los personajes de otros jugadores no tendrán malas predicciones, sino que se renderizarán ligeramente en el pasado.
  • En un juego de carreras, por el contrario, el comportamiento predeterminado significa que los clientes no sabrán si otros jugadores están aplicando el acelerador u otras entradas, por lo que otros coches pueden aparecer detrás del jugador local incluso si están realmente adelante. Para aliviar esto, puedes almacenar las entradas de los jugadores en atributos en el servidor y operar sobre esos atributos sincronizados del lado del cliente utilizando RunService:BindToSimulation() como se demuestra en el siguiente ejemplo de código y la plantilla Racing. Este enfoque te permite utilizar atributos como entradas para tu simulación para tener entradas de jugadores completamente replicadas.
Almacenando la entrada del jugador en atributos (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)
-- Escribe cualquier otra entrada en atributos...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Reenviar entradas del servidor a todos los clientes
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- Escribir entradas del jugador local como atributos
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Utilizar los atributos como entradas para el juego
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Aplicar el acelerador al vehículo del jugador
end
end
end)
end)
return module

Depuración

Hay algunas herramientas y técnicas nuevas que puedes usar para depurar un juego autoritativo del servidor.

Visualizador de autoridad del servidor

Presionar CtrlShiftF6 (Windows) o ShiftF6 (Mac) abre el visualizador de autoridad del servidor de Studio que muestra varias piezas clave de información:

DetallesDescripción
Tasa de éxito de predicción de instanciasEl porcentaje de instancias predichas correctamente durante los últimos 8 segundos.
Tasa de aceptación de entradasEl porcentaje de todas las entradas de los jugadores que llegaron a tiempo al servidor. Las entradas tardías disminuirán este número.
Delta de paso cliente-servidorEl número de fotogramas entre el cliente y el servidor, incluyendo el tiempo de unión del cliente. La estabilidad de este número representa la estabilidad de tu conexión con el servidor.
FPS del latido RCCLa tasa de fotogramas de la simulación en el servidor. Si este número cae por debajo de 59, el servidor no puede mantenerse al día con la simulación y la calidad del juego disminuirá.
Conteo de instancias predichasEl número de instancias que tu cliente está prediciendo.
Conteos de razones de caída de entradas

El número de veces que el servidor ha descartado una entrada por cada razón:

  • [x] demasiado viejo — Las entradas llegaron tarde, significa que tu red empeoró o que el cliente no pudo mantenerse al día con la simulación.
  • [x] fuera de orden — Ocurrió un error de red que hizo que tus entradas se reorganizaran y se descartaran.
  • [x] buffer lleno — El servidor no pudo almacenar tu entrada. O bien, tu red mejoró repentinamente o el servidor no pudo mantenerse al día con la simulación.

Radio de simulación

Cuando dependas de la predicción automática (Enum.PredictionMode.Automatic), puedes visualizar el radio de predicción alrededor de tu personaje de jugador habilitando ¿Están habilitadas las regiones? en la configuración de Studio (AltS en Windows; S en Mac). El cilindro verde indica la zona alrededor de tu personaje en la que se predicen instancias, y su radio crece y decrece en función de las características de rendimiento del dispositivo.

Radio de simulación alrededor del personaje jugador con autoridad del servidor en ejecución
©2026 Roblox Corporation. Roblox, el logotipo de Roblox y "Powering Imagination" son algunas de nuestras marcas registradas y no registradas en los Estados Unidos y otros países.