Técnicas de autoridade do servidor

*Este conteúdo é traduzido por IA (Beta) e pode conter erros. Para ver a página em inglês, clique aqui.

Este guia descreve várias técnicas para criar jogos multiplayer de alta qualidade e suaves usando o modelo de autoridade do servidor.

Criação preditiva de instâncias (stitching de instâncias)

O stitching de instâncias permite que scripts de cliente criem preditivamente Instances dentro de callbacks de RunService:BindToSimulation(). O cliente cria a Instance imediatamente, sem esperar por uma rodada do servidor; quando a cópia autoritativa do servidor chega, a instância criada pelo cliente e a cópia autoritativa do servidor são mescladas em uma só. Do ponto de vista do seu script, a Instance existe imediatamente e está consistente com o servidor.

O stitching de instâncias é útil em casos onde uma instância deve ser visível e ativa no cliente assim que possível. Embora o servidor eventualmente replique qualquer instância que o cliente precise (junto com quaisquer efeitos que tiveram no mundo), esse processo gera pelo menos uma latência de rodada devido à comunicação com o servidor. Exemplos incluem disparar um lançador de foguetes e criar restrições de física — sem stitching, o cliente verá o foguete aparecer a uma grande distância dele, ou algum tremor quando as novas restrições se replicarem para ele.

Comportamento técnico

O stitching de instâncias funciona gerando o mesmo GUID determinístico tanto no cliente quanto no servidor. O GUID é derivado de quatro entradas: o tipo da Instance sendo criada, a identidade da fonte (veja abaixo), o quadro de simulação atual e um contador de chamadas por script que é redefinido a cada quadro.

Se cliente e servidor concordarem com as entradas, eles produzem GUIDs correspondentes e o stitch é bem-sucedido.

Implementação

Para utilizar o stitching de instâncias, chame Instance.new(), Instance:Clone() ou Instance.fromExisting() dentro de um callback de BindToSimulation() de um ModuleScript que é exigido tanto no cliente quanto no servidor. Nada mais é necessário do seu lado; o sistema lida automaticamente com a atribuição de GUID e reconciliação.

Você pode definir livremente propriedades que não têm acesso de simulação, como Name, Size ou Parent em uma instância antes de ela ser parentada no DataModel.

Simulação (ModuleScript) - Criar Instância em um 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 -- A parte agora está no DataModel; quaisquer alterações fora da simulação gerarão erros após isso
-- A parte existe imediatamente no cliente e será reconciliada com o servidor
end)
end
return Simulation

Instance:Clone() e Instance.fromExisting() stitch corretamente quando a instância fonte foi replicada para cliente e servidor; ambos os lados clonam a partir de GUIDs fonte correspondentes e produzem GUIDs preditivos correspondentes.

Simulação (ModuleScript) - Clonar Instância em um Callback de BindToSimulation()

local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- uma Instância replicada
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- A hierarquia clonada é stitched com a cópia autoritativa do servidor
end)
end
return Simulation

Suavização de posição

Você pode suavizar visualmente a posição de objetos sincronizados com previsão errada renderizando um objeto diferente do que está sendo simulado.

  1. Torne o objeto simulado invisível.
  2. Crie um objeto renderer como um clone sem massa, não colidível, apenas visual para rastrear o objeto simulado.
  3. Anexe um script ao objeto renderer que rastreia suavemente a posição do objeto invisível e simulado. Essa separação entre renderização e simulação permite que você altere a posição do objeto renderer para criar uma experiência visualmente suave.

No seguinte exemplo Script, o objeto renderizado (pai) rastreia suavemente o objeto simulado. O objeto renderizado está sempre ligeiramente "atrasado" em relação ao objeto simulado, o que geralmente é aceitável, mas pode ser indesejável em certas situações.

Rastrear Suavemente a Posição de BasePart com um Objeto Renderer

local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
local Workspace = game:GetService("Workspace")
-- Objeto a ser suavemente rastreado
local smoothTarget:BasePart = Workspace.SimulatedPart
-- Objeto visual que será suavizado
local renderer:BasePart = script.Parent
-- Tempo para suavizar; menor significa mais rápido
local smoothTime = 0.07
-- Armazena dados necessários para calcular a posição suave
local smoothVelocity = Vector3.new()
-- Desabilita a física do objeto renderer
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Rastreia suavemente o objeto alvo
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)

O jogo de exemplo Soccer usa uma variação dessa técnica para ativar e desativar a suavização de posição para a bola de futebol de forma mais inteligente. Especificamente, a bola de futebol suaviza sua posição apenas quando a bola simulada "saltou" longe o suficiente da bola renderizada. Essa abordagem oferece o melhor dos dois mundos: a bola de futebol não tem latência visual em condições normais, e o jogo suavemente interpola sua posição apenas após a bola simulada saltar inesperadamente para uma nova localização, provavelmente devido a um artefato de rede ou alteração no lado do servidor.

Escrevendo código de animação

Sob a autoridade do servidor, a simulação do cliente pode ser revertida e re-simulada quando o servidor corrige uma previsão errada. Durante a reversão, o estado da animação é retrocedido, o que significa que AnimationTrack que você armazenou em quadros anteriores pode não ser mais válido.

Lógica de animação de espelho

Como com qualquer lógica fundamental de jogabilidade, a lógica para controlar animações deve estar sincronizada entre servidor e cliente, ou pode haver previsões erradas e comportamento trepidante. Confira sincronização de simulação para um padrão que vincula funções através de RunService:BindToSimulation() em um ModuleScript que é inicializado tanto no cliente quanto no servidor.

Evitar cache de faixas

Um padrão comum em scripts sem autoridade de servidor é armazenar objetos AnimationTrack no tempo de carregamento e reutilizá-los indefinidamente. Esse padrão falha em um jogo autoritativo do servidor quando o servidor corrige uma previsão errada e o cliente retrocede/reproduz sua simulação com dados corrigidos. Se o seu script ainda tiver uma referência a uma faixa parada ou substituída, chamadas como AdjustWeight() ou AdjustSpeed() operarão em uma faixa que não está mais representada visualmente.

Cache de Faixas no Cliente (Não Confiável)

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")
-- Cache de faixas de animação
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)

Em vez de manter objetos de faixa, armazene os IDs de animação (ou instâncias Animation) e consulte o Animator para a faixa ativa sempre que precisar interagir com ela. Duas APIs estão disponíveis para isso:

  • Animator:GetTrackByAnimationId() — Retorna a faixa atualmente ativa para um ID de animação específico ou nil se não houver animações ativas com esse ID. Use isso quando souber qual animação específica está procurando.
  • Animator:GetPlayingAnimationTracks() — Retorna todas as faixas ativas (tocando, esmaecendo ou pausadas). Use isso quando precisar iterar sobre todas as que estão ativas (por exemplo, para parar todas as animações ou encontrar faixas por algum critério).

ModuleScript chamado CustomAnimate em ReplicatedStorage:

CustomAnimate

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Armazena referências de animação (não faixas carregadas)
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

Tocando sons e efeitos visuais

Em uma simulação preditiva, é possível acionar efeitos ou sons para eventos que o cliente previu que aconteceriam, mas que nunca ocorreram no servidor. O sistema de renderização deve estar preparado para "desfazer" quaisquer efeitos preditivos. Por exemplo, um cliente pode prever que uma granada explodiu e acionar um efeito de partículas, mas se outro jogador desativou a granada, o cliente deve ocultar o efeito de partículas.

Uma boa estratégia para renderizar uma simulação preditiva é sincronizar um padrão de máquina de estados dentro do loop de simulação e renderizar mudanças no estado em uma função de passo de renderização. O seguinte exemplo simula uma granada com um padrão de máquina de estados:

Máquina de Estados Simples para Rastrear uma 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)
-- Inicializa o estado vazio da granada
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Incrementa o timer da granada
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Explode granadas acesas
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

Com a máquina de estados anterior em vigor, você pode renderizar efeitos de granada em uma conexão RunService.RenderStepped dentro de um script separado com base no estado de granada sincronizado:

Renderizar Partículas e Sons com base no Estado de Granada Sincronizado

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Destacar instância para indicar o estado da 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")
-- Emite as partículas acesas se a granada estiver acesa
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Reproduz o emissor de explosão se a granada acabou de explodir
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
-- Altera a cor do destaque da granada com base no estado e tempo
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)

Projetando em torno da latência da rede

Certos mecanismos de jogabilidade se adaptam melhor a multiplayer em rede do que outros mecanismos. Os jogadores sempre terão algum atraso entre o momento em que outro jogador realiza uma ação e quando eles recebem a entrada desse jogador. A melhor maneira de criar um jogo multiplayer super suave é projetar seu jogo levando em conta essas limitações.

Por exemplo, um jogo com uma aceleração mais lenta no movimento do jogador parecerá mais suave do que um com maior aceleração, pois a diferença de posição causada pela latência da rede da entrada do jogador será menor do que em um jogo com maior aceleração.

Como outro exemplo, um mecanismo de jogabilidade onde os jogadores podem instantaneamente acionar uma grande explosão pressionando uma entrada terá mais artefatos de rede do que se a explosão for retardada após a entrada, como se acendendo um fusível. Isso coloca a resimulação no efeito do fusível em vez do efeito da explosão, o que é um artefato de rede menos perceptível.

Prevendo entradas de outros jogadores

Por padrão, o Roblox não encaminha as entradas de cada cliente para todos os outros clientes. Se isso é certo para o seu jogo depende do seu design:

  • Para o movimento básico de humanoides, o comportamento padrão significa que os movimentos de outros personagens jogadores não são extrapolados a partir do estado autoritativo do servidor e, como resultado, outros personagens jogadores não terão previsões erradas, mas serão renderizados ligeiramente no passado.
  • Em um jogo de corrida, por outro lado, o comportamento padrão significa que os clientes não saberão se outros jogadores estão aplicando o acelerador ou outras entradas, portanto, outros carros podem parecer atrás do jogador local, mesmo se estiverem realmente à frente. Para aliviar isso, você pode armazenar entradas dos jogadores em atributos no servidor e operar nesses atributos sincronizados do lado do cliente usando RunService:BindToSimulation() como demonstrado no seguinte exemplo de código e no modelo Racing. Essa abordagem permite usar atributos como entradas para sua simulação, para que as entradas dos jogadores sejam completamente replicadas.
Armazenando Entrada do Jogador em 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)
-- Escreva quaisquer outras entradas em atributos...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Encaminha entradas do servidor para todos os 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
-- Escreve entradas do jogador local como atributos
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Use os atributos como entradas para o jogo
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Aplique o acelerador ao veículo do jogador
end
end
end)
end)
return module

Depuração

Existem algumas novas ferramentas e técnicas que você pode usar para depurar um jogo autoritativo do servidor.

Visualizador de autoridade do servidor

Pressionar CtrlShiftF6 (Windows) ou ShiftF6 (Mac) abre o visualizador de autoridade do servidor do Studio, que mostra várias peças-chave de informação:

DetalhesDescrição
Taxa de sucesso de previsão de instânciasA porcentagem de instâncias previstas corretamente nos últimos 8 segundos.
Taxa de aceitação de entradaA porcentagem de todas as entradas dos jogadores que chegaram a tempo no servidor. Entradas tardias reduzirão esse número.
Delta de passo cliente-servidorO número de quadros entre o cliente e o servidor, incluindo o tempo de união do cliente. A estabilidade desse número representa a estabilidade de sua conexão com o servidor.
FPS do heartbeat do RCCA taxa de quadros da simulação no servidor. Se esse número cair abaixo de 59, o servidor não poderá acompanhar a simulação e a qualidade do jogo diminuirá.
Contagem de instâncias previstasO número de instâncias que seu cliente está prevendo.
Contagem de motivos para queda de entrada

O número de vezes que o servidor descartou uma entrada por cada motivo:

  • [x] muito velha — As entradas chegaram tarde, indicando que sua rede piorou ou que o cliente não conseguiu acompanhar a simulação.
  • [x] fora de ordem — Ocorreu um erro de rede que fez com que suas entradas fossem rearranjadas e descartadas.
  • [x] buffer cheio — O servidor não conseguiu armazenar seu input. Ou sua rede melhorou repentinamente ou o servidor não conseguiu acompanhar a simulação.

Raio de simulação

Ao confiar em previsão automática (Enum.PredictionMode.Automatic), você pode visualizar o raio de previsão ao redor do seu personagem jogador ativando Are Regions Enabled nas configurações do Studio (AltS no Windows; S no Mac). O cilindro verde indica o alcance ao redor do seu personagem no qual instâncias são previstas, e seu raio cresce e diminui com base nas características de desempenho do dispositivo.

Raio de simulação ao redor do personagem jogador com autoridade do servidor em execução
©2026 Roblox Corporation, Roblox, o logotipo Roblox e Powering Imagination estão entre nossas marcas registradas e não registradas nos EUA e em outros países.