「Unityで状態管理するのしんどくね??」
ってなったので状態管理をするフレームワークを作ってみました.
作った経緯
最近結構Webフロントエンドの開発をメインにやっているのですが, 先日SPAJAM東北予選にて久々にUnityを使ったのですが 「状態管理しんどくない??」 「UIに値反映するのダルすぎなんだけど??」 っと見事にWebフロントエンドのフレームワーク慣れしてしまった体が拒否反応を起こしてしまったので後日,状態管理フレームワークを作ってみました.
似たようなものに「Unidux」っというReduxをUnityで実装しているものもあるのですが,個人的に他ライブラリへの依存が嫌だったの自分でも作ってみました.
参考にしたもの
Webフロントエンドの状態管理アーキテクチャ・フレームワークというと 「Flux」や「Redux」などがありますが,今回は「The Elm Architecture」をリスペクトして実装しました.
出来たもの
その名も 「UniTEA」 です.
使い方
おそらく一番簡単なカウンターアプリの実装を見ながら使い方を紹介していきます.
0. インストール方法
Unity Package Manager を利用しているのでプロジェクトルート以下にある Packages/manifest.json
に
{
"dependencies" : {
...
"com.uzimaru0000.unitea": "https://github.com/uzimaru0000/UniTEA.git",
...
}
}
と追記してエディタに行くとインストールされます.
1. Modelとなる構造体を作る
アプリケーションの状態となるModelを定義します.
今回はカウンターアプリなので以下のようなものでいいでしょう
public struct Model
{
public int count;
}
注意してほしいのは構造体で定義してください.
2. Modelの変更を伝えるMessageを作る
Modelの変更をフレームワーク側に伝えるMessageを定義します.
今回は
- カウントアップ
- カウントダウン
の2つでいいと思います.
public enum Msg
{
Increase,
Decrease
}
enumで定義してください.
3. Messageをラップしたclassを作る
Messageをラップしたclassを作成します.
このとき,このclassには IMessenger<Msg>
というインターフェースを実装してください.
Msg GetMessage()
というメソッドを実装すれば大丈夫です.返り値には2
で定義したMessageを返してください.
using UniTEA;
public class IncreaseMsg : IMessenger<Msg>
{
public Msg GetMessage() => Msg.Increase;
}
public class DecreaseMsg : IMessenger<Msg>
{
public Msg GetMessage() => Msg.Decrease;
}
4. Updaterを作る
ModelをUpdateするためのメソッドを定義したclassを作成します.
IUpdater<Model, Msg>
というインターフェースを実装してください.
このインターフェイスは (Model, Cmd<Msg>) Update(IMessenger<Msg> msg, Model model)
というメソッドを定義します.C# 7から実装された Tuple
です. Cmd<Msg>
は非同期処理を表す型です.今回は Cmd<Msg>.none
という値を使います.
using UniTEA;
public class Updater : IUpdater<Model, Msg>
{
public (Model, Cmd<Msg>) Update(IMessenger<Msg> msg, Model model)
{
switch(msg)
{
case IncreaseMsg _:
return (new Model
{
count = model.count + 1
}, Cmd<Msg>.none);
case DecreaseMsg _:
return (new Model
{
count = model.count - 1
}, Cmd<Msg>.none);
default:
return (model, Cmd<Msg>.none);
}
}
}
状態が変わるときは新しいModelを生成しましょう.
5. Viewを更新するRendererを作る
ModelからView(UIなど)の見た目を変更するclassを作ります.
IRenderer<Model>
というインターフェースを実装してください.
このインターフェイスは, void Render(Model model)
というメソッドを実装します.
using UnityEngine;
using UnityEngine.UI;
using UniTEA;
public class Renderer : MonoBehaviour, IRenderer<Model>
{
[SerializeField]
Text display;
public void Render(Model model)
{
display.text = model.count.ToString();
}
}
今回はTextコンポーネント
にModel
のcount
の値を入れているだけですね.
6. イベントを発火されるためのclassを作る
イベント発火用のDispatcherクラスを作ります. このclass自体は特に実装するインターフェイスは無いので任意のMonoBehaviourクラス等でイベントを設定したり,publicメソッドにしてuGUIのインスペクタから設定してもいいです.また,あとで出てくる UniTEA
クラスを管理するクラスをインスペクタから設定する・シングルトンにする・DIライブラリを使う等で参照出来るようにしてください.
using UnityEngine;
using UnityEngine.UI;
using UniTEA;
public class Dispatcher : MonoBehaviour
{
[SerializeField]
Manager manager;
[SerializeField]
Button increaseButton;
[SerializeField]
Button decreaseButton;
void Start()
{
increaseButton.onClick.AddListener(() => manager.Dispatch(new IncreaseMsg()));
decreaseButton.onClick.AddListener(() => manager.Dispatch(new DecreaseMsg()));
}
}
7. UniTEAクラスのマネージャークラスを作る
フレームワークのCoreであるUniTEA
クラスを管理するマネージャークラスを作ります.
管理と言っても初期化とDispatchメソッドを公開するだけです.
using UniTEA;
using UnityEngine;
public class Manager : MonoBehaviour
{
UniTEA<Model, Msg> _instance;
UniTEA<Model, Msg> instance
{
get {
if (_instance == null)
{
_instance = new UniTEA<Model, Msg>(
() => (new Model(), Cmd<Msg>.none),
new Updater(),
renderer);
}
return _instance;
}
}
[SerializeField]
new Renderer renderer;
public void Dispatch(IMessenger<Msg> msg)
{
instance.Dispatch(msg);
}
}
8. Unityのオブジェクトにattachする
Renderer
, Dispatcher
, Manager
を任意のGameObjectにattachしてインスペクターから依存のあるものを注入していきます.
これで以下のような動作をするカウンターができます!!
Counterの実装できた! pic.twitter.com/z5AMiEbE61
— うじまる🐣 (@uzimaru0000) July 2, 2019
詳しい説明
Messageをラップする理由
Messageに値を含ませるためです.
例えばInputFieldに入力した値をMessageに含ませるとき,enumをMessageに使うと値をUpdateに伝えることができません.
なので以下のようなコードでMessageをラップしたクラスに値を含ませてあげます.
public enum Msg
{
Input
}
public class InputMsg : IMessenger<Msg>
{
public string value;
public Msg GetMessage() => Msg.Input;
public InputMsg(string value)
{
this.value = value;
}
}
これでInputMsgに入力された値を含ませることができます.
ただ,値を含めるためのMessageを作る際に上のようなコードを書かなければいけないので UniTEA.Utils
namespaceの中に OneValueMessage<T, U>
というクラスがあるので
public class InputMsg : OnValueMessage<Msg, string>
{
public override GetMessage() => Msg.Input;
public InputMsg(string value): base(value) {}
}
っとすれば最初のコードと同じになります.
Cmd<Msg> って?
Cmd<Msg>
型は非同期処理を表す型です.Task型をラップしている型になります.
作成するにはコンストラクタに Task<IMessenger<Msg>>
を渡してあげます.
他にも質問があったらコメントでお願いします
最後に
最初の方でも述べましたがこのライブラリ自体は他のライブラリへの依存はないです.ですが,CmdにTaskを渡すために「UniTask」を使ったり,Dispatcherの部分に「UniRx」をつかったり,DIをするために「Zenject」を使うのはいいと思います.むしろそのほうがいいかもしれないです.
また,コアの部分の開発にそんなに時間がかかってないので「ここをもっとこうしたらいいと思う」「この部分はなんでこうなの?」っと言ったissue, PullRequest お待ちしてます!!