平行 Luau

*此內容是使用 AI(Beta 測試版)翻譯,可能含有錯誤。若要以英文檢視此頁面,請按一下這裡

使用 平行 Luau 程序設計模型,您可以同時在多個線程上運行代碼,這可以提高您體驗的性能。隨著您擴展您的體驗內容,您可以採用此模型來幫助維持您的 Luau 腳本的性能和安全性。

平行程序設計模型

默認情況下,腳本按順序執行。如果您的體驗具有複雜的邏輯或內容,例如非玩家角色 (NPC)、光線投射驗證和程序生成,那麼序列執行可能會導致用戶出現延遲。使用平行程序設計模型,您可以 將任務拆分為多個腳本 並並行運行。這樣可以讓您的體驗代碼運行更快,從而改善用戶體驗。

平行程序設計模型還為您的代碼增加了安全性好處。通過將代碼拆分為多個線程,當您在一個線程中編輯代碼時,不會影響正在平行運行的其他代碼。這降低了代碼中出現一個錯誤而導致整個體驗受損的風險,並在您推送更新時最小化了實時服務器中用戶的延遲。

採用平行程序設計模型並不意味著將所有內容放在多個線程中。例如,伺服器端光線投射驗證 在平行的情況下為每個用戶設置遠程事件,但仍需要初始代碼按序列執行來更改全局屬性,這是平行執行的一個常見模式。

大多數情況下,您需要將序列和平行階段結合起來,以達到所需的輸出,因為目前有些操作不支持平行,可能會阻止腳本運行,例如在平行階段修改實例的操作。要獲得有關 API 在平行中使用級別的更多信息,請參見 線程安全

將代碼拆分為多個線程

要同時在多個線程中運行您的體驗腳本,您需要將它們拆分為不同 角色 下的邏輯塊,這些角色在 數據模型 中表示。角色由 Actor 實例表示,繼承自 DataModel。它們作為執行隔離的單位,分散在多個同時運行的核心上。

放置角色實例

您可以將角色放置在適當的容器中,或使用它們來替代您的 3D 實體(例如 NPC 及光線投射器)的頂層實例類型,然後添加相應的腳本。

一個位於角色下的腳本示例

在大多數情況下,您不應將一個角色放在數據模型中的另一個角色下。然而,若您決定將一個腳本嵌套在多個角色內以滿足您的特定用例,該腳本將被其最近的祖先角色擁有。

角色和腳本的樹形結構,顯示腳本如何由其最近的角色擁有

取消同步線程

儘管將腳本放在角色下使其具備平行執行的能力,但默認情況下,代碼仍然在單個線程上按序列執行,這不會提高運行時性能。您需要調用 task.desynchronize(),這是一個可讓步的函數,會暫停當前協程的執行以在平行運行代碼,並在下一次平行執行機會時恢復它。要將腳本切換回序列執行,請調用 task.synchronize()

另外,當您想要在觸發時立即在平行中運行代碼時,可以使用 RBXScriptSignal:ConnectParallel() 方法調度信號回調。您無需在信號回調內調用 task.desynchronize()

取消同步一個線程

local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- 一些計算狀態更新的平行代碼
task.synchronize()
... -- 一些更改實例狀態的序列代碼
end)

同一角色的一部分的腳本始終按順序執行,因此您需要多個角色。例如,如果您將所有帶平行支持的行為腳本放入一個角色中,則它們仍然會在單個線程上按序列執行,但如果您為不同的 NPC 邏輯擁有多個角色,它們每個都會在自己所在的線程上並行運行。要獲得更多信息,請參見 最佳實踐

在單個線程中按序列運行的角色中的平行代碼
在多個線程中同時運行的角色中的平行代碼

線程安全性

在平行執行期間,您可以像往常一樣訪問 DataModel 層次結構中的大多數實例,但某些 API 屬性和函數不能安全讀取或寫入。如果您在平行代碼中使用它們,Roblox 引擎可以自動檢測並防止這些訪問發生。

API 成員具有一個線程安全級別,指示您在平行代碼中是否以及如何使用它們,如下表所示:

安全級別對於屬性對於函數
不安全不能在平行中讀取或寫入。不能在平行中調用。
平行讀取可以在平行中讀取,但不能寫入。N/A
本地安全可以在同一角色內使用;可以被其他 Actors 以平行方式讀取但不能寫入。可以在同一角色內調用;不能被其他 Actors 在平行中調用。
安全可以讀取和寫入。可以調用。

您可以在 API 參考 中找到 API 成員的線程安全標籤。在使用它們時,您還應考慮 API 調用或屬性更改在平行線程之間可能的交互。通常,對於多個角色來說,讀取相同數據是安全的,但不可以修改其他角色的狀態。

跨線程通信

在多線程上下文中,您仍然可以允許不同角色中的腳本相互通信以交換數據、協調任務和同步活動。引擎支持以下跨線程通信機制:

您可以支持多種機制以滿足您的跨線程通信需求。例如,您可以通過角色消息 API 發送一個共享表。

角色消息

角色消息 API 允許腳本(無論是在序列還是平行上下文中)將數據發送到同一數據模型中的角色。通過此 API 的通信是異步的,即發送者在接收者接收到消息之前不會阻塞。

在使用此 API 發送消息時,您需要定義一個 主題 來對消息進行分類。每條消息只能發送給單個角色,但該角色可以內部綁定多個回調到一條消息。只有作為角色的後代的腳本可以接收消息。

該 API 具有以下方法:

以下示例顯示如何使用 Actor:SendMessage() 在發送者的端定義主題並發送消息:

示例消息發送者

local Workspace = game:GetService("Workspace")
-- 向工作角色發送兩條主題為 "Greeting" 的消息
local workerActor = Workspace.WorkerActor
workerActor:SendMessage("Greeting", "Hello World!")
workerActor:SendMessage("Greeting", "Welcome")
print("已發送消息")

以下示例顯示如何在接收者端使用 Actor:BindToMessageParallel() 在平行上下文中綁定特定主題的回調:

示例消息接收者

-- 獲取此腳本所屬的角色
local actor = script:GetActor()
-- 綁定 "Greeting" 消息主題的回調
actor:BindToMessageParallel("Greeting", function(greetingString)
print(actor.Name, "-", greetingString)
end)
print("已綁定消息")

共享表

SharedTable 是一種類似於表的數據結構,從多個角色運行的腳本中都可以訪問。它對於涉及大量數據並需要多個線程之間的公共共享狀態的情況非常有用。例如,當多個角色共同處理未存儲在數據模型中的公共世界狀態時。

將共享表發送到另一角色不會複製數據。相反,共享表允許多個腳本同時安全、原子地更新。由一個角色對共享表的每次更新對所有角色都是立即可見的。共享表還可以在資源效率的過程中克隆,利用結構共享而不是複製底層數據。

直接數據模型通信

您還可以使用數據模型直接促進多個線程之間的通信,其中不同角色可以寫入然後讀取屬性或屬性。但是,為了維護線程安全,平行運行的腳本通常無法寫入數據模型。因此,直接使用數據模型進行通信會帶來限制,並可能迫使腳本頻繁同步,這可能會影響腳本的性能。

示例

伺服器端光線投射驗證

在戰鬥和戰鬥體驗中,您需要為用戶的武器啟用 光線投射。客戶端模擬武器以實現良好的延遲,伺服器必須確認擊中,這涉及進行光線投射和計算預期角色速度的一些啟發式,並查看過去的行為。

與其使用一個集中的腳本,連接到客戶端用於通信擊中信息的遠程事件,不如在伺服器端平行執行每個擊中驗證過程,為每個用戶字符設置一個獨立的遠程事件。

運行於該角色的 Actor 下的伺服器端腳本使用平行連接連接到此遠程事件,運行相關邏輯以確認擊中。如果邏輯找到擊中的確認,則會扣除傷害,這涉及更改屬性,因此最初會以序列方式運行。


local Workspace = game:GetService("Workspace")
local tool = script.Parent.Parent
local remoteEvent = Instance.new("RemoteEvent") -- 創建新的遠程事件並將其作為工具的子項
remoteEvent.Name = "RemoteMouseEvent" -- 重命名以便本地腳本可以查找它
remoteEvent.Parent = tool
local remoteEventConnection -- 創建遠程事件連接的引用
-- 監聽遠程事件的函數
local function onRemoteMouseEvent(player: Player, clickLocation: CFrame)
-- 序列:在序列中執行設置代碼
local character = player.Character
-- 在光線投射時忽略用戶的角色
local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = { character }
-- 平行:在平行中執行光線投射
task.desynchronize()
local origin = tool.Handle.CFrame.Position
local epsilon = 0.01 -- 用來稍微延伸光線,因為點擊位置可能與物體稍微偏離
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")
-- 序列:下面的代碼更改角色外部的狀態
task.synchronize()
explosion.DestroyJointRadiusPercent = 0 -- 使爆炸不致命
explosion.Position = clickLocation.Position
-- 多個角色可能在光線投射中獲取同一個部分並決定摧毀它
-- 這是完全安全的,但會導致兩次爆炸而非一次
-- 以下代碼會雙重檢查執行是否首先到達這一部分
if hitPart.Parent then
explosion.Parent = Workspace
hitPart:Destroy() -- 摧毀它
end
end
end
end
-- 最初以序列連接信號,因為一些設置代碼無法在平行中運行
remoteEventConnection = remoteEvent.OnServerEvent:Connect(onRemoteMouseEvent)

伺服器端程序生成地形

要為您的體驗創建一個廣闊的世界,您可以動態填充世界。程序生成通常創建獨立的地形塊,生成器為物體放置、材料使用和體素填充執行相對複雜的計算。在平行中運行生成代碼可以提高過程的效率。以下代碼示例即為一個示例。


-- 平行執行需要使用角色
-- 此腳本克隆自身;原始腳本啟動過程,而克隆作為工作者
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
-- 將所有角色作為子項放在自身下
for _, actor in workers do
actor.Parent = script
end
-- 通過發送消息指示角色生成地形
-- 在此示例中,角色隨機選擇
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)
-- 從原始腳本退出;剩餘代碼在每個角色中運行
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
-- 在平行執行上下文中綁定回調
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)
-- 當前,WriteVoxels() 必須在序列階段調用
task.synchronize()
Workspace.Terrain:WriteVoxels(
Region3.new(corner, corner + Vector3.new(16, 16, 16)),
4,
voxels.materials,
voxels.occupancy
)
end)

最佳實踐

為了最大限度地應用平行程序設計的好處,請參考以下最佳實踐在添加您的 Luau 代碼時:

  • 避免長時間計算 — 即使在平行中,長時間計算也會阻塞其他腳本的執行並導致延遲。避免使用平行程序設計來處理大量長期、不讓步的計算。

    圖示說明如何過載平行執行階段仍會導致延遲
  • 使用合適數量的角色 — 為了獲得最佳性能,使用更多的 Actors。即使設備的核心數量少於 Actors,細粒度也允許更有效的負載平衡。

    演示如何使用更多角色平衡跨核心的負載

    這並不意味著您應該使用盡可能多的 Actors。您仍應根據邏輯單位將代碼拆分為 Actors,而不是將連接邏輯的代碼打破為不同的 Actors。例如,如果您想在平行中啟用 光線投射驗證,使用 64 個甚至更多的 Actors 是合理的,而不是僅僅 4 個,即使您針對的是 4 核系統。這對系統的可擴展性是有價值的,並能根據底層硬件的能力分配工作。然而,您也不應該使用過多的 Actors,這會難以維護。

©2026 Roblox Corporation、Roblox、Roblox 標誌及 Powering Imagination 是我們在美國及其他國家地區的部分註冊與未註冊商標。