Parallel Luau プログラミングモデルを使用すると、複数のスレッドでコードを同時に実行でき、体験のパフォーマンスを向上させることができます。コンテンツを増やすにつれて、このモデルを採用することで、Luau スクリプトのパフォーマンスと安全性を維持するのに役立ちます。
パラレルプログラミングモデル
デフォルトでは、スクリプトは逐次実行されます。あなたの体験に複雑なロジックやコンテンツ、例えば非プレイヤーキャラクター (NPC) 、レイキャスト検証、手続き型生成がある場合、逐次実行はユーザーに遅延を引き起こす可能性があります。パラレルプログラミングモデルを使うことで、タスクを複数のスクリプトに分割して複数のスレッドで実行することができ、体験のコードがより早く実行され、ユーザー体験が改善されます。
また、パラレルプログラミングモデルは、コードの安全性を向上させるメリットもあります。コードを複数のスレッドに分割することで、1つのスレッドでコードを編集しても、並行して実行されている他のコードには影響しません。これにより、コード内の1つのバグが全体の体験を損なうリスクが減少し、ライブサーバーでの更新をプッシュした際のユーザーへの遅延が最小限に抑えられます。
パラレルプログラミングモデルを採用することは、すべてを複数のスレッドに置くことを意味するわけではありません。例えば、サーバーサイドのレイキャスト検証では、各ユーザーにリモートイベントを並行して設定することができますが、グローバルプロパティを変更するために初期コードが逐次実行される必要があります。これはパラレル実行の一般的なパターンです。
多くの場合、望ましい出力を得るためには、逐次処理と並列処理を組み合わせる必要があります。現在、スクリプトの実行を妨げるような、並列ではサポートされていないいくつかの操作があります。たとえば、並列処理フェーズでのインスタンスの修正などがあります。パラレルでの API 使用のレベルについての詳細は、スレッド安全性を参照してください。
コードを複数のスレッドに分割する
体験のスクリプトを複数のスレッドで同時に実行するには、データモデル内の異なる アクター の下に論理的なチャンクに分割する必要があります。アクターは、Actor インスタンスで DataModel から継承されます。これらは、同時に実行される複数のコアにロードを分散させる実行隔離のユニットとして機能します。
アクターインスタンスを配置する
アクターを適切なコンテナに配置するか、NPC やレイキャスターなどの 3D エンティティの最上位インスタンスタイプを置き換えるために使用して、それに対応するスクリプトを追加することができます。

ほとんどの状況では、データモデル内の別のアクターの子としてアクターを配置すべきではありません。ただし、特定のユースケースのために複数のアクター内にネストされたスクリプトを配置することを決定した場合、スクリプトは最も近い先祖アクターに属します。

スレッドの非同期化
スクリプトをアクターの下に置くことで、並行実行の能力が与えられますが、デフォルトではコードは依然として単一のスレッドで逐次実行され、実行パフォーマンスは向上しません。task.desynchronize() を呼び出す必要があります。これは、並行してコードを実行するために現在のコルーチンの実行を一時停止し、次の並行実行の機会に再開することができる yieldable 関数です。スクリプトを再び逐次実行に切り替えるには、task.synchronize() を呼び出します。
また、シグナルコールバックを即時に並行実行するためにスケジュールする場合、RBXScriptSignal:ConnectParallel() メソッドを使用できます。シグナルコールバック内で task.desynchronize() を呼び出す必要はありません。
スレッドを非同期化する
local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- 一部の並行コードが状態更新を計算する
task.synchronize()
... -- 一部の逐次コードがインスタンスの状態を変更する
end)
同じアクターの一部であるスクリプトは、互いに対して常に逐次実行されるため、複数のアクターが必要です。例えば、NPC のためにすべての並行動作スクリプトを1つのアクターに置くと、それらは依然として単一のスレッドで逐次実行されますが、異なる NPC ロジックのために複数のアクターを持っている場合、それぞれが独自のスレッドで並行して実行されます。詳細については、ベストプラクティスを参照してください。


スレッド安全性
並行実行中、DataModel 階層のほとんどのインスタンスに通常通りアクセスできますが、いくつかの API プロパティや関数は読み取りまたは書き込みが安全ではありません。これらを並行コードで使用すると、Roblox エンジンが自動的にこれらのアクセスを検出して防止することができます。
API メンバーにはスレッド安全レベルがあり、それを示すテーブルは次の通りです。
| 安全レベル | プロパティのために | 関数のために |
|---|---|---|
| Unsafe | 並行して読み取ったり書き込んだりできません。 | 並行して呼び出すことはできません。 |
| Read Parallel | 並行しては読み取れますが、書き込むことはできません。 | N/A |
| Local Safe | 同じアクター内で使用可能。並行して他の Actors からは書き込めないが、読み取ることができます。 | 同じアクター内で呼び出せますが、並行して他の Actors からは呼び出せません。 |
| Safe | 読み書きできます。 | 呼び出すことができます。 |
API メンバーのスレッド安全性タグは、API リファレンスで確認できます。使用する際には、API の呼び出しやプロパティの変更が並行スレッド間でどのように相互作用するかを考慮する必要があります。通常、複数のアクターが他のアクターと同じデータを読み取ることは安全ですが、他のアクターの状態を変更することは安全ではありません。
スレッド間通信
マルチスレッドの文脈では、異なるアクター内のスクリプトが互いに通信し、データを交換し、タスクを調整し、アクティビティを同期することを許可できます。エンジンは、スレッド間通信のために次のメカニズムをサポートしています。
- アクターメッセージング API を使用して、スクリプトからアクターにメッセージを送信します。
- 共有テーブル データ構造を使用して、複数のアクター間で大量のデータを効率的に共有します。
- ダイレクトデータモデル通信 により、簡単な通信が制限付きで行われます。
スレッド間通信のニーズに応じて、複数のメカニズムをサポートすることができます。たとえば、アクターメッセージング API を通じて共有テーブルを送信できます。
アクターメッセージング
アクターメッセージング API を使用すると、スクリプトが逐次または並行のコンテキストで、同じデータモデル内のアクターにデータを送信できます。この API を通じた通信は非同期であり、送信者は受信者がメッセージを受け取るまでブロックされません。
この API を使用してメッセージを送信する場合、メッセージのカテゴリを定義するための トピック を設定する必要があります。各メッセージは単一のアクターにのみ送信できますが、そのアクターは内部でメッセージにバウンドされた複数のコールバックを持つことができます。アクターの子孫であるスクリプトのみがメッセージを受信できます。
API には次のメソッドがあります。
- Actor:SendMessage() により、アクターにメッセージを送信します。
- Actor:BindToMessage() を使用して、指定されたトピックに対して逐次コンテキストで Luau コールバックをバインドします。
- Actor:BindToMessageParallel() を使用して、指定されたトピックに対して並行コンテキストで Luau コールバックをバインドします。
以下の例は、Actor:SendMessage() を使用して、トピックを定義し、送信者側でメッセージを送信する方法を示しています。
メッセージ送信者の例local Workspace = game:GetService("Workspace")-- トピック「Greeting」でワーカーアクターに2つのメッセージを送信local workerActor = Workspace.WorkerActorworkerActor: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 は、複数のアクターで実行されるスクリプトからアクセス可能なテーブルのようなデータ構造です。大量のデータを扱い、複数のスレッド間で共通の共有状態を必要とする状況に便利です。たとえば、複数のアクターがデータモデルに保存されていない共通のワールド状態に取り組んでいる場合です。
共有テーブルを別のアクターに送信しても、データのコピーは作成されません。代わりに、共有テーブルは複数のスクリプトによる安全で原子的な更新を同時に可能にします。1人のアクターによる共有テーブルのすべての更新は、すべてのアクターに即座に表示されます。共有テーブルは、基盤となるデータをコピーするのではなく、構造的な共有を利用したリソース効率の良いプロセスでクローンすることもできます。
ダイレクトデータモデル通信
データモデルを使用して複数のスレッド間の通信を直接促進することもできます。この場合、異なるアクターがプロパティや属性に書き込むことができ、その後それを読むことができます。ただし、スレッド安全性を維持するために、並行して実行されるスクリプトは一般にデータモデルに書き込むことができません。したがって、通信のためにデータモデルを直接使用する場合は制限があり、スクリプトがしばしば同期させる必要が生じ、スクリプトのパフォーマンスに影響を与えることがあります。
例
サーバーサイドのレイキャスト検証
戦闘やバトル体験のために、ユーザーの武器にレイキャストを有効にする必要があります。クライアントが良好なレイテンシを達成するために武器をシミュレートし、サーバーはヒットを確認する必要があります。これには、レイキャストを行い、期待されるキャラクターの速度を計算し、過去の動作を確認するためのある程度のヒューリスティックが含まれます。
クライアントがヒット情報を通信するためにリモートイベントに接続する単一の集中化されたスクリプトを使用する代わりに、各ヒット検証プロセスをサーバーサイドで実行し、各ユーザーキャラクターが別々のリモートイベントを持つようにすることができます。
そのキャラクターの 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
-- 複数のアクターがレイキャストで同じパートを取得し、それを破壊することを決定できます
-- これは完全に安全ですが、1回ではなく2回の爆発が発生します
-- 以下のコードは、実行がこの部分に最初に到達したことを確認します
if hitPart.Parent then
explosion.Parent = Workspace
hitPart:Destroy() -- それを破壊します
end
end
end
end
-- 一部のセットアップコードが並行して実行できないため、最初は逐次で信号を接続します
remoteEventConnection = remoteEvent.OnServerEvent:Connect(onRemoteMouseEvent)
サーバーサイドの手続き型地形生成
あなたの体験のために広大な世界を作成するには、世界を動的に populated することができます。手続き型生成は通常、独立した地形チャンクを生成し、ジェネレーターがオブジェクトの配置、材料の使用、およびボクセルの充填のための比較的複雑な計算を実行します。生成コードを並行して実行することによって、プロセスの効率が向上します。以下のコードサンプルは、その例です。
-- 并行执行需要使用当演员
-- 这个脚本克隆自己;原始的启动过程,而克隆的作为工作者
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 コードを追加するときに次のベストプラクティスを参照してください。
長い計算を避ける — 並行でも、長い計算は他のスクリプトの実行をブロックし、ラグを引き起こす可能性があります。長く非yieldな計算を処理するためにパラレルプログラミングを使用することは避けてください。

適切な数のアクターを使用する — 最適なパフォーマンスのためには、より多くの Actors を使用してください。デバイスが Actors よりも少ないコアを持っていても、細分化によりコア間のより効率的なロードバランシングが可能になります。

これが、できるだけ多くの Actors を使用すべきだという意味ではありません。ロジック単位に基づいて Actors にコードを分割する必要があり、異なる Actors に接続されたロジックを分割しないようにしてください。例えば、レイキャスト検証を並行で有効にする場合、4コアシステムをターゲットにしていても、64 Actors を使用することが理にかなっています。これはシステムのスケーラビリティにとって重要であり、基盤となるハードウェアの能力に基づいて作業を分散させることができます。しかし、また、多すぎる Actors を使用しないようにしてください。メンテナンスが難しくなります。