LoginSignup
19
10

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-18

この記事は Unity Advent Calendar 2019 19日目の記事です。

環境

  • Unity2019.2.15f1
  • UniFlow 0.1.0 - preview40

📝UniFlowについて

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

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

ノードベースEditorの実装には GraphView が採択されています。
GraphViewについては Unity #2 Advent Calendar 2019 にて分かりやすく入門向きな記事が投稿されていたので共有します。

🖥UniFlowのインストール

scopedRegistriesの追加

UniFlowは Unofficial Unity Package Manager Registry にて提供されています。
こちらは @monry さんが立てた UnityPackageManager からアクセス可能なパッケージレジストリです。
詳細: Unofficial Unity Package Manager Registry を建ててみました - もんりぃ is undefined.

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

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

Unity - Manual: Scoped package registries
上記のUnityのマニュアルを元に、独自のパッケージレジストリをPackageManager上から参照できるようにします。

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

{
  "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.

PackageManagerからUniFlowをインストール

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

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

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

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

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

例えば上記のケースだと SimpleAnimation が不足しているということなので scopescom.unity.simpleanimation を追加して Unofficial Unity Package Manager Registry上のSimpleAnimationPackage を参照できるようにする必要があります。

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

注意事項

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

🔧UniFlowの使い方

エディタを開く

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

image.png

ノードの追加/参照

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

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

2019-12-14 11.19.27.gif

ノードの接続

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

2019-12-14 11.28.00.gif

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

🏙UniFlowのサンプル (基本)

image.png

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

ボタンを押したらTimeine再生/終了を待ってScene遷移

image.png

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

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

衝突時にランダムに音を再生

image.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を利用したメッセージングの基本は以下の記事が分かりやすくまとまっていてオススメです)

UniFlowにおけるグラフから見ていきます。
Monosnap 2019-12-19 00-26-12.png

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

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

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 に指定する必要があります
  • UniFlowInstallerPackages/UniFlow/Installers 以下に ScriptableObject として存在します。

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

独自Signal/ノードの作成

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

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

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

🏁最後に

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

UniFlowは現時点においてまだpreviewバージョンです。
何かあれば @monry さんにフィードバックするか リポジトリ にissueを立てると良さそうです!

19
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
10