この記事は Unity Advent Calendar 2019 19日目の記事です。
- 前日: @Nitudonさん: VFX GraphにおけるVFX Property Binderの活用 - Qiita
- 翌日: @Yuzu_Unityさん: UnityのAnimation・AnimatorControllerについて解説 - Qiita
環境
- Unity2019.2.15f1
- UniFlow 0.1.0 - preview40
📝UniFlowについて
UniFlowは @monry さんの作ったOSSライブラリです。
ノードベースでUnityAPIを順番に実行したり、実行した処理を待ち受けたりすることが可能です。
- monry/UniFlow: Connect presentation events
- Visual Programming Framework for Unity – UniFlow のご紹介 – Unity Learning Materials
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 が不足しているということなので scopes
に com.unity.simpleanimation
を追加して Unofficial Unity Package Manager Registry上のSimpleAnimationPackage を参照できるようにする必要があります。
Packages以下にUniFlowおよび依存パッケージがインストールされました。
注意事項
依存ライブラリとして例えば UniRx
や Extenject
等もインストールされますが、既存のプロジェクトで既に Assets
以下に導入されている場合、既存のパッケージを削除して Packages
上のパッケージに (asmdef等の) 参照を切り替えるなどの工夫が必要な点に注意。
🔧UniFlowの使い方
エディタを開く
UniFlowは各種コンポーネント群およびGraphViewにて実装されたノードベースエディターにて構成されています。
UniFlowGraphは Window > UniFlow > Open UniFlow Graph
で開くことが出来ます。
ノードの追加/参照
UniFlowビュー上で 右クリック > Create Node
することでノードを作成することができます。
UniFlowにおけるノードはComponentとして任意のGameObject/Prefabに紐付く形で追加されます。
Hierarchy 上のGameObject、もしくはPoject上のPrefabが選択されていれば、AddComponentされます。
Hierarchy 上のGameObject、もしくはPoject上のPrefabが選択されていない状態であれば、新しくGameObjectが生成されます。
ノードを削除するとGameObject/Prefab上からもRemoveComponentされます。
GameObject/Prefabを選択した状態で UniFlowGraph
ビューの Load
を押すとオブジェクトに紐付いたグラフを見ることができます。
ノードの接続
UniFlowのノードはIn/Outの接続において逐次実行を制御します。
今回は以下の二つのノードを接続し、「ボタンをクリックしたらログを表示する」という処理にします。
-
UIBehaviourEventTrigger
: 任意のUIBehaviourにて指定した処理を実行時に通知する -
LoggerReceiver
: Consoleにログを出す
(余談ですが UniFlowGraph
ビューにおける AutoLayout
でノードを整列させることが可能です)
ノードを接続するとInspectorにおける TargetComponents
に接続先のノードの情報が格納されます。
GameObjectを跨いで参照するケース等、直接 TargetComponents
に対象のコンポーネントを指定するケースもあります。
🏙UniFlowのサンプル (基本)
UniFlowのノード(コンポーネント)にはカテゴリがあります。
各カテゴリ毎に多数のノードが存在していますが、実例を通してその内のいくつかを紹介します。
ボタンを押したらTimeine再生/終了を待ってScene遷移
-
UIBehaviourEventTrigger
(Event) : 任意のUIBehaviourにて指定した処理を実行時に通知する -
ActivateController
(Controller) : GraohicRaycasterの有効/無効を制御する -
PlayableController
Controller) : PlayableDirectorにおける再生/停止を制御する -
TimelineEvent
(Event) : Timelineの再生/停止等のイベントにフックして通知する -
LoadScene
(Controller) : 指定した名前のシーンをロードする
Controller
は任意のUnityAPIを実行したり、プロパティを変更したり、ふるまいを持ちます。
Event
は任意のイベントにフックして、通知を行います。
衝突時にランダムに音を再生
-
PhysicsCollisionEvent
(Event) : Colliderとの衝突にフックして通知する -
RandomInt
(Logic) : 指定した範囲の整数のランダム値を生成して、次のノードに通知する -
AudioClipListSelector
(ValueSelector) : Key(整数値)を受け取って、対応するValue(AudioClip)に変換して通知する -
AudioController
(Controller) : 音の再生/停止を制御する
Logic
はUnityAPIに関わらない値の発生や通知のロジック群です。
ValueSelector
はKey-Valueを設定することができ、任意の値を受け取って対応する要素に変換して通知します。
🌆UniFlowのサンプル (応用)
以下、ZenjectおよびUniRxの知識が前提となります。
ロジックに通知/内部状態を変更/ロジックから通知を受け取って処理を実行
UniFlowでは Pub/Sub の仕組みを使ったロジック側との疎通の仕組みが用意されています。
(Zenjectを利用したメッセージングの基本は以下の記事が分かりやすくまとまっていてオススメです)
-
StringSignalPublisher
(SignalPublisher) : StringSignalを通知する (ISignalPublisher<StringSignal>
が通知を受け取る) -
StringSignalReceiver
(SignalReceiver) : StringSignalを受け取ったら通知する -
StringComparer
(ValueComparer) : 文字列を比較して、合致していたら次のノードに通知する
StringSignalの通知を受け取り、またを発火するロジックのサンプルは以下となります。
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 として存在します。
これでロジックへの通知/ロジックからの通知処理を実装することができました。
独自Signal/ノードの作成
先ほどは文字列ベースでどのSignalの通知かを識別しましたが、文字列比較は事故が起きやすく不安です。
回避するために、独自のSignalを用いた SignalReceiver
/ SignalPublisher
を作って判別するアプローチが取れます。
基底クラス(Baseクラス)が用意されているので、継承してクラスを作っていきます。
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>
{
}
}
赤枠が今回作成したGameStateに関係するノードです。
このように基底クラスを継承することで、独自ノードを作成することも可能となります。
既存のノードのコードを真似て、プロジェクトに使いやすいノードを作成するのが良さそうです。
🏁最後に
キッズスターではCAFU (Clean Architecture for Unity) を採択しているのですが View のレイヤーに関しては割とUniFlowで回せるような所感はあります。簡単な紙芝居形式のものであれば、デザイナー/アニメーターさんも触っていけそうです。一方ロジックが絡むところの勘所を掴むのが個人的にまだ難しく、色々試したいと思っています。
UniFlowは現時点においてまだpreviewバージョンです。
何かあれば @monry さんにフィードバックするか リポジトリ にissueを立てると良さそうです!