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的位置

Copy
` 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标签,就可以在编辑器中显示出来了。

Copy
` 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,用来表示一个浮点型数值节点。

Copy
`public class YaoJZFloatNodeView:Node { public YaoJZFloatNodeView() { title = "Float"; } }`

3 AddElement添加Node到GraphView中#

将我们实现的YaoJZFloatNodeView子类通过AddElement方法添加到GraphView中,为了简单起见,直接在构造函数里添加。

Copy
`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,实现多级菜单功能

Copy
`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中处理了。

Copy
`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类

Copy
`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:

Copy
` //创建一个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方法:

Copy
`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.其他和业务相关的逻辑检测

下面我们改写一下:

Copy
` 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,实现表达式的各个节点功能

posted @   jeoyao  阅读(3903)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
点击右上角即可分享
微信分享提示
目录