Unityの状態管理フレームワークを作ってみた

**「Unityで状態管理するのしんどくね？？」**
ってなったので状態管理をするフレームワークを作ってみました．

# 作った経緯

最近結構Webフロントエンドの開発をメインにやっているのですが， 先日SPAJAM東北予選にて久々にUnityを使ったのですが **「状態管理しんどくない？？」** **「UIに値反映するのダルすぎなんだけど？？」** っと見事にWebフロントエンドのフレームワーク慣れしてしまった体が拒否反応を起こしてしまったので後日，状態管理フレームワークを作ってみました．

似たようなものに「[Unidux](https://github.com/mattak/Unidux)」っというReduxをUnityで実装しているものもあるのですが，個人的に他ライブラリへの依存が嫌だったの自分でも作ってみました．



# 参考にしたもの

Webフロントエンドの状態管理アーキテクチャ・フレームワークというと 「Flux」や「Redux」などがありますが，今回は「The Elm Architecture」をリスペクトして実装しました．



# 出来たもの

その名も **「[UniTEA](https://github.com/uzimaru0000/UniTEA)」** です.

![](https://github.com/uzimaru0000/UniTEA/raw/master/images/logo.png)



# 使い方

おそらく一番簡単なカウンターアプリの実装を見ながら使い方を紹介していきます．

## 0. インストール方法
Unity Package Manager を利用しているのでプロジェクトルート以下にある `Packages/manifest.json` に

```json
{
  "dependencies" : {
    ...
    "com.uzimaru0000.unitea": "https://github.com/uzimaru0000/UniTEA.git",
    ...
  }
}
``` 

と追記してエディタに行くとインストールされます．

## 1. Modelとなる構造体を作る

アプリケーションの状態となるModelを定義します．
今回はカウンターアプリなので以下のようなものでいいでしょう

```csharp
public struct Model
{
  public int count;
}
```

注意してほしいのは構造体で定義してください．

## 2. Modelの変更を伝えるMessageを作る

Modelの変更をフレームワーク側に伝えるMessageを定義します．
今回は

- カウントアップ
- カウントダウン

の２つでいいと思います．

```csharp
public enum Msg
{
	Increase,
	Decrease
}
```

enumで定義してください．

## 3. Messageをラップしたclassを作る

Messageをラップしたclassを作成します．
このとき，このclassには `IMessenger<Msg>` というインターフェースを実装してください．
`Msg GetMessage()` というメソッドを実装すれば大丈夫です．返り値には`２`で定義したMessageを返してください．

```csharp
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`という値を使います．

```csharp
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)` というメソッドを実装します．

```csharp
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ライブラリを使う等で参照出来るようにしてください．

```csharp
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メソッドを公開するだけです．

```csharp
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してインスペクターから依存のあるものを注入していきます．


これで以下のような動作をするカウンターができます！！

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Counterの実装できた！ <a href="https://t.co/z5AMiEbE61">pic.twitter.com/z5AMiEbE61</a></p>&mdash; うじまる🐣 (@uzimaru0000) <a href="https://twitter.com/uzimaru0000/status/1145904117224636422?ref_src=twsrc%5Etfw">July 2, 2019</a></blockquote>

# 詳しい説明

## Messageをラップする理由

Messageに値を含ませるためです．
例えばInputFieldに入力した値をMessageに含ませるとき，enumをMessageに使うと値をUpdateに伝えることができません．
なので以下のようなコードでMessageをラップしたクラスに値を含ませてあげます．

```csharp
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>` というクラスがあるので

```csharp
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>>`を渡してあげます．



他にも質問があったらコメントでお願いします :pray:



# 最後に

最初の方でも述べましたがこのライブラリ自体は他のライブラリへの依存はないです．ですが，CmdにTaskを渡すために「[UniTask](https://github.com/Cysharp/UniTask)」を使ったり，Dispatcherの部分に「[UniRx](https://github.com/neuecc/UniRx)」をつかったり，DIをするために「[Zenject](https://github.com/modesttree/Zenject)」を使うのはいいと思います．むしろそのほうがいいかもしれないです．

また，コアの部分の開発にそんなに時間がかかってないので「ここをもっとこうしたらいいと思う」「この部分はなんでこうなの？」っと言ったissue, PullRequest お待ちしてます！！

https://github.com/uzimaru0000/UniTEA