Synamon’s Engineer blog

Synamonはリアルとデジタルの融合を加速させるため、メタバース領域で法人向けにサービス提供を行うテックカンパニーです。現在開発を進めている「メタバース総合プラットフォーム」をはじめ、メタバース市場の発展に向けた事業展開を行っています。このブログでは、メタバース技術とその周辺の技術、開発全般に関してエンジニアがお話しします。

GUIにおける状態管理の考え方について

はじめに

エンジニアの松原です。業務以前に個人開発でUnityを触っていたのですが、まずはサードパーティ製ライブラリを取り込んでから開発がスタートするのが多かったと思います。(分かりやすい例として、Oculus IntegrationUltraleap Plugin For Unityを真っ先に入れていた気がしています)

当時の個人開発のアプリではあまりアプリケーションの設計を考えておらず、UnityのScene間でデータをやり取りすることが少なく、何かしらの処理は理由もなく多数のクラスや関数に依存していました。 業務でUnityを使った開発をするようになってからは、サーバーサイドのAPIと連携するようになったため、個人開発でもある程度設計を意識するようになりました。

今回は設計のうち、GUIにおける状態管理に対する考えのパターンについて記事にしてみました。(Unityの話だけでなく、Webフロントエンド開発で使われているトピックも入ってきます)Unityで実際にコードを書くことに関してはまた別の機会で行いたいと思います。

今回の記事の注意として、UnityのAnimator Controllerなどで登場するStateはFSM(Finite State Machine)と呼ばれるステートマシンで、同じ状態を表していますが、扱うトピックが異なるため、今回は登場しません。

なぜGUIに状態管理が必要なのか

Unityのみならず、GUIを扱う場合、以下の課題に遭遇することが多いと思います。

  • ステートレスなサーバーサイドAPIとやり取りした結果をGUIに反映したい
  • 複数のUIのコンポーネントやクラス間で共有したいプロパティがある
  • 画面の切り替え(またはUnityのシーン)の切り替え時に変更したプロパティを維持したい

これらの課題を応急処置的なコードを使って開発をすすめていくと、複雑な依存関係が増えてきます。依存関係が増えるということは状態の扱いが難しくなり、設計の変更も容易でなくなってきます。

そこで状態を管理しているオブジェクトをいくつかの設計パターンに切り出して考えてみます。

状態管理を外部オブジェクトとして設置する(Storeパターン)

共有化したい状態をStoreというグローバルなオブジェクトを利用して管理する方法を紹介します。 こちらのパターンはWebフロントエンド開発のJavaScriptフレームワークであるVueの公式ページで紹介されている状態管理のパターンを参考にしたものになります。

jp.vuejs.org

Vue公式の方ではObserverの記述は省略されているのですが、状態管理にリアクティブなプロパティが利用できる、もしくはStateを監視する仕組みをVueでは暗黙的に利用することが多く、JavaScriptフレームワーク以外で一般的に説明するためObserverを書き加えています。

f:id:fb8r5jymw6fd:20220124005617j:plain:h500

上の図に追加して、State書き換え時の処理ギミックを加えたものが以下の図になります。ここのギミックに関しては実装方針でも変わってきますが、最低限イメージしやすい内容にしています。

初期値の設定まわりは省略していますが、Stateの更新サイクルとしては以下のようになります。

  1. GUIパーツの操作(ボタン押下、スライドの値変更時など)に紐づいたトリガーが発火時、そのトリガーに対応する処理が呼び出される
  2. 処理が実行され、Stateの変更がある場合はStateの変更処理を行う命令を呼び出す
  3. Stateの変更命令を受け取り、保持しているStateを書き換える
  4. Stateの書き換えを検知する
  5. 変更されたStateに依存しているPresenterやViewに対して変更を通知する

f:id:fb8r5jymw6fd:20220124173631j:plain:h500

上記で登場したPresenterやViewは MVC / MVP パターンで登場する概念ですが、Unityで分かりやすい例えを考えてみます。
UnityではuGUIを利用してGUIを組むことが一般的だとは思いますが、Store内でStateが更新されても、画面上に反映するためにはGUIコンポーネントの表示部をスクリプトから更新する必要があります。

下図のTextコンポーネントも同様で、このコンポーネントのTextフィールドに対して直接更新処理をかけないと画面上に変更が反映されません。このように、特定のStateに依存しており、Stateの変更に伴ってState変更処理以外の手続きを行う必要がある部分の実装をViewと捉えると分かりやすいかと思います。このStateの書き換え後のギミックを仲介する役割がObserverとなります。

f:id:fb8r5jymw6fd:20220124175051j:plain:h500

状態管理を行うモダンなアーキテクチャを考える(Fluxパターン)

上記のように状態管理をしつつ、処理の依存関係を極力減らすために、処理の構造を単方向にするFluxというコンセプト(またはアーキテクチャ)があります。 現在はFacebookのGitHubにアーカイブとして残っています。このコンセプトを基に実装・派生したReduxMobXを使うことをFacebookは推奨しています。

github.com

Fluxのコンセプトの実装例として以下のようにまとめることができます。先ほど紹介したStoreパターンではStateの書き換えは外部から参照、命令を呼び出すことで直接書き換えができていましたが、こちらは役割をさらに細分化して処理を単方向に扱えるようにしています。 それぞれ以下の役割を持っています。

  • Action - 具体的な処理を記述するためのパターンで、処理単位のテンプレートとして定義されている。成功や失敗などの状態変更の起点を持つことができる
  • Action Creator - Actionを作成する責務を持つ
  • Dispatcher - Action Creatorから作られたActionをReducerに送る責務を持つ(実装によっては省略されることもある)
  • Reducer - Actionを実際に実行し、Stateを書き換える責務を持つ。StateはReducerからでしか書き換えができない
  • State - 保持したい情報の単位または入れ物
  • Watcher(Observer) - Stateの更新を監視し、依存しているコンポーネントに更新を通知する

f:id:fb8r5jymw6fd:20220124192212j:plain:h500

Fluxの実装によっては定義されている言葉が違うことがありますが、かねがねそれぞれの役割としては上記のようになっていることが多いと思います。 Fluxではより役割を厳格に定めることによって、双方向の依存関係を無くすように設計されたアーキテクチャであるといえると思います。

今回のまとめ

今回はWebフロントエンドで登場する状態管理の考え方をJavaScriptフレームワークで利用されている状態管理のパターンを取り上げました。

これらを実際にUnityで扱うにはUniduxなどがありますが、こちらは画面遷移も取り扱っているため、今回はGUIでの状態管理について解説したかったので、また別の機会に紹介したいと思います。 github.com

また、StoreパターンやFluxパターンは MVC / MVP や MVVM のようにアプリケーションの設計に深く根付いているというより、状態管理という責務分担が難しいものをうまくやりくりするためのコンセプトのようなので、派生形のReduxやMobXもどこかの機会で追ってみたいと思います。