預計會打很多,所以特別分一篇。想直接拿工具可以看上一篇:Scene 圖形化流程管理工具。關於Unity editor 的Graph view,可以看舊筆記(看這頻道學的,不過他是做對話系統,我比較喜歡自己的那套XD)。
不應該直接存Scene類型,而是SceneAsset類型。 為了避免Scene檔案路徑改變導致連結失效,我是直接存場景檔的Guid。 存取只是AssetDatabase的Guid-ScenePath正反操作而已。(存guid而非SceneAsset的原因下面會解釋)
之前研究過一下Async,放棄的原因不外乎就是Unity不支援多線程,主thread以外是無法取得UnityEngine的值(例如transform等等...)。本工具因為需要在場景載入完成後呼叫callback,所以需要在背景等待場景載入完成。用Corotuine等待的方法:(來源)執行corotuine的必要條件有:
(1.)必須由已實例化的物件去Start。(2.)corotuine不能被靜態方法呼叫。
一個工具還要手動實例化Manager就覺得很low,所以無論如何都要把它弄成public static才行,於是我看向以前學的Async,困難點在於如何克服thread問題。Async有個守則:呼叫Async方法的方法也得是Async方法。整個像是病毒一樣往上感染,盡量控制在一個腳本內,別讓Async前綴跑出去別的腳本了。舊的作法:因為非主線程無法得知是否isDone資訊,只能用Unity OnSceneLoaded當回傳。新的做法(第163行):註解起來的是舊方法的呼叫方式。有人可能會問:為什麼不直接傳入scene path就好,還得從資料裡面找?上面說過非主線程是無法取得UnityEngine資訊的,Scene.path亦無法。在Unity使用Async的原則:只使用基本變數型態(如:int / string / float...)傳遞參數。場景資料不是存SceneAsset而是檔案guid的緣故就是如此。最後,因為Async不像Corotuine需要實例化物件,所以Manager方法也都能順利設成靜態了。(完整程式碼)
製作一個編輯視窗:
public class LevelFlowGraphView : GraphView { private string styleSheetName = "GraphViewStyleSheet"; private LevelEditorWindow editorWindow; private NodeSearchWindow searchWindow; public LevelFlowGraphView(LevelEditorWindow _editorWindow) { editorWindow = _editorWindow; //套用Style StyleSheet tmpStyleSheet = Resources.Load<StyleSheet>(styleSheetName); styleSheets.Add(tmpStyleSheet); //設定大小 SetupZoom(ContentZoomer.DefaultMinScale, ContentZoomer.DefaultMaxScale); //設定滑鼠操作 this.AddManipulator(new ContentDragger()); this.AddManipulator(new SelectionDragger()); this.AddManipulator(new RectangleSelector()); this.AddManipulator(new FreehandSelector()); //填充網格 GridBackground grid = new GridBackground(); Insert(0, grid); grid.StretchToParentSize(); AddSearchWindow(); } } |
新增Search Tree:
private void AddSearchWindow()
{
searchWindow = ScriptableObject.CreateInstance<NodeSearchWindow>();
searchWindow.Configure(editorWindow, this);
nodeCreationRequest = context => SearchWindow.Open(new SearchWindowContext(context.screenMousePosition), searchWindow);
}建立Search Window選單:
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.GraphView;
using UnityEngine;
using UnityEngine.UIElements;
public class NodeSearchWindow : ScriptableObject, ISearchWindowProvider
{
private LevelEditorWindow editorWindow;
private LevelFlowGraphView graphView;
public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
{
List<SearchTreeEntry> tree = new List<SearchTreeEntry>() {
new SearchTreeGroupEntry(new GUIContent("Level Flow"),0),
new SearchTreeGroupEntry(new GUIContent("Level Node"),1),
AddNodeSearch("Start Node",new StartNode()),
AddNodeSearch("Level Node",new LevelNode())
};
return tree;
}
private SearchTreeEntry AddNodeSearch(string _name, BaseNode _baseNode)
{
SearchTreeEntry tmp = new SearchTreeEntry(new GUIContent(_name))
{
level = 2,
userData = _baseNode
};
return tmp;
}
public void Configure(LevelEditorWindow _editorWindow, LevelFlowGraphView _graphView)
{
editorWindow = _editorWindow;
graphView = _graphView;
}
public bool OnSelectEntry(SearchTreeEntry _SearchTreeEntry, SearchWindowContext _context)
{
Vector2 mousePosition = editorWindow.rootVisualElement.ChangeCoordinatesTo(
editorWindow.rootVisualElement.parent, _context.screenMousePosition - editorWindow.position.position
);
Vector2 grphviewMousePosition = graphView.contentViewContainer.WorldToLocal(mousePosition);
return CheckForNodeType(_SearchTreeEntry, grphviewMousePosition);
}
private bool CheckForNodeType(SearchTreeEntry _searchTreeEntry, Vector2 _pos)
{
switch (_searchTreeEntry.userData)
{
case LevelNode node:
graphView.AddElement(graphView.CreateLevelNode(_pos));
return true;
case StartNode node:
graphView.AddElement(graphView.CreateStartNode(_pos));
return true;
default:
break;
}
return false;
}
}各種資料欄位創建範例:
Node:
public class BaseNode : UnityEditor.Experimental.GraphView.Node { public BaseNode(Vector2 _position ) { //CSS樣式 StyleSheet styleSheet = Resources.Load<StyleSheet>("NodeStyleSheet"); styleSheets.Add(styleSheet); //Node title文字 title = "Start"; //Node 位置與大小 SetPosition(new Rect(_position , defaultNodeSide)); nodeGuid = Guid.NewGuid().ToString(); //Guid能產生獨特的id //新增節點須refresh RefreshExpandedState(); RefreshPorts(); } } |
Enum Field:
private EnumField enumField;
private EndNodeType endNodeType = EndNodeType.End;
public EndNodeType EndNodeType { get => endNodeType; set => endNodeType = value; }
//******in constructer:*******
enumField = new EnumField()
{
value = endNodeType
};
enumField.Init(endNodeType);
enumField.RegisterValueChangedCallback((value) =>
{
//賦予新value
endNodeType = (EndNodeType)value.newValue;
});
enumField.SetValueWithoutNotify(endNodeType);
mainContainer.Add(enumField);Image Field:
private Sprite Image;
public Sprite image { get => Image; set => Image= value; }
private ObjectField image_Field ;
//*****in constructer:******
image_Field = new ObjectField
{
objectType = typeof(Sprite),
allowSceneObjects = false,
value = image
};
image_Field .RegisterValueChangedCallback(ValueTuple =>
{
image = ValueTuple.newValue as Sprite;
});
mainContainer.Add(image_Field );Text Field:
private string name = "";
public string Name { get => name; set => name = value; }
Label label_name = new Label("title");
mainContainer.Add(label_name);
TextField textField = new TextField("title"); //傳入label標籤,會跟對應到的label同行,否則獨立一行
textField.RegisterValueChangedCallback(value =>
{
name = value.newValue;
});
name_Field.SetValueWithoutNotify(name);
//Apply style
name_Field.AddToClassList("css");
mainContainer.Add(textField );Label:
Label label_texts = new Label("Texts Box");
label_texts.AddToClassList("Label");
mainContainer.Add(label_texts);Button:
Button button = new Button()
{
text = "Add Choice"
};
button.clicked += () =>
{
//TODO: callback
};
//另一種container也OK
titleButtonContainer.Add(button);Port:
public Port AddOutputPort(string name, Port.Capacity capacity = Port.Capacity.Single)
{
Port outputPort = GetPortInstance(Direction.Output, capacity);
outputPort.portName = name;
outputPort.portColor = Color.green;
outputContainer.Add(outputPort);
outPorts.Add(outputPort);
return outputPort;
}
public Port AddInputPort(string name, Port.Capacity capacity = Port.Capacity.Multi)
{
Port inputPort = GetPortInstance(Direction.Input, capacity);
inputPort.portName = name;
inputContainer.Add(inputPort);
inPorts.Add(inputPort);
return inputPort;
}
//返回的值一樣加進Container連接Port:連線規則要自己定義。
public class MyGraphView : GraphView
public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
List<Port> compatiblePorts = new List<Port>();
Port startPortView = startPort;
//ports 會取得全部的port
ports.ForEach((port) =>
{
Port _portView = port;
if (startPortView != _portView && //不能自己連自己
startPortView.node != _portView.node && //不能自己連自己的sub node
startPortView.direction != port.direction //out不能連out in不能連in
)
{
//連接
compatiblePorts.Add(port);
}
});
return compatiblePorts;
}
}
雙擊自動開啟編輯器:
[OnOpenAsset(1)] //當資料夾裡的檔案被點擊時的callback
//每個project裡面的檔案都有自己的id
public static bool ShowWindowInfo(int _instanceID, int line)
{
UnityEngine.Object item = EditorUtility.InstanceIDToObject(_instanceID);
if (item is LevelMapSO) //點擊這類檔案的資料,開啟對應的編輯視窗
{
LevelEditorWindow window = (LevelEditorWindow)GetWindow(typeof(LevelEditorWindow));
window.titleContent = new GUIContent("Level Flow Editor");
window.flowData = item as LevelMapSO;
window.minSize = new Vector2(500, 250);
window.Load();
}
return false;
}建立Graph View:*注意:順序應是先創建Graph view、再創建Tool bar。
//建立網格背景
private void ConstructGraphView()
{
graphView = new LevelFlowGraphView(this);
graphView.StretchToParentSize();
rootVisualElement.Add(graphView);
mapSaveLoad = new MapSaveLoad(graphView);
}連線Port:
private void LinkNodesTogether(Port _outputPort, Port _inputPort)
{
Edge tempEdge = new Edge()
{
output = _outputPort,
input = _inputPort
};
tempEdge.input.Connect(tempEdge);
tempEdge.output.Connect(tempEdge);
graphView.Add(tempEdge);
}
後記: