roblox-dev について
roblox-dev は、Roblox と Luau の開発向けに、エンジンの実行時動作とコーディング手法を扱うスキルです。レプリケーション、スクリプト、データサービス、パフォーマンスなどを解説し、詳細な参照資料も備えています。
主な機能
- 01Roblox の実行時動作の解説
- 02スクリプトの種類と配置の指針
- 03サーバー権威型設計の原則
- 04レプリケーションとリモート処理の検証
- 05詳細なトピックの参照資料
活用シーン
- 01Roblox ゲーム構成のレビュー
- 02クライアントとサーバー間の処理実装
- 03レプリケーション対象やシグナルの扱い
- 04データストアやストリーミング規則の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ afrxo/ roblox-agent-skills --skill roblox-devインストール後、Claude Code で/roblox-devを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
examples/.gitkeepcharacter-lifecycle.luauconnection-tracker.luaudatastore-session.luauremote-handler.luauservices-header.luaureferences/buffer-library.mdcloud-services.mddatastore-rules.mdparallel-luau.mdperformance-optimization.mdquery-descendants.mdreplication-model.mdstreaming-enabled.md詳細
Roblox Engine
Roblox プラットフォームのランタイム時の動作について説明します。情報源:API と概念については create.roblox.com/docs、エンジニアによる補足説明や RFC については devforum.roblox.com を参照してください。
詳しくは、次の資料をご覧ください。
references/replication-model.md— どこから何がレプリケーションされるか、プロパティごとのルール、遅延シグナルのタイミングreferences/cloud-services.md— データストア/メモリストア/設定/シークレット/インメモリの選択ガイドreferences/datastore-rules.md— レート制限、再試行/バックオフ、セッションロックreferences/streaming-enabled.md— チャンクのセマンティクス、ModelStreamingMode、クライアント側のルールreferences/query-descendants.md—Instance:QueryDescendantsのセレクター構文全体references/buffer-library.md—bufferライブラリの API 全体references/parallel-luau.md— アクターモデル、ConnectParallel、SharedTable、スレッドセーフティreferences/performance-optimization.md— よくある落とし穴と対策の一覧
統合ポリシー
このスキルでは推奨パターンを説明しますが、既存コードを書き換える権限を与えるものではありません。ルールを適用するのは、新しいコードと、ユーザーから明示的にリファクタリングを依頼されたコードです。スキルの既定値を適用する前に、コードベースの既存スタイル(大文字・小文字の使い方、インデント、引用符のスタイル、モジュールの構成)に合わせてください。プロジェクトで使われていないツールは導入せず、競合があれば一度だけ指摘し、既存のパターンを優先してください。
すべてのルールについては、../../shared/integration-policy.md を参照してください。
原則
- サーバーが権威を持ちます。 クライアントの情報は信用できません。ゲームの状態に影響するもの(通貨、インベントリ、ダメージ、信頼する位置情報)はサーバー側で管理します。クライアントは意図を伝え、サーバーが検証して適用します。
- 状態の所有者は一つだけです。 すべての状態には、サーバー、操作中のクライアント、またはレプリケーションのいずれか一つの所有者を割り当てます。書き込み元が二つあると競合が発生します。
- レプリケーションは非同期です。 サーバー側で作成されたものは、同じフレームではなく、後続のフレームでクライアントに表示されます。クライアント側で作成されたものは、サーバーにレプリケーションされません。
- yield は観測可能な動作です。 イベントハンドラー内で yield する呼び出しを行うと、その間にほかのハンドラーが実行されます。処理順序は記述した順序どおりとは限りません。
- API を引用する前に確認してください。 Roblox の API は非常に広範で、頻繁に変わります。プロパティ名をでっち上げず、不確かな場合はその旨を示してください。
サービス — すべて GetService を使用する
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
- 常に
game:GetService("Name")を使用してください。game.ServiceNameは使用しないでください。- スクリプトの実行時にデフォルトでは利用できないサービスがあります。
GetServiceはそれらを作成しますが、ドット記法でアクセスするとエラーになります。 - サービスによっては、アクセサー名とクラス名が一致しません(例:
RunServiceの内部名は「Run Service」です)。
- スクリプトの実行時にデフォルトでは利用できないサービスがあります。
- 小文字の
workspaceグローバル変数ではなく、GetService経由でWorkspaceを取得してください。 どちらも機能しますが、グローバル変数はGetServiceより前からあり、互換性のために存在しています。GetServiceに統一すると、すべてのサービスへのアクセス方法が一貫し、タイプミスを未定義変数エラーとして検出できます。 GetServiceの呼び出しはファイルの先頭にアルファベット順でまとめ、後の箇所には分散させないでください。
DataModel と Instance
- DataModel は
gameをルートとするツリーです。サービスは最上位の子で、それ以外はすべて Instance です。 - Instance は、
Parent = nilであり、かつどのスクリプトも参照を保持していない場合に破棄されます。Parent = nilを設定しても破棄にはなりません。 イベントを切断し、破棄済みとしてマークし、Parentを nil に設定するには、:Destroy()を使用してください。 :Destroy()は取り消せません。 破棄された Instance は再び親に設定できず、その後プロパティにアクセスするとエラーになります。:Clone()を使うにはArchivable = true(デフォルト)である必要があります。モデルを複製すると子孫も含まれますが、兄弟間の参照は再バインドされません。
子孫の検索
-
:GetChildren()— 直接の子のみです。 -
:GetDescendants()— フィルターなしで、サブツリー全体を返します。小さなツリーでは低コストですが、大きなツリー(ストリーミングされた Workspace など)では高コストです。 -
:FindFirstChild(name)/:FindFirstChild(name, true)— 第 2 引数は再帰検索を指定します。 -
:FindFirstChildOfClass(class)/:FindFirstChildWhichIsA(class)— クラスを指定して直接の子を検索します。 -
:QueryDescendants(selector: string)→{ Instance }。CSS のようなセレクター文法(ClassName、.Tag、#Name、[prop = val]、[$attr]、[$attr = val]、結合子>>>,、疑似クラス:not(...):has(...))を使い、エンジン側でフィルター済みの子孫を検索します。大きなツリーでは、GetDescendantsと Luau 側でのフィルター処理よりこちらを優先してください。スレッドセーフではありません。文法の全容と例については、references/query-descendants.mdを参照してください。Workspace:QueryDescendants("MeshPart.SwordPart") -- MeshParts tagged SwordPart Workspace:QueryDescendants("Model >> [$OnFire = true]") -- attribute-matching descendants of any Model Workspace:QueryDescendants(":not(SpotLight, PointLight)") -- excluding lights
WaitForChild のルール
- クライアントでは、複製されるコンテナー(
ReplicatedStorage、Workspaceなど)の子が最初のフレームでは存在しないことがあります。自分で配置していないものには、:WaitForChild("Name")を使用してください。 - サーバーでは、Studio に配置したすべての Instance はスクリプトの開始時に存在するため、
:FindFirstChildで十分です。 - 本番コードでは、タイムアウトなしで
WaitForChildを呼び出さないでください。子が届かない場合、永久に待機します。:WaitForChild("Name", 5)は 5 秒後にnilを返します。
スクリプトの種類と配置場所
スクリプトには 3 種類あります。
Script—Script.RunContextと配置場所に応じて、サーバーまたはクライアントで実行されます。RunContext = Legacy(デフォルト)→ サーバー専用で、サーバーコンテナー(ServerScriptService、Workspaceなど)でのみ実行されます。RunContext = Server→ サーバーで実行され、ReplicatedStorageからも実行できます(ただし、クライアントがソースを読めるため、そうしないでください)。RunContext = Client→ クライアントで実行され、ReplicatedStorageから実行されます。新しいクライアントスクリプトにはこちらを推奨します(LocalScriptよりも意図が明確です)。
LocalScript— クライアント専用です。RunContextはありません。クライアントコンテナー(StarterPlayerScripts、StarterCharacterScripts、StarterGui、StarterPack、ReplicatedFirst)でのみ実行されます。ModuleScript— どちら側からでもrequire()できます。呼び出し元のコンテキストで実行されます。
推奨レイアウト
| 場所 | ここに配置するもの | RunContext |
|---|---|---|
ServerScriptService | サーバースクリプトとサーバー専用の ModuleScript | Server |
ServerStorage | サーバー専用データ(アセット、テーブル)— クライアントには内容が見えません | n/a |
ReplicatedStorage | 共有 ModuleScript(サーバーとクライアント)およびクライアントスクリプト | クライアントスクリプトは Client |
ReplicatedFirst | 最小限のクライアントローダー(読み込み画面など) | Client |
StarterPlayerScripts / StarterCharacterScripts / StarterGui / StarterPack | プレイヤーごとに実行される LocalScript | n/a |
Workspace | ワールドのコンテンツ。オブジェクトの動作を定義するサーバー側の Script。 | 明示的に指定する場合は Server |
一般的な方法として、コードの大部分は ReplicatedStorage(共有)または ServerStorage/サーバー専用の場所に ModuleScript として配置し、モジュールを読み込むサーバーのエントリーポイント Script を ServerScriptService に 1 つ、クライアントのエントリーポイントを ReplicatedStorage に 1 つ(RunContext = Client)置きます。
ルール
- サーバーロジックを
ReplicatedStorageに置かないでください。 クライアントがソースを読めるため、そこにあるものはすべて漏えいすると考えてください。 - モデルやパッケージ内のスクリプトでは、
RunContextを明示的に設定してください。 これにより、モデルの親が変更された場合や共有された場合の曖昧さをなくせます。 - 共有 ModuleScript で実行時の分岐を行うには、
RunService:IsServer()/:IsClient()を使用してください。script.Parentから推測しないでください。 Workspace内のLocalScriptは実行されません。 クライアントで実行可能なコンテナーにのみ配置してください。よくあるバグです。Workspace内のサーバースクリプトは実行されますが、ジオメトリの移動時に意図が伝わりやすく、予想外の動作も少ないため、ServerScriptServiceでRunContext = Serverを設定する方が明確です。
出典:create.roblox.com/docs/scripting/locations。
レプリケーション
- サーバー → クライアントは、レプリケートされるコンテナ(
Workspace、ReplicatedStorage、Players[X].PlayerGui、StarterGuiからPlayerGuiにクローンされたもの)では自動です。 プロパティの変更と親の変更がレプリケートされます。 - クライアント → サーバーはレプリケートされません。 クライアントが作成または変更したものはすべてローカルにとどまり、サーバーからは見えません。例外は
Players[X].Character(FilteringEnabled 導入前後の時代とキャラクターの所有権によるもの)、入力イベント、明示的なRemoteEvent呼び出しです。 - レプリケーションはすべて一括ではなく、プロパティごとに行われます。 両側に存在するコンテナでも、内容がクライアントローカルのものがあります(
Player.PlayerScripts、StarterGuiの初回レプリケーション後のPlayer.PlayerGui)。 - 親に設定する前にプロパティを設定してください。 インスタンスがレプリケートされるコンテナに入ると、それ以降のプロパティ変更はそれぞれ個別の更新としてレプリケートされ、クライアントが中間状態を観測する可能性があります。インスタンスを完全に構築してから
Parentを設定してください。 - シグナルの動作は重要です。
Workspace.SignalBehavior = Enum.SignalBehavior.Deferred(新しいプレースでの現在のデフォルト)では、次に実行が再開されるタイミングまでシグナルの発火がまとめられます。ChildAddedが同期的に発火すると仮定するコードは脆弱です。遅延シグナルを有効にしてテストしてください。 - 詳細な表については、
references/replication-model.mdを参照してください。
RemoteEvents と RemoteFunctions
-- Server
local TakeDamage = ReplicatedStorage:WaitForChild("TakeDamage") :: RemoteEvent
TakeDamage.OnServerEvent:Connect(function(player, amount: unknown)
if typeof(amount) ~= "number" or amount < 0 or amount > 100 then return end
-- validated; apply
end)
-- Client
TakeDamage:FireServer(25)
OnServerEventの最初の引数は、常に発火元のPlayerです。 これは Roblox が提供する真正な値です。それ以外の値は信用しないでください。- ほかのすべてのパラメーターは
unknownとして型付けし、境界で検証してください。numberとして型付けすると、型チェッカーが悪意のあるユーザーからの入力を受け入れてしまいます。(完全なパターンについてはluau-expertスキルを参照してください。) RemoteFunctionは:InvokeServer()/:InvokeClient()を使います。同期処理であり、呼び出し中は実行が中断されます。可能であれば、クライアントからサーバーへのInvokeClientは避けてください。悪意のあるクライアントによって呼び出しが停止する可能性があります。代わりにRemoteEventと返信イベントを使用してください。- すべての Remote をレート制限してください。 プレイヤーごとの呼び出し回数を記録し、明らかなスパムを拒否します。デフォルトでは、上限のあるキューを使い、満杯になったら古いものを破棄するか、新しいものを拒否してください。
- 論理的なアクションごとに Remote を1つ使ってください。
kindフィールドによる多重化は避けてください。型が明確になり、検証も簡単になります。
キャラクターのライフサイクル
local function onCharacterAdded(character: Model)
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
-- ...
end
Players.PlayerAdded:Connect(function(player)
if player.Character then onCharacterAdded(player.Character) end
player.CharacterAdded:Connect(onCharacterAdded)
end)
PlayerAddedの発火時に、プレイヤーの最初のキャラクターがすでに存在している場合があります。player.Characterを確認し、今後のCharacterAddedに接続してください。- リスポーンのたびに新しい
Modelが作成されます。 古いキャラクターへの参照は古くなります。CharacterAddedでキャラクターごとのシグナルをすべて再接続し、character.AncestryChangedまたはhumanoid.Diedを使って以前の一連の接続をクリーンアップしてください。 Humanoid.Diedはキャラクターごとに1回発火します。死亡後もモデルは残り(デフォルトでは約5秒。Players.RespawnTimeで制御されます)、その後、自動リスポーンによって破棄されます。Player.CharacterRemovingはモデルがWorkspaceから離れる前に発火します。モデルから状態を読み取る最後の機会です。
ストリーミング
現在のプレースでは Workspace.StreamingEnabled が有効になっています。ワールドの一部が各プレイヤーの周囲で読み込まれたり、読み込み解除されたりします。
- クライアントからは、サーバーに見えているパーツが見えない場合があります。クライアントにジオメトリが存在すると決めつけないでください。 タイムアウトを指定した
WaitForChildを使うか、nil かどうかを確認してください。 - モデルごとのストリーミングは
Model.ModelStreamingModeで制御します。Default— モデルは通常どおりストリーミングされ、子孫は個別にストリーミングされます。Atomic— モデル全体が1つの単位としてストリーミングされ、読み込みまたは読み込み解除されます(部分的な読み込みはありません)。Persistent— すべてのクライアントでモデルが常に読み込まれます。PersistentPerPlayer— 該当するAdd/RemoveAPI で追加されたプレイヤーには常に読み込まれます。それ以外のプレイヤーにはデフォルトで読み込まれません。
- 子孫をまとめて到着させる必要があるオブジェクト(車両のアセンブリ、NPC のリグ)には
Atomicを使ってください。Persistentは、すべてのクライアントでメモリを消費するため、控えめに使用してください。 - 読み込み解除されたパーツのプロパティ変更も引き続きレプリケートされますが、クライアントでは描画されません。読み込み解除されたオブジェクトの
BasePart.CFrameを更新しないでください。帯域幅の無駄になります。 - チャンクのセマンティクスと
ModelStreamingModeの相互作用については、references/streaming-enabled.mdを参照してください。
物理演算の所有権
- アンカーされていない各
BasePartにはネットワーク所有者がいます。デフォルトはサーバーです。プレイヤーのキャラクターがパーツに触れる/乗る/持つと、所有権はそのプレイヤーのクライアントに移ります。 - 所有者が物理演算をシミュレートし、その結果がレプリケーションされます。クライアント所有のパーツの位置はサーバー権威ではありません。ゲーム状態の判断に、その
CFrameを決して信頼しないでください。 - サーバー側で所有権を強制するには、
part:SetNetworkOwner(player)を使います。サーバーに戻すにはnilを渡します。:GetNetworkOwner()で所有者を取得できます。 - アンカーされたパーツには所有者がいません。 アンカーされたパーツに対して
SetNetworkOwnerを呼び出すとエラーになります。入力を制御できない場合は、事前にBasePart:CanSetNetworkOwnership()で確認してください。 - デフォルトでは自動転送が有効です。
:SetNetworkOwner(player)または:SetNetworkOwner(nil)を呼び出すと、そのパーツでは自動転送が無効になり、所有権は設定した状態に維持されます。自動に戻すには:SetNetworkOwnershipAuto()を呼び出します。 - プレイヤーがテレポートできてはならないもの(宝箱、NPC、飛翔体、不正対策に関係するパーツなど)では、一度
SetNetworkOwner(nil)を呼び出し、その後はSetNetworkOwnershipAuto()を決して呼び出さないでください。
クラウドサービス(永続化とサーバー間通信)
Roblox には複数のストレージオプションがあります。慣れで選ぶのではなく、アクセスパターンに応じて選んでください。
| サービス | 永続性 | 範囲 | 用途 |
|---|---|---|---|
標準データストア(DataStoreService:GetDataStore) | 永続 | サーバー間 | ユーザーの進行状況、インベントリ、セーブデータ。数値/文字列/ブール値/テーブルに対応。NoSQL のように利用できます。 |
順序付きデータストア(:GetOrderedDataStore) | 永続 | サーバー間 | 全期間のランキング(数値のみ、並べ替え可能)。 |
メモリストア(MemoryStoreService) | 最大 45 日 | サーバー間 | マッチメイキング、日次/月次ランキング、一時的なサーバー間キュー。データストアより高速です。 |
| 設定(Creator Hub) | 永続 | サーバー間 | フィーチャーフラグ、調整可能な値。ゲーム内からは読み取り専用です。 |
| シークレットストア | 永続 | サーバー間 | API キー、サードパーティのトークン。ゲーム内からは読み取り専用です。 |
| インメモリ Luau テーブル | セッション | 単一サーバー | 一時的な状態(タイマー、状態効果、現在の体力)。無料で、即時に利用できます。 |
クラウドサービスはサーバー専用です。 クライアントは DataStore、MemoryStore などを読み取れません。クライアントからの要求はすべて RemoteEvent 経由で処理してください。
データストアの基本ルール
- 必ず
pcallを使ってください。 ネットワーク障害、スロットリング、クォータ超過により、読み取り/書き込みでエラーが発生します。 - 同時更新が起こる状態には
SetAsyncではなくUpdateAsyncを使ってください。 以前の値を受け取り、アトミックにマージできます。変換関数からnilを返すと、クォータを消費せずに中断できます。 - セッションロックのパターン:
PlayerAddedで一度読み込み、セッション中はメモリ上で変更し、PlayerRemovingとBindToCloseで保存します。変更のたびに読み込まないでください。 game:BindToCloseでは、すべてのプレイヤーを合わせた合計処理時間が約 30 秒です。task.spawnを使って並列に保存してください。- レート制限はキーごと、ゲームごとに適用され、時間の経過とともに回復します。また、キーごとに約 6 秒の厳格なルールもあります。
メモリストアの基本ルール
- 一時的なデータ用です:データは最大 45 日で期限切れになります。セーブデータをここに保存しないでください。
- 便利なプリミティブには、キュー、ソート済みマップ、ハッシュマップがあります。データストアよりも書き込みスループットが高くなっています。
- サーバー専用 API です。
どのサービスをいつ使うかの詳しい選び方については references/cloud-services.md を、データストアのレート制限、再試行/バックオフ、セッションロックのパターンについては references/datastore-rules.md を参照してください。
出典:create.roblox.com/docs/cloud-services/data-stores-vs-memory-stores.
タスクスケジューリング
従来の wait/spawn/delay は task ライブラリに置き換えられています。こちらを使用してください。
| 呼び出し | 使用する場面 |
|---|---|
task.spawn(f, ...) | 新しいスレッドで直ちに f を実行します(現在の再開ポイントの後に再開します)。イベントハンドラーから、yield する本体を切り離す場合に使用します。 |
task.defer(f, ...) | 現在の再開サイクルが終わった後、spawn より遅いタイミングで f を実行します。yield せずに処理をまとめる場合に使用します。 |
task.delay(t, f, ...) | 新しいスレッドで t 秒後に f を実行します。従来の delay に代わるものです。 |
task.wait(t) | 現在のスレッドを t 秒間(省略した場合は 1 フレーム)yield します。wait に代わるものです。 |
task.cancel(thread) | task.spawn/task.delay で開始したスレッドを終了します。明示的なクリーンアップと組み合わせてください。 |
task.synchronize() / task.desynchronize() | Parallel Luau のアクター間の調整に使用します。以下のParallel Luau(マルチスレッド)セクションを参照してください。 |
- 従来のグローバル関数
wait、spawn、delayは使用しないでください。 これらは処理を制限し(waitは約 30 Hz に制限されます)、task版では発生しないパターンでリソースをリークします。 - イベントハンドラー内で
task.spawnを使うと、頻繁に発火するイベントを並行して処理できます。本体が共有状態を変更する場合は、独自のキューやロックを追加してください。 - 現在のイベント処理パスの後に処理を実行したい場合は、
task.spawnの前にtask.deferを使ってください。 ワールドを変更してほかのシグナルを発火させる:Connectの本体内で便利です。 - キャンセルが必要な場合に備えて、
task.spawn/task.delayのスレッドを追跡してください。Instance の破棄時に自動でキャンセルされることはありません。
Buffer ライブラリ(バイナリデータ)
buffer は Luau の固定サイズで変更可能なバイトブロックです。ソース:creator-docs の buffer.yaml。
次の用途に使用します。
- コンパクトな RemoteEvent ペイロード(パック済みのバッファは、ネットワーク経由では Luau テーブルより小さくなります)。
- 小さく保ちたい DataStore 値のカスタムバイナリシリアル化。
- 既存のバイナリ形式の読み取り/書き込み。
string.pack/string.unpackの使用場面での置き換え。
local b = buffer.create(16) -- 16-byte buffer, zero-initialized
buffer.writeu32(b, 0, 0xCAFEBABE) -- 4 bytes at offset 0
buffer.writef32(b, 4, 1.5) -- 4 bytes at offset 4
buffer.writestring(b, 8, "abc") -- 3 bytes at offset 8
local magic = buffer.readu32(b, 0)
local payload = buffer.readstring(b, 8, 3)
主なルール:
- すべてのオフセットは 0 から始まるバイトオフセットです。 バッファの範囲を超えて読み書きするとエラーが発生します。
- すべての数値の読み書きにはリトルエンディアンを使用します。
- バッファの最大サイズ:1 GiB(
buffer.createはこれより大きいサイズを拒否します。また、この上限に近いサイズの割り当ても失敗する場合があります)。 - Roblox API(RemoteEvents など)経由で送信するとコピーされます。 受信側で同一性は維持されません。Actor スクリプト間でバッファを共有しないでください(Parallel Luau)。
- サイズは固定です。 拡大/縮小はできません。適切なサイズを割り当てるか、新しいバッファにコピーしてください(
buffer.copy)。
関数は次の 3 グループに分かれています。
- ライフサイクル:
buffer.create(size)、buffer.fromstring(s)、buffer.tostring(b)、buffer.len(b)。 - 固定幅数値の読み書き:
readi8/u8/i16/u16/i32/u32/f32/f64と、対応するwrite*。 - ビット単位の処理と一括処理:
readbits(b, bitOffset, bitCount)/writebits(1 回の呼び出しで 0~32 ビット)、readstring(b, offset, count)/writestring(b, offset, value, count?)、buffer.copy(target, targetOffset, source, sourceOffset?, count?)、buffer.fill(b, offset, value, count?)。
完全なシグネチャのリファレンスは references/buffer-library.md を参照してください。
Parallel Luau(マルチスレッド)
デフォルトでは、Luau はシングルスレッドです。Parallel Luau では、Actor モデルを介して独立した処理を複数の OS スレッドで実行できます。分割可能な CPU 負荷の高い処理に使用してください。例:プレイヤーごとのレイキャスト検証、プロシージャル生成、インスタンスごとの大規模な計算。
Actor モデル
ActorInstance 配下のスクリプトは、並列実行の対象になります。- アクターごとに 1 つのスレッドです。 同じアクター内の複数のスクリプトは、互いに対して直列に実行されます。
- アクターの数を増やすと、アクター数よりコア数の少ないデバイスでも負荷分散が改善します。
- アクターをほかのアクターの子孫としてネストしないでください。スクリプトを所有するアクターは、最も近い祖先です。
フェーズ:直列実行と並列実行
コードはデフォルトで直列に実行されます。次の方法で切り替えます。
-- Inside an Actor's script
RunService.Heartbeat:ConnectParallel(function()
-- parallel phase
local result = computeExpensive()
task.synchronize()
-- serial phase
applyResult(result)
end)
task.desynchronize()— 現在のスレッドを並列実行に切り替えます。処理を一時停止し、次に並列実行の機会が訪れたときに再開します。task.synchronize()— 直列実行に戻します。ほとんどの Instance を変更する前に必要です。Signal:ConnectParallel(callback)— シグナルのコールバックを自動的に並列実行するようスケジュールします。内部でdesynchronizeを呼び出す必要はありません。- 並列実行フェーズでは
require()を使用できません。 先に直列実行でモジュールを読み込んでください。
スレッド安全性のレベル
API メンバーにはスレッド安全性を示すタグが付いています。
| レベル | プロパティ | 関数 |
|---|---|---|
| Unsafe | 並列での読み取りも書き込みも不可 | 並列での呼び出し不可 |
| Read Parallel | 読み取り可、書き込み不可 | 該当なし |
| Local Safe | 同一アクター内ではすべて可、アクターをまたぐ場合は読み取りのみ | 同一アクター内のみ |
| Safe | 読み取りと書き込みが可能 | どこからでも呼び出し可能 |
タグが付いていない場合、デフォルトは Unsafe です。ジオメトリクエリやレイキャストなど、読み取り中心のアクセスの多くは安全ですが、変更の多くは安全ではありません。メンバーごとに API リファレンスを確認してください。
スレッド間通信
Actor:SendMessage(topic, ...)とActor:BindToMessage(topic, fn)/:BindToMessageParallel(topic, fn)— 非同期の一方向メッセージングです。トピックごとに複数のバインドを設定できます。SharedTable— アクター間で共有され、アトミックな更新が可能なテーブル状の構造です。送信時にコピーされません。「DataModel に保存されない共通のワールド状態」に使用します。- 直列実行フェーズでの DataModel への直接書き込みは機能しますが、頻繁な同期が必要になります。
使用すべきでない場面
- 中断のない長時間の計算。 並列スレッドであっても、譲歩しない 100ms のループはそのスレッドを 100ms 間ブロックします。処理を分割してください。
- フレームごとの軽量な処理。 単純なタスクでは、アクターのオーバーヘッドが削減効果を上回ります。
- DataModel の変更が中心のコード。 ほとんどの時間を
task.synchronize状態で過ごすことになり、並列実行の利点が失われます。
詳細な解説は references/parallel-luau.md を参照してください。出典:create.roblox.com/docs/scripting/multithreading。
パフォーマンス
パフォーマンス最適化は、あらかじめ設計に組み込み、MicroProfiler でボトルネックを特定し、軽減して、監視するサイクルです。以下は特に重要な項目を抜粋したものです。落とし穴の完全な一覧は references/performance-optimization.md を参照してください。
フレーム予算
- 目標:クライアントは 60 FPS = 1 フレームあたり 16.67 ms です。サーバーにフレームレートはありませんが、
RunService.Heartbeatが同じ役割を果たします。 - Luau 側で 1 フレームあたり一貫して 1 ms を超えて実行される処理は、最適化の候補です。
よくある重大なミス
RunService.Heartbeat(またはPreSimulation/PostSimulation/PreRender/PreAnimation)で負荷の高い処理を行う。 Heartbeatは毎フレーム発生します。間隔を空けて実行する(if now - last < 0.1 then return end)か、処理をより低頻度のループに移してください。- サーバー側の
TweenService。 トゥイーン対象のプロパティが毎フレーム複製されるため、レイテンシーがあるとカクつきます。結果をサーバーで確定させる必要がない限り、クライアント側でトゥイーンしてください。 - 切断されないままの
Player/キャラクターへの接続。Playerインスタンスは退出時に自動破棄されません(自動破棄を有効にするにはWorkspace.PlayerCharacterDestroyBehaviorを参照してください)。接続を明示的に追跡して切断するか、PlayerRemovingで手動で破棄してください。 - 際限なく増えるテーブル。 参加時に
playerInfo[player] = ...を設定し、退出時にplayerInfo[player] = nilにしないのは、サーバーでよくあるメモリーリークです。 - 大きなテーブルのディープコピー。 シャローコピーを使い、変更があったパスだけを変更してください(
luau-expertスキルも参照)。 - 大きなRemoteEventペイロード。 インベントリー全体ではなく、差分を送信してください。高頻度の状態には
bufferにパックして送信してください。 - インスタンスの過剰な作成/破棄。 実行時に大きなリグをクローンすると、子孫ツリー全体がクライアントに複製されます。イベントのたびにインスタンスを作成するより、事前にインスタンス化したプールを使うほうが効果的です。
- 動かないのに非アンカー状態のパーツ。
Anchored = trueにすると、物理シミュレーションから完全に除外されます。 - 高い
CollisionFidelity。PreciseはCPUとメモリーを大量に消費します。プレイヤーが違いに気づかないものにはBox/Hullを使ってください。 - アセットの重複。 同じメッシュ/テクスチャを複数回アップロードすると、同じコンテンツがメモリーに複数回読み込まれます。アセットIDを再利用してください。
- 公開したリグにアニメーションのメタデータが含まれている。 クローンしたリグのAnimation Editorデータは、クローンのたびに複製されます。公開前に削除してください。
- 「ポップインのない」ローディング画面のために、Workspace全体に
PreloadAsyncを実行する。 ローディング画面と開始エリアに必要なものだけをプリロードしてください。 - **ネイティブコード生成(
--!native)**は、ホットで処理を譲らない計算パスであれば、少ない手間で効果が得られます。まず、その関数が実際にホットパスで実行されていることを確認してください。
知っておくと便利なMicroProfilerスコープ
RunService.Heartbeat/PreRender/PreSimulation/PostSimulation— スクリプト。physicsStepped/worldStep— 物理演算の負荷。ProcessPackets/Allocate Bandwidth— ネットワークの負荷。updateInvalidatedFastClusters— Humanoid/スキンメッシュの更新による負荷。
debug.profilebegin("Name")/debug.profileend()で独自のスコープにタグを付けてください。
出典:create.roblox.com/docs/performance-optimization、/improve。
バグカタログ
接続の重複
-- ❌ every CharacterAdded adds another connection; old ones never disconnect
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(char)
char.Humanoid.Died:Connect(function() ... end) -- leaks per respawn
end)
end)
スコープごとに接続を追跡し、切断してください。よく使われるパターンは、ライフサイクルを管理するオーナー(プレイヤー、キャラクター、インスタンス)をキーにして{ RBXScriptConnection }を保存し、クリーンアップ時に反復処理して:Disconnect()を呼び出す方法です。
古い参照
local character = player.Character
task.wait(5)
character.Humanoid.Health = 0 -- ❌ player may have respawned; old model is destroyed
yieldの後はplayer.Characterを再取得するか、参照を取得して検証し、if character.Parent == nil then return endで確認してください。
イベントハンドラー内でのyield
RemoteEvent.OnServerEvent:Connect(function(player)
task.wait(1) -- ❌ all subsequent fires from this player wait behind this one
grantReward(player)
end)
ハンドラー内でyieldしないか、本文をtask.spawnで実行して、発火ごとに独立して処理してください。task.spawnには注意が必要です。並行する状態更新にはロックが必要です。
メモリーリーク
Instance:Destroy()はイベント接続を切断してDataModelから削除しますが、Lua側の参照は解放しません。長期間保持する変数はnilに設定してください。- Roblox以外のシグナル(カスタムシグナル、ライブラリなど)への接続は自動でクリーンアップされません。追跡して切断してください。
:Connect(...)内で大きなテーブルをキャプチャするクロージャーは、接続が存在する限り、そのテーブルを保持し続けます。
CollectionServiceのクリーンアップ
-- ❌ tag handler runs once per existing tagged instance, then once per new one
CollectionService:GetTagged("Door"):forEach(setupDoor)
CollectionService:GetInstanceAddedSignal("Door"):Connect(setupDoor)
クリーンアップには必ずGetInstanceRemovedSignalを組み合わせてください。インスタンスごとの状態はクロージャーではなく、{ [Instance]: State }マップで追跡してください。instance.AncestryChanged(parent == nilの場合)または削除シグナルを使ってください。
セットアップ中に参加するプレイヤーへの対応
PlayerAdded はスクリプトの実行開始時にサーバー内にいるプレイヤーに対しても発火します(:Connect によるバインドでは、以降の発火しか捕捉できません)。既存のプレイヤーは手動で処理してください。
for _, player in Players:GetPlayers() do onPlayerAdded(player) end
Players.PlayerAdded:Connect(onPlayerAdded)
アーキテクチャのルール
- サーバーとの境界ではすべて検証します。 RemoteEvent の引数、DataStore の読み取り値、MessagingService のペイロード、HttpService のレスポンスなど、ユーザーの影響を受けるものはすべて対象です。
- クライアント側のチェックより、サーバーが権限を持つチェックを優先します。 クライアント側の検証は UX(即時フィードバック)のためのものです。サーバー側の検証はセキュリティのためのものです。どちらか一方だけにしないでください。
- 1 つの ModuleScript には 1 つの関心事を持たせます。 巨大な「Manager」モジュールにまとめず、責務が明確な小さなモジュールを組み合わせてください。(モジュールとファイルの構成ルールについては、
luau-expertを参照してください。) - 状態の各要素には所有者を 1 つだけ設定します。 フィールドはサーバー所有、複製専用、またはクライアント所有のいずれかです。書き込み元を 2 つにしないでください。複数のアクターが何かを更新する必要がある場合は、書き込みを直列化する 1 つのモジュールを経由させてください。
- 複製は一元管理します。 複製されるドメイン(インベントリ、通貨、クエストの進行状況など)ごとに 1 つのモジュールを用意し、コードベースのあちこちに Remote を散在させないでください。監査しやすくなり、検証やレート制限も追加しやすくなります。
- クライアントから送信された UserId を信頼しないでください。 実際の
PlayerInstance から取得したUserId(OnServerEventの第 1 引数、Players:GetPlayers()、Players:GetPlayerByUserId)は真正です。クライアントが RemoteEvent の引数として送信するUserIdは単なる数値なので、検証するか無視してください。 - サーバーハンドラーは冪等にします。 ネットワークの一時的な不具合による再送も、エクスプロイトを行うユーザーによる再送も現実に起こります。同じ RemoteEvent が 2 回発火しても、最終状態が同じになるように設計してください。
- 負荷の高い処理はサーバー側で行います。 経路探索、大規模なクエリ、CPU 負荷の高い処理などが対象です。クライアント端末の性能には大きな差がありますが、サーバー環境は均一です。
MessagingServiceだけでサーバー間の状態を構築しないでください。 ベストエフォートであり、配信は保証されず、ゲームごとにレート制限があります。「注目すべきイベント」の通知には使えますが、状態の同期には使わないでください。- スクリプトのコンテナーを実行環境に合わせます。 サーバーロジックは
ServerScriptService/ServerStorage、クライアントロジックはStarterPlayerScripts/StarterCharacterScripts/StarterGui、共有モジュールはReplicatedStorageに配置してください。サーバー専用ロジックをReplicatedStorageに置かないでください。クライアントから読み取られる可能性があります。
検証のルール
- 最も安定した範囲のプロパティ(
Instance.Parent、BasePart.CFrame、Humanoid.Healthなど)を除き、API シグネチャを記憶だけで引用しないでください。 Roblox は名称変更、非推奨化、追加を行います。不確かな場合は create.roblox.com/docs を案内し、不確かであることを明示してください。 - API より動作に基づいて説明します。 「
Workspace.StreamingMinRadius = 256を使ってください」ではなく、「サーバーが…の権限を持ちます」のようにルールを説明してください。プロパティ名は変更される可能性がありますが、原則は変わりません。 - 安定している要素と変化する要素を区別します。 DataModel の階層、サービス、複製の方向、キャラクターのライフサイクルは、長年にわたって安定しています。新しい API(最近追加された
Workspaceのプロパティ、新しいRunServiceのメソッド、スクリプト関連のベータ機能など)は、推奨する前に確認してください。 - エンジンと言語を混同しないでください。 型付けや
--!strictの挙動に関する質問は、luau-expertに委ねてください。Rojo やファイルシステムの構成に関する質問は、roblox-toolchainに委ねてください。UI ライブラリに関する質問は、roblox-uiに委ねてください。
避けるべきアンチパターン
| ❌ しないこと | ✅ すること |
|---|---|
game.Workspace / workspace グローバル | game:GetService("Workspace") |
game.Players.PlayerAdded:Connect(f) のみを使う | :GetPlayers() を反復処理してから、起動中に参加するプレイヤーに備えて :Connect する |
クライアントで child = parent:FindFirstChild("X") を使う | :WaitForChild("X", timeout) |
| 型アノテーションだけを根拠にクライアントの RemoteEvent 引数を信頼する | unknown 型にして、検証してから型を絞り込む |
RemoteFunction:InvokeClient | RemoteEvent の往復通信 |
kind フィールドを使って複数のアクションを 1 つの Remote に集約する | アクションごとに Remote を 1 つ用意する |
同時に更新される状態に SetAsync を使う | UpdateAsync |
pcall なしで DataStore を呼び出す | 常に pcall とリトライ/バックオフを使う |
| 変更のたびに DataStore から読み込む | セッション内でキャッシュし、PlayerRemoving と BindToClose で書き込む |
| yield をまたいでキャラクターへの参照を保持する | yield の後に player.Character を再取得する |
| リスポーンをまたいで接続し、切断しない | キャラクターごとに接続を追跡して切断する |
クリーンアップに Instance.Parent = nil を使う | :Destroy() |
タイムアウトなしで WaitForChild を使う | :WaitForChild("X", 5) を使い、nil を処理する |
| クライアント側だけで検証する | 常にサーバーで検証する |
| 頻繁に発火するイベントハンドラー内で yield する | 本体を task.spawn し、独自のキューで順序を管理する |
| 重要なパーツのネットワーク所有権を自動設定する | 明示的に SetNetworkOwner(nil) を設定する |
削除シグナルとのペアなしでタグセットを pairs で反復処理する | GetInstanceAddedSignal と GetInstanceRemovedSignal をペアにする |
サーバーのロジックを ReplicatedStorage に置く | ServerScriptService / ServerStorage |
Workspace に LocalScript を置く | クライアント用コンテナー(StarterPlayerScripts など)、または ReplicatedStorage 内で RunContext = Client に設定した Script |
従来のグローバル wait / spawn / delay | task.wait / task.spawn / task.defer / task.delay |
RunService.Heartbeat 上で重い処理を直接行う | 頻度を下げる/低速なループに移す/task.delay を使う |
視覚効果の Tween にサーバー側の TweenService を使う | クライアントで Tween を実行し、目標値だけをレプリケートする |
| イベントごとに実行時に大きなリグをクローンする | プールする/事前にインスタンス化する |
小さなパーツに CollisionFidelity = Precise を設定する | Box / Hull |
| 並列フェーズ内で Instance を変更する | 先に task.synchronize() を呼び出す |
| API キーを DataStore に保存する | シークレットストア |
| MemoryStore にデータを保存する | 標準のデータストア |
例
examples/services-header.luau— 標準的なサービスインポートブロックです。examples/character-lifecycle.luau— 参加、キャラクターの追加/削除、キャラクターごとのクリーンアップです。examples/remote-handler.luau— 検証とレート制限を備えた RemoteEvent のサーバーハンドラーです。examples/datastore-session.luau— 参加時に読み込み、セッション内でキャッシュし、退出時と BindToClose で保存します。examples/connection-tracker.luau—:Disconnect()をまとめて呼び出すための破棄可能なスコープです。