【Unity】SerializeReference、Inspectorウィンドウでinterfaceを使用する
Unity 2019.3で利用可能になった SerializeReference
Attributeについて紹介します。
SerializeReference
SerializeReferenceで任意のInterfaceを継承したクラスや構造体をコンポーネントに格納し、SceneやPrefabにシリアライズしたり、Inspectorで編集したりします。
正しくは、値としてではなく参照型としてC#をシリアライズ出来るようになります。これによりinterfaceをスロットとして任意のクラスを格納することが可能になるみたいです。
interfaceをシリアライズ
まずはinterfaceとクラスを定義します。interfaceはアクションを行う役割を持っており、処理の進行待機としてコルーチンを使用する予定です。
public interface ITankAction { IEnumerator Process(Transform transform); }
処理の中身を実装します。前方(上)に移動する処理で、publicとして処理時間と移動距離が含まれます。
// 前方に移動するアクション [Serializable] public class MoveForward : ITankAction { public float time = 1; // 処理時間 public float distance = 1; // 移動距離 IEnumerator ITankAction.Process(Transform transform) { float endTime = Time.timeSinceLevelLoad + time; Vector3 startPos = transform.position; Vector3 endPos = startPos + transform.up * distance; while (endTime > Time.timeSinceLevelLoad) { var diff = (endTime - Time.timeSinceLevelLoad) / time; transform.position = Vector3.Lerp(endPos, startPos, diff); yield return null; } transform.position = endPos; } }
これをコンポーネントで使用してみます。コンポーネントにITankAction tankAction
と定義し[SerializeReference]
Attributeを追加します。またtankActionにはMoveRotateクラスをnew
しておきます。MoveForward
クラスはUnityEngine.Object
もMonoBehaviour
も継承していないので、そのままnew
で構築が可能です。これでTankController
にはITankAction
を継承した何らかのクラスを追加し、それをInspectorで編集出来るようになりました。
public class TankController : MonoBehaviour { [SerializeReference] ITankAction tankAction = new MoveRotate(); IEnumerator Start() { yield return StartCoroutine( tankAction.Process(transform)); } }
複数のアクションがある場合にアクションを差し替えられるようにする
この ITankAction tankAction = new MoveRotate();
ですがシリアライズされてしまうのでコードの変更を無視されるようになります。もし異なる型を注入したい場合、拡張を実装する必要があります。下は複数のアクションがある場合の例です。
まず、移動・回転・待機のアクションをつけてみました。
[Serializable] public class MoveForward : ITankAction { public float time = 1; // 処理時間 public float distance = 1; // 移動距離 IEnumerator ITankAction.Process(Transform transform) { float endTime = Time.timeSinceLevelLoad + time; Vector3 startPos = transform.position; Vector3 endPos = startPos + transform.up * distance; while (endTime > Time.timeSinceLevelLoad) { transform.position = Vector3.Lerp(endPos, startPos, (endTime - Time.timeSinceLevelLoad) / time); yield return null; } transform.position = endPos; } } [Serializable] public class MoveRotate : ITankAction { public float time = 1; public float angle = 90; IEnumerator ITankAction.Process(Transform transform) { float endTime = Time.timeSinceLevelLoad + time; Quaternion startRot = transform.rotation; Quaternion endRot = startRot * Quaternion.AngleAxis(angle, -Vector3.forward); while (endTime > Time.timeSinceLevelLoad) { var diff = (endTime - Time.timeSinceLevelLoad) / time; transform.rotation = Quaternion.Lerp(endRot, startRot, diff); yield return null; } transform.rotation = endRot; } } [Serializable] public class WaitTank : ITankAction { public float sec = 1; public IEnumerator Process(Transform transform) { yield return new WaitForSeconds(sec); } }
次に使用する側です。こちらは拡張コードを実装しておきます。アクションの注入は「ContextMenu」で行っています。アクションを切り替えるとTankControllerのアクションが利用できるパラメーターが変化していることを確認出来ます。
public class TankController : MonoBehaviour { [SerializeReference] ITankAction tankAction = new MoveRotate(); IEnumerator Start() { yield return StartCoroutine( tankAction.Process(transform)); } [ContextMenu("Act/Move")] void AddMove() => tankAction = new MoveForward(); [ContextMenu("Act/Rotate")] void AddRotate() => tankAction = new MoveRotate(); [ContextMenu("Act/Wait")] void AddWait() => tankAction = new WaitTank(); }
アクションを複数登録する
最後にアクションを複数登録できる形にしてみます。アクションの中身は同じですが、TankControllerを少し修正して規定の動きを行うようにする感じです。やっていることはITankAction
をリストに変更しているだけです。これで同じインターフェースを継承したクラスを複数登録することが出来ます。
public class TankController : MonoBehaviour { [SerializeReference] List<ITankAction> tankActions = new List<ITankAction>(); IEnumerator Start() { foreach( var act in tankActions) yield return StartCoroutine(act.Process(transform)); } [ContextMenu("Act/Move")] void AddMove() => tankActions.Add(new MoveForward()); [ContextMenu("Act/Rotate")] void AddRotate() => tankActions.Add(new MoveRotate()); [ContextMenu("Act/Wait")] void AddWait() => tankActions.Add(new WaitTank()); }
感想
正直、任意のinterfaceを継承したMonoBehaviourもしくはScriptableObjectを登録出来るオブジェクトフィールドをInspectorに表示する機能と思ってました。
今まではアクションを行う処理をSceneもしくはprefabにシリアライズする場合、UnityEngine.Objectを継承しなければいけなかった(つまり複数の継承が出来ない)のが、インターフェース単位デシリアライズ出来るようになったので楽出来る所もあるかなという印象です。ほしかった機能とは違いますが、まぁコレはこれで。