はじめに
Unity2018ぐらいからExperimentalで追加されたGraphViewの使い方のメモです。
日本のGraphViewに関する記事があんまりないので書こうと思いました。
(最初の部分は引用に書いてある「GraphView完全理解した(2019年末版)」とほぼ同じ内容です)
自己紹介
学生です。
この記事を読む人
GraphViewで何か作りたいけど何から始めればいいかわからんという人におすすめです。
GraphViewについて
このAPIはExperimentalなので公式のリファレンスにも書いてあるとおり、GraphView APIは実験的な物なので将来的に変更されたり削除される可能があります。(公式リファレンスから引用)
GraphViewを使って何ができるの?
簡単にいうとShaderGraphみたいなものを自作できるようになります。
(流石に個人でここまでは難しいかもしれませんが)
(公式より画像を引用)
GraphViewの構成要素
Graphviewは大まかに4つの要素でできています。
・GraphView
・Node
・Port
・Edge
簡単に説明すると、
GraphView
→NodeやPortの親
Node
→下の写真の青で囲ってある物です。
Port
→In と Outとか。後で説明するEdgeの始点と終点になる物です。
Edge
→Node同士を繋ぐカラフルな線のことです。カラフルな理由は後ほど...
実際に作ってみる
バージョン : Unity2019.2.11f1
使用OS : MacOS Catalina 10.15.3
この記事はここを参考(ほぼ引用)しながら少し注釈を加えて説明していきます。
まずEditorWindowを作成します。
GraphEditorWindowという名前のスクリプトを作成して以下の内容を入力してください。
これから作成するスクリプトはEditorというフォルダの中に入れてください。
↓写真のように
//GraphEditorWindow.cs
using UnityEditor;
using UnityEngine;
public class GraphEditorWindow : EditorWindow
{
[MenuItem("Window/Open GraphView")]
public static void Open()
{
GraphEditorWindow graphEditorWindow = CreateInstance<GraphEditorWindow>();
graphEditorWindow.Show();
graphEditorWindow.titleContent = new GUIContent("GraphEditor");
}
}
ここから開くことができます。
こんな感じの画面が開たら成功です!
次にお待ちかね、GraphViewを作成していきます
MyGraphViewという名前のスクリプトを作成して以下の内容を入力してください。
"GraphView"という名前だとエラーが出るので注意してください。
//MyGraphView.cs
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView {
}
一回何も書かなくてOKです。
そして、さっき作成したGraphEditorWindowに以下の内容を入力してください。
//GraphEditorWindow.cs
using UnityEditor;
using UnityEngine;
public class GraphEditorWindow : EditorWindow
{
[MenuItem("Window/Open GraphView")]
public static void Open()
{
GraphEditorWindow graphEditorWindow = CreateInstance<GraphEditorWindow>();
graphEditorWindow.Show();
graphEditorWindow.titleContent = new GUIContent("GraphEditor");
}
void OnEnable()
{
rootVisualElement.Add(new MyGraphView());
}
}
今のところエディターに何にも表示されないはずですがこれでOKです。
次にNodeを作成していきます。
MyNodeという名前のスクリプトを作成して以下の内容を入力してください。
//MyNode.cs
using UnityEditor.Experimental.GraphView;
public class MyNode : Node
{
}
そして、さっき作成したMyGraphViewに以下の内容を入力してください。
//MyGraphView.cs
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView
{
public MyGraphView() : base()
{
AddElement(new MyNode());
}
}
確認してみると、あれ?表示されない!!なんで...なんで..
そういう時はUIElements Debuggerを使いましょう。
やっぱり生成されています。なぜでしょうか。
しかし、理由は簡単です。高さが0になっていて生成されているのに見えていないのです。
GraphEditorWindowに以下の内容を入力してください。
//GraphEditorWindow.cs
using UnityEditor;
using UnityEngine;
public class GraphEditorWindow : EditorWindow
{
[MenuItem("Window/Open GraphView")]
public static void Open()
{
GraphEditorWindow graphEditorWindow = CreateInstance<GraphEditorWindow>();
graphEditorWindow.Show();
graphEditorWindow.titleContent = new GUIContent("GraphEditor");
}
void OnEnable()
{
var graphView = new MyGraphView()
{
style = { flexGrow = 1 }
};
rootVisualElement.Add(graphView);
}
}
無事、Nodeが作成されました。感動の瞬間です!
でもこれじゃただの箱ですw
Portを追加してNode同士をつなげられるようにしましょう。
MyNodeに以下の内容を入力してください。
//MyNode.cs
using UnityEditor.Experimental.GraphView;
public class MyNode : Node
{
public MyNode()
{
title = "Sample";
var inputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input,Port.Capacity.Single, typeof(Port));
inputContainer.Add(inputPort);
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
outputContainer.Add(outputPort);
}
}
これで確認してみると
お、NodeにPortがつきました!
でも思いませんか?NodeとNodeを繋ぎたいと、でも繋ぐNodeがないですね。
Nodeを量産できるようにしましょう。
MyGraphViewを以下のようにしてください。
//MyGraphView.cs
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView
{
public MyGraphView() : base()
{
this.AddManipulator(new SelectionDragger());//移動できるように
nodeCreationRequest += context =>
{
AddElement(new MyNode());
};
}
}
これで右クリック>Create NodeでNodeをたくさん生成できるようになって移動することはできますが、Npde同士を接続できません。
OutputPortから正しいInputPortにつなげてあげる必要があります。
//MyGraphView.cs
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView
{
public MyGraphView() : base()
{
this.AddManipulator(new SelectionDragger());//移動できるように
nodeCreationRequest += context =>
{
AddElement(new MyNode());
};
}
public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
return ports.ToList();
}
}
でもEdgeが見にくいですね...見えないことはないですけど。
そういうことで背景の色を変えましょう。
まず画像のようにResourcesフォルダを作成
その中にUIElements Editor Window から
C#、UXMLのチェックを外してUSSだけにチェックを入れTextFieldにBackGroundと入れ、Confirmを押します
USSと書かれたファイルが作成できたらOKです
USSの説明ですが、簡単にいうとHTMLのCSSのような物です。
色を変えたりできます。
そこで、BackGround.ussを下のようにします。今回はUnityのAnimatorのような色使いにしてみました。
GridBackground{
--grid-background-color:rgb(90,90,90);
--line-color:rgba(80,80,80,1);
--thick-line-color:rgba(40,40,40,0.6);
--spacing:10;
}
--grid-background-color → 背景色
--line-color → 細かい線の色
--thick-line-color → 大まかな線の色
--spacing →細かいマスの長さ
期待して見てみると、あれ?何も変わっていない。
だって 作成したUSSをEditorWindowに適応してないんですから。
そこで、MyGraphView.csを次のようにします。
//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView
{
public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentZoomer());//拡大縮小できるように
this.AddManipulator(new SelectionDragger());//Nodeを移動できるように
this.AddManipulator(new ContentDragger());//画面を移動できるように
this.AddManipulator(new RectangleSelector());//範囲選択
nodeCreationRequest += context =>
{
AddElement(new MyNode());
};
}
public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
return ports.ToList();
}
}
そうするとこうなるのですが
一回わかりやすいように色を変えてみます
・背景色の色を白
・細かい線の色を緑
・大まかな線の色を青
・細かいマスの長さを15にしてみました
これでわかりやすくなったのではないのでしょうか?
細かいマスの大きさが少し大きくなってるのがわかると思います。
SampleじゃないNodeを作成する
MyNodeを以下のようにします
//MyNode.cs
using UnityEditor.Experimental.GraphView;
public abstract class MyNode : Node { }
抽象クラスにします。クラスの中身は何にも書かなくて大丈夫です。
新しくProcessNode.csというものを作り以下のようにします。
//ProcessNode.cs
using UnityEditor.Experimental.GraphView;
public abstract class ProcessNode : MyNode
{
public ProcessNode()
{
var inputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(Port));
inputPort.portName = "In";
inputContainer.Add(inputPort);
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
outputPort.portName = "Out";
outputContainer.Add(outputPort);
}
public abstract void Execute();
}
こいつを一回噛ませることでインターフェースじゃないですが、In と Out のPortを強制的に作成してくれます。便利ですね。
LogNode.csを作成して以下のようにします。
using UnityEditor.Experimental.GraphView;
public class LogNode : ProcessNode
{
private Port inputString;
public LogNode() : base()
{
title = "Log";
inputString = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(string));
inputContainer.Add(inputString);
}
public override void Execute()
{
}
}
あれ?Debug.Logがない、と思った人、焦らないでくださいw
その前に出力する文字を指定できるStringNodeを作らなければいけません。
StringNodeというスクリプトを作成し、以下のようにします。
using UnityEngine.UIElements;
using UnityEditor.Experimental.GraphView;
public class StringNode : MyNode
{
private TextField textField;
public string Text { get { return textField.value; } }
public StringNode() : base()
{
title = "String";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(string));
outputContainer.Add(outputPort);
textField = new TextField();
mainContainer.Add(textField);
}
}
○○Field系については後で説明します。
そして最後にRootNodeを作成します。これはすべてのNodeの親とも言えます。
RootNodeというスクリプトを作成し、以下のようにします。
using UnityEditor.Experimental.GraphView;
public class RootNode : MyNode
{
public RootNode() : base()
{
title = "Root";
capabilities -= Capabilities.Deletable;
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
outputPort.portName = "Out";
outputContainer.Add(outputPort);
}
}
親が何個もあるというのはおかしいので、Wndowを生成したときに一個だけ生成するようにします。
RootNodeはcapabilities -= Capabilities.Deletable;の部分で消せないようになっています。
MyGraphViewを以下のようにします。
//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView
{
public RootNode root;
public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentZoomer());//移動できるように
this.AddManipulator(new SelectionDragger());//移動できるように
this.AddManipulator(new ContentDragger());//移動できるように
this.AddManipulator(new RectangleSelector());//範囲選択
root = new RootNode();
AddElement(root);
nodeCreationRequest += context =>
{
//AddElement(new MyNode());
};
public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
return ports.ToList();
}
}
CreateNodeでSampleNodeしか作れないのはおかしいので、選択してから生成できるようにします。
SearchWindowProviderというスクリプトを作成して以下のようにします。
//SearchWindowProvider.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor.Experimental.GraphView;
public class SearchWindowProvider : ScriptableObject, ISearchWindowProvider
{
private MyGraphView graphView;
public void Initialize(MyGraphView graphView)
{
this.graphView = graphView;
}
List<SearchTreeEntry> ISearchWindowProvider.CreateSearchTree(SearchWindowContext context)
{
var entries = new List<SearchTreeEntry>();
entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Node")));
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (var type in assembly.GetTypes())
{
if (type.IsClass && !type.IsAbstract && type.IsSubclassOf(typeof(MyNode))
&& type != typeof(RootNode))
{
entries.Add(new SearchTreeEntry(new GUIContent(type.Name)){ level = 1, userData = type });
}
}
}
return entries;
}
bool ISearchWindowProvider.OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
{
var type = searchTreeEntry.userData as Type;
var node = Activator.CreateInstance(type) as MyNode;
graphView.AddElement(node);
return true;
}
}
そしたら検索できるようにMyGraphViewを次のようにします
//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
Public class MyGraphView : GraphView
{
public RootNode root;
public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentZoomer());//移動できるように
this.AddManipulator(new SelectionDragger());//移動できるように
this.AddManipulator(new ContentDragger());//移動できるように
root = new RootNode();
AddElement(root);
var searchWindowProvider = new SearchWindowProvider();
searchWindowProvider.Initialize(this);
nodeCreationRequest += context => public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindowProvider);
};
}
{
return ports.ToList();
}
}
そしたら画面に戻ってCreateNodeして見てください下の写真のようになれば成功です。
でもこれは少しおかしくないですか?
outがstringに、stringがoutに繋がります。これを修正しましょう。
MyGraphViewを以下のようにします。
//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView
{
public RootNode root;
public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentZoomer());//移動できるように
this.AddManipulator(new SelectionDragger());//選択できるように
this.AddManipulator(new ContentDragger());//画面を移動できるように
this.AddManipulator(new RectangleSelector());//範囲選択
root = new RootNode();
AddElement(root);
var searchWindowProvider = new SearchWindowProvider();
searchWindowProvider.Initialize(this);
nodeCreationRequest += context =>
{
SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindowProvider);
};
}
public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
foreach (var port in ports.ToList())
{
if (startAnchor.node == port.node || startAnchor.direction == port.direction || startAnchor.portType != port.portType)
{
continue;
}
compatiblePorts.Add(port);
}
return compatiblePorts;
}
}
これで良い感じですね 。
そしたら実際に動くようにしましょう。
ProcessNode、LogNode、RootNodeを以下のようにします
//ProcessNode.cs
using UnityEditor.Experimental.GraphView;
public abstract class ProcessNode : MyNode
{
public Port InputPort;
public Port OutputPort;
public ProcessNode()
{
InputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(Port));
InputPort.portName = "In";
inputContainer.Add(InputPort);
OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
OutputPort.portName = "Out";
outputContainer.Add(OutputPort);
}
public abstract void Execute()
{
}
}
//LogNode.cs
using System.Linq;
using UnityEngine;
using UnityEditor.Experimental.GraphView;
public class LogNode : ProcessNode
{
private Port inputString;
public LogNode() : base()
{
title = "Log";
inputString = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(string));
inputContainer.Add(inputString);
}
public override void Execute()
{
var edge = inputString.connections.FirstOrDefault();
var node = edge.output.node as StringNode;
if (node == null) return;
Debug.Log(node.Text);
}
}
//RootNode.cs
using UnityEditor.Experimental.GraphView;
public class RootNode : MyNode
{
public Port OutputPort;
public RootNode() : base()
{
title = "Root";
capabilities -= Capabilities.Deletable;
OutputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Single, typeof(Port));
OutputPort.portName = "Out";
outputContainer.Add(OutputPort);
}
}
これをRootから順に処理していきます。
しかしNodeには繋がってるNodeを取得する機能がないそうなので、Port生成時にキャッシュしておきます。
MyGraphViewを以下のようにします
//MyGraphView.cs
using UnityEngine;
using UnityEngine.UIElements;//AddManipulatorを使うために必要
using System.Linq;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
public class MyGraphView : GraphView
{
public RootNode root;
public MyGraphView() : base()
{
styleSheets.Add(Resources.Load<StyleSheet>("BackGround"));
GridBackground gridBackground = new GridBackground();
Insert(0, gridBackground);
gridBackground.StretchToParentSize();
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
this.AddManipulator(new ContentZoomer());//移動できるように
this.AddManipulator(new SelectionDragger());//移動できるように
this.AddManipulator(new ContentDragger());//移動できるように
root = new RootNode();
AddElement(root);
var searchWindowProvider = new SearchWindowProvider();
searchWindowProvider.Initialize(this);
nodeCreationRequest += context =>
{
SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindowProvider);
};
}
public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List<Port>();
foreach (var port in ports.ToList())
{
if (startAnchor.node == port.node || startAnchor.direction == port.direction || startAnchor.portType != port.portType)
{
continue;
}
compatiblePorts.Add(port);
}
return compatiblePorts;
}
public void Execute()
{
var rootEdge = root.OutputPort.connections.FirstOrDefault();
if (rootEdge == null) return;
var currentNode = rootEdge.input.node as ProcessNode;
while (true)
{
currentNode.Execute();
var edge = currentNode.OutputPort.connections.FirstOrDefault();
if (edge == null) break;
currentNode = edge.input.node as ProcessNode;
}
}
}
そして実行するようなボタンを作ります。
GraphEditorWindowを以下のようにします。
//GraphEditorWindow.cs
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
public class GraphEditorWindow : EditorWindow
{
[MenuItem("Window/Open GraphView")]
public static void Open()
{
GraphEditorWindow graphEditorWindow = CreateInstance<GraphEditorWindow>();
graphEditorWindow.Show();
graphEditorWindow.titleContent = new GUIContent("GraphEditor");
}
void OnEnable()
{
var graphView = new MyGraphView()
{
style = { flexGrow = 1 }
};
rootVisualElement.Add(graphView);
rootVisualElement.Add(new Button(graphView.Execute) { text = "Execute" });
}
}
これで完成ですね!!
ここまではQiitaにあるのと同じですよね。
ここからFieldとかについて細かく説明していきます。
Fieldについて
TextだけではなくIntやFloatEnumや自作コンポーネントもD&Dできるようにしたいですよね?
まず、公式で元々あるのは、
UnityEmgine
-UIElements
-TextField
UnityEditor
-UIElememts
-BoundsField
-BoundsIntField
-ColorField
-CurveField
-DoubleField
-EnumField
-FloatField
-GradientField
-LayerField
-LayeraMaskField
-ObjectField
-TagField
-Vector2Field
-Vector2IntField
-Vector3Field
-Vector3IntField
-Vector4Field
使えそうなのはこの辺りですね。BoolFieldがないのは少し引っかかりますが。
細かく解説していきます。
TextField
string型の文字を入力できるフィールドを作成できます。
BoundsField
Bounds型の数字を入力できるフィールドを作成できます。
BoxCollderの大きさとか設定するやつと似ていますね。
BoundsIntField
BoundsInt型の数字を入力できるフィールドを作成できます。
BoundsFieldとの違いはCenterとExtentsに少数を入力できるかどうかです。
ColorField
Color32型の色を入力できるフィールドを作成できます。
CurveField
Curve型の曲線を入力できるフィールドを作成できます。
DoubleField
Double型の少数を入力できるフィールドです。
後述するFloatFieldより細かい値を設定できます。
EnumField
Enum型のドロップダウンメニューを入力できるフィールドです。
FloatField
Float型の少数を入力できるフィールドです。
GradientField
Gradient型のグラデーションを入力できるフィールドです。
LayerField
Layer型のレイヤーを入力できるフィールドです。
OnCollisionEnterとかで判定に使えそう。
LayeraMaskField
LayerMask型のレイヤーを入力できるフィールドです。
任意のレイヤーとだけ衝突させたい時などに使えます。
ObjectField
Object型のObject?を入力できるフィールドです。
こいつをうまく使えばカスタムコンポーネントもフィールドにD&Dできるようになります。これは後でじっくり解説します。
この写真はRigidbodyをフィールドに設定してる状態です。
TagField
Tag型のタグを入力できるフィールドです。
Vector2Field
Vector2型の数字を入力できるフィールドです。
Vector2IntField
Vector2Fieldと違ってフィールドにIntしか入れることができません。
Vector3Field
Vector3型の数字を入力できるフィールドです。
Vector3IntField
Vector3Fieldと違ってフィールドにIntしか入れることができません。
Vector4Field
Vector4型の数字を入力できるフィールドです。
基本的にはこんな感じです。
実装方法はObjectFieldとEnumField以外は同じです。
逆にObjectFieldとEnumFieldが厄介なんですよ。
さっき言った二つ以外は、
↓こんな感じです。StringNodeとほぼ同じですね
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;
public class TestNode : MyNode
{
private 型Field testField;
public 型 変数名 { get { return testField.value; } }
public ObjectNode() : base()
{
title = "NodeName";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(型));
outputContainer.Add(outputPort);
testField = new 型Field();
mainContainer.Add(testField);
}
}
これでOKです。
そしてvar outputPortと書いてある行のPort.Capacityというところがあると思います。Multiにすると何個でも繋げることができますが、Singleにするとひとつしか繋げることができません。
比較GIFです。
左がMultiで右がSingleです。
でもやっぱりRigidbodyとか自作コンポーネントもフィールドにD&Dしたいですよね??
そういう時はObjectFieldを使いましょう。
これがまたクセがあるんですよ。今回はRigidbodyを例に解説していきます。
さっきのように記述するとこうなると思います。しかし、みてみると
//ObjectNode.cs
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;
public class ObjectNode : MyNode
{
private ObjectField objectField;
public object Text { get { return objectField.value; } }
public ObjectNode() : base()
{
title = "Object";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(object));
outputContainer.Add(outputPort);
objectField = new ObjectField();
mainContainer.Add(objectField);
}
}
あれFieldは生成されてるけどno typeって出てる!?D&Dもできません。
ハマりましたね.....
これで1日くらい悩んでました。しかし、公式のリファレンスにはobjectTypeという物で型を指定できると書いてあったのでこうしてみました。
//ObjectNode.cs
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;
public class ObjectNode : MyNode
{
private ObjectField objectField;
public object Text { get { return objectField.value; } }
public ObjectNode() : base()
{
title = "Object";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(object));
outputContainer.Add(outputPort);
objectField = new ObjectField();
objectField.objectType = typeof(Rigidbody);
mainContainer.Add(objectField);
}
}
無事解決しましたね!
ここまできたらEnumとかも使いたいですよね。
まず普通書いてみます。
//EnumField.cs
using System;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;
public class EnumNode : MyNode
{
private EnumField enumField;
public int Text { get { return Convert.ToInt32(enumField.value); } }
public Jyanken jyanken;
public enum Jyanken
{
Gu,
Tyoki,
Pa,
}
public EnumNode() : base()
{
title = "Enum";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(int));
outputContainer.Add(outputPort);
enumField = new EnumField();
mainContainer.Add(enumField);
}
}
選択するEnum、今回はJyankenというものを作ってあげて、画面を見てみると、
なんか...あるんだよね。でもGuとかはないし、選べないですね。
うーん困りましたね....
さっきのObjectFieldのようにobjectTypeならぬSetEnumというものもありませんし。
丸一日ハマりました。
実はEnumFieldにInitというものがあり第一引数で選択したいenumを設定すると選べるようになるんですね〜Initは初期化って意味らしいです。
もっとわかりやすい名前にして欲しかったです....
コードです。
//EnumField.cs
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;
public class EnumNode : MyNode
{
private EnumField enumField;
public int Text { get { return (int)enumField.userData; } }
public Jyanken jyanken;
public enum Jyanken
{
Gu,
Tyoki,
Pa,
}
public EnumNode() : base()
{
title = "Enum";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(int));
outputContainer.Add(outputPort);
enumField = new EnumField();
enumField.Init(jyanken);
mainContainer.Add(enumField);
}
}
無事ドロップダウンを表示することができました!
Audio再生とかをしたい
Logを出す以外にもEnumFiledとかObjectFieldを使って何かやってみたいですよね?
そこで今回はAudioの再生をやってみます。
まずAudioClipをD&DするためのNodeを作ります。
//AudioClipNode.cs
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;
public class AudioClipNode : MyNode
{
private ObjectField audioClipField;
public AudioClip value { get { return audioClipField.value as AudioClip; } }
public AudioClipNode() : base()
{
title = "AudioClip";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(AudioClip));
outputContainer.Add(outputPort);
audioClipField = new ObjectField();
audioClipField.objectType = typeof(AudioClip);
mainContainer.Add(audioClipField);
}
}
次にAudioSourceをD&DするためのNodeを作ります
//AudioSourceNode.cs
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;
public class AudioSourceNode : MyNode
{
private ObjectField audioSourceField;
public AudioSource value { get { return audioSourceField.value as AudioSource; } }
public AudioSourceNode() : base()
{
title = "AudioSource";
var outputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(AudioSource));
outputContainer.Add(outputPort);
audioSourceField = new ObjectField();
audioSourceField.objectType = typeof(AudioSource);
mainContainer.Add(audioSourceField);
}
}
最後にAudioを再生したりするAudioEventNodeを作成しましょう。
このNodeはStringNodeやLogNodeと違って、自分にもField(Enum)がありますが、InputPortもあるということです。
//AudioEventNode.cs
using System;
using System.Linq;
using UnityEngine;
using UnityEditor.UIElements;
using UnityEditor.Experimental.GraphView;
public class AudioEventNode : ProcessNode
{
private Port inputClip;
private Port inputSource;
private EnumField enumField;
public int value { get { return Convert.ToInt32(enumField.value); } }
enum EventType
{
Play,
Stop,
Pause,
UnPause,
Loop,
};
public AudioEventNode() : base()
{
title = "AudioEvent";
inputClip = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(AudioClip));
inputContainer.Add(inputClip);
inputSource = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Single, typeof(AudioSource));
inputContainer.Add(inputSource);
enumField = new EnumField();
EventType eventType = EventType.Play;
enumField.Init(eventType);
mainContainer.Add(enumField);
}
public override void Execute()
{
AudioClip audioClip = null;
AudioSource audioSource = null;
if (inputClip.connections.FirstOrDefault() != null)
{
Edge Clip_edge = inputClip.connections.FirstOrDefault();
var Clip_node = Clip_edge.output.node as AudioClipNode;
audioClip = Clip_node.value;
}
if (inputSource.connections.FirstOrDefault() != null)
{
Edge Source_edge = inputSource.connections.FirstOrDefault();
var Source_node = Source_edge.output.node as AudioSourceNode;
audioSource = Source_node.value;
}
if (audioClip != null && audioSource != null)
{
audioSource.clip = audioClip;
Play(audioSource, audioClip, value);
}
}
private static void Play(AudioSource source, AudioClip clip, int state)
{
source.clip = clip;
source.playOnAwake = false;
source.mute = false;
source.loop = false;
if (state == 0)
{
source.Play();
}
if (state == 1)
{
source.Stop();
}
if (state == 2)
{
source.Pause();
}
if (state == 3)
{
source.UnPause();
}
if (state == 4)
{
source.loop = true;
source.Play();
}
}
}
これでExecuteを押したら音が再生されるようになったでしょうか?
今回はここまでです。もう一回言いますが、これはExperimentalなので、将来的に削除、変更される可能性があります。
まとめ
これで、ノードベースでスクリプトを作成できるものを作ろうと思った人はいませんか?僕ですね。しかし、もんりぃ先生もんりぃ先生 (@monry) | Twitter)という方がUniflowというものを開発しています。自作するのもロマンがあっていいと思いますがさすがって感じですよね。Uniflowについては青木ととさんが記事にしてくれています。使いたいと思った方はぜひこちらの記事を参考にしてみてください。
[Unity] ノードベースでUnityAPIを制御するライブラリUniFlowを触ってみる - Qiita
お疲れ様でした!!
中の人が実際に作ってる途中のやつです。
参考程度に〜