DoorwayInspector.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. using UnityEngine;
  2. using UnityEditor;
  3. using UnityEditorInternal;
  4. using System.Linq;
  5. using System;
  6. namespace DunGen.Editor
  7. {
  8. [CustomEditor(typeof(Doorway))]
  9. [CanEditMultipleObjects]
  10. public class DoorwayInspector : UnityEditor.Editor
  11. {
  12. #region Constants
  13. private static readonly GUIContent socketGroupLabel = new GUIContent("Socket", "Determines if two doorways can connect. By default, only doorways with matching socket groups can be connected to one another");
  14. private static readonly GUIContent hideConditionalObjectsLabel = new GUIContent("Hide Conditional Objects?", "If checked, any in-scene door or blocked objects will be hidden for the purpose of reducing clutter. Has no effect on the runtime results");
  15. private static readonly GUIContent connectorSceneObjectsLabel = new GUIContent("Scene Objects", "In-scene objects to be KEPT when the doorway is in use (connected). Objects are kept on both sides of the doorway");
  16. private static readonly GUIContent blockerSceneObjectsLabel = new GUIContent("Scene Objects", "In-scene objects to be REMOVED when the doorway is in use (connected)");
  17. private static readonly GUIContent priorityLabel = new GUIContent("Priority", "When two doorways are connected, the one with the higher priority will have their door prefab used");
  18. private static readonly GUIContent doorPrefabLabel = new GUIContent("Random Prefab Weights", "When this doorway is in use (connected), a single prefab will be spawned from this list (and the connected doorway) at random");
  19. private static readonly GUIContent blockerPrefabLabel = new GUIContent("Random Prefab Weights", "When this doorway is NOT in use (unconnected), a single prefab will be spawned from this list (and the connected doorway) at random");
  20. private static readonly GUIContent avoidRotationLabel = new GUIContent("Avoid Rotation?", "If checked, the placed prefab will NOT be oriented to match the doorway");
  21. private static readonly GUIContent prefabPositionOffsetLabel = new GUIContent("Position Offset", "An optional position offset to apply when spawning this prefab, relative to the doorway's transform");
  22. private static readonly GUIContent prefabRotationOffsetLabel = new GUIContent("Rotation Offset", "An optional rotation offset to apply when spawning this prefab, reltative to the doorway's transform");
  23. private static readonly GUIContent connectorsLabel = new GUIContent("Connectors", "In-scene objects and prefabs used when the doorway is in use (connected)");
  24. private static readonly GUIContent blockersLabel = new GUIContent("Blockers", "In-scene objects and prefabs used when the doorway is not in use (not connected)");
  25. private static readonly GUIContent tagsLabel = new GUIContent("Tags", "A collection of tags that can be used in code to define custom connection logic (see DoorwayPairFinder.CustomConnectionRules)");
  26. #endregion
  27. private SerializedProperty socketProp;
  28. private SerializedProperty hideConditionalObjectsProp;
  29. private SerializedProperty priorityProp;
  30. private SerializedProperty avoidDoorPrefabRotationProp;
  31. private SerializedProperty doorPrefabPositionOffsetProp;
  32. private SerializedProperty doorPrefabRotationOffsetProp;
  33. private SerializedProperty avoidBlockerPrefabRotationProp;
  34. private SerializedProperty blockerPrefabPositionOffsetProp;
  35. private SerializedProperty blockerPrefabRotationOffsetProp;
  36. private SerializedProperty tagsProp;
  37. private ReorderableList connectorSceneObjectsList;
  38. private ReorderableList blockerSceneObjectsList;
  39. private ReorderableList connectorPrefabsList;
  40. private ReorderableList blockerPrefabsList;
  41. private void OnEnable()
  42. {
  43. socketProp = serializedObject.FindProperty("socket");
  44. hideConditionalObjectsProp = serializedObject.FindProperty("hideConditionalObjects");
  45. priorityProp = serializedObject.FindProperty(nameof(Doorway.DoorPrefabPriority));
  46. avoidDoorPrefabRotationProp = serializedObject.FindProperty(nameof(Doorway.AvoidRotatingDoorPrefab));
  47. doorPrefabPositionOffsetProp = serializedObject.FindProperty(nameof(Doorway.DoorPrefabPositionOffset));
  48. doorPrefabRotationOffsetProp = serializedObject.FindProperty(nameof(Doorway.DoorPrefabRotationOffset));
  49. avoidBlockerPrefabRotationProp = serializedObject.FindProperty(nameof(Doorway.AvoidRotatingBlockerPrefab));
  50. blockerPrefabPositionOffsetProp = serializedObject.FindProperty(nameof(Doorway.BlockerPrefabPositionOffset));
  51. blockerPrefabRotationOffsetProp = serializedObject.FindProperty(nameof(Doorway.BlockerPrefabRotationOffset));
  52. tagsProp = serializedObject.FindProperty(nameof(Doorway.Tags));
  53. connectorSceneObjectsList = new ReorderableList(serializedObject, serializedObject.FindProperty(nameof(Doorway.ConnectorSceneObjects)), true, true, true, true);
  54. connectorSceneObjectsList.drawElementCallback = (rect, index, isActive, isFocused) => DrawGameObject(connectorSceneObjectsList, rect, index, true);
  55. connectorSceneObjectsList.drawHeaderCallback = (rect) => EditorGUI.LabelField(rect, connectorSceneObjectsLabel);
  56. blockerSceneObjectsList = new ReorderableList(serializedObject, serializedObject.FindProperty(nameof(Doorway.BlockerSceneObjects)), true, true, true, true);
  57. blockerSceneObjectsList.drawElementCallback = (rect, index, isActive, isFocused) => DrawGameObject(blockerSceneObjectsList, rect, index, true);
  58. blockerSceneObjectsList.drawHeaderCallback = (rect) => EditorGUI.LabelField(rect, blockerSceneObjectsLabel);
  59. connectorPrefabsList = new ReorderableList(serializedObject, serializedObject.FindProperty(nameof(Doorway.ConnectorPrefabWeights)), true, true, true, true);
  60. connectorPrefabsList.drawElementCallback = (rect, index, isActive, isFocused) => DrawGameObjectWeight(connectorPrefabsList, rect, index, false);
  61. connectorPrefabsList.drawHeaderCallback = (rect) => EditorGUI.LabelField(rect, doorPrefabLabel);
  62. connectorPrefabsList.onAddCallback = OnAddGameObjectChance;
  63. blockerPrefabsList = new ReorderableList(serializedObject, serializedObject.FindProperty(nameof(Doorway.BlockerPrefabWeights)), true, true, true, true);
  64. blockerPrefabsList.drawElementCallback = (rect, index, isActive, isFocused) => DrawGameObjectWeight(blockerPrefabsList, rect, index, false);
  65. blockerPrefabsList.drawHeaderCallback = (rect) => EditorGUI.LabelField(rect, blockerPrefabLabel);
  66. blockerPrefabsList.onAddCallback = OnAddGameObjectChance;
  67. }
  68. private void OnAddGameObjectChance(ReorderableList list)
  69. {
  70. list.serializedProperty.arraySize++;
  71. int newIndex = list.serializedProperty.arraySize - 1;
  72. var newElement = list.serializedProperty.GetArrayElementAtIndex(newIndex);
  73. newElement.FindPropertyRelative(nameof(GameObjectWeight.Weight)).floatValue = 1f;
  74. }
  75. private void DrawGameObject(ReorderableList list, Rect rect, int index, bool requireSceneObject)
  76. {
  77. rect = new Rect(rect.x, rect.y + 2, rect.width, EditorGUIUtility.singleLineHeight);
  78. EditorGUI.BeginChangeCheck();
  79. var element = list.serializedProperty.GetArrayElementAtIndex(index);
  80. var newObject = EditorGUI.ObjectField(rect, element.objectReferenceValue, typeof(GameObject), requireSceneObject);
  81. bool isValidEntry = true;
  82. if (newObject != null)
  83. {
  84. bool isAsset = EditorUtility.IsPersistent(newObject);
  85. isValidEntry = isAsset != requireSceneObject;
  86. }
  87. if (EditorGUI.EndChangeCheck() && isValidEntry)
  88. element.objectReferenceValue = newObject;
  89. }
  90. private void DrawGameObjectWeight(ReorderableList list, Rect rect, int index, bool requireSceneObject)
  91. {
  92. rect = new Rect(rect.x, rect.y + 2, rect.width, EditorGUIUtility.singleLineHeight);
  93. const float weightWidth = 100f;
  94. Rect gameObjectRect = rect;
  95. gameObjectRect.width -= weightWidth;
  96. Rect weightRect = rect;
  97. weightRect.width = weightWidth;
  98. weightRect.x += gameObjectRect.width;
  99. EditorGUI.BeginChangeCheck();
  100. var element = list.serializedProperty.GetArrayElementAtIndex(index);
  101. var gameObjectProperty = element.FindPropertyRelative(nameof(GameObjectWeight.GameObject));
  102. var weightProperty = element.FindPropertyRelative(nameof(GameObjectWeight.Weight));
  103. var newObject = EditorGUI.ObjectField(gameObjectRect, gameObjectProperty.objectReferenceValue, typeof(GameObject), requireSceneObject);
  104. bool isValidEntry = true;
  105. if (newObject != null)
  106. {
  107. bool isAsset = EditorUtility.IsPersistent(newObject);
  108. isValidEntry = isAsset != requireSceneObject;
  109. }
  110. if (EditorGUI.EndChangeCheck() && isValidEntry)
  111. gameObjectProperty.objectReferenceValue = newObject;
  112. EditorGUI.PropertyField(weightRect, weightProperty, GUIContent.none);
  113. }
  114. public override void OnInspectorGUI()
  115. {
  116. var doorways = targets.OfType<Doorway>();
  117. serializedObject.Update();
  118. if (socketProp.objectReferenceValue == null)
  119. socketProp.objectReferenceValue = DunGenSettings.Instance.DefaultSocket;
  120. EditorGUILayout.PropertyField(socketProp, socketGroupLabel);
  121. EditorGUI.BeginChangeCheck();
  122. EditorGUILayout.PropertyField(hideConditionalObjectsProp, hideConditionalObjectsLabel);
  123. if (EditorGUI.EndChangeCheck())
  124. {
  125. foreach(var d in doorways)
  126. d.HideConditionalObjects = hideConditionalObjectsProp.boolValue;
  127. }
  128. EditorGUILayout.Space();
  129. EditorGUILayout.Space();
  130. EditorGUI.indentLevel++;
  131. // Connectors
  132. EditorGUILayout.BeginVertical("box");
  133. priorityProp.isExpanded = EditorGUILayout.Foldout(priorityProp.isExpanded, connectorsLabel, true);
  134. if (priorityProp.isExpanded)
  135. {
  136. EditorGUILayout.PropertyField(priorityProp, priorityLabel);
  137. EditorGUILayout.PropertyField(avoidDoorPrefabRotationProp, avoidRotationLabel);
  138. EditorGUILayout.PropertyField(doorPrefabPositionOffsetProp, prefabPositionOffsetLabel);
  139. EditorGUILayout.PropertyField(doorPrefabRotationOffsetProp, prefabRotationOffsetLabel);
  140. EditorGUILayout.Space();
  141. EditorGUILayout.BeginVertical(); // We create a group here so the whole list is a drag and drop target
  142. connectorPrefabsList.DoLayoutList();
  143. EditorGUILayout.EndVertical();
  144. HandlePropDragAndDrop(GUILayoutUtility.GetLastRect(), false, true, (doorway, obj) => doorway.ConnectorPrefabWeights.Add(new GameObjectWeight(obj)));
  145. EditorGUILayout.Space();
  146. EditorGUILayout.BeginVertical(); // We create a group here so the whole list is a drag and drop target
  147. connectorSceneObjectsList.DoLayoutList();
  148. EditorGUILayout.EndVertical();
  149. HandlePropDragAndDrop(GUILayoutUtility.GetLastRect(), true, false, (doorway, obj) => doorway.ConnectorSceneObjects.Add(obj));
  150. }
  151. EditorGUILayout.EndVertical();
  152. // Blockers
  153. EditorGUILayout.BeginVertical("box");
  154. avoidBlockerPrefabRotationProp.isExpanded = EditorGUILayout.Foldout(avoidBlockerPrefabRotationProp.isExpanded, blockersLabel, true);
  155. if (avoidBlockerPrefabRotationProp.isExpanded)
  156. {
  157. EditorGUILayout.PropertyField(avoidBlockerPrefabRotationProp, avoidRotationLabel);
  158. EditorGUILayout.PropertyField(blockerPrefabPositionOffsetProp, prefabPositionOffsetLabel);
  159. EditorGUILayout.PropertyField(blockerPrefabRotationOffsetProp, prefabRotationOffsetLabel);
  160. EditorGUILayout.Space();
  161. EditorGUILayout.BeginVertical(); // We create a group here so the whole list is a drag and drop target
  162. blockerPrefabsList.DoLayoutList();
  163. EditorGUILayout.EndVertical();
  164. HandlePropDragAndDrop(GUILayoutUtility.GetLastRect(), false, true, (doorway, obj) => doorway.BlockerPrefabWeights.Add(new GameObjectWeight(obj)));
  165. EditorGUILayout.Space();
  166. EditorGUILayout.BeginVertical(); // We create a group here so the whole list is a drag and drop target
  167. blockerSceneObjectsList.DoLayoutList();
  168. EditorGUILayout.EndVertical();
  169. HandlePropDragAndDrop(GUILayoutUtility.GetLastRect(), true, false, (doorway, obj) => doorway.BlockerSceneObjects.Add(obj));
  170. }
  171. EditorGUILayout.EndVertical();
  172. EditorGUI.indentLevel--;
  173. EditorGUILayout.PropertyField(tagsProp, tagsLabel);
  174. serializedObject.ApplyModifiedProperties();
  175. bool isPlacementInvalid = false;
  176. // Check if any of the doorways have an invalid transform
  177. foreach (var doorway in doorways)
  178. {
  179. if (!doorway.ValidateTransform(out _, out _, out _))
  180. {
  181. isPlacementInvalid = true;
  182. break;
  183. }
  184. }
  185. // Show a warning message if the doorway(s) appear to be placed incorrectly and offer to fix the issue
  186. if (isPlacementInvalid)
  187. {
  188. EditorGUILayout.Space(20);
  189. EditorGUILayout.HelpBox("The doorway placement may not be correct. Doorways should be:\n\n- Facing away from the tile\n- Rotated to align with a world axis\n- Positioned at the edge of the tile's bounding box\n\nIf the doorway works as expected this message can be ignored, otherwise you can press the button below to try to automatically fix any placement issues\n", MessageType.Warning, true);
  190. EditorGUILayout.Space();
  191. if (GUILayout.Button(new GUIContent("Fix Doorway Placement")))
  192. {
  193. Undo.RecordObjects(doorways.Select(d => d.transform).ToArray(), "Snap Doorway");
  194. foreach (var doorway in doorways)
  195. doorway.TrySnapToCorrectedTransform();
  196. Undo.FlushUndoRecordObjects();
  197. }
  198. }
  199. }
  200. private void HandlePropDragAndDrop(Rect dragTargetRect, bool allowSceneObjects, bool allowAssetObjects, Action<Doorway, GameObject> addGameObject)
  201. {
  202. var evt = Event.current;
  203. var doorways = targets.OfType<Doorway>();
  204. if (evt.type == EventType.DragUpdated || evt.type == EventType.DragPerform)
  205. {
  206. var validGameObjects = EditorUtil.GetValidGameObjects(DragAndDrop.objectReferences, allowSceneObjects, allowAssetObjects);
  207. if (dragTargetRect.Contains(evt.mousePosition) && validGameObjects.Any())
  208. {
  209. DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
  210. if (evt.type == EventType.DragPerform)
  211. {
  212. Undo.RecordObjects(doorways.ToArray(), "Modify Doorway");
  213. DragAndDrop.AcceptDrag();
  214. foreach (var doorway in doorways)
  215. foreach (var dragObject in validGameObjects)
  216. addGameObject(doorway, dragObject);
  217. Undo.FlushUndoRecordObjects();
  218. }
  219. }
  220. }
  221. }
  222. }
  223. }