[Unity] ノードベースでUnityAPIを制御するライブラリUniFlowを触ってみる

この記事は [Unity Advent Calendar 2019](https://qiita.com/advent-calendar/2019/unity) 19日目の記事です。

* 前日: @Nitudonさん: [VFX GraphにおけるVFX Property Binderの活用 - Qiita](https://qiita.com/Nitudon/items/c28cb45bc4c1ddfbe8e6)
* 翌日: @Yuzu_Unityさん: [UnityのAnimation・AnimatorControllerについて解説 - Qiita](https://qiita.com/Yuzu_Unity/items/0e6831c96eae782a0e58)

# 環境
* Unity2019.2.15f1
* UniFlow 0.1.0 - preview40

# 📝UniFlowについて
UniFlowは @monry さんの作ったOSSライブラリです。
ノードベースでUnityAPIを順番に実行したり、実行した処理を待ち受けたりすることが可能です。

* [monry/UniFlow: Connect presentation events](https://github.com/monry/UniFlow)
* [Visual Programming Framework for Unity – UniFlow のご紹介 – Unity Learning Materials](https://learning.unity3d.jp/4069/)

previewではありますが [株式会社キッズスター](https://www.kidsstar.co.jp/) の開発する [ごっこランド](https://app.kidsstar.co.jp/code0007) では一部のパビリオン(ミニゲーム) の開発にて既に採択されています。

ノードベースEditorの実装には [GraphView](https://docs.unity3d.com/ScriptReference/Experimental.GraphView.GraphView.html) が採択されています。
GraphViewについては `Unity #2 Advent Calendar 2019` にて分かりやすく入門向きな記事が投稿されていたので共有します。

* [GraphView完全理解した(2019年末版) - Qiita](https://qiita.com/ma_sh/items/7627a6151e849f5a0ede)

# 🖥UniFlowのインストール
## scopedRegistriesの追加
UniFlowは [Unofficial Unity Package Manager Registry](https://upm-packages.dev/) にて提供されています。
こちらは @monry さんが立てた UnityPackageManager からアクセス可能なパッケージレジストリです。
詳細: [Unofficial Unity Package Manager Registry を建ててみました - もんりぃ is undefined.](https://monry.hatenablog.com/entry/2019/06/06/204840) 

PackageManagerから非公式なパッケージをインストールするためには、レジストリを登録する必要があります。


### 方法1: manifest.json に直接記載する

[Unity - Manual: Scoped package registries](https://docs.unity3d.com/Manual/upm-scoped.html)
上記のUnityのマニュアルを元に、独自のパッケージレジストリをPackageManager上から参照できるようにします。

具体的には `./Packages/manifest.json` に以下の `scopedRegistries` の項目を追記します。
`scopes` には UniFlow が提供されている `dev.monry` だけでなく UniFlow が依存しているライブラリ群を参照するためのスコープを追記します。

```json
{
  "scopedRegistries": [
    {
      "name": "Unofficial Unity Package Manager Registry",
      "url": "https://upm-packages.dev",
      "scopes": [
        "com.unity.simpleanimation",
        "com.stevevermeulen",
        "jp.cysharp",
        "dev.monry",
        "dev.upm-packages"
      ]
    }
  ],
  "dependencies": {
     ...
  }
}
```


### 方法2: upmコマンドを導入する
Jsonを直接手で触りたくない方は @monry さんが作成した [upm-cli]() を導入してコマンドライン経由で `scopedRegistries` を追加できるようにしましょう (ここでは割愛)
詳細: [Unity Package Manager をコマンドラインから操作するための upm コマンドを作ってみました - もんりぃ is undefined.](https://monry.hatenablog.com/entry/2019/08/29/140308)

## PackageManagerからUniFlowをインストール
`Window > PackageManager` で PackageMnagaer を開きます。
現時点ではUniFlowは preview 状態のPackageなため、デフォルトの設定ではPackage一覧に表示されません。
`Advanced > Show preview package` を有効にする必要があります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/451643cf-6372-4d95-e972-232e74b36d6c.png" width=400>

一覧に `UniFlow` が表示されるので `Install` を選択します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/8a0c0452-3aa6-4053-2a6e-a81230cba948.png" width=600>

その際に、以下のような例外が出ることがあります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/df337432-2c92-0724-c706-6d19cda22dd1.png" width=600>

`Dependencies` に記載されているパッケージの依存関係の解決が出来なかったということなので `scopedRegistries` を見直して該当のパッケージが入るようにする必要があります。

>  com.unity.simpleanimation: Package [com.unity.simpleanimation@1.0.0] cannot be found [NotFound]

例えば上記のケースだと [SimpleAnimation](https://github.com/Unity-Technologies/SimpleAnimation) が不足しているということなので `scopes` に `com.unity.simpleanimation` を追加して [Unofficial Unity Package Manager Registry上のSimpleAnimationPackage](https://upm-packages.dev/-/web/detail/com.unity.simpleanimation) を参照できるようにする必要があります。

Packages以下にUniFlowおよび依存パッケージがインストールされました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/892b9b8a-7503-3ff6-47e1-fdaf42c4271f.png" width=600>

### 注意事項
依存ライブラリとして例えば `UniRx` や `Extenject` 等もインストールされますが、既存のプロジェクトで既に `Assets` 以下に導入されている場合、既存のパッケージを削除して `Packages` 上のパッケージに (asmdef等の) 参照を切り替えるなどの工夫が必要な点に注意。

# 🔧UniFlowの使い方
## エディタを開く
UniFlowは各種コンポーネント群およびGraphViewにて実装されたノードベースエディターにて構成されています。
UniFlowGraphは `Window > UniFlow > Open UniFlow Graph` で開くことが出来ます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/5efb79bc-745c-ec33-7bdd-35a6f0eb7d69.png)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/7388f7d7-a705-b489-055b-865995aaaa69.png" width=600>

## ノードの追加/参照
UniFlowビュー上で `右クリック > Create Node` することでノードを作成することができます。

UniFlowにおけるノードはComponentとして任意のGameObject/Prefabに紐付く形で追加されます。
Hierarchy 上のGameObject、もしくはPoject上のPrefabが選択されていれば、AddComponentされます。
Hierarchy 上のGameObject、もしくはPoject上のPrefabが選択されていない状態であれば、新しくGameObjectが生成されます。
ノードを削除するとGameObject/Prefab上からもRemoveComponentされます。

![2019-12-14 11.19.27.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/9f3017de-fb44-d2f7-18b5-3c01c56f9eeb.gif)

GameObject/Prefabを選択した状態で `UniFlowGraph` ビューの `Load` を押すとオブジェクトに紐付いたグラフを見ることができます。

![2019-12-14 11.19.27.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/85e137d8-0392-f59d-8219-f13a896c4d5d.gif)


## ノードの接続
UniFlowのノードはIn/Outの接続において逐次実行を制御します。
今回は以下の二つのノードを接続し、「ボタンをクリックしたらログを表示する」という処理にします。
* `UIBehaviourEventTrigger` : 任意のUIBehaviourにて指定した処理を実行時に通知する
* `LoggerReceiver` : Consoleにログを出す

![2019-12-14 11.28.00.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/d6fd0c5e-647a-c879-3cb3-e9bf6e7673b4.gif)

(余談ですが `UniFlowGraph`ビューにおける `AutoLayout` でノードを整列させることが可能です)
ノードを接続するとInspectorにおける `TargetComponents` に接続先のノードの情報が格納されます。
GameObjectを跨いで参照するケース等、直接 `TargetComponents` に対象のコンポーネントを指定するケースもあります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/024b9c11-5267-177f-8a3f-f4b12d96af64.png" width=400>

# 🏙UniFlowのサンプル (基本)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/25a7e995-de21-7271-09d0-9c1d3f73310b.png)

UniFlowのノード(コンポーネント)にはカテゴリがあります。
各カテゴリ毎に多数のノードが存在していますが、実例を通してその内のいくつかを紹介します。

## ボタンを押したらTimeine再生/終了を待ってScene遷移
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/4623c908-6299-cc1d-a830-c3347e03fa12.png)


* `UIBehaviourEventTrigger` (Event) : 任意のUIBehaviourにて指定した処理を実行時に通知する
* `ActivateController` (Controller) : GraohicRaycasterの有効/無効を制御する
* `PlayableController` Controller) : PlayableDirectorにおける再生/停止を制御する
* `TimelineEvent` (Event) : Timelineの再生/停止等のイベントにフックして通知する
* `LoadScene` (Controller)  : 指定した名前のシーンをロードする

`Controller` は任意のUnityAPIを実行したり、プロパティを変更したり、ふるまいを持ちます。
`Event` は任意のイベントにフックして、通知を行います。

## 衝突時にランダムに音を再生
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/355a55da-2e45-c9c4-2d17-6e9a33517986.png)

* `PhysicsCollisionEvent` (Event) : Colliderとの衝突にフックして通知する
* `RandomInt` (Logic) : 指定した範囲の整数のランダム値を生成して、次のノードに通知する
* `AudioClipListSelector` (ValueSelector) : Key(整数値)を受け取って、対応するValue(AudioClip)に変換して通知する
* `AudioController` (Controller) : 音の再生/停止を制御する

`Logic` はUnityAPIに関わらない値の発生や通知のロジック群です。
`ValueSelector` はKey-Valueを設定することができ、任意の値を受け取って対応する要素に変換して通知します。

# 🌆UniFlowのサンプル (応用)
以下、ZenjectおよびUniRxの知識が前提となります。

## ロジックに通知/内部状態を変更/ロジックから通知を受け取って処理を実行
UniFlowでは Pub/Sub の仕組みを使ったロジック側との疎通の仕組みが用意されています。

(Zenjectを利用したメッセージングの基本は以下の記事が分かりやすくまとまっていてオススメです)

* [Zenject Signals - imog](https://adarapata.hatenablog.com/entry/2018/12/13/041721)

UniFlowにおけるグラフから見ていきます。
![Monosnap 2019-12-19 00-26-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/b200aa38-7956-9df7-546c-5f45c48c4e30.png)

* `StringSignalPublisher` (SignalPublisher) : StringSignalを通知する ( `ISignalPublisher<StringSignal>` が通知を受け取る)
* `StringSignalReceiver` (SignalReceiver) : StringSignalを受け取ったら通知する
* `StringComparer` (ValueComparer) : 文字列を比較して、合致していたら次のノードに通知する

StringSignalの通知を受け取り、またを発火するロジックのサンプルは以下となります。

```csharp:GameStateInstaller.cs
using System;
using UniFlow.Signal;
using UniFlow.Utility;
using UniRx;
using Zenject;

namespace UniFlowSample
{
    public class GameStateInstaller : MonoInstaller<GameStateInstaller>
    {
        public override void InstallBindings()
        {
            // SignalBusを利用する
            SignalBusInstaller.Install(Container);

            Container.BindInterfacesTo<GameStateController>().AsCached();
        }
    }

    public class GameStateController : IGameStateController, IInitializable, IDisposable
    {
        private string CurrentStateName { get; set; }
        private ISignalReceiver<StringSignal> StringSignalReceiver { get; }
        private ISignalPublisher<StringSignal> StringSignalPublisher { get; }
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();

        public GameStateController(
            ISignalReceiver<StringSignal> stringSignalReceiver,
            ISignalPublisher<StringSignal> stringSignalPublisher)
        {
            StringSignalReceiver = stringSignalReceiver;
            StringSignalPublisher = stringSignalPublisher;
        }

        void IInitializable.Initialize()
        {
            // UniFlow側のStringSignalReceiverの通知を受け取る
            StringSignalReceiver
                .OnReceiveAsObservable()
                .Where(x => x.ComparableValue == "ChangeState")
                .Subscribe(x =>
                {
                    ((IGameStateController) this).SetState(x.Parameter.StringValue);
                    ((IGameStateController) this).NotifyChangeState(x.Parameter.StringValue);
                })
                .AddTo(Disposable);
        }

        void IDisposable.Dispose() => Disposable?.Dispose();

        void IGameStateController.SetState(string stateName) => CurrentStateName = stateName;

        void IGameStateController.NotifyChangeState(string stateName)
        {
            // 通知を行う
            // UniFlow側で "OnChangedState" な StringSignalReceiver がいる場合、そこにも通知される
            var signalParamater = new StringSignal.SignalParameter();
            signalParamater.StringValue = stateName;
            StringSignalPublisher.Publish(StringSignal.Create("OnChangedState", signalParamater));
        }
    }

    public interface IGameStateController
    {
        void SetState(string stateName);
        void NotifyChangeState(string stateName);
    }
}
```

* `ISignalReceiver<StringSignal>` および `ISignalPublisher<StringSignal>` は `UniFlowInstaller` によってBindされます。
* `UniFlowInstaller` および今回定義した `GameStateInstaller` を (Zenjectにおける) 任意の Context に指定する必要があります
* `UniFlowInstaller` は `Packages/UniFlow/Installers` 以下に ScriptableObject として存在します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/7d8e6118-14bc-4322-a66c-ccc8c2b0c387.png" width=700>

これでロジックへの通知/ロジックからの通知処理を実装することができました。

## 独自Signal/ノードの作成
先ほどは文字列ベースでどのSignalの通知かを識別しましたが、文字列比較は事故が起きやすく不安です。
回避するために、独自のSignalを用いた `SignalReceiver` / `SignalPublisher` を作って判別するアプローチが取れます。
基底クラス(Baseクラス)が用意されているので、継承してクラスを作っていきます。

```csharp:GameStateInstaller.cs
using System;
using System.Collections.Generic;
using UniFlow;
using UniFlow.Connector;
using UniFlow.Connector.ValueComparer;
using UniFlow.Signal;
using UniFlow.Utility;
using UniRx;
using UnityEngine;
using Zenject;

namespace UniFlowSample
{
    public class GameStateInstaller : MonoInstaller<GameStateInstaller>
    {
        public override void InstallBindings()
        {
            Container.BindInterfacesTo<GameStateController>().AsCached();

            SignalBusInstaller.Install(Container);
            Container.DeclareUniFlowSignal<ChangeGameStateSignal>();
            Container.DeclareUniFlowSignal<OnChangedGameStateSignal>();
        }
    }

    public class GameStateController : IGameStateController, IInitializable, IDisposable
    {
        private GameState CurrentState { get; set; }
        private ISignalReceiver<ChangeGameStateSignal> SignalReceiver { get; }
        private ISignalPublisher<OnChangedGameStateSignal> SignalPublisher { get; }
        private CompositeDisposable Disposable { get; } = new CompositeDisposable();

        public GameStateController(
            ISignalReceiver<ChangeGameStateSignal> signalReceiver,
            ISignalPublisher<OnChangedGameStateSignal> signalPublisher)
        {
            SignalReceiver = signalReceiver;
            SignalPublisher = signalPublisher;
        }

        void IInitializable.Initialize()
        {
            SignalReceiver
                .OnReceiveAsObservable()
                .Subscribe(x =>
                {
                    ((IGameStateController) this).SetState(x.ComparableValue);
                    ((IGameStateController) this).NotifyChangeState(x.ComparableValue);
                })
                .AddTo(Disposable);
        }

        void IDisposable.Dispose() => Disposable?.Dispose();

        void IGameStateController.SetState(GameState gameState) => CurrentState = gameState;
        void IGameStateController.NotifyChangeState(GameState gameState) => SignalPublisher.Publish(OnChangedGameStateSignal.Create(gameState));
    }

    public interface IGameStateController
    {
        void SetState(GameState gameState);
        void NotifyChangeState(GameState gameState);
    }

    public enum GameState
    {
        Start,
        Finish,
    }

    [Serializable]
    public class ChangeGameStateSignal : EnumSignalBase<ChangeGameStateSignal, GameState>
    {
    }

    [Serializable]
    public class OnChangedGameStateSignal : EnumSignalBase<OnChangedGameStateSignal, GameState>
    {
    }

    // GameStateSignalを通知する
    [AddComponentMenu("UniFlow/Custom/UniFlowSample/GameStateSignalPublisher", (int) ConnectorType.Custom)]
    public class GameStateSignalPublisher : SignalPublisherBase<ChangeGameStateSignal>
    {
        [SerializeField] private GameState gameState = default;
        private GameState GameState => gameState;

        protected override ChangeGameStateSignal GetSignal() =>ChangeGameStateSignal.Create(GameState);
    }

    // GameStateSignalを受け取る
    [AddComponentMenu("UniFlow/Custom/UniFlowSample/GameStateSignalReceiver", (int) ConnectorType.Custom)]
    public class GameStateSignalReceiver : SignalReceiverBase<OnChangedGameStateSignal>
    {
        public override IEnumerable<IComposableMessageAnnotation> GetMessageComposableAnnotations() =>
            new[]
            {
                ComposableMessageAnnotationFactory.Create(() => ReceivedSignal.ComparableValue, "GameState"),
            };
    }

    // GameStateを比較する
    [AddComponentMenu("UniFlow/Custom/UniFlowSample/GameStateComparer", (int) ConnectorType.Custom)]
    public class GameStateComparer : ComparerBase<GameState, GameStateCollector>
    {
        protected override bool Compare(GameState actual) => Expect == actual;
    }

    public class GameStateCollector : ValueCollectorBase<GameState>
    {
    }
}
```

![Monosnap 2019-12-19 02-15-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/102664/298493bf-72bd-2c01-958b-e31c9f13e9de.png)

赤枠が今回作成したGameStateに関係するノードです。
このように基底クラスを継承することで、独自ノードを作成することも可能となります。
既存のノードのコードを真似て、プロジェクトに使いやすいノードを作成するのが良さそうです。

# 🏁最後に
キッズスターでは[CAFU (Clean Architecture for Unity)](https://github.com/umm/cafu_core) を採択しているのですが View のレイヤーに関しては割とUniFlowで回せるような所感はあります。簡単な紙芝居形式のものであれば、デザイナー/アニメーターさんも触っていけそうです。一方ロジックが絡むところの勘所を掴むのが個人的にまだ難しく、色々試したいと思っています。

UniFlowは現時点においてまだpreviewバージョンです。
何かあれば @monry さんにフィードバックするか [リポジトリ](https://github.com/monry/UniFlow) にissueを立てると良さそうです！