yotiky Tech Blog

とあるエンジニアの備忘録

Unity - View に interface を使って実装を差し替える

目次

同じプロジェクトの場合

前提

簡単なサンプルとしてHPをゲージと数値で表示する実装を切り替える。

実装

まずはinterfaceを用意する。

    public interface IPlayerStatusViewLogic
    {
        void Initialize();
        void SetHealth(int value);
    }

ゲージの実装

    public class PlayerStatusViewLogic : MonoBehaviour, IPlayerStatusViewLogic
    {
        [SerializeField] private Slider _slider;

        public void Initialize()
        {
            // 何か初期化
        }

        public void SetHealth(int value)
        {
            DOTween.To(
                () => _slider.value,
                x => _slider.value = x,
                (float)value / _state.MaxHp,
                duration: 1.0f);
        }
    }

Hierarchyは以下のように構成してある。

PlayerにはPlayerContainerPlayerStatusViewLogicを追加してある。

public class PlayerContainer : MonoBehaviour
{
    [SerializeField] PlayerController _controller;
    [SerializeField] PlayerViewLogic _playerViewLogic;
    private IPlayerStatusViewLogic _playerStatusViewLogic;

    public void Initialize()
    {
        _playerStatusViewLogic = GetComponent<IPlayerStatusViewLogic>();
        _playerStatusViewLogic.Initialize();
    }
}

PlayerStatusViewLogicをinterfaceで差し替えるため、Inspectorからは設定できない。 Component自体はGameObjectに設定しておき、GetComponent<interface>でScriptから取得するようにする。

数値の実装

    public class PlayerStatusFloatingViewLogic : MonoBehaviour, IPlayerStatusViewLogic
    {
        [SerializeField] private TextMeshPro _hp;

        public void Initialize(){ }

        public void SetHealth(int value)
        {
            DOTween.To(
                () => _state.Hp,
                x => _state.Hp = x,
                value,
                duration: 0.5f)
            .OnUpdate(() => _hp.text = $"{_state.Hp}/{_state.MaxHp}");
        }
    }

Hierarchyは以下のように構成してある。

PlayerにはPlayerContainerPlayerStatusFloatingViewLogicを追加してある。

ゲージの時との違いは、別の子クラス(コンポーネント)を用意して追加するコンポーネントを差し替えただけだ。

実際に使う際はPrefab化して、Sceneに配置する時はHierarchyに追加して中身を調整するか、別途Prefab Variantとして保存しておくか、などする。

別シーン(プロジェクト)の場合

前提

以下の記事のプロジェクト構成を利用する。

サンプルとしてボタンを変えクリックイベントの実装を差し替える。

実装

interfaceは以下の通り。 なおボタンにはMRTK、RxにはR3を利用している。

public interface ICubeUIViewLogic
{
    Observable<Unit> OnButtonClicked { get; }
}

1つ目の実装。

public class CubeUIViewLogic : MonoBehaviour, ICubeUIViewLogic
{
    [SerializeField] private PressableButton button;

    private Subject<Unit> onButtonClicked = new Subject<Unit>();
    public Observable<Unit> OnButtonClicked => onButtonClicked;

    void Start()
    {
        button.OnClicked.AddListener(() => onButtonClicked.OnNext(Unit.Default));
    }
}

2つ目の実装。
サンプルの仕様上中身は全く同じで、PressableButtonに設定されるボタンがそれぞれで違うだけである。

public class HLCubeUIViewLogic : MonoBehaviour, ICubeUIViewLogic
{
    [SerializeField] private PressableButton button;

    private Subject<Unit> onButtonClicked = new Subject<Unit>();
    public Observable<Unit> OnButtonClicked => onButtonClicked;

    void Start()
    {
        button.OnClicked.AddListener(() => onButtonClicked.OnNext(Unit.Default));
    }
}

1つ目の実装は、Contentsプロジェクト内でSceneに直接配置する。
Hierarchyは以下のように構成してあり、UIにCubeUIViewLogic、親のCubeにCubeViewLogicを追加している。UIはPrefab化してある。

バイス側で2つ目の実装をしており、デバイス側のSceneから差し替えを指示する必要がある。 指示がない時はContentsのUIを、指示がある時はContentsのUIを破棄して指示されたものをInstantiateする。

public class CubeViewLogic : MonoBehaviour
{
    private ICubeUIViewLogic cubeUI;

    private void Start()
    {
        cubeUI = GetComponentInChildren<ICubeUIViewLogic>();

        var cubeUIPrefab = ViewConcreteRegister.CubeUI;
        if (cubeUIPrefab != null)
        {
            Destroy(((MonoBehaviour)cubeUI).gameObject);
            cubeUI = Instantiate(cubeUIPrefab, transform).GetComponent<ICubeUIViewLogic>();
        }

        cubeUI.OnButtonClicked
            .Subscribe(_ =>
            {
                Debug.Log("Clicked");
            })
            .RegisterTo(destroyCancellationToken);
    }
}

Scene間の値渡しはstaticクラスなSingletonにしてある。 ContentsのSceneを読み込む前にPrefabのGameObjectを設定して読み込まれるようにする。

public static class ViewConcreteRegister
{
    public static GameObject CubeUI { get; set; }
}
    ViewConcreteRegister.CubeUI = cubeUIPrefab;
    SceneManager.LoadSceneAsync("ContentsScene", LoadSceneMode.Additive);

バイス側では、Prefab Variantを作って変更を加えてある。Prefab Variantを使うと、Hierarchy上になくても変更を加えられる上、元のPrefabの更新も反映される。加えた変更の差分も確認できる。極力変更を少なくする場合には有効だろう。

実行すると、ViewConcreteRegisterにUIのPrefabが登録されていればそちらを、登録されていなければデフォルトのUIを使ってくれる。

補足

タイトルはViewになっているがViewだけとは限らない。 UnityはUI、Input、Graphics、Sound、Physics、Device IOなど、Clean Architecutreで言う最も外側の円の大部分を担っている。そのため、ComponentはViewでもあり、View以外からのInputでもあり、View以外へのOutputでもあり、色々な側面を持っている。