mcpField
R
roblox-dev

roblox-dev

github.com/afrxo開発1262 回インストール26 週間前に更新
リポジトリ

roblox-dev について

roblox-dev は、Roblox と Luau の開発向けに、エンジンの実行時動作とコーディング手法を扱うスキルです。レプリケーション、スクリプト、データサービス、パフォーマンスなどを解説し、詳細な参照資料も備えています。

主な機能

  1. 01Roblox の実行時動作の解説
  2. 02スクリプトの種類と配置の指針
  3. 03サーバー権威型設計の原則
  4. 04レプリケーションとリモート処理の検証
  5. 05詳細なトピックの参照資料

活用シーン

  1. 01Roblox ゲーム構成のレビュー
  2. 02クライアントとサーバー間の処理実装
  3. 03レプリケーション対象やシグナルの扱い
  4. 04データストアやストリーミング規則の確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/afrxo/roblox-agent-skills --skill roblox-dev

インストール後、Claude Code で/roblox-devを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

examples/
.gitkeep
character-lifecycle.luau
connection-tracker.luau
datastore-session.luau
remote-handler.luau
services-header.luau
references/
buffer-library.md
cloud-services.md
datastore-rules.md
parallel-luau.md
performance-optimization.md
query-descendants.md
replication-model.md
streaming-enabled.md

詳細

Roblox Engine

Roblox プラットフォームのランタイム時の動作について説明します。情報源:API と概念については create.roblox.com/docs、エンジニアによる補足説明や RFC については devforum.roblox.com を参照してください。

詳しくは、次の資料をご覧ください。


統合ポリシー

このスキルでは推奨パターンを説明しますが、既存コードを書き換える権限を与えるものではありません。ルールを適用するのは、新しいコードと、ユーザーから明示的にリファクタリングを依頼されたコードです。スキルの既定値を適用する前に、コードベースの既存スタイル(大文字・小文字の使い方、インデント、引用符のスタイル、モジュールの構成)に合わせてください。プロジェクトで使われていないツールは導入せず、競合があれば一度だけ指摘し、既存のパターンを優先してください。

すべてのルールについては、../../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サーバースクリプトとサーバー専用の ModuleScriptServer
ServerStorageサーバー専用データ(アセット、テーブル)— クライアントには内容が見えませんn/a
ReplicatedStorage共有 ModuleScript(サーバーとクライアント)およびクライアントスクリプトクライアントスクリプトは Client
ReplicatedFirst最小限のクライアントローダー(読み込み画面など)Client
StarterPlayerScripts / StarterCharacterScripts / StarterGui / StarterPackプレイヤーごとに実行される LocalScriptn/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 / Remove API で追加されたプレイヤーには常に読み込まれます。それ以外のプレイヤーにはデフォルトで読み込まれません。
  • 子孫をまとめて到着させる必要があるオブジェクト(車両のアセンブリ、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 モデル
  • Actor Instance 配下のスクリプトは、並列実行の対象になります。
  • アクターごとに 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 を信頼しないでください。 実際の Player Instance から取得した 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:InvokeClientRemoteEvent の往復通信
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 / delaytask.wait / task.spawn / task.defer / task.delay
RunService.Heartbeat 上で重い処理を直接行う頻度を下げる/低速なループに移す/task.delay を使う
視覚効果の Tween にサーバー側の TweenService を使うクライアントで Tween を実行し、目標値だけをレプリケートする
イベントごとに実行時に大きなリグをクローンするプールする/事前にインスタンス化する
小さなパーツに CollisionFidelity = Precise を設定するBox / Hull
並列フェーズ内で Instance を変更する先に task.synchronize() を呼び出す
API キーを DataStore に保存するシークレットストア
MemoryStore にデータを保存する標準のデータストア

例

コメント