1.创建窗口(EditorWindow)
首先绘制元素,然后处理输入,如果GUI由于输入事件而改变,则强制窗口重新绘制。
using
UnityEngine;
using
UnityEditor;
using
System.
Collections.
Generic;
public
class
NodeBasedEditor :
EditorWindow
{
[
MenuItem(
"Window/Node Based Editor")]
private
static
void
OpenWindow()
{
NodeBasedEditor
window =
GetWindow<
NodeBasedEditor>();
window.
titleContent =
new
GUIContent(
"Node Based Editor");
}
private
void
OnGUI()
{
DrawNodes();
ProcessEvents(
Event.
current);
if (
GUI.
changed)
Repaint();
}
private
void
DrawNodes()
{
}
private
void
ProcessEvents(
Event
e)
{
}
}
2.创建节点
因为这是一个节点编辑器,它应该包含一个节点列表,这需要我们定义一个List <Node>。但首先我们应该定义Node类。一个节点将负责绘制本身和处理它自己的事件。不像ProcessEvents(事件e)在NodeBasedEditor,ProcessEvents(事件E)的节点将返回一个布尔值,这样我们就可以检查我们是否应该重新绘制GUI与否。
using
System;
using
UnityEditor;
using
UnityEngine;
public
class
Node
{
public
Rect
rect;
public
string
title;
public
GUIStyle
style;
public
Node(
Vector2
position,
float
width,
float
height,
GUIStyle
nodeStyle)
{
rect =
new
Rect(
position.
x,
position.
y,
width,
height);
style =
nodeStyle;
}
public
void
Drag(
Vector2
delta)
{
rect.
position +=
delta;
}
public
void
Draw()
{
GUI.
Box(
rect,
title,
style);
}
public
bool
ProcessEvents(
Event
e)
{
return
false;
}
}
using
UnityEngine;
using
UnityEditor;
using
System.
Collections.
Generic;
public
class
NodeBasedEditor :
EditorWindow
{
private
List<
Node>
nodes;
[
MenuItem(
"Window/Node Based Editor")]
private
static
void
OpenWindow()
{
NodeBasedEditor
window =
GetWindow<
NodeBasedEditor>();
window.
titleContent =
new
GUIContent(
"Node Based Editor");
}
private
void
OnGUI()
{
DrawNodes();
ProcessEvents(
Event.
current);
if (
GUI.
changed)
Repaint();
}
private
void
DrawNodes()
{
if (
nodes !=
null)
{
for (
int
i =
0;
i <
nodes.
Count;
i++)
{
nodes[
i].
Draw();
}
}
}
private
void
ProcessEvents(
Event
e)
{
}
}
创建节点
节点现在在编辑器中绘制,但如果我们不创建它们,我们就看不到它们。当用户在编辑器中右键单击时,我们应该显示一个带有“添加节点”项的上下文菜单。当用户单击“添加节点”时,我们将创建一个节点并将其添加到节点列表中,以便绘制它。节点需要的位置,宽度,高度和造型; 位置将是鼠标的当前位置,宽度将为200,高度将为50。
using
UnityEngine;
using
UnityEditor;
using
System.
Collections.
Generic;
public
class
NodeBasedEditor :
EditorWindow
{
private
List<
Node>
nodes;
private
GUIStyle
nodeStyle;
[
MenuItem(
"Window/Node Based Editor")]
private
static
void
OpenWindow()
{
NodeBasedEditor
window =
GetWindow<
NodeBasedEditor>();
window.
titleContent =
new
GUIContent(
"Node Based Editor");
}
private
void
OnEnable()
{
nodeStyle =
new
GUIStyle();
nodeStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/node1.png")
as
Texture2D;
nodeStyle.
border =
new
RectOffset(
12,
12,
12,
12);
}
private
void
OnGUI()
{
DrawNodes();
ProcessEvents(
Event.
current);
if (
GUI.
changed)
Repaint();
}
private
void
DrawNodes()
{
if (
nodes !=
null)
{
for (
int
i =
0;
i <
nodes.
Count;
i++)
{
nodes[
i].
Draw();
}
}
}
private
void
ProcessEvents(
Event
e)
{
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
1)
{
ProcessContextMenu(
e.
mousePosition);
}
break;
}
}
private
void
ProcessContextMenu(
Vector2
mousePosition)
{
GenericMenu
genericMenu =
new
GenericMenu();
genericMenu.
AddItem(
new
GUIContent(
"Add node"),
false, () =>
OnClickAddNode(
mousePosition));
genericMenu.
ShowAsContext();
}
private
void
OnClickAddNode(
Vector2
mousePosition)
{
if (
nodes ==
null)
{
nodes =
new
List<
Node>();
}
nodes.
Add(
new
Node(
mousePosition,
200,
50,
nodeStyle));
}
}
使节点可拖动
好的,现在我们可以添加节点,但我们无法拖动它们。正如我前面提到的,节点将处理自己的事件,因此我们将在Node类中处理drag事件。这里要注意的一件重要事情是我们应该使用Use()方法“使用”拖动事件。稍后,我们将添加画布拖动,我们不希望同时拖动节点和整个画布(“使用”事件阻止其被其他进程使用,即它停止事件冒泡)。另请注意,ProcessNodeEvents(事件e)中的for循环向后遍历节点列表,因为最后一个节点是在顶部绘制的,因此它应该首先处理事件。
using
System;
using
UnityEditor;
using
UnityEngine;
public
class
Node
{
public
Rect
rect;
public
string
title;
public
bool
isDragged;
public
GUIStyle
style;
public
Node(
Vector2
position,
float
width,
float
height,
GUIStyle
nodeStyle)
{
rect =
new
Rect(
position.
x,
position.
y,
width,
height);
style =
nodeStyle;
}
public
void
Drag(
Vector2
delta)
{
rect.
position +=
delta;
}
public
void
Draw()
{
GUI.
Box(
rect,
title,
style);
}
public
bool
ProcessEvents(
Event
e)
{
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
0)
{
if (
rect.
Contains(
e.
mousePosition))
{
isDragged =
true;
GUI.
changed =
true;
}
else
{
GUI.
changed =
true;
}
}
break;
case
EventType.
MouseUp:
isDragged =
false;
break;
case
EventType.
MouseDrag:
if (
e.
button ==
0 &&
isDragged)
{
Drag(
e.
delta);
e.
Use();
return
true;
}
break;
}
return
false;
}
}
public class NodeBasedEditor : EditorWindow{
。。。。。。。。。。
}
private
void
OnGUI()
{
DrawNodes();
ProcessNodeEvents(
Event.
current);
ProcessEvents(
Event.
current);
if (
GUI.
changed)
Repaint();
}
private
void
DrawNodes()
{
if (
nodes !=
null)
{
for (
int
i =
0;
i <
nodes.
Count;
i++)
{
nodes[
i].
Draw();
}
}
}
private
void
ProcessEvents(
Event
e)
{
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
1)
{
ProcessContextMenu(
e.
mousePosition);
}
break;
}
}
private
void
ProcessNodeEvents(
Event
e)
{
if (
nodes !=
null)
{
for (
int
i =
nodes.
Count -
1;
i >=
0;
i--)
{
bool
guiChanged =
nodes[
i].
ProcessEvents(
e);
if (
guiChanged)
{
GUI.
changed =
true;
}
}
}
}
private
void
ProcessContextMenu(
Vector2
mousePosition)
{
GenericMenu
genericMenu =
new
GenericMenu();
genericMenu.
AddItem(
new
GUIContent(
"Add node"),
false, () =>
OnClickAddNode(
mousePosition));
genericMenu.
ShowAsContext();
}
在节点之间创建连接
我们的节点编辑器现在有节点,但我们也应该能够连接它们。为此,我们需要节点上的两个连接点(输入和输出)以及它们之间的连接。连接点有一个矩形(这样我们可以绘制它),有一个类型(in或out),有一个样式,它引用它的父节点。因此,我们的ConnectionPoint类将是一个非常简单的类; 在特定位置绘制按钮,并在单击此按钮时执行操作。
另一方面,连接有两个连接点和一个删除它的操作。Connection类比ConnectionPoint简单得多,但它引入了一个新概念:Handles。此类实际上用于在“ 场景”视图中绘制3D GUI控件,但它是唯一具有贝塞尔绘图方法的类:Handles.DrawBezier(Vector3,Vector3,Vector3,Vector3,Color,Texture2D,float)。它需要7个参数,前4个参数是位置控制(起始位置,结束位置,开始切线和结束切线),而其余参数确定贝塞尔曲线的外观。
using
System;
using
UnityEngine;
public
enum
ConnectionPointType {
In,
Out }
public
class
ConnectionPoint
{
public
Rect
rect;
public
ConnectionPointType
type;
public
Node
node;
public
GUIStyle
style;
public
Action<
ConnectionPoint>
OnClickConnectionPoint;
public
ConnectionPoint(
Node
node,
ConnectionPointType
type,
GUIStyle
style,
Action<
ConnectionPoint>
OnClickConnectionPoint)
{
this.
node =
node;
this.
type =
type;
this.
style =
style;
this.
OnClickConnectionPoint =
OnClickConnectionPoint;
rect =
new
Rect(
0,
0,
10f,
20f);
}
public
void
Draw()
{
rect.
y =
node.
rect.
y + (
node.
rect.
height *
0.5f) -
rect.
height *
0.5f;
switch (
type)
{
case
ConnectionPointType.
In:
rect.
x =
node.
rect.
x -
rect.
width +
8f;
break;
case
ConnectionPointType.
Out:
rect.
x =
node.
rect.
x +
node.
rect.
width -
8f;
break;
}
if (
GUI.
Button(
rect,
"",
style))
{
if (
OnClickConnectionPoint !=
null)
{
OnClickConnectionPoint(
this);
}
}
}
}
using
System;
using
UnityEditor;
using
UnityEngine;
public
class
Connection
{
public
ConnectionPoint
inPoint;
public
ConnectionPoint
outPoint;
public
Action<
Connection>
OnClickRemoveConnection;
public
Connection(
ConnectionPoint
inPoint,
ConnectionPoint
outPoint,
Action<
Connection>
OnClickRemoveConnection)
{
this.
inPoint =
inPoint;
this.
outPoint =
outPoint;
this.
OnClickRemoveConnection =
OnClickRemoveConnection;
}
public
void
Draw()
{
Handles.
DrawBezier(
inPoint.
rect.
center,
outPoint.
rect.
center,
inPoint.
rect.
center +
Vector2.
left *
50f,
outPoint.
rect.
center -
Vector2.
left *
50f,
Color.
white,
null,
2f
);
if (
Handles.
Button((
inPoint.
rect.
center +
outPoint.
rect.
center) *
0.5f,
Quaternion.
identity,
4,
8,
Handles.
RectangleCap))
{
if (
OnClickRemoveConnection !=
null)
{
OnClickRemoveConnection(
this);
}
}
}
}
绘图连接
由于 Connection和ConnectionPoint类已准备就绪,我们所要做的就是在Node类中绘制连接点并在NodeBasedEditor中绘制连接。Node类的变化将是最小的; 我们将定义两个连接点,修改构造函数,以便我们可以为它们传递样式和操作,并在Draw()方法中绘制它们。
但是,NodeBasedEditor需要进行重大修改。首先,我们需要为连接点定义样式。你可以为它们使用单一的样式,但我希望它们看起来不同,所以我将为每个样式使用单独的样式。我们将在OnEnable()中初始化这些样式,就像我们初始化节点样式一样。
其次,我们需要跟踪点击连接点,这样,当用户选择的和出来,我们应该创建它们之间的连接。此步骤包括大多数添加内容:
- OnClickInPoint(ConnectionPoint)处理单击入点。
- OnClickOutPoint(ConnectionPoint)处理单击出点。
- OnClickRemoveConnection(连接)处理单击连接上的删除按钮。
- 创建连接()创建一个当连接在和一个出点被选择。
- ClearConnectionSelection()清除选定的点。
最后,我们需要在OnGUI()中绘制连接,就像我们绘制节点一样。
using
System;
using
UnityEditor;
using
UnityEngine;
public
class
Node
{
public
Rect
rect;
public
string
title;
public
bool
isDragged;
public
ConnectionPoint
inPoint;
public
ConnectionPoint
outPoint;
public
GUIStyle
style;
public
Node(
Vector2
position,
float
width,
float
height,
GUIStyle
nodeStyle,
GUIStyle
inPointStyle,
GUIStyle
outPointStyle,
Action<
ConnectionPoint>
OnClickInPoint,
Action<
ConnectionPoint>
OnClickOutPoint)
{
rect =
new
Rect(
position.
x,
position.
y,
width,
height);
style =
nodeStyle;
inPoint =
new
ConnectionPoint(
this,
ConnectionPointType.
In,
inPointStyle,
OnClickInPoint);
outPoint =
new
ConnectionPoint(
this,
ConnectionPointType.
Out,
outPointStyle,
OnClickOutPoint);
}
public
void
Drag(
Vector2
delta)
{
rect.
position +=
delta;
}
public
void
Draw()
{
inPoint.
Draw();
outPoint.
Draw();
GUI.
Box(
rect,
title,
style);
}
public
bool
ProcessEvents(
Event
e)
{
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
0)
{
if (
rect.
Contains(
e.
mousePosition))
{
isDragged =
true;
GUI.
changed =
true;
}
else
{
GUI.
changed =
true;
}
}
break;
case
EventType.
MouseUp:
isDragged =
false;
break;
case
EventType.
MouseDrag:
if (
e.
button ==
0 &&
isDragged)
{
Drag(
e.
delta);
e.
Use();
return
true;
}
break;
}
return
false;
}
}
using
UnityEngine;
using
UnityEditor;
using
System.
Collections.
Generic;
public
class
NodeBasedEditor :
EditorWindow
{
private
List<
Node>
nodes;
private
List<
Connection>
connections;
private
GUIStyle
nodeStyle;
private
GUIStyle
inPointStyle;
private
GUIStyle
outPointStyle;
private
ConnectionPoint
selectedInPoint;
private
ConnectionPoint
selectedOutPoint;
[
MenuItem(
"Window/Node Based Editor")]
private
static
void
OpenWindow()
{
NodeBasedEditor
window =
GetWindow<
NodeBasedEditor>();
window.
titleContent =
new
GUIContent(
"Node Based Editor");
}
private
void
OnEnable()
{
nodeStyle =
new
GUIStyle();
nodeStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/node1.png")
as
Texture2D;
nodeStyle.
border =
new
RectOffset(
12,
12,
12,
12);
inPointStyle =
new
GUIStyle();
inPointStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn left.png")
as
Texture2D;
inPointStyle.
active.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn left on.png")
as
Texture2D;
inPointStyle.
border =
new
RectOffset(
4,
4,
12,
12);
outPointStyle =
new
GUIStyle();
outPointStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn right.png")
as
Texture2D;
outPointStyle.
active.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn right on.png")
as
Texture2D;
outPointStyle.
border =
new
RectOffset(
4,
4,
12,
12);
}
private
void
OnGUI()
{
DrawNodes();
DrawConnections();
ProcessNodeEvents(
Event.
current);
ProcessEvents(
Event.
current);
if (
GUI.
changed)
Repaint();
}
private
void
DrawNodes()
{
if (
nodes !=
null)
{
for (
int
i =
0;
i <
nodes.
Count;
i++)
{
nodes[
i].
Draw();
}
}
}
private
void
DrawConnections()
{
if (
connections !=
null)
{
for (
int
i =
0;
i <
connections.
Count;
i++)
{
connections[
i].
Draw();
}
}
}
private
void
ProcessEvents(
Event
e)
{
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
0)
{
ClearConnectionSelection();
}
if (
e.
button ==
1)
{
ProcessContextMenu(
e.
mousePosition);
}
break;
}
}
private
void
ProcessNodeEvents(
Event
e)
{
if (
nodes !=
null)
{
for (
int
i =
nodes.
Count -
1;
i >=
0;
i--)
{
bool
guiChanged =
nodes[
i].
ProcessEvents(
e);
if (
guiChanged)
{
GUI.
changed =
true;
}
}
}
}
private
void
ProcessContextMenu(
Vector2
mousePosition)
{
GenericMenu
genericMenu =
new
GenericMenu();
genericMenu.
AddItem(
new
GUIContent(
"Add node"),
false, () =>
OnClickAddNode(
mousePosition));
genericMenu.
ShowAsContext();
}
private
void
OnClickAddNode(
Vector2
mousePosition)
{
if (
nodes ==
null)
{
nodes =
new
List<
Node>();
}
nodes.
Add(
new
Node(
mousePosition,
200,
50,
nodeStyle,
inPointStyle,
outPointStyle,
OnClickInPoint,
OnClickOutPoint));
}
private
void
OnClickInPoint(
ConnectionPoint
inPoint)
{
selectedInPoint =
inPoint;
if (
selectedOutPoint !=
null)
{
if (
selectedOutPoint.
node !=
selectedInPoint.
node)
{
CreateConnection();
ClearConnectionSelection();
}
else
{
ClearConnectionSelection();
}
}
}
private
void
OnClickOutPoint(
ConnectionPoint
outPoint)
{
selectedOutPoint =
outPoint;
if (
selectedInPoint !=
null)
{
if (
selectedOutPoint.
node !=
selectedInPoint.
node)
{
CreateConnection();
ClearConnectionSelection();
}
else
{
ClearConnectionSelection();
}
}
}
private
void
OnClickRemoveConnection(
Connection
connection)
{
connections.
Remove(
connection);
}
private
void
CreateConnection()
{
if (
connections ==
null)
{
connections =
new
List<
Connection>();
}
connections.
Add(
new
Connection(
selectedInPoint,
selectedOutPoint,
OnClickRemoveConnection));
}
private
void
ClearConnectionSelection()
{
selectedInPoint =
null;
selectedOutPoint =
null;
}
}
选择节点
我们应该在用户点击某个节点时提供反馈,以便他们知道他们选择了哪个节点(或者根本就选择了一个节点)。当用户想要删除节点时,这将非常有用。
using
System;
using
UnityEditor;
using
UnityEngine;
public
class
Node
{
public
Rect
rect;
public
string
title;
public
bool
isDragged;
public
bool
isSelected;
public
ConnectionPoint
inPoint;
public
ConnectionPoint
outPoint;
public
GUIStyle
style;
public
GUIStyle
defaultNodeStyle;
public
GUIStyle
selectedNodeStyle;
public
Node(
Vector2
position,
float
width,
float
height,
GUIStyle
nodeStyle,
GUIStyle
selectedStyle,
GUIStyle
inPointStyle,
GUIStyle
outPointStyle,
Action<
ConnectionPoint>
OnClickInPoint,
Action<
ConnectionPoint>
OnClickOutPoint)
{
rect =
new
Rect(
position.
x,
position.
y,
width,
height);
style =
nodeStyle;
inPoint =
new
ConnectionPoint(
this,
ConnectionPointType.
In,
inPointStyle,
OnClickInPoint);
outPoint =
new
ConnectionPoint(
this,
ConnectionPointType.
Out,
outPointStyle,
OnClickOutPoint);
defaultNodeStyle =
nodeStyle;
selectedNodeStyle =
selectedStyle;
}
public
void
Drag(
Vector2
delta)
{
rect.
position +=
delta;
}
public
void
Draw()
{
inPoint.
Draw();
outPoint.
Draw();
GUI.
Box(
rect,
title,
style);
}
public
bool
ProcessEvents(
Event
e)
{
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
0)
{
if (
rect.
Contains(
e.
mousePosition))
{
isDragged =
true;
GUI.
changed =
true;
isSelected =
true;
style =
selectedNodeStyle;
}
else
{
GUI.
changed =
true;
isSelected =
false;
style =
defaultNodeStyle;
}
}
break;
case
EventType.
MouseUp:
isDragged =
false;
break;
case
EventType.
MouseDrag:
if (
e.
button ==
0 &&
isDragged)
{
Drag(
e.
delta);
e.
Use();
return
true;
}
break;
}
return
false;
}
}
using
UnityEngine;
using
UnityEditor;
using
System.
Collections.
Generic;
public
class
NodeBasedEditor :
EditorWindow
{
private
List<
Node>
nodes;
private
List<
Connection>
connections;
private
GUIStyle
nodeStyle;
private
GUIStyle
selectedNodeStyle;
private
GUIStyle
inPointStyle;
private
GUIStyle
outPointStyle;
private
ConnectionPoint
selectedInPoint;
private
ConnectionPoint
selectedOutPoint;
[
MenuItem(
"Window/Node Based Editor")]
private
static
void
OpenWindow()
{
NodeBasedEditor
window =
GetWindow<
NodeBasedEditor>();
window.
titleContent =
new
GUIContent(
"Node Based Editor");
}
private
void
OnEnable()
{
nodeStyle =
new
GUIStyle();
nodeStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/node1.png")
as
Texture2D;
nodeStyle.
border =
new
RectOffset(
12,
12,
12,
12);
selectedNodeStyle =
new
GUIStyle();
selectedNodeStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/node1 on.png")
as
Texture2D;
selectedNodeStyle.
border =
new
RectOffset(
12,
12,
12,
12);
inPointStyle =
new
GUIStyle();
inPointStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn left.png")
as
Texture2D;
inPointStyle.
active.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn left on.png")
as
Texture2D;
inPointStyle.
border =
new
RectOffset(
4,
4,
12,
12);
outPointStyle =
new
GUIStyle();
outPointStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn right.png")
as
Texture2D;
outPointStyle.
active.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn right on.png")
as
Texture2D;
outPointStyle.
border =
new
RectOffset(
4,
4,
12,
12);
}
...
NodeBasedEditor{。。。。。}
private
void
OnClickAddNode(
Vector2
mousePosition)
{
if (
nodes ==
null)
{
nodes =
new
List<
Node>();
}
nodes.
Add(
new
Node(
mousePosition,
200,
50,
nodeStyle,
selectedNodeStyle,
inPointStyle,
outPointStyle,
OnClickInPoint,
OnClickOutPoint));
}
删除节点
某些节点编辑器更喜欢将删除节点按钮放在节点本身上,但在我们的情况下,它可能很危险:用户可能会意外删除节点。因此,我们将做下一个最好的事情:将该按钮放在上下文菜单上。用户应首先选择节点,然后右键单击该节点以访问 删除节点按钮。当用户单击删除节点时,我们将从节点列表中删除该节点。但是,节点可能与其他节点有连接,因此我们应首先删除这些连接。
using
System;
using
UnityEditor;
using
UnityEngine;
public
class
Node
{
public
Rect
rect;
public
string
title;
public
bool
isDragged;
public
bool
isSelected;
public
ConnectionPoint
inPoint;
public
ConnectionPoint
outPoint;
public
GUIStyle
style;
public
GUIStyle
defaultNodeStyle;
public
GUIStyle
selectedNodeStyle;
public
Action<
Node>
OnRemoveNode;
public
Node(
Vector2
position,
float
width,
float
height,
GUIStyle
nodeStyle,
GUIStyle
selectedStyle,
GUIStyle
inPointStyle,
GUIStyle
outPointStyle,
Action<
ConnectionPoint>
OnClickInPoint,
Action<
ConnectionPoint>
OnClickOutPoint,
Action<
Node>
OnClickRemoveNode)
{
rect =
new
Rect(
position.
x,
position.
y,
width,
height);
style =
nodeStyle;
inPoint =
new
ConnectionPoint(
this,
ConnectionPointType.
In,
inPointStyle,
OnClickInPoint);
outPoint =
new
ConnectionPoint(
this,
ConnectionPointType.
Out,
outPointStyle,
OnClickOutPoint);
defaultNodeStyle =
nodeStyle;
selectedNodeStyle =
selectedStyle;
OnRemoveNode =
OnClickRemoveNode;
}
public
void
Drag(
Vector2
delta)
{
rect.
position +=
delta;
}
public
void
Draw()
{
inPoint.
Draw();
outPoint.
Draw();
GUI.
Box(
rect,
title,
style);
}
public
bool
ProcessEvents(
Event
e)
{
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
0)
{
if (
rect.
Contains(
e.
mousePosition))
{
isDragged =
true;
GUI.
changed =
true;
isSelected =
true;
style =
selectedNodeStyle;
}
else
{
GUI.
changed =
true;
isSelected =
false;
style =
defaultNodeStyle;
}
}
if (
e.
button ==
1 &&
isSelected &&
rect.
Contains(
e.
mousePosition))
{
ProcessContextMenu();
e.
Use();
}
break;
case
EventType.
MouseUp:
isDragged =
false;
break;
case
EventType.
MouseDrag:
if (
e.
button ==
0 &&
isDragged)
{
Drag(
e.
delta);
e.
Use();
return
true;
}
break;
}
return
false;
}
private
void
ProcessContextMenu()
{
GenericMenu
genericMenu =
new
GenericMenu();
genericMenu.
AddItem(
new
GUIContent(
"Remove node"),
false,
OnClickRemoveNode);
genericMenu.
ShowAsContext();
}
private
void
OnClickRemoveNode()
{
if (
OnRemoveNode !=
null)
{
OnRemoveNode(
this);
}
}
}
NodeBasedEditor{。。。。。}
private
void
OnClickAddNode(
Vector2
mousePosition)
{
if (
nodes ==
null)
{
nodes =
new
List<
Node>();
}
nodes.
Add(
new
Node(
mousePosition,
200,
50,
nodeStyle,
selectedNodeStyle,
inPointStyle,
outPointStyle,
OnClickInPoint,
OnClickOutPoint,
OnClickRemoveNode));
}
NodeBasedEditor{。。。。。}
private
void
OnClickRemoveNode(
Node
node)
{
if (
connections !=
null)
{
List<
Connection>
connectionsToRemove =
new
List<
Connection>();
for (
int
i =
0;
i <
connections.
Count;
i++)
{
if (
connections[
i].
inPoint ==
node.
inPoint ||
connections[
i].
outPoint ==
node.
outPoint)
{
connectionsToRemove.
Add(
connections[
i]);
}
}
for (
int
i =
0;
i <
connectionsToRemove.
Count;
i++)
{
connections.
Remove(
connectionsToRemove[
i]);
}
connectionsToRemove =
null;
}
nodes.
Remove(
node);
}
最后的接触
此时节点编辑器已完成,但它缺少一些可以提升用户体验的重要功能:
- 可拖动的帆布,
- 从选定连接点到鼠标位置的贝塞尔曲线,
- 背景中的网格。
让我们的画布可拖动是最简单的,所以让我们从这开始。我们所要做的就是将鼠标拖动应用于节点列表中的每个节点。
NodeBasedEditor
{。。。。。}
public
class
NodeBasedEditor :
EditorWindow
{
private
List<
Node>
nodes;
private
List<
Connection>
connections;
private
GUIStyle
nodeStyle;
private
GUIStyle
selectedNodeStyle;
private
GUIStyle
inPointStyle;
private
GUIStyle
outPointStyle;
private
ConnectionPoint
selectedInPoint;
private
ConnectionPoint
selectedOutPoint;
private
Vector2
drag;
...
private
void
ProcessEvents(
Event
e)
{
drag =
Vector2.
zero;
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
0)
{
ClearConnectionSelection();
}
if (
e.
button ==
1)
{
ProcessContextMenu(
e.
mousePosition);
}
break;
case
EventType.
MouseDrag:
if (
e.
button ==
0)
{
OnDrag(
e.
delta);
}
break;
}
}
private
void
OnDrag(
Vector2
delta)
{
drag =
delta;
if (
nodes !=
null)
{
for (
int
i =
0;
i <
nodes.
Count;
i++)
{
nodes[
i].
Drag(
delta);
}
}
GUI.
changed =
true;
}
接下来,将贝塞尔曲线从选定的连接点绘制到鼠标位置。通过绘制这个bezier,我们将让用户知道选择了哪个连接点以及它们的连接方式。
private
void
OnGUI()
{
DrawNodes();
DrawConnections();
DrawConnectionLine(
Event.
current);
ProcessNodeEvents(
Event.
current);
ProcessEvents(
Event.
current);
if (
GUI.
changed)
Repaint();
}
private
void
DrawConnectionLine(
Event
e)
{
if (
selectedInPoint !=
null &&
selectedOutPoint ==
null)
{
Handles.
DrawBezier(
selectedInPoint.
rect.
center,
e.
mousePosition,
selectedInPoint.
rect.
center +
Vector2.
left *
50f,
e.
mousePosition -
Vector2.
left *
50f,
Color.
white,
null,
2f
);
GUI.
changed =
true;
}
if (
selectedOutPoint !=
null &&
selectedInPoint ==
null)
{
Handles.
DrawBezier(
selectedOutPoint.
rect.
center,
e.
mousePosition,
selectedOutPoint.
rect.
center -
Vector2.
left *
50f,
e.
mousePosition +
Vector2.
left *
50f,
Color.
white,
null,
2f
);
GUI.
changed =
true;
}
}
最后,绘制网格:
public
class
NodeBasedEditor :
EditorWindow
{
private
List<
Node>
nodes;
private
List<
Connection>
connections;
private
GUIStyle
nodeStyle;
private
GUIStyle
selectedNodeStyle;
private
GUIStyle
inPointStyle;
private
GUIStyle
outPointStyle;
private
ConnectionPoint
selectedInPoint;
private
ConnectionPoint
selectedOutPoint;
private
Vector2
offset;
private
Vector2
drag;
...
private
void
OnGUI()
{
DrawGrid(
20,
0.2f,
Color.
gray);
DrawGrid(
100,
0.4f,
Color.
gray);
DrawNodes();
DrawConnections();
DrawConnectionLine(
Event.
current);
ProcessNodeEvents(
Event.
current);
ProcessEvents(
Event.
current);
if (
GUI.
changed)
Repaint();
}
private
void
DrawGrid(
float
gridSpacing,
float
gridOpacity,
Color
gridColor)
{
int
widthDivs =
Mathf.
CeilToInt(
position.
width /
gridSpacing);
int
heightDivs =
Mathf.
CeilToInt(
position.
height /
gridSpacing);
Handles.
BeginGUI();
Handles.
color =
new
Color(
gridColor.
r,
gridColor.
g,
gridColor.
b,
gridOpacity);
offset +=
drag *
0.5f;
Vector3
newOffset =
new
Vector3(
offset.
x %
gridSpacing,
offset.
y %
gridSpacing,
0);
for (
int
i =
0;
i <
widthDivs;
i++)
{
Handles.
DrawLine(
new
Vector3(
gridSpacing *
i, -
gridSpacing,
0) +
newOffset,
new
Vector3(
gridSpacing *
i,
position.
height,
0f) +
newOffset);
}
for (
int
j =
0;
j <
heightDivs;
j++)
{
Handles.
DrawLine(
new
Vector3(-
gridSpacing,
gridSpacing *
j,
0) +
newOffset,
new
Vector3(
position.
width,
gridSpacing *
j,
0f) +
newOffset);
}
Handles.
color =
Color.
white;
Handles.
EndGUI();
}
完整脚本
using
UnityEngine;
using
UnityEditor;
using
System.
Collections.
Generic;
public
class
NodeBasedEditor :
EditorWindow
{
private
List<
Node>
nodes;
private
List<
Connection>
connections;
private
GUIStyle
nodeStyle;
private
GUIStyle
selectedNodeStyle;
private
GUIStyle
inPointStyle;
private
GUIStyle
outPointStyle;
private
ConnectionPoint
selectedInPoint;
private
ConnectionPoint
selectedOutPoint;
private
Vector2
offset;
private
Vector2
drag;
[
MenuItem(
"Window/Node Based Editor")]
private
static
void
OpenWindow()
{
NodeBasedEditor
window =
GetWindow<
NodeBasedEditor>();
window.
titleContent =
new
GUIContent(
"Node Based Editor");
}
private
void
OnEnable()
{
nodeStyle =
new
GUIStyle();
nodeStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/node1.png")
as
Texture2D;
nodeStyle.
border =
new
RectOffset(
12,
12,
12,
12);
selectedNodeStyle =
new
GUIStyle();
selectedNodeStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/node1 on.png")
as
Texture2D;
selectedNodeStyle.
border =
new
RectOffset(
12,
12,
12,
12);
inPointStyle =
new
GUIStyle();
inPointStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn left.png")
as
Texture2D;
inPointStyle.
active.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn left on.png")
as
Texture2D;
inPointStyle.
border =
new
RectOffset(
4,
4,
12,
12);
outPointStyle =
new
GUIStyle();
outPointStyle.
normal.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn right.png")
as
Texture2D;
outPointStyle.
active.
background =
EditorGUIUtility.
Load(
"builtin skins/darkskin/images/btn right on.png")
as
Texture2D;
outPointStyle.
border =
new
RectOffset(
4,
4,
12,
12);
}
private
void
OnGUI()
{
DrawGrid(
20,
0.2f,
Color.
gray);
DrawGrid(
100,
0.4f,
Color.
gray);
DrawNodes();
DrawConnections();
DrawConnectionLine(
Event.
current);
ProcessNodeEvents(
Event.
current);
ProcessEvents(
Event.
current);
if (
GUI.
changed)
Repaint();
}
private
void
DrawGrid(
float
gridSpacing,
float
gridOpacity,
Color
gridColor)
{
int
widthDivs =
Mathf.
CeilToInt(
position.
width /
gridSpacing);
int
heightDivs =
Mathf.
CeilToInt(
position.
height /
gridSpacing);
Handles.
BeginGUI();
Handles.
color =
new
Color(
gridColor.
r,
gridColor.
g,
gridColor.
b,
gridOpacity);
offset +=
drag *
0.5f;
Vector3
newOffset =
new
Vector3(
offset.
x %
gridSpacing,
offset.
y %
gridSpacing,
0);
for (
int
i =
0;
i <
widthDivs;
i++)
{
Handles.
DrawLine(
new
Vector3(
gridSpacing *
i, -
gridSpacing,
0) +
newOffset,
new
Vector3(
gridSpacing *
i,
position.
height,
0f) +
newOffset);
}
for (
int
j =
0;
j <
heightDivs;
j++)
{
Handles.
DrawLine(
new
Vector3(-
gridSpacing,
gridSpacing *
j,
0) +
newOffset,
new
Vector3(
position.
width,
gridSpacing *
j,
0f) +
newOffset);
}
Handles.
color =
Color.
white;
Handles.
EndGUI();
}
private
void
DrawNodes()
{
if (
nodes !=
null)
{
for (
int
i =
0;
i <
nodes.
Count;
i++)
{
nodes[
i].
Draw();
}
}
}
private
void
DrawConnections()
{
if (
connections !=
null)
{
for (
int
i =
0;
i <
connections.
Count;
i++)
{
connections[
i].
Draw();
}
}
}
private
void
ProcessEvents(
Event
e)
{
drag =
Vector2.
zero;
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
0)
{
ClearConnectionSelection();
}
if (
e.
button ==
1)
{
ProcessContextMenu(
e.
mousePosition);
}
break;
case
EventType.
MouseDrag:
if (
e.
button ==
0)
{
OnDrag(
e.
delta);
}
break;
}
}
private
void
ProcessNodeEvents(
Event
e)
{
if (
nodes !=
null)
{
for (
int
i =
nodes.
Count -
1;
i >=
0;
i--)
{
bool
guiChanged =
nodes[
i].
ProcessEvents(
e);
if (
guiChanged)
{
GUI.
changed =
true;
}
}
}
}
private
void
DrawConnectionLine(
Event
e)
{
if (
selectedInPoint !=
null &&
selectedOutPoint ==
null)
{
Handles.
DrawBezier(
selectedInPoint.
rect.
center,
e.
mousePosition,
selectedInPoint.
rect.
center +
Vector2.
left *
50f,
e.
mousePosition -
Vector2.
left *
50f,
Color.
white,
null,
2f
);
GUI.
changed =
true;
}
if (
selectedOutPoint !=
null &&
selectedInPoint ==
null)
{
Handles.
DrawBezier(
selectedOutPoint.
rect.
center,
e.
mousePosition,
selectedOutPoint.
rect.
center -
Vector2.
left *
50f,
e.
mousePosition +
Vector2.
left *
50f,
Color.
white,
null,
2f
);
GUI.
changed =
true;
}
}
private
void
ProcessContextMenu(
Vector2
mousePosition)
{
GenericMenu
genericMenu =
new
GenericMenu();
genericMenu.
AddItem(
new
GUIContent(
"Add node"),
false, () =>
OnClickAddNode(
mousePosition));
genericMenu.
ShowAsContext();
}
private
void
OnDrag(
Vector2
delta)
{
drag =
delta;
if (
nodes !=
null)
{
for (
int
i =
0;
i <
nodes.
Count;
i++)
{
nodes[
i].
Drag(
delta);
}
}
GUI.
changed =
true;
}
private
void
OnClickAddNode(
Vector2
mousePosition)
{
if (
nodes ==
null)
{
nodes =
new
List<
Node>();
}
nodes.
Add(
new
Node(
mousePosition,
200,
50,
nodeStyle,
selectedNodeStyle,
inPointStyle,
outPointStyle,
OnClickInPoint,
OnClickOutPoint,
OnClickRemoveNode));
}
private
void
OnClickInPoint(
ConnectionPoint
inPoint)
{
selectedInPoint =
inPoint;
if (
selectedOutPoint !=
null)
{
if (
selectedOutPoint.
node !=
selectedInPoint.
node)
{
CreateConnection();
ClearConnectionSelection();
}
else
{
ClearConnectionSelection();
}
}
}
private
void
OnClickOutPoint(
ConnectionPoint
outPoint)
{
selectedOutPoint =
outPoint;
if (
selectedInPoint !=
null)
{
if (
selectedOutPoint.
node !=
selectedInPoint.
node)
{
CreateConnection();
ClearConnectionSelection();
}
else
{
ClearConnectionSelection();
}
}
}
private
void
OnClickRemoveNode(
Node
node)
{
if (
connections !=
null)
{
List<
Connection>
connectionsToRemove =
new
List<
Connection>();
for (
int
i =
0;
i <
connections.
Count;
i++)
{
if (
connections[
i].
inPoint ==
node.
inPoint ||
connections[
i].
outPoint ==
node.
outPoint)
{
connectionsToRemove.
Add(
connections[
i]);
}
}
for (
int
i =
0;
i <
connectionsToRemove.
Count;
i++)
{
connections.
Remove(
connectionsToRemove[
i]);
}
connectionsToRemove =
null;
}
nodes.
Remove(
node);
}
private
void
OnClickRemoveConnection(
Connection
connection)
{
connections.
Remove(
connection);
}
private
void
CreateConnection()
{
if (
connections ==
null)
{
connections =
new
List<
Connection>();
}
connections.
Add(
new
Connection(
selectedInPoint,
selectedOutPoint,
OnClickRemoveConnection));
}
private
void
ClearConnectionSelection()
{
selectedInPoint =
null;
selectedOutPoint =
null;
}
}
using
System;
using
UnityEditor;
using
UnityEngine;
public
class
Node
{
public
Rect
rect;
public
string
title;
public
bool
isDragged;
public
bool
isSelected;
public
ConnectionPoint
inPoint;
public
ConnectionPoint
outPoint;
public
GUIStyle
style;
public
GUIStyle
defaultNodeStyle;
public
GUIStyle
selectedNodeStyle;
public
Action<
Node>
OnRemoveNode;
public
Node(
Vector2
position,
float
width,
float
height,
GUIStyle
nodeStyle,
GUIStyle
selectedStyle,
GUIStyle
inPointStyle,
GUIStyle
outPointStyle,
Action<
ConnectionPoint>
OnClickInPoint,
Action<
ConnectionPoint>
OnClickOutPoint,
Action<
Node>
OnClickRemoveNode)
{
rect =
new
Rect(
position.
x,
position.
y,
width,
height);
style =
nodeStyle;
inPoint =
new
ConnectionPoint(
this,
ConnectionPointType.
In,
inPointStyle,
OnClickInPoint);
outPoint =
new
ConnectionPoint(
this,
ConnectionPointType.
Out,
outPointStyle,
OnClickOutPoint);
defaultNodeStyle =
nodeStyle;
selectedNodeStyle =
selectedStyle;
OnRemoveNode =
OnClickRemoveNode;
}
public
void
Drag(
Vector2
delta)
{
rect.
position +=
delta;
}
public
void
Draw()
{
inPoint.
Draw();
outPoint.
Draw();
GUI.
Box(
rect,
title,
style);
}
public
bool
ProcessEvents(
Event
e)
{
switch (
e.
type)
{
case
EventType.
MouseDown:
if (
e.
button ==
0)
{
if (
rect.
Contains(
e.
mousePosition))
{
isDragged =
true;
GUI.
changed =
true;
isSelected =
true;
style =
selectedNodeStyle;
}
else
{
GUI.
changed =
true;
isSelected =
false;
style =
defaultNodeStyle;
}
}
if (
e.
button ==
1 &&
isSelected &&
rect.
Contains(
e.
mousePosition))
{
ProcessContextMenu();
e.
Use();
}
break;
case
EventType.
MouseUp:
isDragged =
false;
break;
case
EventType.
MouseDrag:
if (
e.
button ==
0 &&
isDragged)
{
Drag(
e.
delta);
e.
Use();
return
true;
}
break;
}
return
false;
}
private
void
ProcessContextMenu()
{
GenericMenu
genericMenu =
new
GenericMenu();
genericMenu.
AddItem(
new
GUIContent(
"Remove node"),
false,
OnClickRemoveNode);
genericMenu.
ShowAsContext();
}
private
void
OnClickRemoveNode()
{
if (
OnRemoveNode !=
null)
{
OnRemoveNode(
this);
}
}
}
using
System;
using
UnityEditor;
using
UnityEngine;
public
class
Connection
{
public
ConnectionPoint
inPoint;
public
ConnectionPoint
outPoint;
public
Action<
Connection>
OnClickRemoveConnection;
public
Connection(
ConnectionPoint
inPoint,
ConnectionPoint
outPoint,
Action<
Connection>
OnClickRemoveConnection)
{
this.
inPoint =
inPoint;
this.
outPoint =
outPoint;
this.
OnClickRemoveConnection =
OnClickRemoveConnection;
}
public
void
Draw()
{
Handles.
DrawBezier(
inPoint.
rect.
center,
outPoint.
rect.
center,
inPoint.
rect.
center +
Vector2.
left *
50f,
outPoint.
rect.
center -
Vector2.
left *
50f,
Color.
white,
null,
2f
);
if (
Handles.
Button((
inPoint.
rect.
center +
outPoint.
rect.
center) *
0.5f,
Quaternion.
identity,
4,
8,
Handles.
RectangleCap))
{
if (
OnClickRemoveConnection !=
null)
{
OnClickRemoveConnection(
this);
}
}
}
}
using
System;
using
UnityEngine;
public
enum
ConnectionPointType {
In,
Out }
public
class
ConnectionPoint
{
public
Rect
rect;
public
ConnectionPointType
type;
public
Node
node;
public
GUIStyle
style;
public
Action<
ConnectionPoint>
OnClickConnectionPoint;
public
ConnectionPoint(
Node
node,
ConnectionPointType
type,
GUIStyle
style,
Action<
ConnectionPoint>
OnClickConnectionPoint)
{
this.
node =
node;
this.
type =
type;
this.
style =
style;
this.
OnClickConnectionPoint =
OnClickConnectionPoint;
rect =
new
Rect(
0,
0,
10f,
20f);
}
public
void
Draw()
{
rect.
y =
node.
rect.
y + (
node.
rect.
height *
0.5f) -
rect.
height *
0.5f;
switch (
type)
{
case
ConnectionPointType.
In:
rect.
x =
node.
rect.
x -
rect.
width +
8f;
break;
case
ConnectionPointType.
Out:
rect.
x =
node.
rect.
x +
node.
rect.
width -
8f;
break;
}
if (
GUI.
Button(
rect,
"",
style))
{
if (
OnClickConnectionPoint !=
null)
{
OnClickConnectionPoint(
this);
}
}
}
}