roblox-architecture について
roblox-architectureは、Robloxゲームのサーバー側・クライアント側のコードと状態を整理する開発スキルです。プロジェクト構成、ライフサイクル処理、タグ付きコンポーネント、状態共有、ストリーミングを説明します。
主な機能
- 01サーバー側とクライアント側のコード配置
- 02Rojo形式のプロジェクト構成
- 03サービスとコントローラーの起動
- 04プレイヤーとキャラクターのライフサイクル処理
- 05タグ付きコンポーネントと属性の利用
- 06状態共有とストリーミングの指針
活用シーン
- 01Robloxプロジェクトの初期構成の計画
- 02サーバー側とクライアント側のシステム整理
- 03ストリーミングされるワールド要素とプレイヤー状態の処理
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ el4cteo/ roblox-skills --skill roblox-architectureインストール後、Claude Code で/roblox-architectureを入力して読み込まれたか確認します。
同梱ファイルreferences/ecosystem.mdreferences/streaming.md
詳細
Roblox ゲームアーキテクチャ
一段落でわかるモデル
すべての Roblox ゲームは、サーバー(権威を持ち、Script を実行し、すべてを把握し、DataStore を所有する)
と、プレイヤーごとに 1 つのクライアント(LocalScript / クライアント Script を実行し、レンダリングし、入力を読み取る)
で構成されます。
サーバーの DataModel はクライアントへ自動的にレプリケートされます。クライアント側の変更は
レプリケートされません。ただし、クライアントがネットワーク所有権を持つパーツの物理(既定では自身のキャラクター)、
自身の Humanoid の状態・アニメーション、およびリモート呼び出しは例外です。すべてのクライアントが
侵害されていると想定してください(roblox-security を参照)。すべての機能は次のように設計します:
クライアントが要求する → サーバーが検証して判断する → 状態がレプリケートされる → クライアントがレンダリングする。
何をどこに置くか
| コンテナ | 実行・表示される場所 | 置くもの |
|---|---|---|
ServerScriptService | サーバーのみ | サーバー Script とサーバー専用の ModuleScript(ゲームロジック、データ、アンチチート)。 |
ServerStorage | サーバーのみ | サーバー専用アセット: マップ、NPC テンプレート、クローンするツール、機密性の高い設定。 |
ReplicatedStorage | 両方 | 共有モジュール(型、設定、純粋なユーティリティ)、RemoteEvent、クライアントがクローンするアセット。 |
ReplicatedFirst | クライアント、最初 | ローディング画面のみ。他のすべてより先にレプリケートされます。 |
StarterPlayer.StarterPlayerScripts | クライアント(一度 PlayerScripts にコピーされる) | クライアントのエントリポイントとコントローラー。 |
StarterPlayer.StarterCharacterScripts | クライアント(スポーンごとにキャラクターへコピーされる) | キャラクター単位のクライアントスクリプト(必要なことはまれ)。 |
StarterGui | クライアント(PlayerGui にコピーされる) | ScreenGui。死亡時にリセットしたくない場合は ResetOnSpawn = false を設定します。 |
Workspace | 両方(クライアントへストリーミングされる) | 3D ワールドのみ。コードやテンプレートを置く場所ではありません。 |
Lighting、SoundService、Teams、TextChatService | 両方 | それぞれの設定オブジェクト。 |
ルール:
ReplicatedStorage内のコードはエクスプロイターがダウンロードできます。サーバーロジック、機密情報、 管理者リスト、アンチチートのしきい値を決して置かないでください。Script.RunContext:Legacy(既定: 動作はコンテナに依存)、Server、Client、またはPlugin。非 Legacy の RunContext では、Scriptはコンテナに関係なく実行されます(例:ReplicatedStorage内のClientScript はすべてのクライアントで実行される)— そのため、クローンするテンプレートに そのようなスクリプトを意図せず含めてはいけません。Legacy のScriptはReplicatedStorage/ServerStorageでは実行されません。
プロジェクト構成(Rojo スタイル、推奨)
src/
server/ -> ServerScriptService.Server (init.server.luau = entry point)
Services/ -> one ModuleScript per system (DataService, CombatService, ...)
client/ -> StarterPlayer.StarterPlayerScripts.Client (init.client.luau)
Controllers/ -> one ModuleScript per client system (InputController, UIController, ...)
shared/ -> ReplicatedStorage.Shared (types, config, pure logic, network definitions)
Packages/ -> ReplicatedStorage.Packages (Wally/pesde dependencies)
ServerPackages/ -> ServerScriptService.ServerPackages
各サイドに 1 つのエントリスクリプトを置き、それ以外はすべて ModuleScript にします。これにより、
決定的な読み込み順序、テスト可能なモジュール、依存関係を接続する単一の場所が得られます。Roblox 自身の
推奨もこれと同等です: ServerScriptService に 1 つの Script(RunContext = Server)と、
ReplicatedStorage に 1 つの Script(RunContext = Client)を置き、それぞれがモジュールを require して
その start() を呼び出します。ツールのセットアップ: roblox-tooling。
サービスのブートストラップ(フレームワーク不要)
--!strict
-- ServerScriptService/Server/init.server.luau
type Service = {
init: ((self: Service) -> ())?, -- sync setup; may reference other services; must not yield
start: ((self: Service) -> ())?, -- runs after every init; may yield / connect events
}
local servicesFolder = script:WaitForChild("Services")
local services: { [string]: Service } = {}
for _, module in servicesFolder:GetChildren() do
if module:IsA("ModuleScript") then
services[module.Name] = (require :: any)(module)
end
end
for name, service in services do
if service.init then
debug.setmemorycategory(name)
service:init()
debug.resetmemorycategory()
end
end
for name, service in services do
if service.start then
task.spawn(function()
debug.setmemorycategory(name)
service:start()
end)
end
end
- サービス同士は単純な
requireで参照する(循環なし)か、initで依存関係を受け取ります。 - クライアントはこれを
Controllerで鏡写しにします。名前は対称に保ちます(ShopService/ShopController)。 - このために Knit のようなフレームワークは不要です。プロジェクトがすでに使用している場合は、その規約に従ってください。
プレイヤーとキャラクターのライフサイクル
--!strict
local Players = game:GetService("Players")
local function onCharacterAdded(player: Player, character: Model)
local humanoid = character:WaitForChild("Humanoid") :: Humanoid
humanoid.Died:Once(function()
print(`{player.Name} died`)
end)
end
local function onPlayerAdded(player: Player)
-- Load data first (see roblox-data-stores), then set up character handling.
if player.Character then
task.spawn(onCharacterAdded, player, player.Character)
end
player.CharacterAdded:Connect(function(character)
onCharacterAdded(player, character)
end)
end
local function onPlayerRemoving(player: Player)
-- Save data, release session locks, clear every table keyed by this player.
print(`{player.Name} left`)
end
Players.PlayerAdded:Connect(onPlayerAdded)
Players.PlayerRemoving:Connect(onPlayerRemoving)
for _, player in Players:GetPlayers() do -- players who joined before this script connected
task.spawn(onPlayerAdded, player)
end
game:BindToClose(function()
-- Server shutdown: flush saves for remaining players (max ~30 s total).
end)
- コードの開始時にすでに存在するプレイヤー・キャラクターを必ず処理してください。
- キャラクターはリスポーンします: キャラクターに紐づくものは
CharacterAddedで再作成する必要があります。 - クライアントでは、キャラクターの子孫がまだ存在しない場合があります(ストリーミング):
WaitForChildを使ってください。
CollectionService タグを使用したコンポーネント
インスタンスにタグを付け(Studio の Tags セクションまたは CollectionService:AddTag を使用)、コードで動作を追加します。
各モデルにスクリプトをコピーするよりも拡張しやすく、インスタンスがストリーミングで読み込まれたり、読み込み解除されたりすると追加・削除シグナルが発火するため、ストリーミングにも対応できます。
--!strict
local CollectionService = game:GetService("CollectionService")
local TAG = "KillBrick"
local cleanups: { [Instance]: () -> () } = {}
local function onAdded(instance: Instance)
if not instance:IsA("BasePart") then
return
end
local connection = instance.Touched:Connect(function(hit)
local humanoid = hit.Parent and hit.Parent:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.Health = 0
end
end)
cleanups[instance] = function()
connection:Disconnect()
end
end
local function onRemoved(instance: Instance)
local cleanup = cleanups[instance]
if cleanup then
cleanup()
cleanups[instance] = nil
end
end
CollectionService:GetInstanceAddedSignal(TAG):Connect(onAdded)
CollectionService:GetInstanceRemovedSignal(TAG):Connect(onRemoved)
for _, instance in CollectionService:GetTagged(TAG) do
task.spawn(onAdded, instance)
end
IntValue/StringValue の子ではなく、属性(instance:GetAttribute("Damage")、
GetAttributeChangedSignal)でコンポーネントを設定します。属性はサーバーからクライアントへレプリケートされ、コストが低く、Properties パネルに表示されます。
状態の共有
| 必要なもの | 使用するもの |
|---|---|
| クライアントから参照できる、インスタンスまたはプレイヤーごとの小さな値 | インスタンスまたは Player の属性(サーバーが設定)。 |
| イベントとリクエスト | RemoteEvent/UnreliableRemoteEvent(roblox-networking を参照)。 |
| 大きな状態、またはプレイヤーごとの非公開状態(インベントリ、クエスト) | サーバー所有のテーブルと、そのプレイヤーだけにスナップショット/差分を送信するリモート。 |
| 同じ側での通信 | モジュール関数の呼び出し、または型付き Signal モジュール。BindableEvent、_G、shared は使用しません。 |
| 永続データ | roblox-data-stores。サーバー間の通信には、MessagingService/MemoryStore を使用します。 |
ストリーミング(新しいプレイスではデフォルトで有効)
Workspace.StreamingEnabled が有効な場合、クライアントには近くのパーツだけが表示されます。クライアントコードでは、次の点に注意してください。
Workspace内のものにはWaitForChild(到着しない可能性がある場合はタイムアウト付き)を使うか、nil チェックを行います。- パーツをまとめてスクリプトで処理する必要があるモデルには
ModelStreamingMode = Atomicを使用します。 モデルを常に存在させる必要がある場合に限り、メモリを消費するPersistentを使用します。 - ストリーミングで読み込み解除されたパーツに対する
GetChildren、レイキャスト、プロパティの読み取りは、不完全または古い情報として扱います。ワールド全体のクエリはサーバーで実行します。 - キャラクターをテレポートする前に、サーバーで
player:RequestStreamAroundAsync(position)を使って目的地を先行ストリーミングします。 - スコープ、レース、フリーカメラでは、
player.FrustumStreaming = Enum.FrustumStreamingMode.Automaticを設定すると、半径を広げる代わりにカメラに映るものがストリーミングされます。
パターンの一覧と Workspace の推奨設定については、references/streaming.md を参照してください。
アンチパターン
- パーツやモデル内に、多数の単独の
Scriptを配置する(タグと単一のサービスを使用します)。 - イベントや
deltaTimeを使ったHeartbeatの代わりに、ポーリングループ(while task.wait(0.1) do check() end)を使用する。 - ゲームの状態をクライアントに任せる、または毎フレームリモートを発火する。
_G/sharedのグローバル、循環 require、モジュールのトップレベルでの yield。- クライアントからマップを
Workspaceに複製する(クライアントだけのコピーにはサーバーからの更新が反映されません)。 - プレイヤーごとのデータをキャラクターに保存する(キャラクターは死亡時に破棄されます)。
シグナル、クリーンアップ、ネットワーク、UI、ECS、データに関するライブラリエコシステムについては、references/ecosystem.md を参照してください。
関連スキル
roblox-networking、roblox-security、roblox-data-stores、roblox-tooling、roblox-performance、roblox-game-systems。