翻訳者から
こんにちは、Habr!
私は偽の溶接工であり、記事を翻訳するのが難しかったので、彼に無料で電話します-オリジナルをどこかで言いすぎたら良心を事前にクリアします。 翻訳、文法などの誤りを見つけてうれしいです。 午後。
著者のエドゥアルド・ディアス・ダ・コスタのオリジナルが掲載されているToptalのウェブサイトの許可を得て翻訳を公開しています。
通常、Hello Worldから始めて、プログラマーは職業を知るようになります。 その後、彼らはますます多くの目標を設定し、それぞれの新しいタスクは重要な教訓につながります。プロジェクトが大きくなればなるほど、コードが混乱します。
また、大小のチームでは、誰もが好きなようにコーディングすることはありません。 コードはサポートされ、拡張可能でなければなりません。 結局のところ、あなたが働いていた会社は、あなたがバグを修正したりコードを改善したりする必要があるときにはいつでもあなたに連絡しません。 はい、ほとんど必要ありません。
したがって、設計パターンが存在します。 これらは、標準化されたプロジェクト構造化のためのルールのコレクションであり、大規模なコードベースを共有および整理し、馴染みのないコードでの作業を簡素化するのに役立ちます。
これらのルールは、すべてのプロジェクト開発者が従うと、サポートが容易になり、古いコードをナビゲートし、新しいコードを作成します。 開発アプローチの計画に費やす時間が短縮されます。 しかし、問題はケースごとに異なるため、パターンは特効薬ではありません。 特定の状況に適したものを選択する前に、それぞれの長所と短所を慎重に検討する必要があります。
このチュートリアルでは、人気のあるUnity3Dゲームエンジンでの経験と、Model-View-Controller(MVC)テンプレートの使用について説明します。 7年間の仕事とゲームプロジェクトでのスパゲッティコードとの戦いで、私はそれを使用することで優れたコード構造と開発速度を達成しました。
まず、Unityの基本アーキテクチャであるEntity-Component(EC)テンプレートについて説明し、次にMVCがその上にどのように構築されるかを説明します。 そして、小さなモックアッププロジェクトの例を示します。
やる気
既存の設計パターンを特定のタスクに適合させるために、プログラマーはそれらを変更する必要があります。 このプログラミングの自由は、真のソフトウェアアーキテクチャを見つけられなかったことの証明です。 この記事は、すべての問題の解決策を意図したものでもありません。 ECとMVCの2つのよく知られた設計パターンの機能を示しています。
エンティティコンポーネントテンプレート
Entity-Component(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は多重継承の問題を軽減するための良いテンプレートです:BとCから継承するクラスDに共通のクラスAがあり、BクラスによるAの機能の異なる再定義による競合が含まれる場合がありますおよびC.
同様の問題は、継承を積極的に使用する場合によく発生します。
タスクとデータハンドラーを小さなコンポーネントに分解すると、それらはエンティティにアタッチされ、複数の継承なしで再利用できます。これは、C#とJavaScript(メインプログラミング言語Unity)にはありません。
エンティティコンポーネントが期待どおりに機能しない場合
OOPを超えるレベルでは、ECはコードアーキテクチャの最適化と整理を支援します。 しかし、大規模なプロジェクトでは、「余りにも自由」であり、「機会の海」では、エンティティとコンポーネントを正しく分離し、それらの相互作用を整理することは困難です。 エンティティとコンポーネントを構築するためのオプションは無限にあります。
混乱を回避する1つの方法は、ECに加えて追加の原則に従うことです。 プログラムを3つのカテゴリに分けます。
- 生データを処理し、それらを作成、受信、更新、削除、検索できるようにするものもあります(CRUDなど)。
- 他のものは、インターフェースを実装します(ソフトウェアではなく、ユーザーごと)。他の要素とやり取りするために、責任範囲に関連するイベントを検出し、これらのイベントに関する通知を送信します。
- さらに、これらの通知を受信し、ビジネスロジックを実装し、データの変更方法を決定する責任者もいます。
幸いなことに、この動作を説明するデザインパターンが既に存在します。
モデル表現コントローラー(MVC)テンプレート
Model-View-Controllerは、プログラムを3つの主要コンポーネントに分割します:モデル(CRUD)、ビュー(インターフェース/検出)、コントローラー(決定/アクション)。 MVCは十分な柔軟性があり、ECおよびOOPの上に実装されています。
ゲームとユーザーインターフェイスの開発では、ユーザーの入力を待ったりトリガーをトリガーしたり、イベントに関する通知を送信したり、イベントに応答したり、データを更新したりする毎日の手段があります。 これらの手順は、MVCとアプリケーションの互換性を示しています。
この方法論では、ソフトウェアの計画と大規模プロジェクトでも新しい開発者の操作に役立つ抽象化の別のレイヤーが導入されます。 データ、インターフェイス、およびビジネスロジックに分離することで、アプリケーション機能を追加または変更するために開発者が触れる必要のあるファイルの数が減ります。
UnityとEC
では、Unityの機能を詳しく見てみましょう。
UnityはECベースのプラットフォームで、 GameObject
インスタンスがエンティティとして表示され、インスタンスを表示、移動などする機能があります。 Component
クラスの子孫によって提供されます。
階層パネルとインスペクターパネルは、アプリケーションのアセンブル、コンポーネントのエンティティへの割り当て、初期化状態の設定、ゲームのロードを行うための強力なツールです。 それらがなければ、はるかに多くのコードが必要でした。
右側に4つのGameObjectがある階層パネル。
GameObjectコンポーネントを備えたインスペクターパネル。
上記で説明したように、機会が多すぎるという問題に遭遇し、ビジネスロジックがあちこちに散らばる巨大な階層になってしまう可能性があります。
ただし、MVCは役立ちます。スクリーンショットに示すように、エンティティを目的とアプリケーションの構造に応じて分割します。
ゲーム開発向けのMVCの適応
MVCを使用したUnityに固有の状況に合わせて、一般的なMVCパターンを2つ修正するときが来ました。
- MVCクラスへの参照は簡単にコーディングできるはずです。
- 別のオブジェクトにインスタンスへのアクセスを許可するには、Unityで、開発者はエディター内のオブジェクトへのリンクをドラッグアンドドロップするか、かさばる(そして遅い、およそ)
GetComponent(...)
呼び出しをGetComponent(...)
するGetComponent(...)
。 - Unityがクラッシュした場合、またはエディターに組み込まれたリンクが破損したために、本当の地獄が始まります。
- このため、 アプリケーションで使用されるすべてのインスタンスにアクセスできるルートオブジェクトが必要です。
- 別のオブジェクトにインスタンスへのアクセスを許可するには、Unityで、開発者はエディター内のオブジェクトへのリンクをドラッグアンドドロップするか、かさばる(そして遅い、およそ)
- 一部の要素は、MVCカテゴリのいずれにも属さないロジックをカプセル化し、多くの場合再利用されます。 それらをComponentsと呼びます 。 これらはEntity-Component構造のコンポーネントでもありますが、MVCフレームワークでは単なる補助クラスです。
- 例:オブジェクトを単純に回転させる
Rotator
コンポーネント。 ただし、通知は送信されず、何も保存されず、ビジネスロジックは含まれません。
- 例:オブジェクトを単純に回転させる
これらの問題を解決するために、元のテンプレートを変更し、 AMVCCまたはApplication-Model-View-Controller-Componentという名前を付けました。
- アプリケーション -すべての重要なインスタンスとアプリケーション依存データのアプリケーションとコンテナへの単一のエントリポイント。
- MVC-これで何であるかがわかりました:)
- コンポーネントは、自己完結型で簡単に再利用可能な小さなスクリプトです。
私のプロジェクトでは、これらの2つのイノベーションで十分です。
例:10バウンス
AMVCCテンプレートを小さなゲームに適用し、「10バウンス」と呼びましょう。 ゲームのSphereCollider
は単純です: SphereCollider
とRigidbody
をSphereCollider
したBall
は、ゲームの開始時に落下し始めます。 土地としてのCube
とAMVCCを作成する5つのスクリプト。
階層
コードを開始する前に、AMVCCスタイルに従って、クラスとアセットの階層をスケッチします。
GameObject view
は、すべての視覚要素とView
スクリプトが含まれています。 通常、小規模プロジェクトのmodel
およびcontroller
オブジェクトには、対応するスクリプトが1つだけ含まれます。大規模プロジェクトでは、特定のアクション、データなどを担当する多くのスクリプトがあります。
誰かがアクセスしたいとき:
- データ別 :
application > model > ...
従ってくださいapplication > model > ...
- ロジック/ワークフロー :
application > controller > ...
- レンダリング/インターフェース/検出 :
application > view > ...
すべてのチームがこれらの3つのルールに従う場合、レガシープロジェクトは問題になりません。
階層には、異なる要素で個別に使用できるため、 Component
個別のコンテナがないことに注意してください。
スクリプティング
注意 :以下に示すスクリプトは、実際のコードを一般化したものです。 質問をさらに詳しく調べたい場合は 、Unity MVC MVCフレームワークへのリンクをご覧ください 。 AMVCCの下で構造化されているため、ほとんどのアプリケーションに必要な基本クラスがあります。
次に、10個のバウンススクリプトの構造を見てみましょう。
Unityデバイスに不慣れな方のために、GameObjectがどのように相互作用するかを簡単に説明します。 Entity-Componentテンプレートの「コンポーネント」は、 MonoBehaviour
クラスで表されます。 アプリケーションの実行中に使用可能にするには、開発者はドラッグアンドドロップソースファイルをAddComponent<YourMonobehaviour>()
ECテンプレートの「エンティティ」)にドラッグアンドドロップするか、 AddComponent<YourMonobehaviour>()
コマンドを使用する必要があります。 その後、スクリプトがインスタンス化され、使用できる状態になります。
2つのクラスを宣言します。
Application
(AMVCCの「A」)がメインクラスであり、ゲームでインスタンス化されたすべての要素へのリンクを含むインスタンスが1つだけあります。 内部では、ルートMVCオブジェクトへのアクセスを提供する3つのパブリック変数、 model
、 view
およびcontroller
を宣言します。
Element
は、MVC子インスタンスにApplication
へのアクセスを提供するヘルパー基本クラスです。
両方のクラスがMonoBehaviour
相続人であることを忘れないでください。 これらは、ゲームオブジェクト「エンティティ」にアタッチされた「コンポーネント」です。
// 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クラスをBounceElement
ます。 通常、 BounceModel
、 BounceView
、およびBounceController
は、特殊なMVCオブジェクトのコンテナーとして機能しますが、簡略化された例では、ネストされた構造を持つのはビューのみです。 モデルとコントローラーには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エディターでどのように見えるかをBounceModel
みましょう。
bounces
とwinCondition
をwinCondition
。
スクリプトをインストールしてゲームを起動すると、コンソールへの出力は次のようになります。
通知
ボールが地面に当たると、その表現はapp.controller.OnBallGroundHit()
メソッドを呼び出します。 これは、これがアプリケーションで通知を送信する「間違った」方法であると言うことではありませんが、私の経験では、 Application
クラスで実装された単純な通知システムを使用する方がはるかに便利です。
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
「10バウンス」は、AMVCCテンプレートの最も単純な使用例を示しています。 実際に使用するには、MVCの3つのカテゴリ内で思考を磨き、順序付けられた階層の形でエンティティを視覚化することを学ぶ必要があります。
大規模なプロジェクトでは、開発者はより複雑なシナリオに直面し、このエンティティまたはそのエンティティがビューであるかコントローラーであるか、またはクラスを部分に分割する価値があるかどうかを疑います。
実践的なルール(エドゥアルド作)
これは「ユニバーサルMVCガイド」ではなく、モデル、ビュー、コントローラーの分離に役立つ一連の簡単なルールです。
これは通常、アーキテクチャを考えたりスクリプトを書いたりするときに自然に発生します。
クラス分離
モデル
- これらには、プレーヤーの健康状態や弾薬の在庫など、アプリケーションの基本データが含まれています。
- 型をシリアル化、逆シリアル化、および/または変換します。
- データのダウンロード/保存(ローカルまたはネットワーク経由)。
- 操作の進行状況をコントローラーに通知します。
- ゲームのステートマシンの状態を保存します。
- 投稿を呼び出さないでください。
視聴回数
- モデルからデータを受信して、ゲームの現在の状態を表示できます。 たとえば、Viewメソッド
player.Run()
はmodel.speed
を使用して、プレーヤーの能力を視覚化できます。 - モデルを変更しないでください。
- クラスの目的に厳密に従います。 例:
-
PlayerView
は、入力またはゲームの状態の変化を検出しません。 - このビューは、インターフェースを備えたブラックボックスのように機能し、重要なイベントを警告します。
- 速度、健康、生活などの重要なデータは保存されません。
-
コントローラー
- 基本的なゲームデータを保存しないでください。
- ビューから通知をフィルタリングできます。
- モデルデータを更新して使用します。
- Unityシーンでアクションを管理します。
クラス階層
変数に含まれるプレフィックスが多すぎる場合や、分岐の可能性が明確にトレースされている場合(MMOのPlayer
クラスやFPSのGun
タイプなど)、どのクラスを分離する必要があるかを理解しています。 しかし、完全を期すために、私はこの点を無視できませんでした。
たとえば、プレーヤーデータを使用するモデルには、 OnPlayerDidA, OnPlayerDidB,…
などの変数や、プレーヤー通知を処理するコントローラー( OnPlayerDidA, OnPlayerDidB,…
メソッド)があり、コードの量を減らしてplayer
とOnPlayer
をOnPlayer
ます。 モデルの方が理解しやすいので、例でこれを示します。
私は通常、すべてのゲームデータを含む単一のモデルから始めます。
// 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
クラスのデータを使用すると、武器の種類ごとにプレハブのセットを作成し、ゲームで使用できます。 Prefabは事前に準備されたGameObjectであり、すぐにコピーして再利用できます。
そして、武器に関する情報がgun0Ammo
、 gun1Ammo
、 gun0Clips
などの変数の1つのクラスに含まれている場合、 Gun
データを保存する必要に直面した場合、 Player
に関する不要なデータを含むModel
全体を保存する必要があります。 この場合、新しいGunModel
クラスを作成する方が良いことは明らかです。
クラス階層の改善
いつものように、コインには2つの側面があります。 過度の分類によりコードが複雑になる場合があります。 プロジェクトでMVCを最適な方法で構成するには、経験のみが役立ちます。
新しいゲーム開発スキルがオープンしました:Unity with MVC。
おわりに
多くのデザインパターンがあります。 この記事では、最近のプロジェクトの経験に基づいて、最も有用なものを示しました。 開発者は常に知識を吸収し、より多くを求めています。 このガイドが何か新しいことを学ぶのに役立ち、同時にあなた自身の開発スタイルの開発の段階になったことを願っています。
また、他のテンプレートを調べて有用なテンプレートを探すことを強くお勧めします。 Wikipediaの記事から始めることができます。テンプレートとその特性の優れたリストがあります。
AMVCCテンプレートが気に入ってテストしたい場合は、AMVCCスタイルのアプリケーションに必要なすべての基本クラスが含まれているUnity MVCライブラリを試してください。