植物は、プレイヤーが種を植え、水をやり、後に収穫して販売することができるリファレンスゲームです。

このプロジェクトは、Robloxでゲームを開発する際に遭遇する可能性のある一般的なユースケースに焦点を当てています。該当する場合は、トレードオフ、妥協、およびさまざまな実装選択の理由についてのメモが見つかるので、自分のゲームに最適な決定を下すことができます。
ファイルを取得する
- 植物ゲームページに移動します。
- **⋯**ボタンをクリックし、スタジオで編集します。
ユースケース
植物は、以下のユースケースをカバーしています。
- セッションデータとプレイヤーデータの永続性
- UIビュー管理
- クライアント-サーバーネットワーキング
- 初回ユーザー体験 (FTUE)
- ハードおよびソフト通貨の購入
さらに、このプロジェクトは、多くのゲームに適用可能な、より狭い問題のセットを解決します。これには以下が含まれます。
- プレイヤーに関連付けられた場所のエリアのカスタマイズ
- プレイヤーキャラクターの移動速度の管理
- キャラクターの周りを追従するオブジェクトの作成
- キャラクターがいる世界の部分を検出する
このゲームには、あまりにも小さすぎる、ニッチすぎる、または興味深いデザインの課題に対する解決策を示さないユースケースがいくつかあることに注意してください。これらはカバーされていません。
プロジェクト構造
ゲームを作成する際の最初の決定は、プロジェクトの構造を決定することです。これは主に、特定のインスタンスをデータモデルに配置する場所と、クライアントおよびサーバーコードのエントリポイントを整理および構造化する方法を含みます。
データモデル
以下の表は、データモデルのインスタンスが配置されるコンテナサービスを説明しています。
| サービス | インスタンスタイプ |
|---|---|
| Workspace | プレイヤーに属さない世界の部分を具体的に表す静的モデルを含みます。これらのインスタンスは、ランタイムで動的に作成、変更、または破棄する必要がないため、ここに置いておくことが許可されています。 ランタイムでプレイヤーの農場モデルが追加される空のFolderもあります。 |
| Lighting | 大気および照明効果。 |
| ReplicatedFirst | ローディング画面を表示し、ゲームを初期化するために必要な最小限のインスタンスのサブセットを含みます。ReplicatedFirstに配置されるインスタンスが多いほど、ReplicatedFirst内のコードが実行される前にレプリケートされるまでの待機時間が長くなります。
|
| ReplicatedStorage | クライアントとサーバーの両方でアクセスが必要なすべてのインスタンスのストレージコンテナとして機能します。
|
| ServerScriptService | プロジェクト内のすべてのサーバー側コードのエントリポイントとして機能するScriptを含みます。 |
| ServerStorage | クライアントにレプリケートする必要のないすべてのインスタンスのストレージコンテナとして機能します。
|
| SoundService | ゲーム内のサウンド効果に使用されるSoundオブジェクトを含みます。SoundServiceの下では、これらのSoundオブジェクトは位置を持たず、3D空間でシミュレートされません。 |
エントリポイント
ほとんどのプロジェクトは、再利用可能なModuleScripts内にコードを整理し、コードベース全体でインポートできるようにします。 ModuleScriptsは再利用可能ですが、自身では実行されず、ScriptまたはLocalScriptによってインポートされる必要があります。多くのRobloxプロジェクトには、ゲーム内の動作や特定のシステムに関連する多数のScriptおよびLocalScriptオブジェクトがあり、複数のエントリポイントを作成します。
植物マイクロゲームでは、すべてのクライアントコードのエントリポイントとして単一の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は属性値を自動的にレプリケートするため、オブジェクトに関連付けられた状態をレプリケートするためのコードパスを維持する必要はありません。もう一つの利点は、このレプリケーションがインスタンス自体と一緒に行われることです。
これは、ランタイムで作成されたインスタンスに特に便利です。データモデルに親子関係を持つ前に新しいインスタンスに設定された属性は、インスタンス自体と原子的にレプリケートされます。これにより、RemoteEventやStringValueを介って追加のデータがレプリケートされるのを「待つ」ためのコードを書く必要がなくなります。
クライアントまたはサーバーのいずれかからデータモデルから属性を直接読み取ることもでき、GetAttribute()メソッドを使用して、変更を購読することができます。GetAttributeChangedSignal()メソッドを使用します。植物プロジェクトでは、このアプローチは、植物の現在の状態をクライアントにレプリケートするために使用されています。
タグを介したレプリケーション
CollectionServiceを使用すると、Instanceに文字列タグを適用できます。これは、インスタンスを分類し、その分類をクライアントにレプリケートするのに便利です。
たとえば、CanPlantタグは、特定の鉢が植物を受け入れることができることをクライアントに示すためにサーバーで適用されます。
ネットワークモジュールを介して直接メッセージを送信
前述のオプションが適用されない場合は、ネットワークモジュールを介してカスタムネットワーク呼び出しを使用できます。これは、クライアントからサーバーへの通信を可能にするプロジェクト内の唯一のオプションであり、したがって、クライアントリクエストを送信し、サーバーの応答を受信するのに最も便利です。
植物は、以下のようなさまざまなクライアントリクエストに対して直接ネットワーク呼び出しを使用します。
- 植物に水をやる
- 種を植える
- アイテムを購入する
このアプローチの欠点は、各個別のメッセージが特別な構成を必要とし、プロジェクトの複雑さを増す可能性があることですが、特にサーバーからクライアントへの通信に関しては、可能な限りこれを避けています。
クラスとシングルトン
植物プロジェクトのクラスは、Robloxのインスタンスと同様に、作成および破棄できます。そのクラス構文は、オブジェクト指向プログラミングに対する慣用的なLuaアプローチに触発されており、厳密な型チェックサポートを有効にするためにいくつかの変更が加えられています。
インスタンス化
プロジェクト内の多くのクラスは、1つ以上のInstancesに関連付けられています。特定のクラスのオブジェクトは、new()メソッドを使用して作成され、これはRobloxでインスタンスがInstance.new()を使用して作成される方法と一致しています。
このパターンは、クラスがデータモデル内に物理的な表現を持つオブジェクトに一般的に使用され、クラスはその機能を拡張します。良い例は、2つの指定されたAttachmentオブジェクトの間にBeamオブジェクトを作成し、ビームが常に上を向くようにそれらのアタッチメントを保持するBeamBetweenです。これらのインスタンスは、ReplicatedStorage内のプレファブバージョンからクローンされるか、new()の引数として渡され、オブジェクト内のselfの下に保存されます。
対応するインスタンス
上記のように、このプロジェクトの多くのクラスにはデータモデルの表現があり、クラスに対応するインスタンスがあり、それによって操作されます。
クラスオブジェクトがインスタンス化されるときにこれらのインスタンスを作成するのではなく、コードは一般的にClone()を使用して、ReplicatedStorageまたはServerStorageに保存されたプレファブバージョンをクローンすることを選択します。これらのインスタンスのプロパティをシリアライズして、クラスのnew()関数でゼロから作成することも可能ですが、そうするとオブジェクトの編集が非常に面倒になり、読者が解析しにくくなります。さらに、インスタンスをクローンすることは、ランタイムで新しいインスタンスを作成し、そのプロパティをカスタマイズするよりも一般的に高速な操作です。
コンポジション
Luauではメタテーブルを使用して継承が可能ですが、プロジェクトでは代わりにクラスがコンポジションを通じて互いに拡張できるようにしています。コンポジションを通じてクラスを組み合わせるとき、"子"オブジェクトはクラスのnew()メソッド内でインスタンス化され、selfの下にメンバーとして含まれます。
これが実際にどのように機能するかの例として、CloseButtonクラスがButtonクラスをラップしています。
クリーンアップ
InstanceがDestroy()メソッドで破棄できるのと同様に、インスタンス化できるクラスも破棄できます。プロジェクトクラスのデストラクタメソッドは、コードベースのメソッド全体でcamelCaseの一貫性を保つために小文字のdを持つdestroy()です。また、プロジェクトのクラスとRobloxインスタンスを区別するためでもあります。
destroy()メソッドの役割は、オブジェクトによって作成されたインスタンスを破棄し、接続を切断し、子オブジェクトのdestroy()を呼び出すことです。これは、アクティブな接続を持つインスタンスは、インスタンスや接続に対する参照が残っていなくてもLuauのガベージコレクタによってクリーンアップされないため、特に重要です。
シングルトン
シングルトンは、その名の通り、1つのオブジェクトしか存在できないクラスです。これは、プロジェクトのRobloxのサービスに相当します。シングルトンオブジェクトへの参照を保存し、Luauコード内で渡すのではなく、植物は、ModuleScriptを要求することでその返された値がキャッシュされるという事実を利用します。これにより、異なる場所から同じシングルトンModuleScriptを要求すると、一貫して同じ返されたオブジェクトが提供されます。 このルールの唯一の例外は、異なる環境(クライアントまたはサーバー)がModuleScriptにアクセスする場合です。
シングルトンは、new()メソッドを持たないことで、インスタンス化可能なクラスと区別されます。代わりに、オブジェクトとそのメソッドおよび状態は、ModuleScriptを介して直接返されます。シングルトンはインスタンス化されないため、self構文は使用されず、メソッドはコロン(:)ではなくドット(.)で呼び出されます。
厳密な型推論
Luauは段階的な型付けをサポートしており、コードの一部またはすべてにオプションの型定義を追加することができます。このプロジェクトでは、すべてのスクリプトに対してstrict型チェックが使用されています。これは、Robloxのスクリプト分析ツールに対して最も許可されていないオプションであり、したがって、ランタイム前に型エラーをキャッチする可能性が最も高いです。
型付きクラス構文
Luaでクラスを作成するための確立されたアプローチはよく文書化されていますが、強いLuau型付けにはあまり適していません。Luauでは、クラスの型を取得する最も簡単な方法は、typeof()メソッドです。
type ClassType = typeof(Class.new())これは機能しますが、クラスがランタイムでのみ存在する値(たとえば、Playerオブジェクト)で初期化される場合にはあまり役に立ちません。さらに、慣用的なLuaクラス構文で行われる仮定は、クラスselfのメソッドを宣言すると、それが常にそのクラスのインスタンスであるということですが、これは型推論エンジンが仮定できることではありません。
厳密な型推論をサポートするために、植物プロジェクトでは、いくつかの点で慣用的なLuaクラス構文とは異なる解決策を使用しています。これらのいくつかは直感的でないかもしれません。
- selfの定義は、型宣言とコンストラクタの両方で重複しています。これにより、保守性の負担が増しますが、2つの定義が同期しなくなった場合には警告がフラグされます。
- クラスメソッドはドットで宣言されるため、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データモデル階層を横断
場合によっては、コードベースがランタイムで作成されたオブジェクトのツリーのデータモデル階層を横断する必要があります。これは型チェックにとって興味深い課題を提示します。執筆時点では、型として一般的なデータモデル階層を定義することはできません。その結果、データモデル構造に対して利用可能な型情報は、最上位インスタンスの型だけである場合があります。
この課題に対処する1つのアプローチは、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型エンジンのデータモデルに対する理解が進化するにつれて、このようなパターンがもはや必要なくなる可能性があります。
ユーザーインターフェース
植物には、さまざまな複雑でシンプルな2Dユーザーインターフェースが含まれています。これには、コインカウンターのような非インタラクティブなヘッドアップディスプレイ(HUD)アイテムや、ショップのような複雑なインタラクティブメニューが含まれます。
UIアプローチ
RobloxのUIは、HTML DOMに似ていると考えることができます。これは、ユーザーが見るべきものを説明するオブジェクトの階層です。Roblox UIを作成および更新するアプローチは、主に命令的および宣言的な実践に分かれます。
| アプローチ | 利点と欠点 |
|---|---|
| 命令的 | 命令的アプローチでは、UIはRobloxの他のインスタンス階層と同様に扱われます。UI構造は、スタジオでランタイムの前に作成され、通常はStarterGuiに直接追加されます。次に、ランタイムで、コードがUIの特定の部分を操作して、クリエイターが要求する状態を反映させます。 このアプローチにはいくつかの利点があります。スタジオでUIをゼロから作成し、データモデルに保存できます。これはシンプルで視覚的な編集体験であり、UI作成を加速できます。命令的UIコードは、何を変更する必要があるかにのみ関心を持つため、シンプルなUI変更を実装するのも簡単です。 注目すべき欠点は、命令的UIアプローチでは、変換の形で状態を手動で実装する必要があるため、状態の複雑な表現を見つけてデバッグするのが非常に難しくなることです。命令的UIコードを開発する際には、特に状態とUIが予期しない順序で相互作用することによって非同期化されるときに、エラーが発生することが一般的です。 命令的アプローチのもう1つの課題は、UIを意味のあるコンポーネントに分解するのが難しいことです。これにより、1度宣言して再利用できるようになります。UIツリー全体が編集時に宣言されるため、一般的なパターンがデータモデルの複数の部分で繰り返されることがあります。 |
| 宣言的 | 宣言的アプローチでは、UIインスタンスの望ましい状態が明示的に宣言され、この状態の効率的な実装はRoactやFusionなどのライブラリによって抽象化されます。 このアプローチの利点は、状態の実装が簡単になり、UIがどのように見えるかを説明するだけで済むことです。これにより、バグの特定と解決が大幅に容易になります。 主な欠点は、UIツリー全体をコードで宣言する必要があることです。RoactやFusionのようなライブラリには、これを容易にするための構文がありますが、それでも時間がかかるプロセスであり、UIを構成する際の直感的な編集体験が損なわれます。 |
植物は、変換を直接表示することで、RobloxでのUIの作成と操作のより効果的な概要を提供するという考えの下で命令的アプローチを使用しています。これは、宣言的アプローチでは不可能です。また、いくつかの繰り返しのUI構造とロジックも再利用可能なコンポーネントに抽象化され、命令的UIデザインの一般的な落とし穴を避けています。
高レベルアーキテクチャ

レイヤーとコンポーネント
植物では、すべてのUI構造はLayerまたはComponentです。
- Layerは、ReplicatedStorage内のプレファブUI構造をラップするトップレベルのグルーピングシングルトンとして定義されます。レイヤーには、いくつかのコンポーネントが含まれる場合がありますが、独自のロジックを完全にカプセル化することもできます。レイヤーの例には、インベントリメニューやヘッドアップディスプレイ内のコイン数インジケーターがあります。
- Componentは再利用可能なUI要素です。新しいコンポーネントオブジェクトがインスタンス化されると、ReplicatedStorageからプレファブテンプレートをクローンします。コンポーネントは、他のコンポーネントを含むこともできます。コンポーネントの例には、一般的なボタンクラスやアイテムのリストの概念があります。
ビュー管理
一般的なUI管理の問題はビュー管理です。このプロジェクトには、ユーザー入力をリスニングするメニューやHUDアイテムがあり、これらが表示されるかどうかを慎重に管理する必要があります。
植物は、UIレイヤーが表示されるべきかどうかを管理するUIHandlerシステムを使用してこの問題にアプローチします。ゲーム内のすべてのUIレイヤーはHUDまたはMenuとして分類され、その可視性は以下のルールによって管理されます。
- MenuおよびHUDレイヤーの有効状態は切り替え可能です。
- 有効なHUDレイヤーは、Menuレイヤーが有効でない場合にのみ表示されます。
- 有効なMenuレイヤーはスタックに保存され、同時に1つのMenuレイヤーのみが表示されます。Menuレイヤーが有効になると、それはスタックの前に挿入されて表示されます。Menuレイヤーが無効になると、それはスタックから削除され、キュー内の次の有効なMenuレイヤーが表示されます。
このアプローチは直感的であり、メニューを履歴でナビゲートできるようにします。1つのメニューが別のメニューから開かれると、新しいメニューを閉じると古いメニューが再び表示されます。
さらなる読み物
この植物プロジェクトの徹底的な概要から、関連する概念やトピックについてさらに深く掘り下げる以下のガイドを探求したいかもしれません。
- クライアント-サーバーモデル — Robloxにおけるクライアント-サーバーモデルの概要。
- リモートイベントとコールバック — クライアント-サーバー境界を越えた通信のためのリモートネットワークイベントとコールバックのすべて。
- UI — Robloxにおけるユーザーインターフェースオブジェクトとデザインの詳細。