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

posted @ 2020-12-05 14:01  jeoyao  阅读(3801)  评论(0编辑  收藏  举报