使用 並行 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 的所有並行啟用的行為腳本放在一個演員中,它們仍然在單一執行緒上串行運行,但如果你有多個演員執行不同的 NPC 邏輯,每個演員在自己的線程上並行運行。欲了解更多信息,請參閱最佳實踐。


線程安全
在並行執行期間,您可以像往常一樣存取 DataModel 層級的大多數實例,但有些 API 屬性和功能無法安全地閱讀或寫入。如果您在並行代碼中使用它們,Roblox 引擎可自動偵測並防止這些存取發生。
API成員有一個線程安全等級,該等級表示您是否可以在並行代碼中使用它們以及如何使用它們,如下表所示:
安全等級 | 對於屬性 | 對於功能 |
---|---|---|
不安全 | 無法並行讀取或寫入。 | 無法並行呼叫。 |
閱讀並行 | 可以讀取,但不能並行寫入。 | N/A |
本地安全箱 | 可以在同一個行動者中使用;可以並行閱讀但不能寫入其他 Actors 。 | 可以在同一個行動者中呼叫;無法由其他 Actors 並行呼叫。 |
安全 | 可以讀取和寫入。 | 可以呼叫。 |
您可以在 API 參考 找到API成員的線程安全標籤。使用它們時,您也應該考慮 API 呼叫或屬性變更如何在並行線程之間互相影響。通常,多個參與者閱讀相同的資料與其他參與者一樣安全,但不會修改其他參與者的狀態。
跨線程通訊
在多線程上下文中,您仍可以允許不同演員中的腳本相互通訊,交換資料、協調任務並同步活動。引擎支持以下跨線程通訊機制:
您可以支持多種機制來滿足您的跨線程通信需求。例如,您可以透過行動訊息傳送 API 傳送共用表。
演員傳訊
行動者訊息傳送 API 允許一個指令碼,在串行或並行上下文中,將資料傳送給同一資料模型的行動者。透過此 API 的通訊是異步的,在此期間發送方不會阻塞,直到接收方收到訊息。
當使用此 API 傳送訊息時,您需要定義一個 主題 來分類訊息。每個訊息只能傳送給單一個行動者,但該行動者可以內部有多個回呼綁定到一個訊息。只有屬於演員的子孫的腳本才能收到訊息。
API有以下方法:
- Actor:SendMessage() 用於向演員發送消息。
- Actor:BindToMessage() 用於將 Luau 回呼綁定到串行上下文中具有指定主題的訊息。
- Actor:BindToMessageParallel() 用於將 Luau 回呼綁定到並行上下文中擁有指定主題的訊息。
下面的例子顯示了如何使用 Actor:SendMessage() 來定義主題,並在發送者結束發送消息:
範例訊息發送者
local Workspace = game:GetService("Workspace")-- 向工作者角色發送兩個訊息,主題為「問候」local workerActor = Workspace.WorkerActorworkerActor:SendMessage("Greeting", "Hello World!")workerActor:SendMessage("Greeting", "Welcome")print("Sent messages")
下面的例子顯示了如何使用 Actor:BindToMessageParallel() 來在接收者端的並行上下文中綁定特定主題的回調:
範例訊息接收器
-- 取得這個腳本的父輩所屬的演員
local actor = script:GetActor()
-- 綁定「問候」訊息主題的回呼程式
actor:BindToMessageParallel("Greeting", function(greetingString)
print(actor.Name, "-", greetingString)
end)
print("Bound to messages")
共享表
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 代碼時參考以下最佳做法:
避免長計算 — 即使是並行計算,長計算也可能阻止其他腳本的執行並導致延遲。避免使用並行編程來處理大量長而不屈的計算。