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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM