Z modelem programowania Równoległy Luau możesz uruchamiać kod na wielu wątkach jednocześnie, co może poprawić wydajność twojej gry. W miarę rozwoju gry z większą ilością treści, możesz przyjąć ten model, aby pomóc w utrzymaniu wydajności i bezpieczeństwa twoich skryptów Luau.
Model programowania równoległego
Domyślnie skrypty wykonują się sekwencyjnie. Jeśli twoja gra ma złożoną logikę lub treści, takie jak postacie niezależne (NPC), walidacja raycastów i generacja proceduralna, to sekwencyjne wykonanie może powodować opóźnienia dla twoich użytkowników. Dzięki modelowi programowania równoległego możesz podzielić zadania na wiele skryptów i uruchomić je równolegle. To sprawia, że kod twojej gry działa szybciej, co poprawia doświadczenia użytkowników.
Model programowania równoległego dodaje również korzyści bezpieczeństwa do twojego kodu. Dzieląc kod na wiele wątków, gdy edytujesz kod w jednym wątku, nie wpływa to na inny kod działający równolegle. To zmniejsza ryzyko, że jeden błąd w twoim kodzie zniszczy całą grę i minimalizuje opóźnienia dla użytkowników na serwerach na żywo, gdy wprowadzasz aktualizację.
Przyjęcie modelu programowania równoległego nie oznacza, że musisz umieszczać wszystko w wielu wątkach. Na przykład, walidacja raycastów po stronie serwera ustawia dla każdego użytkownika zdarzenie zdalne równolegle, ale nadal wymaga, aby początkowy kod działał sekwencyjnie, aby zmienić globalne właściwości, co jest powszechnym wzorcem dla wykonania równoległego.
Najczęściej musisz połączyć fazy sekwencyjne i równoległe, aby osiągnąć pożądany wynik, ponieważ obecnie istnieją pewne operacje, które nie są obsługiwane w trybie równoległym, co może uniemożliwić działanie skryptów, takie jak modyfikowanie instancji w fazach równoległych. Aby uzyskać więcej informacji na temat poziomu użycia API w trybie równoległym, zobacz bezpieczeństwo wątków.
Podziel kod na wiele wątków
Aby uruchomić skrypty twojej gry w wielu wątkach jednocześnie, musisz podzielić je na logiczne fragmenty pod różnymi aktorami w modelu danych. Aktorzy są reprezentowani przez instancje Actor, które dziedziczą po DataModel. Działają jako jednostki izolacji wykonania, które rozdzielają obciążenie na wiele rdzeni działających jednocześnie.
Umieść instancje aktorów
Możesz umieścić aktorów w odpowiednich kontenerach lub użyć ich do zastąpienia typów instancji na najwyższym poziomie twoich obiektów 3D, takich jak NPC i raycasterzy, a następnie dodać odpowiednie skrypty.

W większości sytuacji nie powinieneś umieszczać aktora jako dziecka innego aktora w modelu danych. Jednak jeśli zdecydujesz się umieścić skrypt zagnieżdżony w wielu aktorach dla swojego konkretnego przypadku użycia, skrypt jest własnością najbliższego przodka aktora.

Desynchronizuj wątki
Chociaż umieszczanie skryptów pod aktorami daje im możliwość równoległego wykonania, domyślnie kod nadal działa na pojedynczym wątku sekwencyjnie, co nie poprawia wydajności czasu wykonania. Musisz wywołać task.desynchronize(), funkcję, która może być wstrzymana, która wstrzymuje wykonanie bieżącej korutyny, aby uruchomić kod równolegle i wznawia go przy następnej okazji wykonania równoległego. Aby przełączyć skrypt z powrotem na wykonanie sekwencyjne, wywołaj task.synchronize().
Alternatywnie, możesz użyć metody RBXScriptSignal:ConnectParallel(), gdy chcesz zaplanować wywołanie sygnału, aby natychmiast uruchomić swój kod równolegle po wyzwoleniu. Nie musisz wywoływać task.desynchronize() wewnątrz wywołania sygnału.
local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- Kod równoległy, który oblicza aktualizację stanu
task.synchronize()
... -- Kod sekwencyjny, który zmienia stan instancji
end)Skrypty, które są częścią tego samego aktora, zawsze wykonują się sekwencyjnie względem siebie, więc potrzebujesz wielu aktorów. Na przykład, jeśli umieścisz wszystkie skrypty zachowań z włączoną równoległością dla swojego NPC w jednym aktorze, nadal będą działać sekwencyjnie na jednym wątku, ale jeśli masz wiele aktorów dla różnych logik NPC, każdy z nich działa równolegle na swoim własnym wątku. Aby uzyskać więcej informacji, zobacz Najlepsze praktyki.


Bezpieczeństwo wątków
Podczas równoległego wykonania możesz uzyskać dostęp do większości instancji hierarchii DataModel jak zwykle, ale niektóre właściwości i funkcje API nie są bezpieczne do odczytu lub zapisu. Jeśli użyjesz ich w swoim równoległym kodzie, silnik Roblox może automatycznie wykryć i zapobiec tym dostępom.
Członkowie API mają poziom bezpieczeństwa wątków, który wskazuje, czy i jak możesz ich używać w swoim równoległym kodzie, jak pokazuje poniższa tabela:
| Poziom bezpieczeństwa | Dla właściwości | Dla funkcji |
|---|---|---|
| Niebezpieczne | Nie można odczytywać ani zapisywać w równolegle. | Nie można wywoływać w równolegle. |
| Odczyt równoległy | Można odczytywać, ale nie można zapisywać w równolegle. | N/D |
| Bezpieczne lokalnie | Można używać w tym samym aktorze; można odczytywać, ale nie można zapisywać przez inne Aktorzy w równolegle. | Można wywoływać w tym samym aktorze; nie można wywoływać przez inne Aktorzy w równolegle. |
| Bezpieczne | Można odczytywać i zapisywać. | Można wywoływać. |
Możesz znaleźć tagi bezpieczeństwa wątków dla członków API w referencji API. Korzystając z nich, powinieneś również rozważyć, jak wywołania API lub zmiany właściwości mogą współdziałać między równoległymi wątkami. Zwykle jest bezpieczne, aby wiele aktorów odczytywało te same dane co inne aktory, ale nie modyfikowało stanu innych aktorów.
Komunikacja między wątkami
W kontekście wielowątkowości możesz nadal pozwolić skryptom w różnych aktorach komunikować się ze sobą, aby wymieniać dane, koordynować zadania i synchronizować działania. Silnik obsługuje następujące mechanizmy komunikacji między wątkami:
- API wiadomości aktora do wysyłania wiadomości do aktora za pomocą skryptów.
- Struktura danych wspólnej tabeli do efektywnego dzielenia się dużą ilością danych między wieloma aktorami na wspólnym stanie.
- Bezpośrednia komunikacja modelu danych do prostej komunikacji z ograniczeniami.
Możesz wspierać wiele mechanizmów, aby dostosować się do swoich potrzeb komunikacji między wątkami. Na przykład, możesz wysłać wspólną tabelę przez API wiadomości aktora.
Wiadomości aktora
API wiadomości aktora pozwala skryptowi, zarówno w kontekście sekwencyjnym, jak i równoległym, wysyłać dane do aktora w tym samym modelu danych. Komunikacja przez to API jest asynchroniczna, w której nadawca nie blokuje, aż odbiorca otrzyma wiadomość.
Podczas wysyłania wiadomości za pomocą tego API musisz zdefiniować temat do kategoryzacji wiadomości. Każda wiadomość może być wysyłana tylko do jednego aktora, ale ten aktor może wewnętrznie mieć wiele wywołań zwrotnych powiązanych z wiadomością. Tylko skrypty, które są potomkami aktora, mogą odbierać wiadomości.
API ma następujące metody:
- Actor:SendMessage() do wysyłania wiadomości do aktora.
- Actor:BindToMessage() do powiązania wywołania zwrotnego Luau z wiadomością o określonym temacie w kontekście sekwencyjnym.
- Actor:BindToMessageParallel() do powiązania wywołania zwrotnego Luau z wiadomością o określonym temacie w kontekście równoległym.
Poniższy przykład pokazuje, jak użyć Actor:SendMessage(), aby zdefiniować temat i wysłać wiadomość po stronie nadawcy:
local Workspace = game:GetService("Workspace")
-- Wyślij dwie wiadomości do aktora roboczego z tematem "Powitanie"
local workerActor = Workspace.WorkerActor
workerActor:SendMessage("Greeting", "Witaj Świecie!")
workerActor:SendMessage("Greeting", "Witamy")
print("Wysłano wiadomości")Poniższy przykład pokazuje, jak użyć Actor:BindToMessageParallel(), aby powiązać wywołanie zwrotne dla określonego tematu w kontekście równoległym po stronie odbiorcy:
-- Uzyskaj aktora, do którego ten skrypt jest przypisany
local actor = script:GetActor()
-- Powiąż wywołanie zwrotne dla tematu wiadomości "Powitanie"
actor:BindToMessageParallel("Greeting", function(greetingString)
print(actor.Name, "-", greetingString)
end)
print("Powiązano z wiadomościami")Wspólna tabela
SharedTable to struktura danych podobna do tabeli, dostępna z skryptów działających pod wieloma aktorami. Jest przydatna w sytuacjach, które obejmują dużą ilość danych i wymagają wspólnego stanu między wieloma wątkami. Na przykład, gdy wiele aktorów pracuje nad wspólnym stanem świata, który nie jest przechowywany w modelu danych.
Wysłanie wspólnej tabeli do innego aktora nie tworzy kopii danych. Zamiast tego, wspólne tabele pozwalają na bezpieczne i atomowe aktualizacje przez wiele skryptów jednocześnie. Każda aktualizacja wspólnej tabeli przez jednego aktora jest natychmiast widoczna dla wszystkich aktorów. Wspólne tabele mogą być również klonowane w procesie efektywnym pod względem zasobów, który wykorzystuje współdzielenie strukturalne zamiast kopiowania podstawowych danych.
Bezpośrednia komunikacja modelu danych
Możesz również ułatwić komunikację między wieloma wątkami bezpośrednio za pomocą modelu danych, w którym różni aktorzy mogą zapisywać i następnie odczytywać właściwości lub atrybuty. Jednak aby zachować bezpieczeństwo wątków, skrypty działające równolegle zazwyczaj nie mogą zapisywać do modelu danych. Dlatego bezpośrednie użycie modelu danych do komunikacji wiąże się z ograniczeniami i może zmusić skrypty do częstego synchronizowania, co może wpłynąć na wydajność twoich skryptów.
Przykłady
Walidacja raycastów po stronie serwera
Dla gry walki i bitwy musisz włączyć raycasting dla broni twoich użytkowników. Z klientem symulującym broń, aby osiągnąć dobrą latencję, serwer musi potwierdzić trafienie, co wiąże się z wykonywaniem raycastów i pewną ilością heurystyki, która oblicza oczekiwaną prędkość postaci i analizuje wcześniejsze zachowanie.
Zamiast używać jednego scentralizowanego skryptu, który łączy się z zdalnym zdarzeniem, z którego klienci korzystają do komunikacji informacji o trafieniach, możesz uruchomić każdy proces walidacji trafienia po stronie serwera równolegle, przy czym każda postać użytkownika ma osobne zdalne zdarzenie.
Skrypt po stronie serwera, który działa pod Actor tej postaci, łączy się z tym zdalnym zdarzeniem za pomocą połączenia równoległego, aby uruchomić odpowiednią logikę potwierdzającą trafienie. Jeśli logika znajdzie potwierdzenie trafienia, obrażenia są odejmowane, co wiąże się ze zmianą właściwości, więc początkowo działa sekwencyjnie.
local Workspace = game:GetService("Workspace")
local tool = script.Parent.Parent
local remoteEvent = Instance.new("RemoteEvent") -- Utwórz nowe zdalne zdarzenie i przypisz je do narzędzia
remoteEvent.Name = "RemoteMouseEvent" -- Zmień nazwę, aby lokalny skrypt mógł je znaleźć
remoteEvent.Parent = tool
local remoteEventConnection -- Utwórz referencję dla połączenia zdalnego zdarzenia
-- Funkcja, która nasłuchuje na zdalne zdarzenie
local function onRemoteMouseEvent(player: Player, clickLocation: CFrame)
-- SEKWENCYJNE: Wykonaj kod konfiguracyjny sekwencyjnie
local character = player.Character
-- Ignoruj postać użytkownika podczas raycastingu
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
-- RÓWNOLEGŁE: Wykonaj raycast równolegle
task.desynchronize()
local origin = tool.Handle.CFrame.Position
local epsilon = 0.01 -- Używane do nieznacznego wydłużenia promienia, ponieważ lokalizacja kliknięcia może być nieznacznie przesunięta od obiektu
local lookDirection = (1 + epsilon) * (clickLocation.Position - origin)
local raycastResult = Workspace:Raycast(origin, lookDirection, params)
if raycastResult then
local hitPart = raycastResult.Instance
if hitPart and hitPart.Name == "block" then
local explosion = Instance.new("Explosion")
-- SEKWENCYJNE: Kod poniżej modyfikuje stan poza aktorem
task.synchronize()
explosion.DestroyJointRadiusPercent = 0 -- Uczyń eksplozję niegroźną
explosion.Position = clickLocation.Position
-- Wiele aktorów mogłoby uzyskać ten sam element w raycast i zdecydować się go zniszczyć
-- To jest całkowicie bezpieczne, ale skutkowałoby dwoma eksplozjami jednocześnie zamiast jednej
-- Poniższe podwójne sprawdza, czy wykonanie dotarło najpierw do tej części
if hitPart.Parent then
explosion.Parent = Workspace
hitPart:Destroy() -- Zniszcz go
end
end
end
end
-- Połącz sygnał sekwencyjnie na początku, ponieważ niektóre kody konfiguracyjne nie mogą działać równolegle
remoteEventConnection = remoteEvent.OnServerEvent:Connect(onRemoteMouseEvent)Proceduralna generacja terenu po stronie serwera
Aby stworzyć ogromny świat dla swojej gry, możesz dynamicznie zapełniać świat. Generacja proceduralna zazwyczaj tworzy niezależne kawałki terenu, przy czym generator wykonuje stosunkowo złożone obliczenia dotyczące umiejscowienia obiektów, użycia materiałów i wypełnienia wokseli. Uruchamianie kodu generacji równolegle może zwiększyć efektywność tego procesu. Poniższy przykład kodu służy jako przykład.
-- Równoległe wykonanie wymaga użycia aktorów
-- Ten skrypt klonuje siebie; oryginał inicjuje proces, podczas gdy klony działają jako pracownicy
local Workspace = game:GetService("Workspace")
local actor = script:GetActor()
if actor == nil then
local workers = {}
for i = 1, 32 do
local actor = Instance.new("Actor")
script:Clone().Parent = actor
table.insert(workers, actor)
end
-- Przypisz wszystkie aktory do siebie
for _, actor in workers do
actor.Parent = script
end
-- Poleć aktorom generować teren, wysyłając wiadomości
-- W tym przykładzie aktorzy są wybierani losowo
task.defer(function()
local rand = Random.new()
local seed = rand:NextNumber()
local sz = 10
for x = -sz, sz do
for y = -sz, sz do
for z = -sz, sz do
workers[rand:NextInteger(1, #workers)]:SendMessage("GenerateChunk", x, y, z, seed)
end
end
end
end)
-- Wyjdź z oryginalnego skryptu; reszta kodu działa w każdym aktorze
return
end
function makeNdArray(numDim, size, elemValue)
if numDim == 0 then
return elemValue
end
local result = {}
for i = 1, size do
result[i] = makeNdArray(numDim - 1, size, elemValue)
end
return result
end
function generateVoxelsWithSeed(xd, yd, zd, seed)
local matEnums = {Enum.Material.CrackedLava, Enum.Material.Basalt, Enum.Material.Asphalt}
local materials = makeNdArray(3, 4, Enum.Material.CrackedLava)
local occupancy = makeNdArray(3, 4, 1)
local rand = Random.new()
for x = 0, 3 do
for y = 0, 3 do
for z = 0, 3 do
occupancy[x + 1][y + 1][z + 1] = math.noise(xd + 0.25 * x, yd + 0.25 * y, zd + 0.25 * z)
materials[x + 1][y + 1][z + 1] = matEnums[rand:NextInteger(1, #matEnums)]
end
end
end
return {materials = materials, occupancy = occupancy}
end
-- Powiąż wywołanie zwrotne, aby było wywoływane w kontekście równoległego wykonania
actor:BindToMessageParallel("GenerateChunk", function(x, y, z, seed)
local voxels = generateVoxelsWithSeed(x, y, z, seed)
local corner = Vector3.new(x * 16, y * 16, z * 16)
-- Obecnie, WriteVoxels() musi być wywoływane w fazie sekwencyjnej
task.synchronize()
Workspace.Terrain:WriteVoxels(
Region3.new(corner, corner + Vector3.new(16, 16, 16)),
4,
voxels.materials,
voxels.occupancy
)
end)Najlepsze praktyki
Aby maksymalnie wykorzystać korzyści z programowania równoległego, zapoznaj się z następującymi najlepszymi praktykami podczas dodawania swojego kodu Luau:
Unikaj długich obliczeń — Nawet w trybie równoległym, długie obliczenia mogą blokować wykonanie innych skryptów i powodować opóźnienia. Unikaj używania programowania równoległego do obsługi dużej ilości długich, nieprzerywających obliczeń.

Używaj odpowiedniej liczby aktorów — Dla najlepszej wydajności używaj więcej Aktorów. Nawet jeśli urządzenie ma mniej rdzeni niż Aktorzy, granularność pozwala na bardziej efektywne równoważenie obciążenia między rdzeniami.

To nie oznacza, że powinieneś używać tak wielu Aktorów, jak to możliwe. Powinieneś nadal dzielić kod na Aktorów na podstawie jednostek logiki, a nie łamać kod z połączoną logiką na różne Aktorów. Na przykład, jeśli chcesz włączyć walidację raycastów równolegle, rozsądnie jest użyć 64 Aktorów i więcej zamiast tylko 4, nawet jeśli celujesz w systemy 4-rdzeniowe. To jest cenne dla skalowalności systemu i pozwala na rozdzielenie pracy w oparciu o możliwości sprzętu. Jednak nie powinieneś również używać zbyt wielu Aktorów, które są trudne do utrzymania.