この記事では、Unityエンジンでのゲームの設計とさらなる実装へのモジュラーアプローチについて検討します。あなたが直面しなければならなかった主な賛否両論と問題が説明されています。
「モジュラーアプローチ」という用語は、独立したプラグ可能な最終アセンブリを内部で使用し、並行して開発し、その場で変更し、構成に応じて異なるソフトウェア動作を実現できるソフトウェア組織を意味します。
モジュール構造
最初に、モジュールとは何か、モジュールの構造、システムのどの部分が何をどのように使用するかを決定することが重要です。
モジュールは、プロジェクトに依存しない比較的独立したアセンブリです。適切な構成とプロジェクト内の共通コアの存在により、まったく異なるプロジェクトで使用できます。モジュールを実装するための必須条件は、トレースの存在です。部品:
インフラストラクチャアセンブリ
このアセンブリには、他のアセンブリで使用できるモデルとコントラクトが含まれています。モジュールのこの部分には、特定の機能の実装へのリンクがあってはならないことを理解することが重要です。理想的には、フレームワークはプロジェクトのコアのみを参照できます。
組立構造は以下のようになります。仕方:
- エンティティ-モジュール内で使用されるエンティティ。
- メッセージング-要求/信号モデル。それらについては後で読むことができます。
- 契約は、インターフェイスを保存する場所です。
インフラストラクチャアセンブリ間のリンクの使用を最小限に抑えることをお勧めすることを覚えておくことが重要です。
機能を使用して構築する
機能の特定の実装。内部では任意のアーキテクチャパターンを使用できますが、システムはモジュール式でなければならないという修正が加えられています。
内部アーキテクチャは次のようになります。
- エンティティ-モジュール内で使用されるエンティティ。
- インストーラー-DIの契約を登録するためのクラス。
- サービスはビジネスレイヤーです。
- マネージャー-マネージャーのタスクは、サービスから必要なデータをプルし、ViewEntityを作成して、ViewManagerを返すことです。
- ViewManagers-マネージャからViewEntityを受信し、必要なビューを作成し、必要なデータを転送します。
- ビュー-ViewManagerから渡されたデータを表示します。
モジュラーアプローチの実装
このアプローチを実装するには、少なくとも2つのメカニズムが必要になる場合があります。コードをアセンブリとDIフレームワークに分割するアプローチが必要です。この例では、アセンブリ定義ファイルとZenjectメカニズムを使用しています。
上記の特定のメカニズムの使用はオプションです。主なことは、それらが何に使用されたかを理解することです。Zenjectは、IoCコンテナなどのDIフレームワーク、およびアセンブリ定義ファイルに置き換えることができます-コードをアセンブリに結合したり、単に独立させたりできる他のシステムに置き換えることができます(たとえば、ペケージ、サブモジュールとして接続できるモジュールごとに異なるリポジトリを使用できますgitaまたは何か他のもの)。
モジュラーアプローチの特徴は、モデルを格納できるインフラストラクチャアセンブリへの参照を除いて、ある機能のアセンブリから別の機能への明示的な参照がないことです。モジュール間の相互作用は、Zenjectフレームワークからの信号のラッパーを使用して実装されます。ラッパーを使用すると、さまざまなモジュールに信号と要求を送信できます。信号は他のモジュールの現在のモジュールによる通知を意味し、要求はデータを返すことができる別のモジュールへの要求を意味することに注意してください。
信号
信号-いくつかの変更についてシステムに通知するためのメカニズム。そして、それらを分解する最も簡単な方法は実際にあります。
2つのモジュールがあるとしましょう。FooとFoo2。Foo2モジュールは、Fooモジュールの変更に応答する必要があります。モジュールの依存関係を取り除くために、2つの信号が実装されています。状態の変化についてシステムに通知するFooモジュール内の1つの信号と、Foo2モジュール内の2番目の信号。Foo2モジュールはこの信号に反応します。OnFoo2SignalでのOnFooSignal信号のルーティングは、ルーティングモジュールで行われます。
概略的には次のようになります。
お問い合わせ
クエリを使用すると、あるモジュールが別のモジュール(他のモジュール)から送受信するデータの通信の問題を解決できます。
上記の信号の例を考えてみましょう。
2つのモジュールがあるとしましょう。FooとFoo2。Fooモジュールには、Foo2モジュールからのデータが必要です。この場合、FooモジュールはFoo2モジュールについて何も知らないはずです。実際、この問題は追加の信号を使用して解決できますが、クエリを使用した解決策はより単純で美しく見えます。
概略的には次のようになります。
モジュール間の通信
機能を備えたモジュール間のリンク(リンクインフラストラクチャ-インフラストラクチャを含む)を最小限に抑えるために、Zenjectフレームワークによって提供される信号のラッパーを作成し、さまざまな信号をルーティングしてデータをマップすることをタスクとするモジュールを作成することにしました。
PS実際、このモジュールには、適切ではないすべてのインフラストラクチャアセンブリへのリンクがあります。しかし、この問題はIoCを介して解決できます。
モジュールの相互作用の例
2つのモジュールがあるとしましょう。LoginModuleとRewardModule。RewardModuleは、FBログイン後にユーザーに報酬を与える必要があります。
namespace RewardModule.src.Infrastructure.Messaging.Signals
{
public class OnLoginSignal : SignalBase
{
public bool IsFirstLogin { get; set; }
}
}
namespace RewardModule.src.Infrastructure.Messaging.RequestResponse.Produce
{
public class GainRewardRequest : EventBusRequest<ProduceResponse>
{
public bool IsFirstLogin { get; set; }
}
}
namespace MessagingModule.src.Feature.Proxy
{
public class LoginModuleProxy
{
[Inject]
private IEventBus eventBus;
public override async void Subscribe()
{
eventBus.Subscribe<OnLoginSignal>((loginSignal) =>
{
var request = new GainRewardRequest()
{
IsFirstLogin = loginSignal.IsFirstLogin;
}
var result = await eventBus.FireRequestAsync<GainRewardRequest, GainRewardResponse>(request);
var analyticsEvent = new OnAnalyticsShouldBeTracked()
{
AnalyticsPayload = new Dictionary<string, string>
{
{
"IsFirstLogin", "false"
},
},
};
eventBus.Fire<OnAnalyticsShouldBeTrackedSignal>(analyticsEvent);
});
上記の例では、モジュール間に直接リンクはありません。ただし、MessagingModuleを介してリンクされています。信号/要求のルーティングとマッピング以外のルーティングには何もないことを覚えておくことが非常に重要です。
実装の置換
モジュラーアプローチと機能トグルパターンを使用すると、アプリケーションへの影響という点で驚くべき結果を得ることができます。サーバー上に特定の構成があると、アプリケーションの開始時にさまざまなモジュールの有効化/無効化を操作して、ゲーム中にそれらを変更できます。
これは、Zenject内のモジュールの(実際にはコンテナーへの)バインド中に、モジュールの可用性フラグがチェックされ、これに基づいて、モジュールがコンテナーにバインドされるかどうかによって実現されます。ゲームセッション中の動作の変更を実現するために(たとえば、ゲームセッション中にメカニズムを変更する必要があります。ソリティアモジュールとクロンダイクモジュールがあります。ユーザーの50%は、カーチーフモジュールが機能するはずです)、あるシーンから別のシーンに切り替えるときにメカニズムが開発されました。特定のモジュールコンテナをクリーンアップし、新しい依存関係をバインドします。
彼は道に取り組んだ。原則:機能が有効になっていて、セッション中に無効にした場合は、コンテナを空にする必要があります。この機能が有効になっている場合は、コンテナにすべての変更を加える必要があります。データと接続の整合性を損なわないように、これを「空の」段階で行うことが重要です。この動作を実装することは可能でしたが、本番機能として、何かを壊すリスクが高くなるため、このような機能を使用することはお勧めしません。
以下は基本クラスの疑似コードであり、その子孫はコンテナに何かを登録する必要があります。
public abstract class GlobalInstallerBase<TGlobalInstaller, TModuleInstaller> : MonoInstaller<TGlobalInstaller>
where TGlobalInstaller : MonoInstaller<TGlobalInstaller>
where TModuleInstaller : Installer
{
protected abstract string SubContainerName { get; }
protected abstract bool IsFeatureEnabled { get; }
public override void InstallBindings()
{
if (!IsFeatureEnabled)
{
return;
}
var subcontainer = Container.CreateSubContainer();
subcontainer.Install<TModuleInstaller>();
Container.Bind<DiContainer>()
.WithId(SubContainerName)
.FromInstance(subcontainer)
.AsCached();
}
protected virtual void SubContainerCleaner(DiContainer subContainer)
{
subContainer.UnbindAll();
}
protected virtual DiContainer SubContainerInstanceGetter(InjectContext containerContext)
{
return containerContext.Container.ResolveId<DiContainer>(SubContainerName);
}
}
プリミティブモジュールの例
モジュールを実装する方法の簡単な例を見てみましょう。
ユーザーが画面の「境界」を超えてカメラを移動できないように、カメラの動きを制限するモジュールを実装する必要があるとします。
モジュールには、カメラが画面外にシステムに移動しようとしたことを通知する信号を含むインフラストラクチャアセンブリが含まれます。
機能-機能の実装。これは、カメラが範囲外にあるかどうかを確認したり、他のモジュールに通知したりするためのロジックになります。
- BorderConfigは、画面の境界を記述するエンティティです。
- BorderViewEntityは、ViewManagerとViewに渡されるエンティティです。
- BoundingBoxManager-サーバーからBorderConfigを取得し、BorderViewEntityを作成します。
- BoundingBoxViewManager — MonoBehaviour'a. , .
- BoundingBoxView — , «» .
- . , , .
- .
- EventHell, , .
- — , . , , — .
- .
- .
- - , . , MVC, — ECS.
- , .
- , .