GraphViewがっつり入門

はじめに

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

→下の写真の青で囲ってある物です。

f:id:ruchi12377:20200417033849p:plain

Port

→In と Outとか。後で説明するEdgeの始点と終点になる物です。

Edge

→Node同士を繋ぐカラフルな線のことです。カラフルな理由は後ほど...

実際に作ってみる

バージョン : Unity2019.2.11f1

使用OS : MacOS Catalina 10.15.3

 

この記事はここを参考(ほぼ引用)しながら少し注釈を加えて説明していきます。

qiita.com

まずEditorWindowを作成します。

GraphEditorWindowという名前のスクリプトを作成して以下の内容を入力してください。

これから作成するスクリプトはEditorというフォルダの中に入れてください。

↓写真のように

f:id:ruchi12377:20200417043152p:plain


//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");
}
}

f:id:ruchi12377:20200417042054p:plain

ここから開くことができます。

f:id:ruchi12377:20200417042140p:plain


こんな感じの画面が開たら成功です!

 

次にお待ちかね、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を使いましょう。

 

f:id:ruchi12377:20200417050552p:plain

 

やっぱり生成されています。なぜでしょうか。

f:id:ruchi12377:20200417050742p:plain
しかし、理由は簡単です。高さが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);
      }
}

f:id:ruchi12377:20200417051535p:plain

無事、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);
}
}

これで確認してみると

f:id:ruchi12377:20200417052911p:plain

お、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が見にくいですね...見えないことはないですけど。
そういうことで背景の色を変えましょう。

f:id:ruchi12377:20200417060033p:plain

まず画像のようにResourcesフォルダを作成

f:id:ruchi12377:20200417061129p:plain

その中にUIElements Editor Window から

f:id:ruchi12377:20200417064941p:plain

C#、UXMLのチェックを外してUSSだけにチェックを入れTextFieldにBackGroundと入れ、Confirmを押します

f:id:ruchi12377:20200417065009p:plain

USSと書かれたファイルが作成できたらOKです

f:id:ruchi12377:20200417065315p:plain

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に適応してないんですから。

f:id:ruchi12377:20200417070608p:plain

そこで、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();
}
}

そうするとこうなるのですが

f:id:ruchi12377:20200417065655p:plain

一回わかりやすいように色を変えてみます

・背景色の色を

・細かい線の色を

・大まかな線の色を

・細かいマスの長さを15にしてみました

f:id:ruchi12377:20200417070250p:plain

これでわかりやすくなったのではないのでしょうか?

細かいマスの大きさが少し大きくなってるのがわかると思います。

 

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 =>
     {
          SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindowProvider);
     };
}

public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
     {
           return ports.ToList();
     }
}

そしたら画面に戻ってCreateNodeして見てください下の写真のようになれば成功です。

f:id:ruchi12377:20200417081740p:plain

でもこれは少しおかしくないですか?

outがstringに、stringがoutに繋がります。これを修正しましょう。f:id:ruchi12377:20200417081936p:plain

 

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型の文字を入力できるフィールドを作成できます。

f:id:ruchi12377:20200417222945p:plain

 

BoundsField

Bounds型の数字を入力できるフィールドを作成できます。

BoxCollderの大きさとか設定するやつと似ていますね。

f:id:ruchi12377:20200417224310p:plain

 

BoundsIntField

BoundsInt型の数字を入力できるフィールドを作成できます。

BoundsFieldとの違いはCenterとExtentsに少数を入力できるかどうかです。

f:id:ruchi12377:20200417224351p:plain

 

ColorField

Color32型の色を入力できるフィールドを作成できます。

f:id:ruchi12377:20200417230842p:plain

 

CurveField

Curve型の曲線を入力できるフィールドを作成できます。

f:id:ruchi12377:20200418000115p:plain

 

DoubleField

Double型の少数を入力できるフィールドです。

後述するFloatFieldより細かい値を設定できます。

f:id:ruchi12377:20200418000213p:plain

 

EnumField

Enum型のドロップダウンメニューを入力できるフィールドです。

f:id:ruchi12377:20200418000241p:plain

 

FloatField

Float型の少数を入力できるフィールドです。

f:id:ruchi12377:20200418000405p:plain

 

GradientField

Gradient型のグラデーションを入力できるフィールドです。

f:id:ruchi12377:20200418000449p:plain

 

LayerField

Layer型のレイヤーを入力できるフィールドです。

OnCollisionEnterとかで判定に使えそう。

f:id:ruchi12377:20200418000516p:plain

 

LayeraMaskField

LayerMask型のレイヤーを入力できるフィールドです。

任意のレイヤーとだけ衝突させたい時などに使えます。

f:id:ruchi12377:20200418000651p:plain

 

ObjectField

Object型のObject?を入力できるフィールドです。

こいつをうまく使えばカスタムコンポーネントもフィールドにD&Dできるようになります。これは後でじっくり解説します。

この写真はRigidbodyをフィールドに設定してる状態です。

f:id:ruchi12377:20200418000819p:plain

 

TagField

Tag型のタグを入力できるフィールドです。

f:id:ruchi12377:20200418000923p:plain

Vector2Field

Vector2型の数字を入力できるフィールドです。

f:id:ruchi12377:20200418001052p:plain

 

Vector2IntField

Vector2Fieldと違ってフィールドにIntしか入れることができません。

f:id:ruchi12377:20200418001059p:plain

 

Vector3Field

Vector3型の数字を入力できるフィールドです。

f:id:ruchi12377:20200418001216p:plain

 

Vector3IntField

Vector3Fieldと違ってフィールドにIntしか入れることができません。

f:id:ruchi12377:20200418001225p:plain

 

Vector4Field

Vector4型の数字を入力できるフィールドです。

f:id:ruchi12377:20200418001318p:plain

 

基本的にはこんな感じです。

実装方法は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です。

f:id:ruchi12377:20200418002555g:plain  f:id:ruchi12377:20200418002605g:plain

 

でもやっぱり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);
}
}

f:id:ruchi12377:20200418003050p:plain

あれ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);
}
}

f:id:ruchi12377:20200418003334p:plain

無事解決しましたね!

 

ここまできたら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というものを作ってあげて、画面を見てみると、

f:id:ruchi12377:20200418004030p:plain

なんか...あるんだよね。でも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);
}
}

f:id:ruchi12377:20200418004528p:plain

無事ドロップダウンを表示することができました!


 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を押したら音が再生されるようになったでしょうか?

f:id:ruchi12377:20200418184224p:plain

 

今回はここまでです。もう一回言いますが、これはExperimentalなので、将来的に削除、変更される可能性があります。

 

まとめ

これで、ノードベースでスクリプトを作成できるものを作ろうと思った人はいませんか?僕ですね。しかし、もんりぃ先生もんりぃ先生 (@monry) | Twitter)という方がUniflowというものを開発しています。自作するのもロマンがあっていいと思いますがさすがって感じですよね。Uniflowについては青木ととさんが記事にしてくれています。使いたいと思った方はぜひこちらの記事を参考にしてみてください。

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

 

お疲れ様でした!!

 

Github

中の人が実際に作ってる途中のやつです。

参考程度に〜

github.com

引用

github.com

github.com

qiita.com

www.youtube.com

docs.unity3d.com