DungeonFlowEditorWindow.cs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576
  1. using DunGen.Graph;
  2. using System.Linq;
  3. using UnityEditor;
  4. using UnityEngine;
  5. namespace DunGen.Editor
  6. {
  7. public sealed class DungeonFlowEditorWindow : EditorWindow
  8. {
  9. #region Layout Constants
  10. private const float LineThickness = 30;
  11. private const float HorizontalMargin = 10;
  12. private const float VerticalMargin = 10;
  13. private const float NodeWidth = 60;
  14. private const float MinorNodeSizeCoefficient = 0.5f;
  15. private const int BorderThickness = 2;
  16. private static readonly Color StartNodeColour = new Color(0.78f, 0.38f, 0.38f);
  17. private static readonly Color GoalNodeColour = new Color(0.39f, 0.69f, 0.39f);
  18. private static readonly Color NodeColour = Color.white;
  19. private static readonly Color LineColour = Color.white;
  20. private static readonly Color BorderColour = Color.black;
  21. #endregion
  22. #region Context Menu Command Identifiers
  23. private enum GraphContextCommand
  24. {
  25. Delete,
  26. AddNode,
  27. SplitLine,
  28. }
  29. #endregion
  30. #region Statics
  31. private static GUIStyle boxStyle;
  32. private static Texture2D whitePixel;
  33. #endregion
  34. public DungeonFlow Flow { get; private set; }
  35. private const float LineBoundaryGrabWidth = 8f;
  36. private static readonly Color SelectedBorderColour = new Color(0.98f, 0.6f, 0.2f);
  37. private const int SelectedBorderThickness = 4;
  38. private bool isMouseDown;
  39. private bool isDraggingNode;
  40. private GraphNode draggingNode;
  41. private GraphObjectObserver inspector;
  42. private GraphNode contextMenuNode;
  43. private GraphLine contextMenuLine;
  44. private Vector2 contextMenuPosition;
  45. private int draggingLineBoundaryIndex = -1;
  46. private bool isDraggingLineBoundary = false;
  47. private GraphNode selectedNode;
  48. private GraphLine selectedLine;
  49. private bool IsInitialised()
  50. {
  51. return boxStyle != null && whitePixel != null;
  52. }
  53. private void Init()
  54. {
  55. minSize = new Vector2(470, 150);
  56. whitePixel = new Texture2D(1, 1, TextureFormat.RGB24, false);
  57. whitePixel.SetPixel(0, 0, Color.white);
  58. whitePixel.Apply();
  59. boxStyle = new GUIStyle(GUI.skin.box);
  60. boxStyle.normal.background = whitePixel;
  61. if (Flow != null)
  62. {
  63. foreach (var node in Flow.Nodes)
  64. node.Graph = Flow;
  65. foreach (var line in Flow.Lines)
  66. line.Graph = Flow;
  67. }
  68. }
  69. public void OnGUI()
  70. {
  71. if (!IsInitialised())
  72. Init();
  73. if (Flow == null)
  74. {
  75. Flow = (DungeonFlow)EditorGUILayout.ObjectField(Flow, typeof(DungeonFlow), false);
  76. return;
  77. }
  78. DrawNodes();
  79. DrawLines();
  80. HandleInput();
  81. if (GUI.changed)
  82. EditorUtility.SetDirty(Flow);
  83. }
  84. private void OnInspectorUpdate()
  85. {
  86. Repaint();
  87. }
  88. private float GetNormalizedPositionOnGraph(Vector2 screenPosition)
  89. {
  90. float width = position.width - (HorizontalMargin + NodeWidth / 2) * 2;
  91. float linePosition = screenPosition.x - (HorizontalMargin + NodeWidth / 2);
  92. return Mathf.Clamp(linePosition / width, 0, 1);
  93. }
  94. private void HandleInput()
  95. {
  96. var evt = Event.current;
  97. int boundaryIndex = GetLineBoundaryAtPoint(evt.mousePosition);
  98. // Change cursor if hovering over a boundary
  99. if (boundaryIndex != -1 && !isDraggingLineBoundary)
  100. EditorGUIUtility.AddCursorRect(new Rect(evt.mousePosition.x - 10, evt.mousePosition.y - 10, 20, 20), MouseCursor.ResizeHorizontal);
  101. if (evt.isMouse && evt.button == 0)
  102. {
  103. switch (evt.type)
  104. {
  105. case EventType.MouseDown:
  106. // Drag a line boundary
  107. if (boundaryIndex != -1)
  108. {
  109. draggingLineBoundaryIndex = boundaryIndex;
  110. isDraggingLineBoundary = true;
  111. evt.Use();
  112. return;
  113. }
  114. // Drag a node
  115. var node = GetNodeAtPoint(evt.mousePosition);
  116. if (node != null && node.NodeType == NodeType.Normal)
  117. {
  118. draggingNode = node;
  119. isDraggingNode = true;
  120. Select(node);
  121. }
  122. isMouseDown = true;
  123. evt.Use();
  124. break;
  125. case EventType.MouseUp:
  126. // Stop dragging line boundary
  127. if (isDraggingLineBoundary)
  128. {
  129. isDraggingLineBoundary = false;
  130. draggingLineBoundaryIndex = -1;
  131. evt.Use();
  132. return;
  133. }
  134. if (!isDraggingNode)
  135. TrySelectGraphObject(evt.mousePosition);
  136. isMouseDown = false;
  137. draggingNode = null;
  138. isDraggingNode = false;
  139. evt.Use();
  140. break;
  141. case EventType.MouseDrag:
  142. if (isDraggingLineBoundary && draggingLineBoundaryIndex != -1)
  143. {
  144. // Calculate new normalized position
  145. float width = position.width - (HorizontalMargin + NodeWidth / 2) * 2;
  146. float mouseNorm = Mathf.Clamp((evt.mousePosition.x - (HorizontalMargin + NodeWidth / 2)) / width, 0f, 1f);
  147. // Get the two lines
  148. var leftLine = Flow.Lines[draggingLineBoundaryIndex];
  149. var rightLine = Flow.Lines[draggingLineBoundaryIndex + 1];
  150. // The left boundary of the left line
  151. float leftEdge = leftLine.Position;
  152. // The right boundary of the right line
  153. float rightEdge = rightLine.Position + rightLine.Length;
  154. // Clamp mouseNorm between leftEdge + min and rightEdge - min
  155. float minLength = 0.02f; // Minimum segment length
  156. mouseNorm = Mathf.Clamp(mouseNorm, leftEdge + minLength, rightEdge - minLength);
  157. // Update lines
  158. float newLeftLength = mouseNorm - leftEdge;
  159. float newRightLength = rightEdge - mouseNorm;
  160. leftLine.Length = newLeftLength;
  161. rightLine.Position = mouseNorm;
  162. rightLine.Length = newRightLength;
  163. Repaint();
  164. evt.Use();
  165. return;
  166. }
  167. if (isMouseDown && !isDraggingNode && draggingNode != null)
  168. isDraggingNode = true;
  169. if (isDraggingNode)
  170. {
  171. draggingNode.Position = GetNormalizedPositionOnGraph(evt.mousePosition);
  172. Repaint();
  173. }
  174. evt.Use();
  175. break;
  176. }
  177. }
  178. // Handle right mouse button actions
  179. else if (evt.type == EventType.ContextClick)
  180. {
  181. bool hasOpenedContextMenu = false;
  182. for (int i = Flow.Nodes.Count - 1; i >= 0; i--)
  183. {
  184. var node = Flow.Nodes[i];
  185. if (GetNodeBounds(node).Contains(evt.mousePosition))
  186. {
  187. HandleNodeContextMenu(node);
  188. hasOpenedContextMenu = true;
  189. contextMenuPosition = evt.mousePosition;
  190. break;
  191. }
  192. }
  193. if (!hasOpenedContextMenu)
  194. {
  195. foreach (var line in Flow.Lines)
  196. if (GetLineBounds(line).Contains(evt.mousePosition))
  197. {
  198. HandleLineContextMenu(line);
  199. hasOpenedContextMenu = true;
  200. contextMenuPosition = evt.mousePosition;
  201. break;
  202. }
  203. }
  204. evt.Use();
  205. }
  206. }
  207. private int GetLineBoundaryAtPoint(Vector2 mousePosition)
  208. {
  209. // Returns the index of the boundary between two lines if the mouse is near it, otherwise -1
  210. float width = position.width - (HorizontalMargin + NodeWidth / 2) * 2;
  211. float centreY = position.center.y - position.y;
  212. float top = centreY - (LineThickness / 2);
  213. float currentX = HorizontalMargin + NodeWidth / 2;
  214. for (int i = 0; i < Flow.Lines.Count - 1; i++)
  215. {
  216. currentX += Flow.Lines[i].Length * width;
  217. Rect grabRect = new Rect(currentX - LineBoundaryGrabWidth / 2, top, LineBoundaryGrabWidth, LineThickness);
  218. if (grabRect.Contains(mousePosition))
  219. return i;
  220. }
  221. return -1;
  222. }
  223. #region Node Context Menu
  224. private void HandleNodeContextMenu(GraphNode node)
  225. {
  226. contextMenuNode = node;
  227. contextMenuLine = null;
  228. var menu = new GenericMenu();
  229. if (node.NodeType == NodeType.Normal)
  230. menu.AddItem(new GUIContent("Delete " + (string.IsNullOrEmpty(node.Label) ? "Node" : node.Label)), false, NodeContextMenuCallback, GraphContextCommand.Delete);
  231. menu.ShowAsContext();
  232. }
  233. private void NodeContextMenuCallback(object obj)
  234. {
  235. GraphContextCommand cmd = (GraphContextCommand)obj;
  236. switch (cmd)
  237. {
  238. case GraphContextCommand.Delete:
  239. if (contextMenuNode.NodeType == NodeType.Normal)
  240. Flow.Nodes.Remove(contextMenuNode);
  241. break;
  242. }
  243. }
  244. #endregion
  245. #region Line Context Menu
  246. private void HandleLineContextMenu(GraphLine line)
  247. {
  248. contextMenuLine = line;
  249. contextMenuNode = null;
  250. var menu = new GenericMenu();
  251. menu.AddItem(new GUIContent("Add Node Here"), false, LineContextMenuCallback, GraphContextCommand.AddNode);
  252. menu.AddItem(new GUIContent("Split Segment"), false, LineContextMenuCallback, GraphContextCommand.SplitLine);
  253. if (Flow.Lines.Count > 1)
  254. menu.AddItem(new GUIContent("Delete Segment"), false, LineContextMenuCallback, GraphContextCommand.Delete);
  255. menu.ShowAsContext();
  256. }
  257. private void LineContextMenuCallback(object obj)
  258. {
  259. GraphContextCommand cmd = (GraphContextCommand)obj;
  260. switch (cmd)
  261. {
  262. case GraphContextCommand.AddNode:
  263. {
  264. GraphNode node = new GraphNode(Flow);
  265. node.Label = "New Node";
  266. node.Position = GetNormalizedPositionOnGraph(contextMenuPosition);
  267. Flow.Nodes.Add(node);
  268. break;
  269. }
  270. case GraphContextCommand.Delete:
  271. {
  272. if (Flow.Lines.Count > 1)
  273. {
  274. int lineIndex = Flow.Lines.IndexOf(contextMenuLine);
  275. Flow.Lines.RemoveAt(lineIndex);
  276. if (lineIndex == 0)
  277. {
  278. var replacementLine = Flow.Lines[0];
  279. replacementLine.Position = 0;
  280. replacementLine.Length += contextMenuLine.Length;
  281. }
  282. else
  283. {
  284. var replacementLine = Flow.Lines[lineIndex - 1];
  285. replacementLine.Length += contextMenuLine.Length;
  286. }
  287. }
  288. break;
  289. }
  290. case GraphContextCommand.SplitLine:
  291. {
  292. float position = GetNormalizedPositionOnGraph(contextMenuPosition);
  293. float originalLength = contextMenuLine.Length;
  294. int index = Flow.Lines.IndexOf(contextMenuLine);
  295. float totalLength = 0;
  296. for (int i = 0; i < index; i++)
  297. totalLength += Flow.Lines[i].Length;
  298. contextMenuLine.Length = position - totalLength;
  299. GraphLine newSegment = new GraphLine(Flow);
  300. foreach (var dungeonArchetype in contextMenuLine.DungeonArchetypes)
  301. newSegment.DungeonArchetypes.Add(dungeonArchetype);
  302. newSegment.Position = position;
  303. newSegment.Length = originalLength - contextMenuLine.Length;
  304. Flow.Lines.Insert(index + 1, newSegment);
  305. break;
  306. }
  307. }
  308. }
  309. #endregion
  310. private bool TrySelectGraphObject(Vector2 mousePosition)
  311. {
  312. var node = GetNodeAtPoint(mousePosition);
  313. if (node != null)
  314. {
  315. Select(node);
  316. return true;
  317. }
  318. var line = GetLineAtPoint(mousePosition);
  319. if (line != null)
  320. {
  321. Select(line);
  322. return true;
  323. }
  324. return false;
  325. }
  326. private void Select(GraphNode node)
  327. {
  328. selectedNode = node;
  329. selectedLine = null;
  330. CreateInspectorInstance();
  331. inspector.Inspect(node);
  332. Selection.activeObject = inspector;
  333. EditorUtility.SetDirty(inspector);
  334. }
  335. private void Select(GraphLine line)
  336. {
  337. selectedLine = line;
  338. selectedNode = null;
  339. CreateInspectorInstance();
  340. inspector.Inspect(line);
  341. Selection.activeObject = inspector;
  342. EditorUtility.SetDirty(inspector);
  343. }
  344. private void CreateInspectorInstance()
  345. {
  346. if (inspector != null)
  347. {
  348. if(Selection.activeObject == inspector)
  349. Selection.activeObject = null;
  350. DestroyImmediate(inspector);
  351. inspector = null;
  352. }
  353. inspector = ScriptableObject.CreateInstance<GraphObjectObserver>();
  354. inspector.Flow = Flow;
  355. }
  356. private GraphNode GetNodeAtPoint(Vector2 screenPosition)
  357. {
  358. // Loop through nodes backwards to prioritise nodes other than the Start & Goal nodes
  359. for (int i = Flow.Nodes.Count - 1; i >= 0; i--)
  360. {
  361. var node = Flow.Nodes[i];
  362. if (GetNodeBounds(node).Contains(screenPosition))
  363. return node;
  364. }
  365. return null;
  366. }
  367. private GraphLine GetLineAtPoint(Vector2 screenPosition)
  368. {
  369. foreach (var line in Flow.Lines)
  370. if (GetLineBounds(line).Contains(screenPosition))
  371. return line;
  372. return null;
  373. }
  374. private void DrawLines()
  375. {
  376. for (int i = 0; i < Flow.Lines.Count; i++)
  377. {
  378. var line = Flow.Lines[i];
  379. var rect = GetLineBounds(line);
  380. // Draw selected border if this line is selected
  381. if (line == selectedLine)
  382. {
  383. GUI.color = SelectedBorderColour;
  384. GUI.Box(ExpandRectCentered(rect, SelectedBorderThickness), "", boxStyle);
  385. }
  386. GUI.color = BorderColour;
  387. GUI.Box(ExpandRectCentered(rect, BorderThickness), "", boxStyle);
  388. GUI.color = LineColour;
  389. GUI.Box(rect, "", boxStyle);
  390. }
  391. }
  392. private void DrawNodes()
  393. {
  394. var originalContentColour = GUI.contentColor;
  395. GUI.contentColor = Color.black;
  396. foreach (var node in Flow.Nodes.OrderBy(x => x.NodeType == NodeType.Normal))
  397. {
  398. var rect = GetNodeBounds(node);
  399. // Draw selected border if this node is selected
  400. if (node == selectedNode)
  401. {
  402. GUI.color = SelectedBorderColour;
  403. GUI.Box(ExpandRectCentered(rect, SelectedBorderThickness), "", boxStyle);
  404. }
  405. GUI.color = BorderColour;
  406. GUI.Box(ExpandRectCentered(rect, BorderThickness), "", boxStyle);
  407. GUI.color = (node.NodeType == NodeType.Start) ? StartNodeColour : (node.NodeType == NodeType.Goal) ? GoalNodeColour : NodeColour;
  408. GUI.Box(rect, node.Label, boxStyle);
  409. }
  410. GUI.contentColor = originalContentColour;
  411. }
  412. private Rect ExpandRectCentered(Rect rect, int margin)
  413. {
  414. return new Rect(rect.x - margin, rect.y - margin, rect.width + margin * 2, rect.height + margin * 2);
  415. }
  416. private Rect GetLineBounds(GraphLine line)
  417. {
  418. float center = position.center.y - position.y;
  419. float top = center - (LineThickness / 2);
  420. float width = position.width - (HorizontalMargin + NodeWidth / 2) * 2;
  421. float left = (HorizontalMargin + NodeWidth / 2) + line.Position * width;
  422. return new Rect(left, top, line.Length * width, LineThickness);
  423. }
  424. private Rect GetNodeBounds(GraphNode node)
  425. {
  426. float top = VerticalMargin;
  427. float width = position.width - (HorizontalMargin + NodeWidth / 2) * 2;
  428. float height = position.height - VerticalMargin * 2;
  429. if (node.NodeType == NodeType.Normal)
  430. {
  431. float offset = (position.height - VerticalMargin * 2) / 4;
  432. top += offset;
  433. height -= offset * 2;
  434. }
  435. float left = (HorizontalMargin + NodeWidth / 2) + node.Position * width - NodeWidth / 2;
  436. return new Rect(left, top, NodeWidth, height);
  437. }
  438. #region Static Methods
  439. [MenuItem("Window/DunGen/Dungeon Flow Editor")]
  440. public static void Open()
  441. {
  442. DungeonFlowEditorWindow.Open(null);
  443. }
  444. public static void Open(DungeonFlow flow)
  445. {
  446. var window = EditorWindow.GetWindow<DungeonFlowEditorWindow>(false, "Dungeon Flow", true);
  447. window.Flow = flow;
  448. }
  449. #endregion
  450. }
  451. }