Unity with MVC:ゲーム開発をレベルアップする方法
公開: 2022-03-11初めてのプログラマーは通常、古典的なHello World
プログラムでトレードを学び始めます。 そこから、ますます大きな割り当てが続くことになります。 それぞれの新しい挑戦は、重要な教訓をもたらします。
プロジェクトが大きいほど、スパゲッティも大きくなります。
すぐに、大小のチームでは、好きなように無謀に行うことができないことが簡単にわかります。 コードは維持する必要があり、長期間続く可能性があります。 あなたが働いてきた会社は、あなたの連絡先情報を調べて、コードベースを修正または改善したいときはいつでもあなたに尋ねることはできません(そしてあなたは彼らにもそうさせたくありません)。
これが、ソフトウェアデザインパターンが存在する理由です。 それらは、ソフトウェアプロジェクトの全体的な構造を指示するための単純なルールを課します。 これらは、1人以上のプログラマーが大規模なプロジェクトのコア部分を分離し、標準化された方法で整理するのに役立ち、コードベースのなじみのない部分に遭遇したときの混乱を排除します。
これらのルールに従うと、すべての人が従うことで、レガシーコードをより適切に維持およびナビゲートし、新しいコードをより迅速に追加できます。 開発の方法論を計画するために費やされる時間が少なくなります。 問題は1つのフレーバーでは発生しないため、銀の弾丸のデザインパターンはありません。 各パターンの長所と短所を注意深く検討し、目前の課題に最適なものを見つける必要があります。
このチュートリアルでは、人気のあるUnityゲーム開発プラットフォームとゲーム開発用のModel-View-Controller(MVC)パターンの経験を関連付けます。 ゲーム開発スパゲッティの公平なシェアと格闘してきた7年間の開発で、このデザインパターンを使用して優れたコード構造と開発速度を達成してきました。
まず、Unityの基本アーキテクチャであるエンティティコンポーネントパターンについて説明します。 次に、MVCがその上にどのように適合するかを説明し、例として小さな模擬プロジェクトを使用します。
モチベーション
ソフトウェアの文献には、多数のデザインパターンがあります。 一連のルールがありますが、開発者は通常、パターンを特定の問題によりよく適合させるために、少しルールを曲げます。
この「プログラミングの自由」は、ソフトウェアを設計するための単一の決定的な方法をまだ見つけていないことの証拠です。 したがって、この記事は、問題の最終的な解決策ではなく、エンティティコンポーネントとモデルビューコントローラーという2つのよく知られたパターンの利点と可能性を示すことを目的としています。
エンティティ-コンポーネントパターン
エンティティコンポーネント(EC)は、アプリケーションを構成する要素の階層(エンティティ)を最初に定義し、その後、それぞれに含まれる機能とデータ(コンポーネント)を定義するデザインパターンです。 より「プログラマー」の用語では、エンティティは0個以上のコンポーネントの配列を持つオブジェクトである可能性があります。 次のようなエンティティを表現しましょう。
some-entity [component0, component1, ...]
ECツリーの簡単な例を次に示します。
- app [Application] - game [Game] - player [KeyboardInput, Renderer] - enemies - spider [SpiderAI, Renderer] - ogre [OgreAI, Renderer] - ui [UI] - hud [HUD, MouseInput, Renderer] - pause-menu [PauseMenu, MouseInput, Renderer] - victory-modal [VictoryModal, MouseInput, Renderer] - defeat-modal [DefeatModal, MouseInput, Renderer]
ECは、多重継承の問題を軽減するための優れたパターンです。複雑なクラス構造は、同じ基本クラスAを持つ2つのクラスBとCを継承するクラスDが競合を引き起こす可能性がある、菱形継承問題のような問題を引き起こす可能性があります。 BとCは、Aの機能を異なる方法で変更します。
この種の問題は、継承が頻繁に使用されるゲーム開発でよく見られます。
機能とデータハンドラーをより小さなコンポーネントに分割することで、多重継承に依存することなく、さまざまなエンティティにアタッチして再利用できます(ちなみに、Unityで使用される主要言語であるC#やJavascriptの機能でもありません) )。
エンティティコンポーネントが不足している場所
ECはOOPの1レベル上にあるため、コードアーキテクチャを最適化して整理するのに役立ちます。 ただし、大規模なプロジェクトでは、まだ「自由すぎる」ため、「機能の海」にいることに気づき、適切なエンティティとコンポーネントを見つけたり、それらがどのように相互作用するかを理解するのに苦労しています。 特定のタスクのエンティティとコンポーネントを組み立てる方法は無限にあります。
混乱を回避する1つの方法は、Entity-Componentの上にいくつかの追加のガイドラインを課すことです。 たとえば、私がソフトウェアについて考える1つの方法は、ソフトウェアを3つの異なるカテゴリに分類することです。
- 生データを処理して、作成、読み取り、更新、削除、または検索できるようにするものもあります(つまり、CRUDの概念)。
- 他の要素は、他の要素が相互作用するためのインターフェースを実装し、それらのスコープに関連するイベントを検出し、それらが発生したときに通知をトリガーします。
- 最後に、一部の要素は、これらの通知の受信、ビジネスロジックの決定、およびデータの操作方法の決定を担当します。
幸いなことに、この正確な方法で動作するパターンがすでにあります。
Model-View-Controller(MVC)パターン
Model-View-Controllerパターン(MVC)は、ソフトウェアを3つの主要なコンポーネント(モデル(データCRUD)、ビュー(インターフェイス/検出)、およびコントローラー(決定/アクション))に分割します。 MVCは、ECSまたはOOPの上でも実装できるほど柔軟です。
ゲームとUIの開発には、ユーザーの入力やその他のトリガー条件を待ち、適切な場所にそれらのイベントの通知を送信し、それに応じて何をするかを決定し、それに応じてデータを更新するという通常のワークフローがあります。 これらのアクションは、これらのアプリケーションとMVCの互換性を明確に示しています。
この方法論は、ソフトウェアの計画に役立つ別の抽象化レイヤーを導入し、新しいプログラマーがより大きなコードベースでもナビゲートできるようにします。 思考プロセスをデータ、インターフェース、および意思決定に分割することにより、開発者は、機能を追加または修正するために検索する必要のあるソースファイルの数を減らすことができます。
UnityとEC
まず、Unityが私たちに提供するものを詳しく見てみましょう。
UnityはECベースの開発プラットフォームであり、すべてのエンティティはGameObject
のインスタンスであり、それらを「表示」、「移動可能」、「対話可能」などにする機能は、 Component
を拡張するクラスによって提供されます。
UnityエディターのHierarchyPanelとInspectorPanelは、アプリケーションをアセンブルし、コンポーネントをアタッチし、初期状態を構成し、通常よりもはるかに少ないソースコードでゲームをブートストラップするための強力な方法を提供します。
それでも、これまでに説明したように、「機能が多すぎる」という問題にぶつかり、機能がいたるところに散らばっている巨大な階層にいることに気付くと、開発者の生活が非常に難しくなります。
MVCの方法で考えると、代わりに、機能に応じて物事を分割し、以下の例のようにアプリケーションを構造化することから始めることができます。
MVCをゲーム開発環境に適応させる
ここで、一般的なMVCパターンに2つの小さな変更を加えます。これは、MVCを使用してUnityプロジェクトを構築するときに遭遇した固有の状況に適応させるのに役立ちます。
- MVCクラス参照は、コード全体に簡単に散らばっています。 --Unity内では、開発者は通常、インスタンスをドラッグアンドドロップしてアクセス可能にするか、
GetComponent( ... )
のような面倒なfindステートメントを介してインスタンスに到達する必要があります。 --Unityがクラッシュしたり、バグによってドラッグされたすべての参照が消えたりすると、参照が失われます。 -これにより、アプリケーション内のすべてのインスタンスに到達して回復できる単一のルート参照オブジェクトが必要になります。 - 一部の要素は、再利用性が高く、モデル、ビュー、またはコントローラーの3つの主要なカテゴリのいずれにも自然に分類されない一般的な機能をカプセル化します。 これらは単にコンポーネントと呼ぶのが好きです。 これらは、エンティティコンポーネントの意味での「コンポーネント」でもありますが、MVCフレームワークのヘルパーとして機能するだけです。 -たとえば、特定の角速度で物事を回転させるだけで、何も通知、保存、または決定しない
Rotator
コンポーネント。
これらの2つの問題を軽減するために、 AMVCCまたはApplication-Model-View-Controller-Componentと呼ばれる変更されたパターンを考え出しました。
- アプリケーション-すべての重要なインスタンスとアプリケーション関連データのアプリケーションとコンテナへの単一のエントリポイント。
- MVC-これはもう知っているはずです。 :)
- コンポーネント-再利用できる小さくて十分に含まれたスクリプト。
これらの2つの変更は、私がそれらを使用したすべてのプロジェクトに対する私のニーズを満たしています。
例: 10バウンス
簡単な例として、 10バウンスと呼ばれる小さなゲームを見てみましょう。ここでは、AMVCCパターンのコア要素を利用します。
ゲームのセットアップは簡単ですSphereCollider
とRigidbody
(「Play」の後に落下し始めます)を備えたBall
、地面としてのCube
、およびAMVCCを構成する5つのスクリプトです。
階層
スクリプトを作成する前に、私は通常、階層から始めて、クラスとアセットのアウトラインを作成します。 常にこの新しいAMVCCスタイルに従います。
ご覧のとおり、 view
GameObjectには、すべての視覚要素と、他のView
スクリプトを含む要素が含まれています。 小さなプロジェクトのmodel
とcontroller
のGameObjectsには、通常、それぞれのスクリプトのみが含まれています。 大規模なプロジェクトの場合、より具体的なスクリプトを含むGameObjectが含まれます。
プロジェクトをナビゲートしている誰かがアクセスしたい場合:
- データ:
application > model > ...
に移動します。 - ロジック/ワークフロー:
application > controller > ...
に移動します。 - レンダリング/インターフェース/検出:
application > view > ...
に移動します。
すべてのチームがこれらの単純なルールに従っている場合、レガシープロジェクトが問題になることはありません。
Component
コンテナはありません。これまでに説明したように、コンポーネントコンテナはより柔軟性があり、開発者が自由にさまざまな要素にアタッチできるためです。
スクリプティング
注:以下に示すスクリプトは、実際の実装の抽象バージョンです。 詳細な実装は、読者にはあまりメリットがありません。 ただし、さらに詳しく知りたい場合は、Unity用の個人的なMVCフレームワークであるUnityMVCへのリンクを次に示します。 ほとんどのアプリケーションに必要なAMVCC構造フレームワークを実装するコアクラスがあります。
10バウンスのスクリプトの構造を見てみましょう。
始める前に、Unityのワークフローに慣れていない人のために、スクリプトとGameObjectsがどのように連携するかを簡単に説明しましょう。 Unityでは、エンティティコンポーネントの意味での「コンポーネント」はMonoBehaviour
クラスで表されます。 実行時に存在するためには、開発者はソースファイルをGameObject(Entity-Componentパターンの「エンティティ」)にドラッグアンドドロップするか、コマンドAddComponent<YourMonobehaviour>()
を使用する必要があります。 この後、スクリプトがインスタンス化され、実行中に使用できるようになります。
まず、Applicationクラス(AMVCCの「A」)を定義します。これは、インスタンス化されたすべてのゲーム要素への参照を含むメインクラスになります。 また、 Element
というヘルパー基本クラスを作成します。これにより、アプリケーションのインスタンスとその子のMVCインスタンスにアクセスできます。
これを念頭に置いて、一意のインスタンスを持つApplication
クラス(AMVCCの「A」)を定義しましょう。 その中には、 model
、 view
、 controller
の3つの変数があり、実行時にすべてのMVCインスタンスのアクセスポイントを提供します。 これらの変数は、目的のスクリプトへのpublic
参照を持つMonoBehaviour
である必要があります。
次に、 Element
というヘルパー基本クラスも作成します。これにより、アプリケーションのインスタンスにアクセスできます。 このアクセスにより、すべてのMVCクラスが相互に到達できるようになります。
どちらのクラスもMonoBehaviour
を拡張することに注意してください。 これらは、GameObjectの「エンティティ」にアタッチされる「コンポーネント」です。
// BounceApplication.cs // Base class for all elements in this application. public class BounceElement : MonoBehaviour { // Gives access to the application and all instances. public BounceApplication app { get { return GameObject.FindObjectOfType<BounceApplication>(); }} } // 10 Bounces Entry Point. public class BounceApplication : MonoBehaviour { // Reference to the root instances of the MVC. public BounceModel model; public BounceView view; public BounceController controller; // Init things here void Start() { } }
BounceElement
から、MVCコアクラスを作成できます。 BounceModel
、 BounceView
、およびBounceController
スクリプトは通常、より特殊なインスタンスのコンテナーとして機能しますが、これは単純な例であるため、ビューのみがネストされた構造になります。 モデルとコントローラーは、それぞれ1つのスクリプトで実行できます。
// BounceModel.cs // Contains all data related to the app. public class BounceModel : BounceElement { // Data public int bounces; public int winCondition; }
// BounceView .cs // Contains all views related to the app. public class BounceView : BounceElement { // Reference to the ball public BallView ball; }
// BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.controller.OnBallGroundHit(); } }
// BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnBallGroundHit() { app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball OnGameComplete(); } } // Handles the win condition public void OnGameComplete() { Debug.Log(“Victory!!”); } }
すべてのスクリプトが作成されたら、それらの添付と構成に進むことができます。

階層レイアウトは次のようになります。
- application [BounceApplication] - model [BounceModel] - controller [BounceController] - view [BounceView] - ... - ball [BallView] - ...
例としてBounceModel
を使用すると、Unityのエディターでどのように表示されるかを確認できます。
bounces
フィールドとBounceModel
フィールドを持つwinCondition
。
すべてのスクリプトを設定してゲームを実行すると、コンソールパネルにこの出力が表示されます。
通知
上記の例に示すように、ボールが地面に当たると、そのビューはメソッドであるapp.controller.OnBallGroundHit()
を実行します。 アプリケーション内のすべての通知に対してこれを行うことは、決して「間違っている」わけではありません。 ただし、私の経験では、AMVCCアプリケーションクラスに実装された単純な通知システムを使用して、より良い結果を達成しました。
これを実装するには、 BounceApplication
のレイアウトを次のように更新しましょう。
// BounceApplication.cs class BounceApplication { // Iterates all Controllers and delegates the notification data // This method can easily be found because every class is “BounceElement” and has an “app” // instance. public void Notify(string p_event_path, Object p_target, params object[] p_data) { BounceController[] controller_list = GetAllControllers(); foreach(BounceController c in controller_list) { c.OnNotification(p_event_path,p_target,p_data); } } // Fetches all scene Controllers. public BounceController[] GetAllControllers() { /* ... */ } }
次に、すべての開発者が通知イベントの名前を追加する新しいスクリプトが必要です。これは、実行中にディスパッチできます。
// BounceNotifications.cs // This class will give static access to the events strings. class BounceNotification { static public string BallHitGround = “ball.hit.ground”; static public string GameComplete = “game.complete”; /* ... */ static public string GameStart = “game.start”; static public string SceneLoad = “scene.load”; /* ... */ }
このように、開発者が実行中に発生する可能性のあるアクションの種類を理解するために、開発者がソースコード全体でcontroller.OnSomethingComplexName
メソッドを検索する必要がないため、コードの読みやすさが向上することは容易に理解できます。 1つのファイルをチェックするだけで、アプリケーションの全体的な動作を理解することができます。
これで、この新しいシステムを処理するためにBallView
とBounceController
を調整するだけで済みます。
// BallView.cs // Describes the Ball view and its features. public class BallView : BounceElement { // Only this is necessary. Physics is doing the rest of work. // Callback called upon collision. void OnCollisionEnter() { app.Notify(BounceNotification.BallHitGround,this); } }
// BounceController.cs // Controls the app workflow. public class BounceController : BounceElement { // Handles the ball hit event public void OnNotification(string p_event_path,Object p_target,params object[] p_data) { switch(p_event_path) { case BounceNotification.BallHitGround: app.model.bounces++; Debug.Log(“Bounce ”+app.model.bounce); if(app.model.bounces >= app.model.winCondition) { app.view.ball.enabled = false; app.view.ball.GetComponent<RigidBody>().isKinematic=true; // stops the ball // Notify itself and other controllers possibly interested in the event app.Notify(BounceNotification.GameComplete,this); } break; case BounceNotification.GameComplete: Debug.Log(“Victory!!”); break; } } }
より大きなプロジェクトには多くの通知があります。 したがって、大きなスイッチケース構造になるのを避けるために、さまざまなコントローラーを作成し、それらにさまざまな通知スコープを処理させることをお勧めします。
実世界のAMVCC
この例は、AMVCCパターンの簡単な使用例を示しています。 MVCの3つの要素の観点から考え方を調整し、エンティティを順序付けられた階層として視覚化することを学ぶことは、洗練されるべきスキルです。
大規模なプロジェクトでは、開発者はより複雑なシナリオに直面し、何かをビューにするかコントローラーにするか、または特定のクラスをより小さなクラスに完全に分離する必要があるかどうかについて疑問を抱きます。
経験則(Eduardoによる)
「MVCソートのユニバーサルガイド」はどこにもありません。 ただし、モデル、ビュー、またはコントローラーとして何かを定義するかどうか、および特定のクラスをより小さな部分に分割するタイミングを決定するために、私が通常従ういくつかの簡単なルールがあります。
通常、これは、ソフトウェアアーキテクチャについて考えているとき、またはスクリプトを作成しているときに有機的に発生します。
クラスの並べ替え
モデル
- プレーヤー
health
や銃のammo
など、アプリケーションのコアデータと状態を保持します。 - タイプ間でシリアル化、逆シリアル化、および/または変換します。
- データのロード/保存(ローカルまたはWeb上)。
- 操作の進行状況をコントローラーに通知します。
- ゲームの有限状態マシンのゲーム状態を保存します。
- ビューには絶対にアクセスしないでください。
ビュー
- 最新のゲーム状態をユーザーに表すために、モデルからデータを取得できます。 たとえば、Viewメソッド
player.Run()
は、内部でmodel.speed
を使用して、プレーヤーの能力を明示できます。 - モデルを変更しないでください。
- そのクラスの機能を厳密に実装します。 例えば:
-
PlayerView
は、入力検出を実装したり、ゲームの状態を変更したりしないでください。 - ビューは、インターフェイスを備え、重要なイベントを通知するブラックボックスとして機能する必要があります。
- コアデータ(速度、健康、生活など)を保存しません。
-
コントローラー
- コアデータを保存しないでください。
- 不要なビューからの通知をフィルタリングできる場合があります。
- モデルのデータを更新して使用します。
- Unityのシーンワークフローを管理します。
クラス階層
この場合、私が従う手順はそれほど多くありません。 通常、変数に表示される「プレフィックス」が多すぎる場合、または同じ要素のバリアントが多すぎる場合(MMOのPlayer
クラスやFPSのGun
タイプなど)に、一部のクラスを分割する必要があると思います。
たとえば、Playerデータを含む単一のModel
には多くのplayerDataA、playerDataB、 playerDataA, playerDataB,...
が含まれるか、Player通知を処理するController
にはOnPlayerDidA,OnPlayerDidB,...
が含まれます。 スクリプトサイズを縮小し、 player
とOnPlayer
のプレフィックスを削除したいと思います。
データのみを使用すると理解しやすいため、 Model
クラスの使用方法を示します。
プログラミング中、私は通常、ゲームのすべてのデータを保持する単一のModel
クラスから始めます。
// Model.cs class Model { public float playerHealth; public int playerLives; public GameObject playerGunPrefabA; public int playerGunAmmoA; public GameObject playerGunPrefabB; public int playerGunAmmoB; // Ops Gun[CDE ...] will appear... /* ... */ public float gameSpeed; public int gameLevel; }
ゲームが複雑になるほど、変数の数が増えることは容易に理解できます。 十分に複雑になると、 model.playerABCDFoo
変数を含む巨大なクラスになってしまう可能性があります。 要素をネストすると、コードの完成が簡素化され、データのバリエーションを切り替える余地が生まれます。
// Model.cs class Model { public PlayerModel player; // Container of the Player data. public GameModel game; // Container of the Game data. }
// GameModel.cs class GameModel { public float speed; // Game running speed (influencing the difficulty) public int level; // Current game level/stage loaded }
// PlayerModel.cs class PlayerModel { public float health; // Player health from 0.0 to 1.0. public int lives; // Player “retry” count after he dies. public GunModel[] guns; // Now a Player can have an array of guns to switch ingame. }
// GunModel.cs class GunModel { public GunType type; // Enumeration of Gun types. public GameObject prefab; // Template of the 3D Asset of the weapon. public int ammo; // Current number of bullets public int clips; // Number of reloads possible }
このクラスの構成により、開発者は一度に1つの概念でソースコードを直感的にナビゲートできます。 一人称シューティングゲームを想定してみましょう。このゲームでは、武器とその構成が非常に多くなる可能性があります。 GunModel
がクラスに含まれているという事実により、各カテゴリのプレハブ(事前構成されたPrefabs
をすばやく複製してゲーム内で再利用)のリストを作成し、後で使用するために保存することができます。
対照的に、銃の情報がすべて単一のGunModel
クラスに、 gun0Ammo
、 gun1Ammo
、 gun0Clips
などの変数に一緒に保存されている場合、ユーザーはGun
のデータを保存する必要がある場合、全体を保存する必要があります。不要なPlayer
データを含むModel
。 この場合、新しいGunModel
クラスの方が優れていることは明らかです。
すべての場合と同様に、コインには2つの面があります。 場合によっては、不必要に区画化しすぎて、コードの複雑さが増すことがあります。 プロジェクトに最適なMVC並べ替えを見つけるのに十分なスキルを磨くことができるのは、経験だけです。
結論
そこにはたくさんのソフトウェアパターンがあります。 この投稿では、過去のプロジェクトで最も役に立ったものを紹介しようとしました。 開発者は常に新しい知識を吸収する必要がありますが、常にそれについても疑問を投げかけます。 このチュートリアルがあなたが何か新しいことを学ぶのに役立つと同時に、あなたがあなた自身のスタイルを開発する際の足がかりとして役立つことを願っています。
また、他のパターンを調べて、自分に最も適したパターンを見つけることを強くお勧めします。 良い出発点の1つは、このWikipediaの記事で、パターンとその特性の優れたリストがあります。
AMVCCパターンが好きで、それをテストしたい場合は、AMVCCアプリケーションを開始するために必要なすべてのコアクラスを含む私のライブラリであるUnityMVCを試すことを忘れないでください。
Toptal Engineeringブログでさらに読む:
- Unity AI開発:有限状態マシンのチュートリアル