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