Unity开发笔记-Editor扩展用GraphView实现逻辑表达式(1)UI基础逻辑实现
写在前面
Unity的官方文档对graphview的api只有粗略描述,想要通过API来理解GraphView如何搭建,是非常低效和让人抓狂的。
也许是因为是实验API的关系,但个人感觉Unity的其他API也需要大量借助其他非官方资料和开源项目才能理解。
我直接参考了如下博客:
https://qiita.com/ma_sh/items/7627a6151e849f5a0ede
日语可以通过谷歌翻译大概可以明白,非常值得一读的教程。
开源项目:
https://github.com/rygo6/GTLogicGraph
下面进入正题:
0 实现GraphView子类
构造函数中,将EditorWindow作为参数传入以便后面使用
另外我们需要添加一些功能函数
SetupZoom实现滚轮缩放
AddManipulator函数可以添加GraphView的操作功能。
1.ContentDragger 按住Alt键可以拖动窗口范围,参考Animator的window功能
2.RectangleSelector 多框选功能,一次选中多个Node,玩过rts的都知道
3.SelectionDragger 选中Node移动功能,否则不能通过鼠标拖动改变node的位置
`
public class YaoJZGraphView : GraphView
{
public YaoJZGraphView(EditorWindow editorWindow)
{
_editorWindow = editorWindow;
//按照父级的宽高全屏填充
this.StretchToParentSize();
//滚轮缩放
SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale);
//graphview窗口内容的拖动
this.AddManipulator(new ContentDragger());
//选中Node移动功能
this.AddManipulator(new SelectionDragger());
//多个node框选功能
this.AddManipulator(new RectangleSelector());
}
}
`
1 实现EditorWindow子类,将GraphView添加到rootVisualElement中
我们需要一个EditorWindow子类来显示window,这一步和其他EditorWindow的的扩展没有任何差别。
然后将上面的GraphView子类YaoJZGraphView通过EditorWindow的rootVisualElement.Add()方法添加到EidtorWindow中。
编写一个静态方法,打上MenuItem标签,就可以在编辑器中显示出来了。
`
public class YaoJZGraphEditorWindow:EditorWindow
{
private YaoJZGraphView _graphView;
public void Init()
{
_graphView = new YaoJZGraphView(this);
rootVisualElement.Add(_graphView);
}
[MenuItem("YJZ/GraphWindow")]
public static void Open()
{
YaoJZGraphEditorWindow window = GetWindow<YaoJZGraphEditorWindow>(ObjectNames.NicifyVariableName(nameof(YaoJZGraphEditorWindow)));
window.Init();
}
}`
2 实现第一个Node子类
现在我们为GraphView实现第一个子类,既然是表达式编辑器,那我们就实现一个FloatNodeView,用来表示一个浮点型数值节点。
`public class YaoJZFloatNodeView:Node
{
public YaoJZFloatNodeView()
{
title = "Float";
}
}`
3 AddElement添加Node到GraphView中
将我们实现的YaoJZFloatNodeView子类通过AddElement方法添加到GraphView中,为了简单起见,直接在构造函数里添加。
`public class YaoJZGraphView : GraphView
{
public YaoJZGraphView(EditorWindow editorWindow)
{
_editorWindow = editorWindow;
AddElement(new YaoJZFloatNodeView()); //将node添加到graphview
}
}`
4 添加右键菜单,实现ISearchWindowProvider接口
当然我们的Node不可能直接写死在GraphView的构造函数里,我们希望通过右键菜单的形式添加一个Node节点,幸好我们可以实现ISearchWindowProvider接口做到这点
4.1 实现Node显示列表接口CreateSearchTree
右键菜单中的每个选项都是一个SearchTreeEntry,在这个接口中添加我们需要显示的所有Node类型
另外也可以添加SearchTreeGroupEntry,实现多级菜单功能
`public class YaoJZSearchMenuWindowProvider:ScriptableObject, ISearchWindowProvider
{
public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
{
var entries = new List<SearchTreeEntry>();
entries.Add(new SearchTreeGroupEntry(new GUIContent("Create Node"))); //添加了一个一级菜单
entries.Add(new SearchTreeGroupEntry(new GUIContent("Example")) { level = 1 }); //添加了一个二级菜单
entries.Add(new SearchTreeEntry(new GUIContent("float")) { level = 2, userData = typeof(YaoJZFloatNodeView) });
return entries;
}
}`
4.2 实现选中回调OnSelectEntry
当我们在右键菜单中点击了SearchTreeEntry就会触发这个回调,所以我们利用这个函数的回调,实现往GraphView中添加Node的功能。
这样的话YaoJZSearchMenuWindowProvider需要引用YaoJZGraphView,这样就产生了耦合。
为了解耦,我们可以实现一个delegate,添加Node的逻辑在YaoJZGraphView中处理了。
`public class YaoJZSearchMenuWindowProvider:ScriptableObject, ISearchWindowProvider
{
public delegate bool SerchMenuWindowOnSelectEntryDelegate(SearchTreeEntry searchTreeEntry, //声明一个delegate类
SearchWindowContext context);
public SerchMenuWindowOnSelectEntryDelegate OnSelectEntryHandler; //delegate回调方法
public bool OnSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
{
if (OnSelectEntryHandler == null)
{
return false;
}
return OnSelectEntryHandler(searchTreeEntry, context);
}
}`
4.3 在YaoJZGraphView 中实例化YaoJZSearchMenuWindowProvider
实现nodeCreationRequest回调方法,打开SearchWindow
是的没错,我们的右键菜单是一个SearchWindow实例,而我们实现的YaoJZSearchMenuWindowProvider是他的数据提供者
我们需要实例化YaoJZSearchMenuWindowProvider然后作为SearchWindowContext的参数传给SearchWindow
然后绑定之前实现的delegate方法OnSelectEntryHandler,方法的参数是searchTreeEntry,
我们通过userData属性获得之前传入的Node的Type类型,然后使用反射创建Node实例,
并用AddElement添加到GraphView中
这样右键功能就实现了
YaoJZGraphView.cs类
`public class YaoJZGraphView : GraphView
{
public YaoJZGraphView(EditorWindow editorWindow)
{
_editorWindow = editorWindow;
var menuWindowProvider = ScriptableObject.CreateInstance<YaoJZSearchMenuWindowProvider>();
menuWindowProvider.OnSelectEntryHandler = OnMenuSelectEntry;
nodeCreationRequest += context =>
{
SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), menuWindowProvider);
};
}
private bool OnMenuSelectEntry(SearchTreeEntry searchTreeEntry, SearchWindowContext context)
{
var type = searchTreeEntry.userData as Type;
Node node = Activator.CreateInstance(type) as Node;
this.AddElement(node);
return true;
}
`
5 为Node添加Port
没有Port的Node是孤单的,Node通过Port和其他Node相连,Port有2个重要的属性Direction和Capacity
Direction:定义了Port是输入还是输出端口
portName:在UI上显示Port的名称,注意:还有title和name属性,设置值后都不会在UI上显示出来
capacity:端口的连线是单个(Port.Capacity.Single)还是多个(Port.Capacity.Multi),连线对应的是Edge类。
通过这个属性我们可以让Port实现一对一,一对多,多对多的连接组合
这个例子里的Port都是Single类型的
下面我们创建一个输入port和一个输出port:
`
//创建一个inputPort
var inputPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Input, Port.Capacity.Multi, typeof(Port));
//设置port显示的名称
inputPort.portName = "in";
//添加到inputContainer容器中
inputContainer.Add(inputPort);
var outPort = Port.Create<Edge>(Orientation.Horizontal, Direction.Output, Port.Capacity.Multi, typeof(Port));
outPort.portName = "out";
outputContainer.Add(outPort);
RefreshExpandedState();
`
Node有几个重要的Container,inputContainer,outputContainer是port的容器
你当然也可以将outputport放入到inputContainer,对Node来说,port是input还是output都是UI的Element
而Node的内容容器是mainContainer,后面我们将Node的扩展功能放入mainContainer容器中。
6 Port的连接
现在你会发现Port之间无法用线连在一起,我们需要覆写GraphView中的GetCompatiblePorts方法:
`public override List<Port> GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter)
{
return ports.ToList();
}`
现在可以连接2个Node了,但是现在Node自己的input可以和output也能相连,这不是我们想要的,我们需要改写一下GetCompatiblePorts的逻辑。
通过GetCompatiblePorts接口我们定义具体的port连接规则,比如
1.2个port如果是属于同一个node,则无法连接。
2.Direction相同的Port无法相互连接,input和input,output和output不能连接
3.portType不匹配的无法连接
4.其他和业务相关的逻辑检测
下面我们改写一下:
`
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;
}`
好的,到此为止Node显示以及Node的Port之间的连接功能完成了,下一个教程我们扩展Node,实现表达式的各个节点功能