Plant は、プレイヤーが種を植え、水を与え、後に収穫して植物を売ることができる参照体験です。

このプロジェクトは、Robloxで体験を開発する際に遭遇する可能性のある一般的なユースケースに焦点を当てています。適用可能な場合には、トレードオフ、妥協、さまざまな実装選択の根拠についてのメモがありますので、自分の体験に最適な決定を下すことができます。
ファイルを取得する
- Plant 体験ページに移動します。
- ⋯ ボタンをクリックし、スタジオで編集を選択します。
ユースケース
Plant は、以下のユースケースをカバーしています。
- セッションデータとプレイヤーデータの永続化
- UIビュー管理
- クライアント-サーバーネットワーキング
- 初めてのユーザー体験 (FTUE)
- 硬貨とソフト通貨の購入
さらに、このプロジェクトは多くの体験に適用可能なより狭い問題セットを解決します。これには以下が含まれます:
- プレイヤーに関連する場所内のエリアのカスタマイズ
- プレイヤーキャラクターの移動速度の管理
- キャラクターの周りを追従するオブジェクトの作成
- キャラクターがいるワールドの部分を検出する
この体験には、サイズが小さすぎる、ニッチすぎる、または興味深い設計上の課題の解決策を示さないユースケースがいくつかありますので、ここではカバーしていません。
プロジェクト構造
体験を作成する際の最初の決定は、プロジェクトの構造をどのようにするかです。これは、主にデータモデル内の特定のインスタンスを配置する場所や、クライアントとサーバーのコード用のエントリポイントを組織し構成する方法を含みます。
データモデル
以下の表は、データモデル内のインスタンスが配置されるコンテナサービスについて説明しています。
| サービス | インスタンスの種類 |
|---|---|
| Workspace | プレイヤーに属さないワールドの部分を含む3Dワールドを表現する静的モデルを含みます。これらのインスタンスは、実行時に動的に作成、変更、または破壊する必要がないため、ここに置いておくことが許容されます。 プレイヤーの農場モデルは、実行時に追加される空の Folder もあります。 |
| Lighting | 大気および照明効果。 |
| ReplicatedFirst | ローディング画面を表示し、体験を初期化するために必要な最小限のインスタンスのサブセットを含みます。 ReplicatedFirstに配置されるインスタンスが多いほど、ReplicatedFirst内のコードが実行される前にレプリケートされるための待機時間が長くなります。
|
| ReplicatedStorage | クライアントとサーバーの両方でアクセスが必要なすべてのインスタンスのストレージコンテナとして機能します。
|
| ServerScriptService | プロジェクト内のすべてのサーバー側コードのエントリポイントとして機能する Script を含みます。 |
| ServerStorage | クライアントにレプリケートする必要のないすべてのインスタンスのストレージコンテナとして機能します。
|
| SoundService | 体験内での効果音に使用される Sound オブジェクトを含みます。 SoundService の下では、これらの Sound オブジェクトには位置がなく、3D空間でシミュレートされません。 |
エントリポイント
ほとんどのプロジェクトは、コードを再利用可能な ModuleScripts 内に整理し、コードベース全体でインポートできるようにします。 ModuleScripts は再利用可能ですが、それ自体では実行されず、Script または LocalScript にインポートされる必要があります。多くのRobloxプロジェクトには、体験内の動作や特定のシステムに関連する多くの Script および LocalScript オブジェクトがあり、複数のエントリポイントを作成します。
Plant マイクロゲームでは、すべてのクライアントコードのエントリポイントとして単一の LocalScript が実装され、すべてのサーバーコードのエントリポイントとして単一の Script が実装されています。あなたのプロジェクトに適したアプローチは、要件に依存しますが、単一のエントリポイントは、システムが実行される順序に対するより大きな制御を提供します。
以下のリストは、両方のアプローチのトレードオフを説明しています。
- 単一の Script と単一の LocalScript がそれぞれサーバーおよびクライアントコードをカバーします。
- すべてのコードが単一のスクリプトから初期化されるため、さまざまなシステムが開始される順序をより大きく制御できます。
- システム間でオブジェクトを参照として渡すことができます。
高レベルのシステムアーキテクチャ
プロジェクト内のトップレベルのシステムは以下に詳述されています。これらのシステムの中には、他のものよりもはるかに複雑なものもあり、多くの場合、機能が他のクラスの階層に抽象化されています。

これらのシステムの各々は「シングルトン」であり、インスタンス化できないクラスであり、関連するクライアントまたはサーバーの start スクリプトによって初期化されます。このガイドの後半でシングルトンパターンについて詳しく読むことができます。
サーバー
以下のシステムはサーバーに関連しています。
| システム | 説明 |
|---|---|
| ネットワーク |
|
| PlayerDataServer |
|
| マーケット |
|
| CollisionGroupManager |
|
| FarmManagerServer |
|
| PlayerObjectsContainer |
|
| TagPlayers |
|
| FtueManagerServer |
|
| CharacterSpawner |
|
クライアント
以下のシステムはクライアントに関連しています。
| システム | 説明 |
|---|---|
| ネットワーク |
|
| PlayerDataClient |
|
| MarketClient |
|
| LocalWalkJumpManager |
|
| FarmManagerClient |
|
| UISetup |
|
| FtueManagerClient |
|
| CharacterSprint |
|
クライアント-サーバー通信
ほとんどのRoblox体験には、クライアントとサーバー間の通信要素が含まれます。これには、クライアントがサーバーに特定のアクションを実行するようリクエストし、サーバーがクライアントに更新をレプリケートすることが含まれます。
このプロジェクトでは、RemoteEvent と RemoteFunction オブジェクトの使用を最小限に抑えることで、クライアント-サーバー通信をできるだけ一般的に保っています。これにより、トラッキングすべき特別なルールを減らすことができます。このプロジェクトでは、好ましい順序で以下のメソッドを使用しています:
- プレイヤーデータシステムを介したレプリケーション。
- 属性を介したレプリケーション。
- タグを介したレプリケーション。
- ネットワークモジュールを介したメッセージング 直接。
プレイヤーデータシステムを介したレプリケーション
プレイヤーデータシステムは、保存セッション間で永続するプレイヤーに関連付けられたデータを提供します。このシステムは、クライアントからサーバーへのレプリケーションと、データをクエリし、変更を購読するためのAPIセットを提供するため、サーバーからクライアントへのプレイヤーの状態の変更をレプリケートするのに理想的です。
例えば、クライアントにコインの数を知らせるために特定の UpdateCoins RemoteEvent を発火させる代わりに、次のように呼び出してクライアントが PlayerDataClient.updated イベントを通じてこれを購読できるようにすることができます。
PlayerDataServer.setValue(player, "coins", 5)
もちろん、これはサーバーからクライアントへのレプリケーションと、セッション間で永続させたい値に対してのみ有用ですが、これはプロジェクト内の驚くべき数のケースに適用されます。これには以下が含まれます:
- 現在のFTUEステージ
- プレイヤーのインベントリ
- プレイヤーが持っているコインの量
- プレイヤーの農場の状態
属性を介したレプリケーション
サーバーが特定の Instance に固有のカスタム値をクライアントにレプリケートする必要がある状況では、属性を使用できます。Robloxは自動的に属性値をレプリケートするため、オブジェクトに関連する状態をレプリケートするためにコードパスを維持する必要がありません。もう1つの利点は、このレプリケーションがインスタンス自体と同時に行われることです。
これは、実行時に作成されるインスタンスに特に便利であり、データモデルに親子付けされる前に新しいインスタンスに設定された属性は、インスタンス自体と原子的にレプリケートされます。これにより、RemoteEvent や StringValue を介って追加のデータがレプリケートされるのを待つためのコードを書く必要がなくなります。
また、GetAttribute() メソッドを使用して、クライアントまたはサーバーのいずれからデータモデルから属性を直接読み取ることができ、GetAttributeChangedSignal() メソッドを使用して変更を購読できます。Plant プロジェクトでは、このアプローチは多くのことに使用され、クライアントに植物の現在の状態をレプリケートすることが含まれます。
タグを介したレプリケーション
CollectionService を使用すると、Instance に文字列タグを適用できます。これは、インスタンスを分類し、その分類をクライアントにレプリケートするのに便利です。
例えば、CanPlant タグは、サーバー上で、特定のポットが植物を受け入れられることを示すためにクライアントに対して付与されます。
ネットワークモジュールを介して直接メッセージを送信
以前のオプションが適用されない状況では、ネットワークモジュールを介してカスタムネットワーク呼び出しを使用できます。これは、クライアントからサーバーへの通信を許可する唯一のオプションであり、クライアントのリクエストを伝達し、サーバーの応答を受け取るのに最も便利です。
Plant は、以下のクライアントリクエストのために直接ネットワーク呼び出しを使用しています。
- 植物に水を与える
- 種を植える
- アイテムを購入する
このアプローチの欠点は、各メッセージに対して特別な構成が必要であり、これがプロジェクトの複雑さを増す可能性があることですが、これは可能な限り回避されています。特にサーバーからクライアントへの通信についてはそうです。
クラスとシングルトン
Plant プロジェクトのクラスは、Robloxのインスタンスと同様に作成および破棄できます。そのクラス構文は、オブジェクト指向プログラミング に対する慣用的なLuaアプローチに触発されており、厳密型検査のサポートを有効にするためにいくつかの変更が加えられています。
インスタンス化
プロジェクト内の多くのクラスは1つ以上の Instances に関連付けられています。指定されたクラスのオブジェクトは、new() メソッドを使用して作成され、これはRobinoxにおいて Instance.new() を使用してインスタンスが作成される方法と一致します。
このパターンは、クラスがデータモデル内に物理的な表現を持つオブジェクトに一般的に使用され、そのクラスが機能を拡張します。良い例は BeamBetween であり、これは二つの Attachment オブジェクトの間に Beam オブジェクトを作成し、それらのアタッチメントを上向きに保つようにします。これらのインスタンスは、ReplicatedStorage からのプレファブリケーションされたバージョンからクローンされるか、引数として new() に渡されて、self の下でオブジェクト内に保存されます。
対応するインスタンス
前述のように、このプロジェクト内の多くのクラスにはデータモデルの表現、つまりクラスに対応し、クラスによって操作されるインスタンスがあります。
クラスオブジェクトがインスタンス化される際にこれらのインスタンスを作成する代わりに、コードは一般的に、ReplicatedStorage または ServerStorage に保存されているプレファブリケーションされた Instance のバージョンを Clone() することを選択します。これらのインスタンスのプロパティをシリアライズしてクラスの new() 関数でゼロから作成することは可能ですが、その場合、オブジェクトの編集が非常に面倒になり、読み手が解析するのが難しくなります。加えて、インスタンスをクローンすることは、実行時に新しいインスタンスを作成し、そのプロパティをカスタマイズするよりも一般的に速い操作です。
コンポジション
Luauではメタテーブルを使用して継承が可能ですが、プロジェクトは代わりにクラスがコンポジションを通じて互いに拡張できるようにしています。コンポジションを介してクラスを組み合わせるとき、"子" オブジェクトはクラスの new() メソッド内でインスタンス化され、self の下でメンバーとして含まれます。
これがどのように機能するかの例として、CloseButton クラスを参照して、Button クラスをラップしています。
クリーンアップ
Instance を Destroy() メソッドで破壊できるように、インスタンス化できるクラスを破壊することもできます。プロジェクトクラスのデストラクタメソッドは destroy() であり、これはコードベースのメソッド間のcamelCase の一貫性を保つためや、プロジェクトのクラスとRobloxのインスタンスを区別するためのものです。
destroy() メソッドの役割は、オブジェクトによって作成されたインスタンスを破壊し、接続を切断し、子オブジェクトに対して destroy() を呼び出すことです。これは、接続がアクティブなインスタンスは、インスタンスまたはその接続に対する参照が残っていなくても、Luauのガベージコレクターによってクリーンアップされないため、特に重要です。
シングルトン
シングルトンは、その名前が示すように、1つのオブジェクトしか存在できないクラスです。これは、プロジェクトのRobloxのサービスの同等物です。シングルトンオブジェクトへの参照を保存し、Luauコード内でそれを渡す代わりに、Plant は ModuleScript を必要とする際に返された値がキャッシュされるという事実を利用しています。これは、異なる場所から同じシングルトン ModuleScript を必要とすると一貫して同じ返されたオブジェクトを提供します。 この規則の唯一の例外は、異なる環境(クライアントまたはサーバー)が ModuleScript にアクセスした場合です。
シングルトンは、インスタンス化可能なクラスと区別されるため、彼らは new() メソッドを持っていません。むしろ、オブジェクトとそのメソッド、および状態は ModuleScript を介して直接返されます。シングルトンはインスタンス化されないため、self 構文は使用されず、メソッドはコロン(:)ではなくドット(.)で呼び出されます。
厳密型推論
Luau は、段階的な型付けをサポートしており、コードの一部またはすべてにオプション型定義を追加できます。このプロジェクトでは、すべてのスクリプトに対して厳密な型検査が使用されています。これは、Robloxのスクリプト分析 ツールにとって最も制限的なオプションであり、そのためランタイムの前に型エラーをキャッチできる可能性が最も高くなります。
型付けされたクラス構文
Luaでクラスを作成する既存のアプローチはよく文書化されていますが、強いLuauタイプにはあまり適していません。Luauでは、クラスのタイプを取得するための最も簡単なアプローチは typeof() メソッドです:
type ClassType = typeof(Class.new())
これは機能しますが、クラスが実行時にのみ存在する値(例えば Player オブジェクト)で初期化されるときにはあまり有用ではありません。加えて、慣用的なLuaクラス構文では、クラスの self にメソッドを宣言すると、そのクラスのインスタンスであるという前提がありますが、これは型推論エンジンが持つことのできる前提ではありません。
厳密な型推論をサポートするために、Plant プロジェクトでは、数の点で慣用的なLuaクラス構文とは異なる解決策を使用しています。そのいくつかは直感的でないと感じるかもしれません:
- self の定義が、型宣言とコンストラクタの両方で重複しています。これは保守性の負担をもたらしますが、二つの定義が同期しなくなると警告がフラグされます。
- クラスメソッドはドットで宣言されるため、self を ClassType 型として明示的に宣言できます。メソッドは予想通りコロンを使って呼び出すことができるままにします。
--!strict
local MyClass = {}
MyClass.__index = MyClass
export type ClassType = typeof(setmetatable(
{} :: {
property: number,
},
MyClass
))
function MyClass.new(property: number): ClassType
local self = {
property = property,
}
setmetatable(self, MyClass)
return self
end
function MyClass.addOne(self: ClassType)
self.property += 1
end
return MyClass
論理ガードの後の型キャスト
執筆時点では、値の型はガード条件文の後に絞り込まれません。例えば、以下のガードの後、 optionalParameter の型は number に絞り込まれません。
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
print(optionalParameter + 1)
end
これを軽減するために、ガードの後に新しい変数を作成し、その型を明示的にキャストします。
--!strict
local function foo(optionalParameter: number?)
if not optionalParameter then
return
end
local parameter = optionalParameter :: number
print(parameter + 1)
end
データモデル階層のトラバース
場合によっては、コードベースが実行時に作成される一連のオブジェクトのデータモデル階層をトラバースする必要があります。これには、型検査の興味深い課題が伴います。執筆時点では、型として一般的なデータモデル階層を定義することは不可能です。その結果、データモデルの構造に関連する型情報は、トップレベルのインスタンスの型だけである場合があります。
この課題に対するアプローチの一つは any にキャストしてから、精査を行うことです。例えば:
local function enableVendor(vendor: Model)
local zonePart: BasePart = (vendor :: any).ZonePart
end
このアプローチの問題は、可読性に影響を与えることです。その代わりに、プロジェクトでは、データモデル階層をトラバースするための getInstance と呼ばれる一般的なモジュールを使用し、内部で any にキャストします。
local function enableVendor(vendor: Model)
local zonePart: BasePart = getInstance(vendor, "ZonePart")
end
タイプエンジンのデータモデルに対する理解が進化するにつれて、このようなパターンがもはや必要でなくなる可能性があります。
ユーザーインターフェース
Plant は、複雑なものとシンプルなものを含むさまざまな2Dユーザーインターフェースを備えています。これには、コインカウンターのような非対話的なヘッドアップディスプレイ (HUD) アイテムや、ショップのような複雑なインタラクティブメニューが含まれます。
UIアプローチ
RobloxのUI は、HTML DOMに比較できます。なぜなら、ユーザーが何を見ているべきかを記述するオブジェクトの階層だからです。Roblox UIを作成し、更新するアプローチは、大きく分けて命令的および宣言的なプラクティスに分類されます。
| アプローチ | 利点と欠点 |
|---|---|
| 命令的 | 命令的アプローチでは、UIはRobloxの他のインスタンス階層と同様に扱われます。UI構造はスタジオで、実行前に作成され、通常は StarterGui に直接追加されます。その後、実行時には、コードがUIの具体的な部分を操作して、クリエイターが要求する状態を反映させます。 このアプローチにはいくつかの利点があります。スタジオでUIをゼロから作成し、データモデルに保存できます。これはシンプルで視覚的な編集体験を提供し、UI作成を加速します。命令的なUIコードは、何を変える必要があるかのみに関心を持つため、シンプルなUIの変更が簡単に実装できる利点もあります。 注目すべき欠点は、命令的なUIアプローチでは、状態を変形の形で手動で実装する必要があるため、状態の複雑な表現が見つけにくく、デバッグが非常に難しくなることです。命令的なUIコードを開発する際には、特に状態とUIが複数の更新によって予期しない順序で相互作用することによってデシンクし、エラーが発生することが一般的です。 命令的アプローチのもう一つの課題は、UIを意味のあるコンポーネントに分解し、それを一度宣言して再利用するのが難しいことです。全体のUIツリーが編集時に宣言されるため、一般的なパターンがデータモデル内の複数の部分で繰り返されることがあります。 |
| 宣言的 | 宣言的アプローチでは、UIインスタンスの希望する状態が明示的に宣言され、その状態の効率的な実装は、Roact や Fusion などのライブラリによって抽象化されます。 このアプローチの利点は、状態の実装が trivial になり、UIがどのように見えるべきかを記述するだけで済むことです。これにより、バグを特定し、解決することが大幅に簡単になります。 主な欠点は、コード内で全体のUIツリーを宣言する必要があることです。RoactやFusionのようなライブラリにはこれを容易にするための構文がありますが、それでも、非常に時間のかかるプロセスであり、UIを構成する際の直感的でない編集体験になります。 |
Plant は、変形を直接示すことで、RobloxでのUIの作成と操作のより効果的な概要を提供するという考えの下で、命令的アプローチを使用しています。これは宣言的アプローチでは実現できません。いくつかの繰り返されたUI構造とロジックも再利用可能なコンポーネントに抽象化され、命令的UIデザインの一般的な落とし穴を避けています。
高レベルのアーキテクチャ

レイヤーとコンポーネント
Plant では、すべてのUI構造は Layer または Component です。
- Layer は、ReplicatedStorage にあるプレファブリケーションされたUI構造をラップするトップレベルのグループ化シングルトンとして定義されています。一つのレイヤーは、複数のコンポーネントを含むこともあれば、その論理を完全にカプセル化することもあります。レイヤーの例としては、インベントリメニューやヘッドアップディスプレイのコイン数インジケーターがあります。
- Component は、再利用可能なUI要素です。新しいコンポーネントオブジェクトがインスタンス化されると、ReplicatedStorage からプレファブリケーションされたテンプレートをクローンします。コンポーネントは自身が他のコンポーネントを含むこともあります。コンポーネントの例としては、一般的なボタンクラスやアイテムリストの概念があります。
ビュー管理
一般的なUI管理の問題はビュー管理です。このプロジェクトには、ユーザー入力をリッスンするメニューやHUDアイテムがいくつかあり、これらが表示されるべきタイミングや有効にされるべきタイミングを慎重に管理する必要があります。
Plant は、UIレイヤーが表示すべきかどうかを管理する UIHandler システムを使ってこの問題に取り組んでいます。体験全体のすべてのUIレイヤーは HUD または Menu として分類され、以下のルールによってその可視性が管理されます:
- Menu と HUD レイヤーの有効状態は切り替え可能です。
- 有効な HUD レイヤーは、いずれかの Menu レイヤーが有効でない場合にのみ表示されます。
- 有効な Menu レイヤーはスタックに格納され、同時に表示されるのは1つの Menu レイヤーのみです。Menu レイヤーが有効になったときは、スタックの先頭に挿入されて表示されます。Menu レイヤーが無効になると、スタックから削除され、次に待機している有効な Menu レイヤーが表示されます。
このアプローチは直感的です。なぜなら、メニューを履歴を持ってナビゲートできるからです。あるメニューから別のメニューが開かれると、新しいメニューを閉じる際に古いメニューが再び表示されます。
さらなる学習
Plant プロジェクトに関するこの詳細な概要から、関連する概念やトピックについてさらに詳しく調べたい場合、以下のガイドを参照することができます。
- クライアント-サーバーモデル — Robloxにおけるクライアント-サーバーモデルの概要。
- リモートイベントとコールバック — クライアント-サーバー境界を超えた通信のためのリモートネットワークイベントとコールバックに関する情報。
- UI — Robloxにおけるユーザーインターフェースオブジェクトとデザインに関する詳細。