Doorway.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. using DunGen.Tags;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. using UnityEngine.Serialization;
  6. namespace DunGen
  7. {
  8. /// <summary>
  9. /// A component to handle doorway placement and behaviour
  10. /// </summary>
  11. [AddComponentMenu("DunGen/Doorway")]
  12. public class Doorway : MonoBehaviour, ISerializationCallbackReceiver
  13. {
  14. public const int CurrentFileVersion = 1;
  15. public bool HasSocketAssigned { get { return socket != null; } }
  16. /// <summary>
  17. /// The socket this doorway uses. Allows you to use different sized doorways and have them connect correctly
  18. /// </summary>
  19. public DoorwaySocket Socket
  20. {
  21. get
  22. {
  23. if (socket == null)
  24. {
  25. if (cachedDefaultSocket == null)
  26. cachedDefaultSocket = DunGenSettings.Instance.DefaultSocket;
  27. return cachedDefaultSocket;
  28. }
  29. else
  30. return socket;
  31. }
  32. }
  33. /// <summary>
  34. /// When placing a door prefab, the doorway with the higher priority will have their prefab used
  35. /// </summary>
  36. public int DoorPrefabPriority;
  37. /// <summary>
  38. /// If true, the chosen Door prefab will not be oriented to match the rotation of the doorway it is placed on
  39. /// </summary>
  40. public bool AvoidRotatingDoorPrefab;
  41. /// <summary>
  42. /// An optional position offset to apply when spawning a door (connector) prefab, relative to the doorway's transform
  43. /// </summary>
  44. public Vector3 DoorPrefabPositionOffset;
  45. /// <summary>
  46. /// An optional rotation offset to apply when spawning a door (connector) prefab, relative to the doorway's transform
  47. /// </summary>
  48. public Vector3 DoorPrefabRotationOffset;
  49. /// <summary>
  50. /// When this doorway is in use, a prefab will be picked at random from this list and is spawned at the doorway location - one per doorways pair (connection)
  51. /// </summary>
  52. public List<GameObjectWeight> ConnectorPrefabWeights = new List<GameObjectWeight>();
  53. /// <summary>
  54. /// When this doorway is in use, objects in this list will remain in the scene, otherwise, they are destroyed
  55. /// </summary>
  56. [FormerlySerializedAs("AddWhenInUse")]
  57. public List<GameObject> ConnectorSceneObjects = new List<GameObject>();
  58. /// <summary>
  59. /// If true, the chosen Blocker prefab will not be oriented to match the rotation of the doorway it is placed on
  60. /// </summary>
  61. public bool AvoidRotatingBlockerPrefab;
  62. /// <summary>
  63. /// An optional position offset to apply when spawning a blocker prefab, relative to the doorway's transform
  64. /// </summary>
  65. public Vector3 BlockerPrefabPositionOffset;
  66. /// <summary>
  67. /// An optional rotation offset to apply when spawning a blocker prefab, relative to the doorway's transform
  68. /// </summary>
  69. public Vector3 BlockerPrefabRotationOffset;
  70. /// <summary>
  71. /// When this doorway is NOT in use, a prefab will be picked at random from this list and is spawned at the doorway location - one per doorway
  72. /// </summary>
  73. public List<GameObjectWeight> BlockerPrefabWeights = new List<GameObjectWeight>();
  74. /// <summary>
  75. /// When this doorway is NOT in use, objects in this list will remain in the scene, otherwise, they are destroyed
  76. /// </summary>
  77. [FormerlySerializedAs("AddWhenNotInUse")]
  78. public List<GameObject> BlockerSceneObjects = new List<GameObject>();
  79. /// <summary>
  80. /// A collection of tags for this doorway. These can be used in code with DoorwayPairFinder.CustomConnectionRules for custom connection logic
  81. /// </summary>
  82. public TagContainer Tags = new TagContainer();
  83. /// <summary>
  84. /// The Tile that this doorway belongs to
  85. /// </summary>
  86. public Tile Tile { get { return tile; } internal set { tile = value; } }
  87. /// <summary>
  88. /// The ID of the key used to unlock this door
  89. /// </summary>
  90. public int? LockID;
  91. /// <summary>
  92. /// Gets the lock status of the door
  93. /// </summary>
  94. public bool IsLocked { get { return LockID.HasValue; } }
  95. /// <summary>
  96. /// Does this doorway have a prefab object placed as a door?
  97. /// </summary>
  98. public bool HasDoorPrefabInstance { get { return doorPrefabInstance != null; } }
  99. /// <summary>
  100. /// The prefab that has been placed as a door for this doorway
  101. /// </summary>
  102. public GameObject UsedDoorPrefabInstance { get { return doorPrefabInstance; } }
  103. /// <summary>
  104. /// The Door component that has been assigned to the door prefab instance (if any)
  105. /// </summary>
  106. public Door DoorComponent { get { return doorComponent; } }
  107. /// <summary>
  108. /// The dungeon that this doorway belongs to
  109. /// </summary>
  110. public Dungeon Dungeon { get; internal set; }
  111. /// <summary>
  112. /// The doorway that this is connected to
  113. /// </summary>
  114. public Doorway ConnectedDoorway { get { return connectedDoorway; } internal set { connectedDoorway = value; } }
  115. /// <summary>
  116. /// Allows for hiding of any GameObject in the "AddWhenInUse" and "AddWhenNotInUse" lists - used to remove clutter at design-time; should not be used at runtime
  117. /// </summary>
  118. public bool HideConditionalObjects
  119. {
  120. get { return hideConditionalObjects; }
  121. set
  122. {
  123. hideConditionalObjects = value;
  124. foreach (var obj in ConnectorSceneObjects)
  125. if (obj != null)
  126. obj.SetActive(!hideConditionalObjects);
  127. foreach (var obj in BlockerSceneObjects)
  128. if (obj != null)
  129. obj.SetActive(!hideConditionalObjects);
  130. }
  131. }
  132. #region Legacy Properties
  133. #pragma warning disable 0414
  134. [SerializeField]
  135. [FormerlySerializedAs("SocketGroup")]
  136. private DoorwaySocketType socketGroup_obsolete = (DoorwaySocketType)(-1);
  137. [SerializeField]
  138. [FormerlySerializedAs("DoorPrefabs")]
  139. private List<GameObject> doorPrefabs_obsolete = new List<GameObject>();
  140. [SerializeField]
  141. [FormerlySerializedAs("BlockerPrefabs")]
  142. private List<GameObject> blockerPrefabs_obsolete = new List<GameObject>();
  143. #pragma warning restore 0414
  144. #endregion
  145. [SerializeField]
  146. private DoorwaySocket socket = null;
  147. [SerializeField]
  148. private GameObject doorPrefabInstance;
  149. [SerializeField]
  150. private Door doorComponent;
  151. [SerializeField]
  152. private Tile tile;
  153. [SerializeField]
  154. private Doorway connectedDoorway;
  155. [SerializeField]
  156. private bool hideConditionalObjects;
  157. [SerializeField]
  158. private GameObject spawnedBlockerPrefab;
  159. [SerializeField]
  160. private int fileVersion;
  161. private DoorwaySocket cachedDefaultSocket;
  162. internal bool placedByGenerator;
  163. private void OnValidate()
  164. {
  165. if (socket == null)
  166. socket = DunGenSettings.Instance.DefaultSocket;
  167. }
  168. internal void SetUsedPrefab(GameObject doorPrefab)
  169. {
  170. this.doorPrefabInstance = doorPrefab;
  171. if (doorPrefab != null)
  172. doorComponent = doorPrefab.GetComponent<Door>();
  173. }
  174. internal void RemoveUsedPrefab()
  175. {
  176. if (doorPrefabInstance != null)
  177. UnityUtil.Destroy(doorPrefabInstance);
  178. doorPrefabInstance = null;
  179. }
  180. #if UNITY_EDITOR
  181. private void OnDrawGizmos()
  182. {
  183. if (!placedByGenerator)
  184. DebugDraw();
  185. }
  186. internal void DebugDraw()
  187. {
  188. Vector2 size = Socket.Size;
  189. Vector2 halfSize = size * 0.5f;
  190. bool isValidPlacement = true;
  191. Color doorwayColour = Color.white;
  192. isValidPlacement = ValidateTransform(out var localTileBounds, out bool isAxisAligned, out bool isEdgePositioned);
  193. if (isValidPlacement)
  194. doorwayColour = EditorConstants.DoorRectColourValid;
  195. else if (!isAxisAligned)
  196. doorwayColour = EditorConstants.DoorRectColourError;
  197. else
  198. doorwayColour = EditorConstants.DoorRectColourWarning;
  199. // Draw Forward Vector
  200. float lineLength = Mathf.Min(size.x, size.y);
  201. Gizmos.color = EditorConstants.DoorDirectionColour;
  202. Gizmos.DrawLine(transform.position + transform.up * halfSize.y, transform.position + transform.up * halfSize.y + transform.forward * lineLength);
  203. // Draw Up Vector
  204. Gizmos.color = EditorConstants.DoorUpColour;
  205. Gizmos.DrawLine(transform.position + transform.up * halfSize.y, transform.position + transform.up * size.y);
  206. // Draw Rectangle
  207. Gizmos.color = doorwayColour;
  208. Vector3 topLeft = transform.position - (transform.right * halfSize.x) + (transform.up * size.y);
  209. Vector3 topRight = transform.position + (transform.right * halfSize.x) + (transform.up * size.y);
  210. Vector3 bottomLeft = transform.position - (transform.right * halfSize.x);
  211. Vector3 bottomRight = transform.position + (transform.right * halfSize.x);
  212. Gizmos.DrawLine(topLeft, topRight);
  213. Gizmos.DrawLine(topRight, bottomRight);
  214. Gizmos.DrawLine(bottomRight, bottomLeft);
  215. Gizmos.DrawLine(bottomLeft, topLeft);
  216. // Draw position correction line
  217. if (!isValidPlacement)
  218. {
  219. GetTileRoot(out var _, out var tile);
  220. // Projected position is meaningless if the Doorway isn't attached to a Tile
  221. if (tile != null)
  222. {
  223. Vector3 projectedPosition = ProjectPositionToTileBounds(localTileBounds);
  224. Gizmos.color = Color.red;
  225. Gizmos.DrawLine(transform.position, projectedPosition);
  226. }
  227. }
  228. }
  229. #endif
  230. private void GetTileRoot(out GameObject tileRoot, out Tile tileComponent)
  231. {
  232. tileComponent = GetComponentInParent<Tile>();
  233. #if UNITY_EDITOR
  234. // We might need to walk up the transform hierarchy manually
  235. if (tileComponent == null)
  236. {
  237. Transform current = transform;
  238. while (current != null)
  239. {
  240. tileComponent = current.GetComponent<Tile>();
  241. if (tileComponent != null)
  242. break;
  243. current = current.parent;
  244. }
  245. }
  246. #endif
  247. if (tileComponent != null)
  248. tileRoot = tileComponent.gameObject;
  249. else
  250. tileRoot = transform.root.gameObject;
  251. }
  252. public bool ValidateTransform(out Bounds localTileBounds, out bool isAxisAligned, out bool isEdgePositioned)
  253. {
  254. GetTileRoot(out var tileRoot, out var tile);
  255. // The Doorway isn't attached to a Tile, it can never be valid
  256. if(tileRoot == null || tile == null)
  257. {
  258. localTileBounds = new Bounds();
  259. isAxisAligned = false;
  260. isEdgePositioned = false;
  261. return false;
  262. }
  263. isAxisAligned = true;
  264. isEdgePositioned = true;
  265. if (tile != null && tile.OverrideAutomaticTileBounds)
  266. localTileBounds = tile.TileBoundsOverride;
  267. else
  268. localTileBounds = tile.Placement.LocalBounds;
  269. if (!UnityUtil.IsVectorAxisAligned(transform.forward))
  270. isAxisAligned = false;
  271. Vector3 projectedPosition = ProjectPositionToTileBounds(localTileBounds);
  272. if ((projectedPosition - transform.position).magnitude > 0.1f)
  273. isEdgePositioned = false;
  274. return isAxisAligned && isEdgePositioned;
  275. }
  276. public void TrySnapToCorrectedTransform()
  277. {
  278. if (ValidateTransform(out var localTileBounds, out _, out _))
  279. return;
  280. Vector3 correctedForward = UnityUtil.GetCardinalDirection(transform.forward, out _);
  281. transform.forward = correctedForward;
  282. transform.position = ProjectPositionToTileBounds(localTileBounds);
  283. }
  284. public Vector3 ProjectPositionToTileBounds(Bounds localTileBounds)
  285. {
  286. GetTileRoot(out var tileRoot, out var tile);
  287. var worldSpaceBounds = tileRoot.transform.TransformBounds(localTileBounds);
  288. Vector3 correctedForward = UnityUtil.GetCardinalDirection(transform.forward, out var magnitude);
  289. Vector3 offsetFromBoundsCenter = transform.position - worldSpaceBounds.center;
  290. // Calculate correction distance along forward vector (snap to edge)
  291. float currentForwardDistance = Vector3.Dot(correctedForward, offsetFromBoundsCenter);
  292. float extentForwardDistance = Vector3.Dot(magnitude < 0 ? -correctedForward : correctedForward, worldSpaceBounds.extents);
  293. float forwardCorrectionDistance = extentForwardDistance - currentForwardDistance;
  294. Vector3 targetPosition = transform.position;
  295. targetPosition += correctedForward * forwardCorrectionDistance;
  296. // Once we're positioned on the correct side of the bounding box based on the forward vector
  297. // of the doorway, clamp the position to keep it restrained within the bounds along the other axes
  298. targetPosition = UnityUtil.ClampVector(targetPosition, worldSpaceBounds.min, worldSpaceBounds.max);
  299. return targetPosition;
  300. }
  301. internal void ResetInstanceData()
  302. {
  303. if (spawnedBlockerPrefab != null)
  304. DestroyImmediate(spawnedBlockerPrefab);
  305. if(doorPrefabInstance != null)
  306. DestroyImmediate(doorPrefabInstance);
  307. connectedDoorway = null;
  308. }
  309. internal void ProcessDoorwayObjects(bool isDoorwayInUse, RandomStream randomStream)
  310. {
  311. foreach (var obj in BlockerSceneObjects)
  312. {
  313. if (obj != null)
  314. obj.SetActive(!isDoorwayInUse);
  315. }
  316. foreach (var obj in ConnectorSceneObjects)
  317. {
  318. if (obj != null)
  319. obj.SetActive(isDoorwayInUse);
  320. }
  321. if (isDoorwayInUse)
  322. {
  323. if (spawnedBlockerPrefab != null)
  324. DestroyImmediate(spawnedBlockerPrefab);
  325. }
  326. else
  327. {
  328. // If there is at least one blocker prefab, select one and spawn it as a child of the doorway
  329. if (BlockerPrefabWeights.HasAnyViableEntries())
  330. {
  331. spawnedBlockerPrefab = GameObject.Instantiate(BlockerPrefabWeights.GetRandom(randomStream)) as GameObject;
  332. spawnedBlockerPrefab.transform.parent = gameObject.transform;
  333. spawnedBlockerPrefab.transform.localPosition = BlockerPrefabPositionOffset;
  334. spawnedBlockerPrefab.transform.localScale = Vector3.one;
  335. if (AvoidRotatingBlockerPrefab)
  336. spawnedBlockerPrefab.transform.rotation = Quaternion.Euler(BlockerPrefabRotationOffset);
  337. else
  338. spawnedBlockerPrefab.transform.localRotation = Quaternion.Euler(BlockerPrefabRotationOffset);
  339. }
  340. }
  341. }
  342. #region ISerializationCallbackReceiver Implementation
  343. public void OnBeforeSerialize()
  344. {
  345. fileVersion = CurrentFileVersion;
  346. }
  347. public void OnAfterDeserialize()
  348. {
  349. // Convert old object lists to weighted lists
  350. if (fileVersion < 1)
  351. {
  352. foreach (var obj in doorPrefabs_obsolete)
  353. ConnectorPrefabWeights.Add(new GameObjectWeight(obj));
  354. foreach (var obj in blockerPrefabs_obsolete)
  355. BlockerPrefabWeights.Add(new GameObjectWeight(obj));
  356. doorPrefabs_obsolete.Clear();
  357. blockerPrefabs_obsolete.Clear();
  358. }
  359. }
  360. #endregion
  361. }
  362. }