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

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

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


スレッドの安全性
並行実行中は、DataModel階層のほとんどのインスタンスに通常通りアクセスできますが、一部のAPIプロパティや関数は読み取りまたは書き込みが安全ではありません。これらを並行コードで使用すると、Robloxエンジンが自動的にこれらのアクセスを検出して防止します。
APIメンバーにはスレッドの安全性レベルがあり、並行コードでの使用方法を示します。以下の表に示すように:
| 安全性レベル | プロパティの場合 | 関数の場合 |
|---|---|---|
| 安全でない | 並行して読み取ったり書き込んだりできません。 | 並行して呼び出すことはできません。 |
| 並行読み取り | 並行して読み取ることはできますが、書き込むことはできません。 | N/A |
| ローカル安全 | 同じアクター内で使用できます。他のActorsからは並行して読み取ることはできますが、書き込むことはできません。 | 同じアクター内で呼び出すことができます。他のActorsからは並行して呼び出すことはできません。 |
| 安全 | 読み取りと書き込みができます。 | 呼び出すことができます。 |
APIメンバーのスレッド安全性タグは、APIリファレンスで確認できます。それらを使用する際は、API呼び出しやプロパティの変更が並行スレッド間でどのように相互作用するかも考慮する必要があります。通常、複数のアクターが同じデータを読み取ることは安全ですが、他のアクターの状態を変更することはできません。
スレッド間通信
マルチスレッドコンテキストでは、異なるアクターのスクリプトが互いに通信してデータを交換し、タスクを調整し、アクティビティを同期させることができます。エンジンは、スレッド間通信のために以下のメカニズムをサポートしています。
- アクターメッセージング APIを使用して、スクリプトを介してアクターにメッセージを送信します。
- 共有テーブルデータ構造を使用して、複数のアクター間で大量のデータを効率的に共有します。
- 直接データモデル通信を使用して、制限付きのシンプルな通信を行います。
スレッド間通信のニーズに応じて、複数のメカニズムをサポートできます。たとえば、アクターメッセージングAPIを介して共有テーブルを送信できます。
アクターメッセージング
アクターメッセージングAPIを使用すると、スクリプトが順次または並行コンテキストで、同じデータモデル内のアクターにデータを送信できます。このAPIを介した通信は非同期であり、送信者は受信者がメッセージを受信するまでブロックされません。
このAPIを使用してメッセージを送信する際は、メッセージを分類するためのトピックを定義する必要があります。各メッセージは単一のアクターにのみ送信できますが、そのアクターは内部でメッセージにバインドされた複数のコールバックを持つことができます。アクターの子孫であるスクリプトのみがメッセージを受信できます。
APIには以下のメソッドがあります:
- Actor:SendMessage()は、アクターにメッセージを送信します。
- Actor:BindToMessage()は、順次コンテキストで指定されたトピックのメッセージにルアウコールバックをバインドします。
- Actor:BindToMessageParallel()は、並行コンテキストで指定されたトピックのメッセージにルアウコールバックをバインドします。
以下の例は、Actor:SendMessage()を使用してトピックを定義し、送信者側でメッセージを送信する方法を示しています:
local Workspace = game:GetService("Workspace")
-- "Greeting"というトピックでワーカーアクターに2つのメッセージを送信
local workerActor = Workspace.WorkerActor
workerActor:SendMessage("Greeting", "こんにちは、世界!")
workerActor:SendMessage("Greeting", "ようこそ")
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)サーバー側の手続き型地形生成
ゲームの広大な世界を作成するために、世界を動的に埋めることができます。手続き型生成は通常、独立した地形チャンクを作成し、ジェネレーターがオブジェクトの配置、材料の使用、ボクセルの充填のために比較的複雑な計算を行います。生成コードを並行して実行することで、プロセスの効率を向上させることができます。以下のコードサンプルはその例です。
-- 並行実行にはアクターの使用が必要です
-- このスクリプトは自分自身をクローンします。元のスクリプトがプロセスを開始し、クローンがワーカーとして機能します
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を使用するべきではありません。