テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

【Unity】SerializeReference、Inspectorウィンドウでinterfaceを使用する

f:id:tsubaki_t1:20191031221044j:plain

 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.ObjectMonoBehaviourも継承していないので、そのままnew で構築が可能です。これでTankControllerにはITankActionを継承した何らかのクラスを追加し、それをInspectorで編集出来るようになりました。

public class TankController : MonoBehaviour
{
    [SerializeReference]
    ITankAction tankAction = new MoveRotate();

    IEnumerator Start()
    {
        yield return StartCoroutine( tankAction.Process(transform));
    }
}

f:id:tsubaki_t1:20191031222058j:plain
登録したクラスの中身を編集できる

複数のアクションがある場合にアクションを差し替えられるようにする

 この 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();
}

f:id:tsubaki_t1:20191031223523g:plain
モードを切り替わる度に、アクションに使用するプロパティが切り替わる

アクションを複数登録する

 最後にアクションを複数登録できる形にしてみます。アクションの中身は同じですが、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());
}

f:id:tsubaki_t1:20191031224701g:plain
アクションを登録する作業風景

f:id:tsubaki_t1:20191031224826g:plain
アクションでタンクを動かす

感想

 正直、任意のinterfaceを継承したMonoBehaviourもしくはScriptableObjectを登録出来るオブジェクトフィールドをInspectorに表示する機能と思ってました。

 今まではアクションを行う処理をSceneもしくはprefabにシリアライズする場合、UnityEngine.Objectを継承しなければいけなかった(つまり複数の継承が出来ない)のが、インターフェース単位デシリアライズ出来るようになったので楽出来る所もあるかなという印象です。ほしかった機能とは違いますが、まぁコレはこれで。

関連

qiita.com