目次
同じプロジェクトの場合
前提
簡単なサンプルとしてHPをゲージと数値で表示する実装を切り替える。
実装
まずはinterfaceを用意する。
publicinterface IPlayerStatusViewLogic { void Initialize(); void SetHealth(intvalue); }
ゲージの実装
publicclassPlayerStatusViewLogic : MonoBehaviour, IPlayerStatusViewLogic { [SerializeField] private Slider _slider; publicvoid Initialize() { // 何か初期化 } publicvoid SetHealth(intvalue) { DOTween.To( () => _slider.value, x => _slider.value= x, (float)value/ _state.MaxHp, duration:1.0f); } }
Hierarchyは以下のように構成してある。
Player
にはPlayerContainer
とPlayerStatusViewLogic
を追加してある。
publicclassPlayerContainer : MonoBehaviour { [SerializeField] PlayerController _controller; [SerializeField] PlayerViewLogic _playerViewLogic; private IPlayerStatusViewLogic _playerStatusViewLogic; publicvoid Initialize() { _playerStatusViewLogic = GetComponent<IPlayerStatusViewLogic>(); _playerStatusViewLogic.Initialize(); } }
PlayerStatusViewLogicをinterfaceで差し替えるため、Inspectorからは設定できない。
Component自体はGameObjectに設定しておき、GetComponent<interface>
でScriptから取得するようにする。
数値の実装
publicclassPlayerStatusFloatingViewLogic : MonoBehaviour, IPlayerStatusViewLogic { [SerializeField] private TextMeshPro _hp; publicvoid Initialize(){ } publicvoid SetHealth(intvalue) { DOTween.To( () => _state.Hp, x => _state.Hp = x, value, duration:0.5f) .OnUpdate(() => _hp.text =$"{_state.Hp}/{_state.MaxHp}"); } }
Hierarchyは以下のように構成してある。
Player
にはPlayerContainer
とPlayerStatusFloatingViewLogic
を追加してある。
ゲージの時との違いは、別の子クラス(コンポーネント)を用意して追加するコンポーネントを差し替えただけだ。
実際に使う際はPrefab化して、Sceneに配置する時はHierarchyに追加して中身を調整するか、別途Prefab Variantとして保存しておくか、などする。
別シーン(プロジェクト)の場合
前提
以下の記事のプロジェクト構成を利用する。
サンプルとしてボタンを変えクリックイベントの実装を差し替える。
実装
interfaceは以下の通り。 なおボタンにはMRTK、RxにはR3を利用している。
publicinterface ICubeUIViewLogic { Observable<Unit> OnButtonClicked { get; } }
1つ目の実装。
publicclassCubeUIViewLogic : 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に設定されるボタンがそれぞれで違うだけである。
publicclassHLCubeUIViewLogic : 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
する。
publicclassCubeViewLogic : MonoBehaviour { private ICubeUIViewLogic cubeUI; privatevoid 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を設定して読み込まれるようにする。
publicstaticclassViewConcreteRegister { publicstatic 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でもあり、色々な側面を持っている。