using DunGen.Tags; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Serialization; namespace DunGen { /// /// A component to handle doorway placement and behaviour /// [AddComponentMenu("DunGen/Doorway")] public class Doorway : MonoBehaviour, ISerializationCallbackReceiver { public const int CurrentFileVersion = 1; public bool HasSocketAssigned { get { return socket != null; } } /// /// The socket this doorway uses. Allows you to use different sized doorways and have them connect correctly /// public DoorwaySocket Socket { get { if (socket == null) { if (cachedDefaultSocket == null) cachedDefaultSocket = DunGenSettings.Instance.DefaultSocket; return cachedDefaultSocket; } else return socket; } } /// /// When placing a door prefab, the doorway with the higher priority will have their prefab used /// public int DoorPrefabPriority; /// /// If true, the chosen Door prefab will not be oriented to match the rotation of the doorway it is placed on /// public bool AvoidRotatingDoorPrefab; /// /// An optional position offset to apply when spawning a door (connector) prefab, relative to the doorway's transform /// public Vector3 DoorPrefabPositionOffset; /// /// An optional rotation offset to apply when spawning a door (connector) prefab, relative to the doorway's transform /// public Vector3 DoorPrefabRotationOffset; /// /// 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) /// public List ConnectorPrefabWeights = new List(); /// /// When this doorway is in use, objects in this list will remain in the scene, otherwise, they are destroyed /// [FormerlySerializedAs("AddWhenInUse")] public List ConnectorSceneObjects = new List(); /// /// If true, the chosen Blocker prefab will not be oriented to match the rotation of the doorway it is placed on /// public bool AvoidRotatingBlockerPrefab; /// /// An optional position offset to apply when spawning a blocker prefab, relative to the doorway's transform /// public Vector3 BlockerPrefabPositionOffset; /// /// An optional rotation offset to apply when spawning a blocker prefab, relative to the doorway's transform /// public Vector3 BlockerPrefabRotationOffset; /// /// 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 /// public List BlockerPrefabWeights = new List(); /// /// When this doorway is NOT in use, objects in this list will remain in the scene, otherwise, they are destroyed /// [FormerlySerializedAs("AddWhenNotInUse")] public List BlockerSceneObjects = new List(); /// /// A collection of tags for this doorway. These can be used in code with DoorwayPairFinder.CustomConnectionRules for custom connection logic /// public TagContainer Tags = new TagContainer(); /// /// The Tile that this doorway belongs to /// public Tile Tile { get { return tile; } internal set { tile = value; } } /// /// The ID of the key used to unlock this door /// public int? LockID; /// /// Gets the lock status of the door /// public bool IsLocked { get { return LockID.HasValue; } } /// /// Does this doorway have a prefab object placed as a door? /// public bool HasDoorPrefabInstance { get { return doorPrefabInstance != null; } } /// /// The prefab that has been placed as a door for this doorway /// public GameObject UsedDoorPrefabInstance { get { return doorPrefabInstance; } } /// /// The Door component that has been assigned to the door prefab instance (if any) /// public Door DoorComponent { get { return doorComponent; } } /// /// The dungeon that this doorway belongs to /// public Dungeon Dungeon { get; internal set; } /// /// The doorway that this is connected to /// public Doorway ConnectedDoorway { get { return connectedDoorway; } internal set { connectedDoorway = value; } } /// /// 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 /// public bool HideConditionalObjects { get { return hideConditionalObjects; } set { hideConditionalObjects = value; foreach (var obj in ConnectorSceneObjects) if (obj != null) obj.SetActive(!hideConditionalObjects); foreach (var obj in BlockerSceneObjects) if (obj != null) obj.SetActive(!hideConditionalObjects); } } #region Legacy Properties #pragma warning disable 0414 [SerializeField] [FormerlySerializedAs("SocketGroup")] private DoorwaySocketType socketGroup_obsolete = (DoorwaySocketType)(-1); [SerializeField] [FormerlySerializedAs("DoorPrefabs")] private List doorPrefabs_obsolete = new List(); [SerializeField] [FormerlySerializedAs("BlockerPrefabs")] private List blockerPrefabs_obsolete = new List(); #pragma warning restore 0414 #endregion [SerializeField] private DoorwaySocket socket = null; [SerializeField] private GameObject doorPrefabInstance; [SerializeField] private Door doorComponent; [SerializeField] private Tile tile; [SerializeField] private Doorway connectedDoorway; [SerializeField] private bool hideConditionalObjects; [SerializeField] private GameObject spawnedBlockerPrefab; [SerializeField] private int fileVersion; private DoorwaySocket cachedDefaultSocket; internal bool placedByGenerator; private void OnValidate() { if (socket == null) socket = DunGenSettings.Instance.DefaultSocket; } internal void SetUsedPrefab(GameObject doorPrefab) { this.doorPrefabInstance = doorPrefab; if (doorPrefab != null) doorComponent = doorPrefab.GetComponent(); } internal void RemoveUsedPrefab() { if (doorPrefabInstance != null) UnityUtil.Destroy(doorPrefabInstance); doorPrefabInstance = null; } #if UNITY_EDITOR private void OnDrawGizmos() { if (!placedByGenerator) DebugDraw(); } internal void DebugDraw() { Vector2 size = Socket.Size; Vector2 halfSize = size * 0.5f; bool isValidPlacement = true; Color doorwayColour = Color.white; isValidPlacement = ValidateTransform(out var localTileBounds, out bool isAxisAligned, out bool isEdgePositioned); if (isValidPlacement) doorwayColour = EditorConstants.DoorRectColourValid; else if (!isAxisAligned) doorwayColour = EditorConstants.DoorRectColourError; else doorwayColour = EditorConstants.DoorRectColourWarning; // Draw Forward Vector float lineLength = Mathf.Min(size.x, size.y); Gizmos.color = EditorConstants.DoorDirectionColour; Gizmos.DrawLine(transform.position + transform.up * halfSize.y, transform.position + transform.up * halfSize.y + transform.forward * lineLength); // Draw Up Vector Gizmos.color = EditorConstants.DoorUpColour; Gizmos.DrawLine(transform.position + transform.up * halfSize.y, transform.position + transform.up * size.y); // Draw Rectangle Gizmos.color = doorwayColour; Vector3 topLeft = transform.position - (transform.right * halfSize.x) + (transform.up * size.y); Vector3 topRight = transform.position + (transform.right * halfSize.x) + (transform.up * size.y); Vector3 bottomLeft = transform.position - (transform.right * halfSize.x); Vector3 bottomRight = transform.position + (transform.right * halfSize.x); Gizmos.DrawLine(topLeft, topRight); Gizmos.DrawLine(topRight, bottomRight); Gizmos.DrawLine(bottomRight, bottomLeft); Gizmos.DrawLine(bottomLeft, topLeft); // Draw position correction line if (!isValidPlacement) { GetTileRoot(out var _, out var tile); // Projected position is meaningless if the Doorway isn't attached to a Tile if (tile != null) { Vector3 projectedPosition = ProjectPositionToTileBounds(localTileBounds); Gizmos.color = Color.red; Gizmos.DrawLine(transform.position, projectedPosition); } } } #endif private void GetTileRoot(out GameObject tileRoot, out Tile tileComponent) { tileComponent = GetComponentInParent(); #if UNITY_EDITOR // We might need to walk up the transform hierarchy manually if (tileComponent == null) { Transform current = transform; while (current != null) { tileComponent = current.GetComponent(); if (tileComponent != null) break; current = current.parent; } } #endif if (tileComponent != null) tileRoot = tileComponent.gameObject; else tileRoot = transform.root.gameObject; } public bool ValidateTransform(out Bounds localTileBounds, out bool isAxisAligned, out bool isEdgePositioned) { GetTileRoot(out var tileRoot, out var tile); // The Doorway isn't attached to a Tile, it can never be valid if(tileRoot == null || tile == null) { localTileBounds = new Bounds(); isAxisAligned = false; isEdgePositioned = false; return false; } isAxisAligned = true; isEdgePositioned = true; if (tile != null && tile.OverrideAutomaticTileBounds) localTileBounds = tile.TileBoundsOverride; else localTileBounds = tile.Placement.LocalBounds; if (!UnityUtil.IsVectorAxisAligned(transform.forward)) isAxisAligned = false; Vector3 projectedPosition = ProjectPositionToTileBounds(localTileBounds); if ((projectedPosition - transform.position).magnitude > 0.1f) isEdgePositioned = false; return isAxisAligned && isEdgePositioned; } public void TrySnapToCorrectedTransform() { if (ValidateTransform(out var localTileBounds, out _, out _)) return; Vector3 correctedForward = UnityUtil.GetCardinalDirection(transform.forward, out _); transform.forward = correctedForward; transform.position = ProjectPositionToTileBounds(localTileBounds); } public Vector3 ProjectPositionToTileBounds(Bounds localTileBounds) { GetTileRoot(out var tileRoot, out var tile); var worldSpaceBounds = tileRoot.transform.TransformBounds(localTileBounds); Vector3 correctedForward = UnityUtil.GetCardinalDirection(transform.forward, out var magnitude); Vector3 offsetFromBoundsCenter = transform.position - worldSpaceBounds.center; // Calculate correction distance along forward vector (snap to edge) float currentForwardDistance = Vector3.Dot(correctedForward, offsetFromBoundsCenter); float extentForwardDistance = Vector3.Dot(magnitude < 0 ? -correctedForward : correctedForward, worldSpaceBounds.extents); float forwardCorrectionDistance = extentForwardDistance - currentForwardDistance; Vector3 targetPosition = transform.position; targetPosition += correctedForward * forwardCorrectionDistance; // Once we're positioned on the correct side of the bounding box based on the forward vector // of the doorway, clamp the position to keep it restrained within the bounds along the other axes targetPosition = UnityUtil.ClampVector(targetPosition, worldSpaceBounds.min, worldSpaceBounds.max); return targetPosition; } internal void ResetInstanceData() { if (spawnedBlockerPrefab != null) DestroyImmediate(spawnedBlockerPrefab); if(doorPrefabInstance != null) DestroyImmediate(doorPrefabInstance); connectedDoorway = null; } internal void ProcessDoorwayObjects(bool isDoorwayInUse, RandomStream randomStream) { foreach (var obj in BlockerSceneObjects) { if (obj != null) obj.SetActive(!isDoorwayInUse); } foreach (var obj in ConnectorSceneObjects) { if (obj != null) obj.SetActive(isDoorwayInUse); } if (isDoorwayInUse) { if (spawnedBlockerPrefab != null) DestroyImmediate(spawnedBlockerPrefab); } else { // If there is at least one blocker prefab, select one and spawn it as a child of the doorway if (BlockerPrefabWeights.HasAnyViableEntries()) { spawnedBlockerPrefab = GameObject.Instantiate(BlockerPrefabWeights.GetRandom(randomStream)) as GameObject; spawnedBlockerPrefab.transform.parent = gameObject.transform; spawnedBlockerPrefab.transform.localPosition = BlockerPrefabPositionOffset; spawnedBlockerPrefab.transform.localScale = Vector3.one; if (AvoidRotatingBlockerPrefab) spawnedBlockerPrefab.transform.rotation = Quaternion.Euler(BlockerPrefabRotationOffset); else spawnedBlockerPrefab.transform.localRotation = Quaternion.Euler(BlockerPrefabRotationOffset); } } } #region ISerializationCallbackReceiver Implementation public void OnBeforeSerialize() { fileVersion = CurrentFileVersion; } public void OnAfterDeserialize() { // Convert old object lists to weighted lists if (fileVersion < 1) { foreach (var obj in doorPrefabs_obsolete) ConnectorPrefabWeights.Add(new GameObjectWeight(obj)); foreach (var obj in blockerPrefabs_obsolete) BlockerPrefabWeights.Add(new GameObjectWeight(obj)); doorPrefabs_obsolete.Clear(); blockerPrefabs_obsolete.Clear(); } } #endregion } }