using DunGen.Pooling; using DunGen.Tags; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Serialization; namespace DunGen { [AddComponentMenu("DunGen/Tile")] public class Tile : MonoBehaviour, ISerializationCallbackReceiver { public const int CurrentFileVersion = 3; #region Legacy Properties // Legacy properties only exist to avoid breaking existing projects // Converting old data structures over to the new ones [SerializeField] [FormerlySerializedAs("AllowImmediateRepeats")] private bool allowImmediateRepeats = true; [SerializeField] [Obsolete("'Entrance' is no longer used. Please use the 'Entrances' list instead", false)] public Doorway Entrance; [SerializeField] [Obsolete("'Exit' is no longer used. Please use the 'Exits' list instead", false)] public Doorway Exit; #endregion /// /// Should this tile be allowed to rotate to fit in place? /// public bool AllowRotation = true; /// /// Should this tile be allowed to be placed next to another instance of itself? /// public TileRepeatMode RepeatMode = TileRepeatMode.Allow; /// /// Should the automatically generated tile bounds be overridden with a user-defined value? /// public bool OverrideAutomaticTileBounds = false; /// /// Optional tile bounds to override the automatically calculated tile bounds /// public Bounds TileBoundsOverride = new Bounds(Vector3.zero, Vector3.one); /// /// An optional collection of entrance doorways. DunGen will try to use one of these doorways as the entrance to the tile if possible /// public List Entrances = new List(); /// /// An optional collection of exit doorways. DunGen will try to use one of these doorways as the exit to the tile if possible /// public List Exits = new List(); /// /// Should this tile override the connection chance globally defined in the DungeonFlow? /// public bool OverrideConnectionChance = false; /// /// The overridden connection chance value. Only used if is true. /// If both tiles have overridden the connection chance, the lowest value is used /// public float ConnectionChance = 0f; /// /// A collection of tags for this tile. Can be used with the dungeon flow asset to restrict which /// tiles can be attached /// public TagContainer Tags = new TagContainer(); /// /// The calculated world-space bounds of this Tile /// [HideInInspector] public Bounds Bounds { get { return transform.TransformBounds(Placement.LocalBounds); } } /// /// Information about the tile's position in the generated dungeon /// public TilePlacementData Placement { get { return placement; } internal set { placement = value; } } /// /// The dungeon that this tile belongs to /// public Dungeon Dungeon { get; internal set; } public List AllDoorways = new List(); public List UsedDoorways = new List(); public List UnusedDoorways = new List(); public GameObject Prefab { get; internal set; } public bool HasValidBounds => Placement != null && Placement.LocalBounds.extents.sqrMagnitude > 0f; [SerializeField] private TilePlacementData placement; [SerializeField] private int fileVersion; private BoxCollider triggerVolume; private BoxCollider2D triggerVolume2D; private readonly List spawnEventReceivers = new List(); public void RefreshTileEventReceivers() { spawnEventReceivers.Clear(); GetComponentsInChildren(true, spawnEventReceivers); } internal void TileSpawned() { foreach (var receiver in spawnEventReceivers) receiver.OnTileSpawned(this); } internal void TileDespawned() { Dungeon = null; foreach (var doorway in AllDoorways) doorway.ResetInstanceData(); placement.SetPositionAndRotation(Vector2.zero, Quaternion.identity); UsedDoorways.Clear(); UnusedDoorways.Clear(); foreach(var receiver in spawnEventReceivers) receiver.OnTileDespawned(this); } internal void AddTriggerVolume(bool use2dCollider) { if (use2dCollider) { if (triggerVolume2D == null) triggerVolume2D = gameObject.AddComponent(); triggerVolume2D.offset = Placement.LocalBounds.center; triggerVolume2D.size = Placement.LocalBounds.size; triggerVolume2D.isTrigger = true; } else { if(triggerVolume == null) triggerVolume = gameObject.AddComponent(); triggerVolume.center = Placement.LocalBounds.center; triggerVolume.size = Placement.LocalBounds.size; triggerVolume.isTrigger = true; } } private void OnTriggerEnter(Collider other) { if (other == null) return; if (other.gameObject.TryGetComponent(out var character)) character.OnTileEntered(this); } private void OnTriggerEnter2D(Collider2D other) { if (other == null) return; if (other.gameObject.TryGetComponent(out var character)) character.OnTileEntered(this); } private void OnTriggerExit(Collider other) { if (other == null) return; if (other.gameObject.TryGetComponent(out var character)) character.OnTileExited(this); } private void OnTriggerExit2D(Collider2D other) { if (other == null) return; if (other.gameObject.TryGetComponent(out var character)) character.OnTileExited(this); } private void OnDrawGizmos() { Gizmos.color = Color.red; Bounds? bounds = null; if (OverrideAutomaticTileBounds) bounds = transform.TransformBounds(TileBoundsOverride); else if (placement != null) bounds = Bounds; if (bounds.HasValue) Gizmos.DrawWireCube(bounds.Value.center, bounds.Value.size); } public IEnumerable GetAdjacentTiles() { return UsedDoorways.Select(x => x.ConnectedDoorway.Tile).Distinct(); } public bool IsAdjacentTo(Tile other) { foreach (var door in UsedDoorways) if (door.ConnectedDoorway.Tile == other) return true; return false; } public Doorway GetEntranceDoorway() { foreach (var doorway in UsedDoorways) { var connectedTile = doorway.ConnectedDoorway.Tile; if (Placement.IsOnMainPath) { if (connectedTile.Placement.IsOnMainPath && Placement.PathDepth > connectedTile.Placement.PathDepth) return doorway; } else { if (connectedTile.Placement.IsOnMainPath || Placement.Depth > connectedTile.Placement.Depth) return doorway; } } return null; } public Doorway GetExitDoorway() { foreach (var doorway in UsedDoorways) { var connectedTile = doorway.ConnectedDoorway.Tile; if (Placement.IsOnMainPath) { if (connectedTile.Placement.IsOnMainPath && Placement.PathDepth < connectedTile.Placement.PathDepth) return doorway; } else { if (!connectedTile.Placement.IsOnMainPath && Placement.Depth < connectedTile.Placement.Depth) return doorway; } } return null; } /// /// Recalculates the Tile's bounds based on the geometry inside the prefab /// /// True if the bounds changed when recalculated public bool RecalculateBounds() { if (Placement == null) Placement = new TilePlacementData(); var oldBounds = Placement.LocalBounds; if (OverrideAutomaticTileBounds) Placement.LocalBounds = TileBoundsOverride; else { var tileBounds = UnityUtil.CalculateObjectBounds(gameObject, false, DunGenSettings.Instance.BoundsCalculationsIgnoreSprites, true); tileBounds = UnityUtil.CondenseBounds(tileBounds, GetComponentsInChildren(true)); // Convert tileBounds to local space tileBounds = transform.InverseTransformBounds(tileBounds); Placement.LocalBounds = tileBounds; } var bounds = Placement.LocalBounds; bool haveBoundsChanged = bounds != oldBounds; // Let the user know that the tile's bounds are invalid if (bounds.size.x <= 0f || bounds.size.y <= 0f || bounds.size.z <= 0f) Debug.LogError(string.Format("Tile prefab '{0}' has automatic bounds that are zero or negative in size. The bounding volume for this tile will need to be manually defined.", gameObject), gameObject); //if (haveBoundsChanged) // Debug.Log($"Updated bounds for '{gameObject.name}'"); //else // Debug.Log($"RecalculateBounds(): Bounds were already up-to-date for '{gameObject.name}'"); return haveBoundsChanged; } public void CopyBoundsFrom(Tile otherTile) { if (otherTile == null) return; if(Placement == null) Placement = new TilePlacementData(); Placement.LocalBounds = otherTile.Placement.LocalBounds; } #region ISerializationCallbackReceiver Implementation public void OnBeforeSerialize() { fileVersion = CurrentFileVersion; } public void OnAfterDeserialize() { #pragma warning disable 618 // AllowImmediateRepeats (bool) -> TileRepeatMode (enum) if (fileVersion < 1) RepeatMode = (allowImmediateRepeats) ? TileRepeatMode.Allow : TileRepeatMode.DisallowImmediate; // Converted individual Entrance and Exit doorways to collections if (fileVersion < 2) { if (Entrances == null) Entrances = new List(); if (Exits == null) Exits = new List(); if (Entrance != null) Entrances.Add(Entrance); if(Exit != null) Exits.Add(Exit); Entrance = null; Exit = null; } #pragma warning restore 618 } #endregion } }