パラレル Luau

*このコンテンツは、ベータ版のAI(人工知能)を使用して翻訳されており、エラーが含まれている可能性があります。このページを英語で表示するには、 こちら をクリックしてください。

パラレル Luau プログラミングモデルでは、複数のスレッドでコードを同時に実行でき、これによりエクスペリエンスのパフォーマンスが向上することができます。より多くのコンテンツでエクスペリエンスを拡大すると、Luau スクリプトのパフォーマンスと安全性を維持するのに役立つこのモデルを採用できます。

パラレルプログラミングモデル

デフォルトでは、スクリプトは順次実行されます。非プレイヤーキャラクター (NPC)、レイキャスト検証、プロシージャル生成など、エクスペリエンスに複雑なロジックやコンテンツがある場合、順次実行でユーザーに遅延が発生する可能性があります。並列プログラミングモデルでは、タスクを複数のスクリプトに分割して並行して実行できます split tasks into multiple scripts 。これにより、エクスペリエンスコードの実行が高速化され、ユーザーエクスペリエンスが向上します。

並列プログラミングモデルは、コードに安全性のメリットも追加します。コードを複数のスレッドに分割することで、1つのスレッドでコードを編集すると、並行して実行されている他のコードには影響しません。これにより、コード内の 1つのバグが全体のエクスペリエンスを損なうリスクが軽減され、アップデートをプッシュするときにライブサーバーのユーザーの遅延が最小限に抑えられます。

並列プログラミングモデルを採用することは、すべてを複数のスレッドに置くことを意味しません。たとえば、サーバー側レイキャスト検証 は、個々のユーザーにリモートイベントを並行して発行しますが、グローバルプロパティを変更するために最初のコードを連続して実行する必要があり、これは並列実行の共通のパターンです。

ほとんどの場合、並列フェーズとシリアルフェーズを組み合わせて、目的の出力を達成する必要があります, 現在並列でサポートされていないいくつかの操作がスクリプトの実行を妨げる可能性があるため, 並列フェーズでインスタンスを変更するなど。並行して API の使用レベルに関する詳細情報は、スレッドセーフティ を参照してください。

コードを複数のスレッドに分割

エクスペリエンスのスクリプトを複数のスレッドで同時に実行するには、データモデルの アクター の異なる 下でロジカルチャンク に分割して、実行する必要がある。アクターは、DataModel から継承された Actor インスタンスによって表現されます。彼らは、同時に実行されている複数のコアに負荷を分配する実行隔離ユニットとして機能します。

アクターインスタンスを配置する

アクターを適切なコンテナに入れたり、NPCやレイキャスターなどの 3D エンティティのトップレベルインスタンスタイプを置き換えたり、対応するスクリプトを追加したりできます。

An example of a Script under an Actor

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

A tree of actors and scripts that shows how a script is owned by its closest actor

スレッドを非同期化

アクターの下にスクリプトを配置することで、並列実行の履行能が提供されますが、デフォルトではコードは引き続き単一スレッドで連続的に実行され、ランタイムパフォーマンスが向上しません。現在のコルーチンの実行を一時停止して並行してコードを実行し、次の並列実行機会で再開するための task.desynchronize() を呼び出す必要があります。You need to call the 、a yieldable function that suspends the execution of the current coroutine for running code in parallel and resumes it at the next parallel execution opportunity.スクリプトをシリアル実行に戻すには、task.synchronize() を呼び出します。

代わりに、シグナルコールバックをスケジュールして即座にコードを並行して実行したいときに RBXScriptSignal:ConnectParallel() メソッドを使用できます。シグナルコールバック内で task.desynchronize() を呼び出す必要はありません。

スレッドを非同期化

local RunService = game:GetService("RunService")
RunService.Heartbeat:ConnectParallel(function()
... -- 状態更アップデートを計算するパラレルコード Some parallel code that computes a state update
task.synchronize()
... -- インスタンスの状態を変更する一部のシリアルコード
end)

同じアクターの一部であるスクリプトは、常に相互に順番に実行されるため、複数のアクターが必要です。たとえば、NPC の並列対応の振る舞いスクリプトをすべて 1 人のアクターに入れた場合、単一スレッドで連続して実行されますが、異なる NPC ロジックの複数のアクターがある場合、それぞれが独自のスレッドで並行して実行されます。詳しくは、ベストプラクティスを参照してください。

1つのスレッドで連続して実行されているアクターの並列コード
>

同時に複数のスレッドで実行されているアクターの並列コード
>

スレッドセーフティ

並列実行中、DataModel 階層のほとんどのインスタンスに通常通りアクセスできますが、一部の API プロパティと機能は読んだり書いたりすることが安全ではありません。パラレルコードで使用すると、Roblox エンジンは自動的にこれらのアクセスが発生するのを検出し、防止できます。

API メンバーにはスレッドセーフティレベルがあり、次の表に示すように、パラレルコードで使用できるかどうか、およびどのように使用できるかを示します:

安全レベルプロパティの場合機能について
安全でない 並行して読んだり書いたりすることはできません。並行して呼び出すことはできません。
パラレル読み取り 並行して読み込まれるが、書き込めない。N/A
ローカルの安全 同じアクター内で使用できます;並行して他の Actors によって読み取ることができますが、書き込みはできません。同じアクター内で呼び出すことができます;並行して他の Actors によって呼び出すことはできません。
安全 読み込みと書き込みが可能。呼び出すことができます。

API メンバーのスレッドセーフティタグは、API リファレンス で見つけることができます。使用するときは、API 呼び出しやプロパティの変更が並列スレッド間でどのように相互作用するかも考慮する必要があります。通常、複数のアクターが他のアクターと同じデータを読んでも、他のアクターの状態を変更しないことが安全です。

交差スレッドコミュニケーション

マルチスレッドコンテキストでは、異なるアクターのスクリプトが互いに通信してデータを交換したり、タスクを調整したり、アクティビティを同期したりすることを許可することができます。エンジンはクロススレッド通信のための次のメカニズムをサポートします:

クロススレッド通信のニーズに対応するために、複数のメカニズムをサポートできます。たとえば、アクターメッセージ APIを通じて共有テーブルを送信できます。

俳優メッセージ

アクターメッセージング APIは、シリアルまたはパラレルコンテキストのスクリプトが、同じデータモデルのアクターにデータを送信できるようにします。この API を介した通信は非同期で、送信者は受信者がメッセージを受け取るまでブロックしません。

この API を使用してメッセージを送信するとき、メッセージをカテゴリ化するための トピック を定義する必要があります。各メッセージは単一のアクターにのみ送信できますが、そのアクターは内部的に複数のコールバックをメッセージにバインドできます。アクターの子孫であるスクリプトのみがメッセージを受信できます。

API には次のメソッドがあります:

  • Actor:SendMessage() アクターにメッセージを送信するため。
  • Actor:BindToMessage() ルアウコールバックをシリアルコンテキストで指定されたトピックのメッセージにバインドするため。
  • Actor:BindToMessageParallel() ルアウコールバックをパラレルコンテキストで指定されたトピックのメッセージにバインドするため。

次の例では、Actor:SendMessage() を使用してトピックを定義し、送信者側にメッセージを送信する方法を示します:

メッセージ送信者の例

local Workspace = game:GetService("Workspace")
-- 「挨拶」のトピックでワーカーアクターに 2つのメッセージを送信
local workerActor = Workspace.WorkerActor
workerActor: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 は、複数のアクターの下で実行されているスクリプトからアクセス可能なテーブルのようなデータ構造です。大量のデータが関与し、複数のスレッド間で共通の共有状態が必要な状況に便利です。たとえば、複数のアクターがデータモデルに保存されていない共通のワールド状態で作業する場合など。

共有テーブルを別のアクターに送信すると、データのコピーは作成されません。代わりに、共有テーブルは、複数のスクリプトで同時に安全かつアトミックな更新を可能にします。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 コードを追加するときに次のベストプラクティスを参照してください:

  • 長い計算を避ける — 並列でも、長い計算は他のスクリプトの実行をブロックし、遅延を引き起こす可能性があります。大量の長く堅固な計算を処理するために並列プログラミングを使用しないでください。

    Diagram demonstrating how overloading the parallel execution phase can still cause lag
  • 適切なアクターの数を使用する — 最高履行パフォーマンスを得るには、より多くの Actors を使用します。デバイスのコアが Actors より少ない場合でも、細かさはコア間のより効率的な負荷分散を可能にします。

    Demonstration of how using more actors balances the load across cores

    これは、できるだけ多くの Actors を使用する必要があるという意味ではありません。コードを別の に接続されたロジックでコードを壊すのではなく、ロジックユニットに基づいてコードを分割する必要があります。たとえば、並行してレイキャスト検証を有効にしたい場合、4コアシステムをターゲットにしていても、64 Actors やそれ以上を使用するのが適切です。これはシステムのスケーラビリティに有益であり、基本ハードウェアの機能に基づいて作業を分配できるようにします。しかし、維持が困難な Actors も多く使用してはなりません。