はじめに
サムザップ #2 AdventCalendar 2020 の12/9の記事です。
株式会社サムザップ Unityエンジニアの尾崎です。
内容
UnityでScriptableObjectを使ってシステム系クラスを実装する手法を紹介します。
ScriptableObjectは一般的にはデータを効率よく格納するために使われます。
今回はシステム系クラスの実装に使ってみます。
※ システムはいろんなクラスから呼び出される共通的なプログラムのことを表しています。サブシステムや基盤と呼ばれることもあります。
例えばゲームでは外部リソースやログ、サウンドなどを扱うクラスなどが該当します。
この手法のメリットとデメリット
メリット
- Playモードでなくても動作する
- MonoBehaviourと違いシーンに依存せずプロジェクトのどこからでも使える
- Transformなどを持たないためMonoBehaviourより軽量
- アセットとして存在するので、他GameObjectのpublic変数(SerializeField)から参照できる
- staticクラスやシングルトンいらず
- インスペクタにツールを作ることができる
- デバッグがしやすくなる
- 開発効率化ツールを作りやすい
- システムをインターフェース化(抽象クラス化)することで柔軟なシステム構成にできる
- 実装を切り替えられる
- クラス内の分岐を減らせる
- Play中に変更した値がPlay終了後も残る
デメリット
- UpdateなどMonoBehaviourのイベントを使った処理ができない
- Play中に変更した値をPlay終了時に残さないためには工夫が必要
実装例 (シンプル版)
まずはシンプルにScriptableObjectを継承してシステム系クラスを実装するコード例を紹介します。
後半、ScriptableObjectとSerializeFieldを活用して複数の実装を簡単に切り替えるコード例を紹介します。
システム
[CreateAssetMenu]
public class SomeSystem : ScriptableObject
{
/// <summary>
/// 何らかのパラメーター
/// </summary>
[SerializeField]
private string _someString;
/// <summary>
/// 何らかのメソッド
/// </summary>
public void SomeMethod()
{
Debug.Log("SomeSystem");
}
/// <summary>
/// インスペクタのボタンからも実行できる何らかのメソッド
/// </summary>
private void Test()
{
Debug.Log("Test");
}
}
ScriptableObjectのアセットを生成すると以下のようになります。
Projectにアセットとして存在するのでプレイしなくてもインスペクタで操作できます。
パラメーター設定したり、Editor拡張でツールを作るのに便利です。
※ インスペクタにボタンを表示するEditor拡張については省略しています
システムを利用
public class SomeScene : MonoBehaviour
{
[SerializeField]
private SomeSystem _system;
void Start()
{
_system.SomeMethod();
}
}
アセットなのでシステムを使いたいクラスのSerializeFieldで参照にセットできます。
代替手段との比較
MonoBehaviour
MonoBehaviourはシーンが変わると破棄されます。シーンをまたいでデータ保持したり処理するには不向きです。
ScriptableObjectはシーンに関わらず常に存在します。
staticクラス
UnityのEditorから見えません。
アセットとして存在するのでインスペクタに表示でき、ビジュアル的に設定やツール提供を行えます。
1つのクラス(実装)に依存することになります。
MonoBehviour + DontDestroyOnLoad (シングルトン)
プレイしてはじめてHierarchyに表示されインスペクタで操作できます。
そのため編集中にインスペクタで操作することはできません。
1つのクラス(実装)に依存することになります。
Prefab
PrefabはInstantiateして使う前提なのでインスタンス化するたびにデータのコピーが発生します。
またシステム系クラスとしてPrefabを利用するには、利用側からのアクセス方法に工夫が必要です。多くは上のシングルトンになるでしょう。
Instantiateしない場合は今回の手法と近しい性質を持っています。しかし、Unity標準ではPrefabはInstantiateしてHierarchyに配置して使うものなのでInstantiateしないというのは避けた方が良いでしょう。
Hierarchyに配置(画面に出す)のが前提なので、TransformやTag、Layerを標準要素として持っています。MonoBehaviourのライフサイクルイベントも扱います。
ScriptableObjectはHierarchyに配置しない前提なのでTransformなど余分な要素はありません。
Prefabに利点があるとすると、複数のコンポーネントを持てることです。
依存性注入との比較
DIコンテナの導入が必要で大掛かりになります。
ScriptableObjectはUntyの標準的な仕組みで簡単に使えます。
実装例 (切り替え版)
1つのシステムに対して複数の実装を行い、切り替えるコード例を紹介します。
システムのインターフェースを抽象クラスとして定義し、いくつかの具象クラスを作ります。
利用するクラスのSerializeFieldで使用する具象クラスのScriptableObjectアセットを選択します。
システム (抽象クラス)
public abstract class SomeSystem : ScriptableObject
{
public abstract void SomeMethod();
}
各実装が持つべきメソッドやプロパティを定義した抽象クラスです。
interfaceにしたいところですが、インスペクタに表示するために抽象クラスを選択します。
システム (実装A)
[CreateAssetMenu]
public class SomeSystemA : SomeSystem
{
public override void SomeMethod()
{
Debug.Log("SomeSystemA");
}
}
SomeSystemの1つ目の実装
何かの機能の正式な実装を行います。
システム (実装B)
[CreateAssetMenu]
public class DebugSomeSystem : SomeSystem
{
public override void SomeMethod()
{
Debug.Log("DebugSomeSystem");
}
}
SomeSystemの2つ目の実装
SomeSystemAに対する別実装を行います。
例. デバッグコードに変更する、実行プラットフォームごとに使用ライブラリ変更する、チュートリアル用など
システムを利用
public class SomeScene : MonoBehaviour
{
[SerializeField]
private SomeSystem _system;
void Start()
{
_system.SomeMethod();
}
}
SerializeFieldで参照するアセットを切り替えて、使用するシステム実装を選択します。
SomeSystem
型で宣言しているのでSomeSystemのサブクラスのみが選択候補にリストアップされます。
大規模プロジェクトでは
上記で紹介したコード例は、小規模なプロジェクトで使いやすいものになっています。
しかし、大規模なプロジェクトになってくるとGameObjectの数が膨大になります。
そうすると各GameObject(コンポーネント)のSerializeField(public変数)に使用するシステムの参照をセットするというのは数が多すぎて現実的でなくなります。
大規模なプロジェクトでは使用する各システムの参照を保持するシステムを1つ作るのがおすすめです。
具体的にはこちらの記事で紹介しているサービスロケーターが使えます。
Unityでサービスロケーター(ServiceLocator)を活用する
サムザップの運用中タイトルではこれらを組み合わせて、デバッグしやすい大規模プログラムを構築しています。