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