Techniki autoryzacji serwera

*Ta zawartość została przetłumaczona przy użyciu narzędzi AI (w wersji beta) i może zawierać błędy. Aby wyświetlić tę stronę w języku angielskim, kliknij tutaj.

Ten przewodnik przedstawia różne techniki tworzenia gier wieloosobowych o wysokiej jakości i płynności, korzystając z modelu autoryzacji serwera.

Predictive instance creation (zszywanie instancji)

Zszywanie instancji stitching pozwala skryptom klienckim przewidywać tworzenie Instances w ramach wywołań RunService:BindToSimulation(). Klient tworzy Instance natychmiast, nie czekając na okrążenie serwera; gdy autorytatywna kopia serwera dotrze, instancja utworzona przez klienta i autorytatywna kopia serwera są scalane w jedną. Z perspektywy Twojego skryptu, Instance istnieje natychmiast i jest zgodna z tym, co ma serwer.

Zszywanie instancji jest przydatne w przypadkach, gdy instancja musi być widoczna i aktywna na kliencie tak szybko, jak to możliwe. Choć serwer ostatecznie zreplikuje wszelkie instancje potrzebne klientowi (razem z wszelkimi efektami, jakie miały na świat), proces ten wiąże się z co najmniej jedną rundą opóźnienia z powodu komunikacji z serwerem. Przykładami mogą być odpalanie wyrzutni rakiet i tworzenie ograniczeń fizycznych – bez zszywania klient zobaczy rakietę pojawiającą się daleko od niego lub pewne drgania, gdy nowe ograniczenia replikują się do niego.

Zachowanie techniczne

Zszywanie instancji działa poprzez generowanie tego samego deterministycznego GUID-u zarówno po stronie klienta, jak i serwera. GUID jest wyprowadzany z czterech elementów: rodzaju Instance, tożsamości źródła (patrz poniżej), bieżącej klatki symulacji oraz licznika wywołań na poziomie skryptu, który resetuje się przy każdej klatce.

Jeśli klient i serwer zgadzają się co do wejść, generują pasujące GUID-y i zszywanie się udaje.

Implementacja

Aby skorzystać z zszywania instancji, wywołaj Instance.new(), Instance:Clone() lub Instance.fromExisting() wewnątrz wywołania zwrotnego BindToSimulation() z ModuleScript, który jest wymagana zarówno po stronie klienta, jak i serwera. Nic więcej nie jest wymagane z Twojej strony; system automatycznie zajmie się przypisywaniem GUID-ów i pojednaniem.

Możesz swobodnie ustawić właściwości bez dostępu do symulacji, takie jak Name, Size, lub Parent w instancji przed przypisaniem jej do DataModel.

Symulacja (ModuleScript) - Tworzenie instancji w wywołaniu 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 -- Część jest teraz w modelu danych; wszelkie zmiany bez dostępu do symulacji spowodują błąd po tym
-- Część istnieje natychmiast po stronie klienta i będzie pogodzona z serwerem
end)
end
return Simulation

Instance:Clone() i Instance.fromExisting() zszywają poprawnie, gdy źródłowa instancja została zreplikowana zarówno na kliencie, jak i na serwerze; obie strony klonują z pasujących GUID-ów źródłowych i generują pasujące przewidywane GUID-y.

Symulacja (ModuleScript) - Klonowanie instancji w wywołaniu BindToSimulation()
local RunService = game:GetService("RunService")
local Simulation = {}
local sourceTemplate -- zreplikowana instancja
Simulation.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
local cloned = sourceTemplate:Clone()
cloned.Parent = workspace
-- Skopiowana hierarchia jest zszywana z autorytatywną kopią serwera
end)
end
return Simulation

Wygładzanie pozycji

Możesz wizualnie wygładzić pozycję źle przewidywanych zsynchronizowanych obiektów, renderując inny obiekt niż ten, który jest symulowany.

  1. Uczyń symulowany obiekt niewidocznym.
  2. Stwórz obiekt renderer jako bezmasowy, niekolizyjny, wizualny klon, aby śledzić symulowany obiekt.
  3. Przypisz skrypt do obiektu renderer, który płynnie śledzi pozycję niewidocznego, symulowanego obiektu. To rozdzielenie renderowania i symulacji pozwala na zmianę pozycji obiektu renderer, aby stworzyć wizualnie płynne doświadczenie.

W następującym przykładzie Script, renderowany obiekt (nadrzędny) płynnie śledzi symulowany obiekt. Renderowany obiekt zawsze jest lekko "za" symulowanym obiektem, co zwykle jest w porządku, ale może być niepożądane w pewnych sytuacjach.

Płynne śledzenie pozycji BasePart za pomocą obiektu renderer
local RunService = game:GetService("RunService")
local TweenService = game:GetService("TweenService")
-- Obiekt do płynnego śledzenia
local smoothTarget:BasePart = workspace.SimulatedPart
-- Wizualny obiekt, który będzie wygładzany
local renderer:BasePart = script.Parent
-- Czas na wygładzanie; mniejszy oznacza szybszy
local smoothTime = 0.07
-- Przechowuj dane potrzebne do obliczenia płynnej pozycji
local smoothVelocity = Vector3.new()
-- Wyłącz fizykę obiektu renderer
renderer.Massless = true
renderer.Anchored = true
renderer.CanCollide = false
RunService.RenderStepped:Connect(function(deltaTime: number)
-- Płynnie śledź obiekt docelowy
local smoothPosition, smoothVelocity = TweenService:SmoothDamp(
renderer.Position,
smoothTarget.Position,
smoothVelocity,
smoothTime,
math.huge,
deltaTime)
renderer.Position = smoothPosition
end)

Gra przykładowa Soccer używa wariantu tej techniki, aby inteligentniej włączać i wyłączać wygładzanie pozycji piłki do nogi. Konkretnie, piłka do nogi wygładza swoją pozycję tylko wtedy, gdy symulowana piłka "skoczyła" zbyt daleko od renderowanej piłki. Takie podejście łączy w sobie najlepsze z obu światów: piłka do nogi nie ma opóźnienia wizualnego w normalnych warunkach, a gra płynnie interpoluje swoją pozycję tylko po tym, jak symulowana piłka niespodziewanie przeskoczyła do nowej lokalizacji, najprawdopodobniej z powodu artefaktu sieciowego lub zmiany po stronie serwera.

Pisanie kodu animacji

Pod autoryzacją serwera symulacja klienta może być cofnięta i powtórzona, gdy serwer skoryguje błędne przewidywanie. Podczas cofania stanu animacji cofa się, co oznacza, że AnimationTrack obsługuje te animacje, które buforowałeś w poprzednich klatkach, mogą być już nieaktualne.

Logika animacji lustrzanej

Jak w każdej podstawowej logice rozgrywki, logika sterująca animacjami musi być synchroniczna między serwerem a klientem, aby uniknąć błędnych przewidywań i drgań. Zobacz synchronizację symulacji, aby uzyskać wzorzec, który wiąże funkcje przez RunService:BindToSimulation() w ModuleScript, który jest inicjowany zarówno po stronie klienta, jak i serwera.

Unikaj przechowywania torów

Powszechnym wzorcem w skryptach bez autoryzacji serwera jest buforowanie obiektów AnimationTrack w czasie ładowania i ich wielokrotne używanie w nieskończoność. Ten wzorzec nie działa w grze z autoryzacją serwera, gdy serwer skoryguje błąd przewidywania, a klient cofa/powtarza swoją symulację z poprawionymi danymi. Jeśli Twój skrypt nadal posiada odniesienie do zatrzymanego lub zastąpionego toru, takie wywołania jak AdjustWeight() czy AdjustSpeed() będą działać na torze, który nie jest już wizualnie reprezentowany.

Buforuj tory po stronie klienta (niepewne)
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")
-- Buforuj tory animacji
local tracks = {}
tracks["WalkForward"] = animator:LoadAnimation(walkForwardAnim)
RunService:BindToSimulation(function(dt: number)
tracks["WalkForward"]:AdjustSpeed(1 + math.cos(time()))
end)

Zamiast trzymać się obiektów torów, przechowuj identyfikatory animacji (lub instancje Animation) i zapytaj Animator, aby uzyskać aktywny tor za każdym razem, gdy musisz z nim interagować. Do tego celu dostępne są dwie API:

  • Animator:GetTrackByAnimationId() — Zwraca aktualnie aktywny tor dla danego identyfikatora animacji lub nil, jeśli nie ma aktywnych animacji z tym identyfikatorem. Użyj tego, gdy wiesz, którą dokładnie animację chcesz znaleźć.
  • Animator:GetPlayingAnimationTracks() — Zwraca wszystkie aktywne tory (grające, wygaszane lub wstrzymane). Użyj tego, gdy musisz iterować po wszystkim, co jest aktywne (na przykład, aby zatrzymać wszystkie animacje lub znaleźć tory według określonego kryterium).

ModuleScript nazwany CustomAnimate w ReplicatedStorage:

CustomAnimate
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local CustomAnimate = {}
-- Przechowuj odniesienia do animacji (nie załadowane tory)
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

Odtwarzanie dźwięków i efektów wizualnych

W przypadku przewidywanej symulacji możliwe jest wywołanie efektów lub dźwięków dla zdarzeń, które klient przewidywał, że się wydarzą, ale które nigdy nie miały miejsca na serwerze. System renderowania powinien być przygotowany na "cofnięcie" wszelkich błędnie przewidzianych efektów. Na przykład klient może przewidzieć, że granat wybuchł i uruchomić efekt cząsteczkowy, ale jeśli inny gracz rozbroił granat, klient powinien ukryć efekt cząsteczek.

Dobrym podejściem do renderowania przewidywanej symulacji jest synchronizacja wzorca maszyny stanów w pętli symulacyjnej i renderowanie zmian stanu w funkcji kroku renderowania. Następujący przykład symuluje granat za pomocą wzorca maszyny stanów:

Prosta maszyna stanów do śledzenia granatu (ModuleScript)
local module = {}
module.GrenadeStates = {
Idle = 0,
Lit = 1,
Exploded = 2,
Defused = 3,
}
module.GrenadeExplodeTime = 3.0
module.Initialize = function(grenade)
RunService:BindToSimulation(function(deltaTime)
-- Inicjalizuj pusty stan granatu
local grenadeState = grenade:GetAttribute("State")
if grenadeState == nil then
grenadeState = module.GrenadeStates.Idle
grenade:SetAttribute("State", grenadeState)
grenade:SetAttribute("Timer", 0.0)
end
-- Inkrementuj timer granatu
local timer = grenade:GetAttribute("Timer")
timer = timer + deltaTime
grenade:SetAttribute("Timer", timer)
-- Wybuchnij zapalone granaty
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

Mając wcześniejszą maszynę stanów na miejscu, możesz renderować efekty granatów w połączeniu RunService.RenderStepped w oddzielnym skrypcie na podstawie zsynchronizowanego stanu granatu:

Renderowanie cząsteczek i dźwięków na podstawie zsynchronizowanego stanu granatu
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Simulation = require(ReplicatedStorage.Simulation)
local grenade = script.Parent
local previousGrenadeState = nil
-- Podświetlenie instancji do wskazania stanu granatu
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")
-- Emituj cząsteczki zapalone, jeśli granat jest zapalony
grenade.LitEmitter.Enabled = grenadeState == Simulation.GrenadeStates.Lit
-- Odtwórz efekt wybuchu, jeśli granat właśnie wybuchł
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
-- Zmień kolor podświetlenia granatu w zależności od stanu i czasu
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)

Projektowanie z uwzględnieniem opóźnienia sieci

Niektóre mechaniki rozgrywki lepiej nadają się do rozgrywki w trybie wieloosobowym w sieci niż inne. Gracze zawsze będą mieli pewne opóźnienie między momentem, gdy inny gracz wykonuje akcję, a momentem, w którym otrzymają dane wejściowe tego gracza. Najlepszym sposobem na stworzenie super płynnej gry wieloosobowej jest zaprojektowanie gry z uwzględnieniem tych ograniczeń.

Na przykład gra z wolniejszą akceleracją ruchu gracza będzie wyglądać na płynniejszą niż jedna z wyższą akceleracją, ponieważ różnica w pozycji spowodowana opóźnieniem sieciowym danych wejściowych gracza będzie mniejsza niż w grze z wyższą akceleracją.

Jako kolejny przykład, mechanika rozgrywki, w której gracze mogą natychmiastowo wywołać dużą eksplozję poprzez naciśnięcie przycisku, będzie miała więcej artefaktów sieciowych niż gdyby eksplozja była opóźniona po wejściu, jakby odpalając zapalnik. To przemieszcza ponowną symulację na efekt zapalnika zamiast samej eksplozji, co jest mniej zauważalnym artefaktem sieciowym.

Przewidywanie wejść innych graczy

Domyślnie Roblox nie przesyła danych wejściowych z każdego klienta do każdego innego klienta. Czy jest to odpowiednie dla Twojej gry, zależy od jej konstrukcji:

  • W przypadku podstawowego ruchu humanoidalnego domyślne zachowanie oznacza, że ruchy innych postaci graczy nie są ekstrapolowane z autorytatywnego stanu serwera i w rezultacie inne postacie graczy nie będą przewidywane błędnie, ale będą renderowane lekko w przeszłości.
  • W grze wyścigowej domyślne zachowanie oznacza, że klienci nie będą wiedzieć, czy inni gracze wywierają nacisk na pedał gazu lub wykonują inne wejścia, więc inne samochody mogą wydawać się za lokalnym graczem, nawet jeśli faktycznie są z przodu. Aby temu zaradzić, możesz przechowywać wejścia gracza w atrybutach na serwerze i działać na tych zsynchronizowanych atrybutach po stronie klienta, korzystając z RunService:BindToSimulation(), jak pokazano w następującym przykładzie kodu oraz w szablonie Wyścigi. Takie podejście pozwala na używanie atrybutów jako danych wejściowych do Twojej symulacji, aby uzyskać w pełni zreplikowane wejścia graczy.
Przechowywanie wejść gracza w atrybutach (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)
-- Zapisz wszelkie inne wejścia w atrybutach...
end
module.Initialize = function()
RunService:BindToSimulation(function(deltaTime)
if RunService:IsServer() then
-- Przekaż wejścia z serwera do wszystkich klientów
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
else
-- Zapisz lokalne wejścia gracza jako atrybuty
local player = Players.LocalPlayer
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local inputContext:InputContext = player.PlayerGui.InputContext
module.storePlayerInput(player, humanoidRootPart)
end
-- Użyj atrybutów jako danych wejściowych do gry
for _, player in Players:GetPlayers() do
local humanoidRootPart:BasePart = player.Character.HumanoidRootPart
local throttle = humanoidRootPart:GetAttribute("Throttle")
if throttle then
-- Zastosuj gaz do pojazdu gracza
end
end
end)
end)
return module

Debugging

Istnieje kilka nowych narzędzi i technik, które można wykorzystać do debugowania gry z autoryzacją serwera.

Wizualizator autoryzacji serwera

Naciśnięcie CtrlShiftF6 (Windows) lub ShiftF6 (Mac) otwiera wizualizator autoryzacji serwera w Studio, który pokazuje kilka kluczowych informacji:

SzczegółyOpis
Wskaźnik sukcesu przewidywań instancjiProcent poprawnie przewidzianych instancji w ciągu ostatnich 8 sekund.
Wskaźnik akceptacji wejśćProcent wszystkich wejść graczy, które dotarły na czas do serwera. Opóźnione wejścia obniżą tę liczbę.
Delta krok klient-serwerLiczba klatek między klientem a serwerem, w tym czas dołączenia klienta. Stabilność tej liczby odzwierciedla stabilność Twojego połączenia z serwerem.
FPS serwera RCCWskaźnik klatek symulacji na serwerze. Jeśli ta liczba spadnie poniżej 59, serwer nie będzie w stanie nadążyć za symulacją, a jakość gry spadnie.
Liczba przewidywanych instancjiLiczba instancji, które Twój klient przewiduje.
Liczby przyczyn zrzucenia wejść

Liczba razy, kiedy serwer zrzucił wejście z każdego powodu:

  • [x] zbyt stare — Wejścia dotarły z opóźnieniem, co oznacza, że Twoja sieć się pogorszyła lub klient nie mógł nadążyć za symulacją.
  • [x] w złej kolejności — Wystąpił błąd sieciowy, który spowodował, że Twoje wejścia zostały przestawione i odrzucone.
  • [x] pełny bufor — Serwer nie mógł zbuforować Twojego wejścia. Albo Twoja sieć nagle poprawiła się, albo serwer nie mógł nadążyć za symulacją.

Promień symulacji

Gdy polegasz na automatycznej prognozie (Enum.PredictionMode.Automatic), możesz wizualizować promień prognozowania wokół swojej postaci gracza, włączając Włączone są regiony w ustawieniach Studio (AltS w Windows; S w Mac). Zielony cylinder wskazuje zasięg wokół Twojej postaci, w którym instancje są prognozowane, a jego promień rośnie i maleje w zależności od wydajności urządzenia.

Promień symulacji wokół postaci gracza z działającą autoryzacją serwera
©2026 Roblox Corporation. Nazwa Roblox, logo Roblox oraz hasło „Powering Imagination” należą do naszych zarejestrowanych i niezarejestrowanych znaków towarowych na terenie Stanów Zjednoczonych oraz w innych krajach.