使用 平行 Luau 程式設計模型,您可以同時在多個執行緒上運行代碼,這可以提高您遊戲的性能。隨著您擴展遊戲內容,您可以採用此模型來幫助維持 Luau 腳本的性能和安全性。
平行程式設計模型
默認情況下,腳本是按順序執行的。如果您的遊戲有複雜的邏輯或內容,例如非玩家角色 (NPC)、射線檢查驗證和程序生成,那麼順序執行可能會導致用戶延遲。使用平行程式設計模型,您可以 將任務拆分為多個腳本 並同時運行它們。這使您的遊戲代碼運行得更快,從而改善用戶體驗。
平行程式設計模型還為您的代碼增加了安全性。通過將代碼拆分為多個執行緒,當您編輯一個執行緒中的代碼時,它不會影響其他平行運行的代碼。這降低了代碼中的一個錯誤損壞整個遊戲的風險,並最小化了您在推送更新時用戶在實時伺服器上的延遲。
採用平行程式設計模型並不意味著將所有內容放入多個執行緒中。例如,伺服器端射線檢查驗證 為每個單獨的用戶設置一個遠程事件,但仍然需要初始代碼按順序運行以更改全局屬性,這是平行執行的常見模式。
大多數情況下,您需要結合順序和平行階段以實現所需的輸出,因為目前有一些操作不支持平行,這可能會阻止腳本運行,例如在平行階段修改實例。欲了解有關平行中 API 使用級別的更多信息,請參見 執行緒安全性。
將代碼拆分為多個執行緒
要在多個執行緒中同時運行您的遊戲腳本,您需要將它們拆分為不同 演員 在 數據模型 下的邏輯塊。演員由繼承自 DataModel 的 Actor 實例表示。它們作為執行隔離的單元,將負載分配到同時運行的多個核心上。
放置演員實例
您可以將演員放置在適當的容器中,或用它們來替換您的 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() 用於向演員發送消息。
- Actor:BindToMessage() 用於在順序上下文中將 Luau 回調綁定到具有指定主題的消息。
- Actor:BindToMessageParallel() 用於在平行上下文中將 Luau 回調綁定到具有指定主題的消息。
以下示例顯示如何使用 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 代碼時參考以下最佳實踐:
避免長時間計算 — 即使在平行中,長時間計算也可能阻塞其他腳本的執行並導致延遲。避免使用平行程式設計來處理大量長時間的、不可讓步的計算。

