1. 程式人生 > 實用技巧 >Unity開發筆記-Editor擴充套件用GraphView實現邏輯表示式(1)UI基礎邏輯實現

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,實現表示式的各個節點功能