はじめに
Unityは自由度が高いため、人によってヒエラルキー構成であったり、スクリプトの実装方法などに大きなばらつきが生じます。
これにより、誰かが作ったUnityプロジェクトを見る際に、処理のフローが掴みづらく、この機能はなんで動いているんだろう?といった疑問を持った経験がある方は少なくないかと思います。
この問題を解決すべく、どのようなフローで処理が行われているかを把握しやすくした、普段私が実際に行っているUnityでのアプリケーション開発手法についてご紹介します。
まず結論
- エントリーポイントを定義する
- ヒエラルキー構成を階層化し責務を明示化する
- エントリーポイントを定義したMainスクリプトで処理をハンドリングする
なぜわかりにくいのか?
Unityの処理のフローが分かりづらい要因として、エントリーポイントが明示的にないことが挙げられます。
例えば、至るスクリプトでStart()が記述されている場合、Start()の実行順序はランダムであるため、どのスクリプトのStart()から呼ばれるかは保証されません。
そのため、スクリプトからはどういう処理フローになっているかを読み解くことは非常に困難です。
解決策
そこで、エントリーポイントとなるStart()を一つのスクリプトのみで記述し、その他のスクリプトではInitialize()といったメソッドを用意して、Start()からInitialize()を呼びます。
これをすることにより、Start()は一つしか存在しないため、そこから処理をたどっていくことが可能となります。
※GameObjectのライフサイクルの問題などで、処理上どうしてもStart()が複数必要になるケースは棚上げします。
実装例
実際どのように実装するか一例をご紹介します。
開発環境
- Windows 10 Version 20H2
- Unity 2020.1.14f1
- UniTask Ver.2.0.37
ヒエラルキー構成
Mainと命名したGameObjectをルートに作成し、その配下にGameObjectを役割ごとに階層構造に配置します。個人的には以下のような構成にするのが分かりやすくておすすめです。
ここで少し話が逸れますが、細かくプレファブ化をすることで、チームで一つのUnityプロジェクトをGitを使って開発する際に、同時に同じプレファブをいじらないようにすることで競合が起こりにくくすることができます。
Mainスクリプトの作成
Main.csを作成し、そこでエントリーポイントとなるStart()を記述します。Start()の中でその他のスクリプトのInitialize()を呼び、初期化処理を行います。また、それぞれの機能を実装したスクリプトのメソッドをMain.csから呼ぶことで、処理のフローの管理もしやすくなります。
これにより、Main.csを見れば大体の処理のフローを掴めるようにすることができました。
(今回の例では、カスタマイズ性が高いので、UniTaskでUpdate処理を置き換えています。)
using Cysharp.Threading.Tasks;
using System.Threading;
using UnityEngine;
public class Main : MonoBehaviour
{
[SerializeField] private PlayerController m_PlayerController = null;
[SerializeField] private AudioManager m_AudioManager = null;
[SerializeField] private DataManager m_DataManager = null;
[SerializeField] private InputManager m_InputManager = null;
[SerializeField] private UIManager m_UIManager = null;
[SerializeField] private XRManager m_XRManager = null;
/// <summary>
/// エントリーポイントです。
/// </summary>
private async UniTask Start()
{
await InitializeAsync();
UpdateLoop(this.GetCancellationTokenOnDestroy()).Forget();
}
/// <summary>
/// 初期化処理を実行します。
/// </summary>
private async UniTask InitializeAsync()
{
m_PlayerController.Initialize();
m_AudioManager.Initialize();
m_DataManager.Initialize();
m_InputManager.Initialize();
m_UIManager.Initialize();
m_XRManager.Initialize();
await UniTask.Yield();
}
/// <summary>
/// Update処理を実行します。
/// </summary>
private async UniTaskVoid UpdateLoop(CancellationToken cancellationToken)
{
while (true)
{
// Updateで実行する処理をここに記述します。
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
}
}
}
まとめ
Mainスクリプトを読めば大体の処理のフローが分かるという安心感は、実装者側も読む側もハッピーにしてくれます。Unityは作るアプリケーションによって、適した構成も変わるので一概には言えませんが、処理のフローを分かりやすく作りたいと思っている方は参考にしてみてください。
すべてのUnity使いに幸あれ。