Edited at

AssetGraphのカスタムフィルター・カスタムノードの作り方


はじめに

この記事はUnity #2 Advent Calendar 20184日目の記事です。

今回はAssetGraphでカスタムフィルター・カスタムノードを作る方法について解説します。


AssetGraphとは?

AssetBundleをビルドするための様々な作業をノーコーディングで済ませることができる神ツールです!

こんな感じでノードベースでAssetBundleのビルド作業を構築することができます。

Unity公式で開発されているため安心感もあります!またリポジトリはオープンになっています。

Unity-Technologies / AssetBundleGraphTool — Bitbucket

概要については以下の資料を読んでもらうのが早いと思います。

【Unity道場】AssetGraph入門 〜ノードを駆使しててUnityの面倒な手作業を自動化する方法〜

また、公式で日本語マニュアルが用意されています。詳細な使い方はこちらを見ると良いです。

AssetGraph ユーザーマニュアル

現在のバージョンはv1.4です。今回の記事ではv1.4を元に解説します。


カスタムフィルターの作り方

IFilterインターフェースを実装したクラスを用意することで、Split By Filterノードで使えるフィルター処理を自前で実装することが可能です。

(※ ややこしいですが、Load By Search Filterとは関係ありません)


1. テンプレートから作る

メニュー -> Window -> AssetGraph -> Create Node Script -> Filter Scriptを実行することで、UnityEngine.AssetGraph/Generatedフォルダ以下にカスタムフィルターのスクリプトが生成されます。これを元に実装していきます。


2. 処理を実装する

まずはテンプレートの初期状態を見ながら、どの部分に何を実装していけばいいかを見ていきます。

以下は生成した直後のコードです。


MyFilter.cs

[CustomFilter("My Filter")]

public class MyFilter : IFilter {

[SerializeField] private string m_filterKeyword;

public string Label {
get {
return m_filterKeyword;
}
}

public MyFilter() {
m_filterKeyword = Model.Settings.DEFAULT_FILTER_KEYWORD;
}

public bool FilterAsset(AssetReference a) {
return Regex.IsMatch(a.importFrom, m_filterKeyword,
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
}

public void OnInspectorGUI (Rect rect, Action onValueChanged) {

var keyword = m_filterKeyword;

using (new EditorGUILayout.HorizontalScope()) {
GUIStyle s = new GUIStyle((GUIStyle)"TextFieldDropDownText");
keyword = EditorGUI.TextField(rect, m_filterKeyword, s);
if (keyword != m_filterKeyword) {
m_filterKeyword = keyword;
onValueChanged();
}
}
}
}


また、IFilterインターフェースを見ると、最低限実装しなければいけない所は以下だと分かります。


  • string Label(getterプロパティ)

  • bool FilterAsset(AssetReference asset);

  • void OnInspectorGUI (Rect rect, Action onValueChanged);

また、既にこのテンプレートの時点で一応動作するものになっており、Filter Conditionから「My Filter」が選べるようになっています。

動作的には「テキストボックスに入力した正規表現の条件にマッチするものを抽出する」という内容になっています。


1. フィルター名を決める

まず最初に、クラスのアトリビュートでフィルター名を入力する必要があります。ここで入力した名前はFilter Conditionsから使用するフィルター名のリストを表示する時に使われます。

また、クラス名も適切なものに変更しておくのが良いでしょう。

[CustomFilter("My Filter")]

public class MyFilter : IFilter {


2. フィルター処理を実装する

bool FilterAsset(AssetReference a)の中にフィルター処理を実装します。実装は単純で、渡されたアセットの情報に対して、フィルター条件にマッチするかどうかをboolで返すだけです。

    public bool FilterAsset(AssetReference a) {

return Regex.IsMatch(a.importFrom, m_filterKeyword,
RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
}

ここで正規表現の条件にm_filterKeywordを使っていますが、これをユーザー側で入力可能にするためにGUI部分を実装する必要があります。それが以下の部分です。

    public void OnInspectorGUI (Rect rect, Action onValueChanged) {

var keyword = m_filterKeyword;

using (new EditorGUILayout.HorizontalScope()) {
GUIStyle s = new GUIStyle((GUIStyle)"TextFieldDropDownText");
keyword = EditorGUI.TextField(rect, m_filterKeyword, s);
if (keyword != m_filterKeyword) {
m_filterKeyword = keyword;
onValueChanged();
}
}
}

OnInspectorGUIの引数で渡されているRectは、インスペクタ上のこの部分に相当するものです。

そのため、このGUIを描画する際はこのRect内に描画するように実装する必要があります。2つ以上のGUI要素を入れたい場合はこのRect内に収まるように分割したRectを作る形になります。例えば横に2分割するならこんな感じです。

Rect rect1 = rect;

Rect rect2 = rect;
rect1.width /= 2f;
rect2.width /= 2f;
rect2.x += rect1.width;

GUIStyle s = new GUIStyle((GUIStyle)"TextFieldDropDownText");
var val1 = EditorGUI.TextField(rect1, m_val1, s);
var val2 = EditorGUI.TextField(rect2, m_val2, s);

また、フィールドの値が変更された時は引数に渡されているonValueChangedを発火させる必要があります。onValueChangedを発火させると、入力値がFilterノードのSerializableFieldとして保存されます。


3. ラベル名の決定方法を決める

ラベル名はグラフウィンドウ上のこの部分に対応するものです。

グラフウィンドウ上でわかりやすくするためのものなので、区別が付きやすいような物が良いでしょう。

Labelプロパティのgetterがこれに対応しています。

    public string Label { 

get {
return m_filterKeyword;
}
}


カスタムノードの作り方

AssetGraphは標準でも十分な種類のノードが用意されていて、それだけで大抵の運用は可能ですが、さらにカスタムノードという自分で処理を実装したノードを追加することができます。


1. テンプレートから作る

メニュー -> Window -> AssetGraph -> Create Node Script -> Custom Node Scriptで、UnityEngine.AssetGraph/Generatedフォルダ以下にカスタムノードのテンプレートが生成されます。


2. 実装する

以下は生成された直後のコードです。


MyNode.cs

[CustomNode("Custom/MyNode", 1000)]

public class MyNode : Node {

[SerializeField] private SerializableMultiTargetString m_myValue;

public override string ActiveStyle {
get {
return "node 8 on";
}
}

public override string InactiveStyle {
get {
return "node 8";
}
}

public override string Category {
get {
return "Custom";
}
}

public override void Initialize(Model.NodeData data) {
m_myValue = new SerializableMultiTargetString();
data.AddDefaultInputPoint();
data.AddDefaultOutputPoint();
}

public override Node Clone(Model.NodeData newData) {
var newNode = new MyNode();
newNode.m_myValue = new SerializableMultiTargetString(m_myValue);
newData.AddDefaultInputPoint();
newData.AddDefaultOutputPoint();
return newNode;
}

public override void OnInspectorGUI(NodeGUI node, AssetReferenceStreamManager streamManager, NodeGUIEditor editor, Action onValueChanged) {

EditorGUILayout.HelpBox("My Custom Node: Implement your own Inspector.", MessageType.Info);
editor.UpdateNodeName(node);

GUILayout.Space(10f);

//Show target configuration tab
editor.DrawPlatformSelector(node);
using (new EditorGUILayout.VerticalScope(GUI.skin.box)) {
// Draw Platform selector tab.
var disabledScope = editor.DrawOverrideTargetToggle(node, m_myValue.ContainsValueOf(editor.CurrentEditingGroup), (bool b) => {
using(new RecordUndoScope("Remove Target Platform Settings", node, true)) {
if(b) {
m_myValue[editor.CurrentEditingGroup] = m_myValue.DefaultValue;
} else {
m_myValue.Remove(editor.CurrentEditingGroup);
}
onValueChanged();
}
});

// Draw tab contents
using (disabledScope) {
var val = m_myValue[editor.CurrentEditingGroup];

var newValue = EditorGUILayout.TextField("My Value:", val);
if (newValue != val) {
using(new RecordUndoScope("My Value Changed", node, true)){
m_myValue[editor.CurrentEditingGroup] = newValue;
onValueChanged();
}
}
}
}
}

/**
* Prepare is called whenever graph needs update.
*/

public override void Prepare (BuildTarget target,
Model.NodeData node,
IEnumerable<PerformGraph.AssetGroups> incoming,
IEnumerable<Model.ConnectionData> connectionsToOutput,
PerformGraph.Output Output)
{
// Pass incoming assets straight to Output
if(Output != null) {
var destination = (connectionsToOutput == null || !connectionsToOutput.Any())?
null : connectionsToOutput.First();

if(incoming != null) {
foreach(var ag in incoming) {
Output(destination, ag.assetGroups);
}
} else {
// Overwrite output with empty Dictionary when there is no incoming asset
Output(destination, new Dictionary<string, List<AssetReference>>());
}
}
}

/**
* Build is called when Unity builds assets with AssetBundle Graph.
*/

public override void Build (BuildTarget target,
Model.NodeData nodeData,
IEnumerable<PerformGraph.AssetGroups> incoming,
IEnumerable<Model.ConnectionData> connectionsToOutput,
PerformGraph.Output outputFunc,
Action<Model.NodeData, string, float> progressFunc)
{
// Do nothing
}
}



1. ノード追加時のメニューでの位置を決める

CustomNodeアトリビュートを付加することで、グラフウィンドウ上でのコンテキストメニューにメニューが追加されます。メニュー階層と表示順(int)を指定します。

また、クラス名も適切なものに変更しておきましょう。

[CustomNode("Custom/MyNode", 1000)]

public class MyNode : Node {


2. グラフウィンドウ上でのノードの見た目を決める

このあたりを変えることでノードの見た目(色、テキスト)を変えることができます。

    public override string ActiveStyle {

get {
return "node 8 on";
}
}

public override string InactiveStyle {
get {
return "node 8";
}
}

public override string Category {
get {
return "Custom";
}
}

ActiveStyle、InactiveStyleに指定する文字列は、以下のような対応関係になっています。

括弧内はデフォルトのノードで使われているものです。


  • node 0: 灰色 (Load, File, Export)

  • node 1: 青色 (Filter)

  • node 2: 緑色 (Group)

  • node 3: 黄色 (Configure)

  • node 4: 茶色 (Create)

  • node 5: 赤色 (Build)

  • node 6: 桃色 (未使用)

  • node 7: 紫色 (Error)

  • node 8: 水色 (Modify)

(※ それぞれ末尾にonをつけるとオレンジ色のボーダーがついた選択状態のスタイルになる)

作りたいノードの種類に合わせてスタイルを選択しましょう。

ちなみに、このスタイルはUnityEngine.AssetGraph/Editor/GUI/GraphicResources/NodeStyle.guiskinに定義されています。これを弄れば他のスタイルを作る事も可能だと思われます。

また、Categoryプロパティはノードのこの部分のテキストに対応しています。


3. ノードの処理を実装する

処理を実装する部分は大きく分けて3つです。


  • void OnInspectorGUI: ノードを選択した時のインスペクタ部分を実装する

  • void Prepare: ノードを繋いだ時の処理を実装する

  • void Build: グラフのビルドを実行した時の処理を実装する

PrepareとBuildの実装がなかなか曲者です。それぞれ初期状態の実装を見てみましょう。

    public override void Prepare (BuildTarget target, 

Model.NodeData node,
IEnumerable<PerformGraph.AssetGroups> incoming,
IEnumerable<Model.ConnectionData> connectionsToOutput,
PerformGraph.Output Output)
{
// Pass incoming assets straight to Output
if(Output != null) {
var destination = (connectionsToOutput == null || !connectionsToOutput.Any())?
null : connectionsToOutput.First();

if(incoming != null) {
foreach(var ag in incoming) {
Output(destination, ag.assetGroups);
}
} else {
// Overwrite output with empty Dictionary when there is no incoming asset
Output(destination, new Dictionary<string, List<AssetReference>>());
}
}
}

public override void Build (BuildTarget target,
Model.NodeData nodeData,
IEnumerable<PerformGraph.AssetGroups> incoming,
IEnumerable<Model.ConnectionData> connectionsToOutput,
PerformGraph.Output outputFunc,
Action<Model.NodeData, string, float> progressFunc)
{
// Do nothing
}

PrepareとBuildの引数はほぼ同じです。


  • Model.NodeData node:自身のノード情報を格納するインスタンス。

  • IEnumerable<PerformGraph.AssetGroups> incoming:ノードの入力が入っている。入力点が複数ある場合にその分のAssetGroupsが入ってくるイメージ。

  • IEnumerable<Model.ConnectionData> connectionsToOutput:自身のノード出力の接続先情報。出力点が複数ある場合にはその分増える。

  • PerformGraph.Output outputFunc:ノードの出力を実行する関数。第一引数に出力先の接続情報、第二引数にAssetGroupsを指定する。これを発火させることで次のノードの処理が実行されるイメージ。

  • Action<Model.NodeData, string, float> progressFunc(Buildのみ):ビルド処理の進捗状況を通知するための関数。

ということで、実装要件をざっくりとまとめると


  1. incomingから入力(アセット情報の配列)を取得

  2. 何らかの処理を行う

  3. outputFuncで次のノードにアセット情報の配列を渡す

という感じになります。

PrepareとBuildの違いは、Prepareはグラフが編集された瞬間実行されるもので、Buildはビルド実行時にのみ実行されるものです。ロード・フィルター系処理はグラフを編集した瞬間に結果が見えないと困りますが、例えばAssetBundleをビルドするノードでグラフが編集されるたびにビルドが実行されたら困りますよね、ということでこのように分かれています。

つまり、ビルド系以外のノードの場合は、基本的にはPrepareとBuildではほとんど同じ実装になります。ビルド系のノードの場合は、Prepareでは「実際にビルドは行わないけど、ビルドされた時に出力される想定のアセットの配列をoutputFuncで渡す」という実装をする必要があります。


ノードの入力タイプを制限する・ノードの出力タイプを指定する

テンプレートには含まれていませんが、NodeInputTypeをoverrideすることで特定のノードからの入力のみを受け付けるようにできます。また、NodeOutputTypeをoverrideすることで自身のノードの出力タイプを指定することができます。例えばAssetBundleをビルドするノードは以下のようになっています。


BundleBuilder.cs

        public override Model.NodeOutputSemantics NodeInputType

{
get
{
return Model.NodeOutputSemantics.AssetBundleConfigurations;
}
}

public override Model.NodeOutputSemantics NodeOutputType
{
get
{
return Model.NodeOutputSemantics.AssetBundles;
}
}


予期せぬ動作を防ぐためには指定しておいたほうが無難でしょう。


わからないことがあったら

ビルトインのノードの実装が参考になります!

UnityEngine.AssetGraph/Editor/System/Node/Buitin以下に入っているので、「このノードみたいな処理がしたい!」という場合は対応するソースを見に行けば良いです。


おわりに

本当は実装例まで挙げられればよかったんですが、ちょっと時間が無かったのでこれで許してください…!そのうち暇ができたら追加するかも(しないやつ)

AssetGraph、便利なのでみんな使いましょう!