DoorwayPairFinder.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351
  1. using DunGen.Graph;
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using UnityEngine;
  6. namespace DunGen
  7. {
  8. #region Helper Types
  9. public struct DoorwayPair
  10. {
  11. public TileProxy PreviousTile { get; private set; }
  12. public DoorwayProxy PreviousDoorway { get; private set; }
  13. public TileProxy NextTemplate { get; private set; }
  14. public DoorwayProxy NextDoorway { get; private set; }
  15. public TileSet NextTileSet { get; private set; }
  16. public float TileWeight { get; private set; }
  17. public float DoorwayWeight { get; private set; }
  18. public DoorwayPair(TileProxy previousTile, DoorwayProxy previousDoorway, TileProxy nextTemplate, DoorwayProxy nextDoorway, TileSet nextTileSet, float tileWeight, float doorwayWeight)
  19. {
  20. PreviousTile = previousTile;
  21. PreviousDoorway = previousDoorway;
  22. NextTemplate = nextTemplate;
  23. NextDoorway = nextDoorway;
  24. NextTileSet = nextTileSet;
  25. TileWeight = tileWeight;
  26. DoorwayWeight = doorwayWeight;
  27. }
  28. }
  29. #endregion
  30. public delegate bool TileMatchDelegate(TileProxy previousTile, TileProxy potentialNextTile, ref float weight);
  31. public delegate TileProxy GetTileTemplateDelegate(GameObject prefab);
  32. public sealed class DoorwayPairFinder
  33. {
  34. #region Statics
  35. public static readonly List<TileConnectionRule> CustomConnectionRules = new List<TileConnectionRule>();
  36. [RuntimeInitializeOnLoadMethod]
  37. private static void ResetStatics()
  38. {
  39. CustomConnectionRules.Clear();
  40. }
  41. private static int CompareConnectionRules(TileConnectionRule a, TileConnectionRule b)
  42. {
  43. return b.Priority.CompareTo(a.Priority);
  44. }
  45. public static void SortCustomConnectionRules()
  46. {
  47. CustomConnectionRules.Sort(CompareConnectionRules);
  48. }
  49. #endregion
  50. public RandomStream RandomStream;
  51. public List<GameObjectChance> TileWeights;
  52. public TileProxy PreviousTile;
  53. public bool IsOnMainPath;
  54. public float NormalizedDepth;
  55. public TilePlacementParameters PlacementParameters;
  56. public bool? AllowRotation;
  57. public Vector3 UpVector;
  58. public TileMatchDelegate IsTileAllowedPredicate;
  59. public GetTileTemplateDelegate GetTileTemplateDelegate;
  60. public DungeonFlow DungeonFlow;
  61. public DungeonProxy DungeonProxy;
  62. private Vector3? currentPathDirection;
  63. private bool shouldStraightenNextConnection;
  64. private List<GameObjectChance> tileOrder;
  65. public Queue<DoorwayPair> GetDoorwayPairs(int? maxCount)
  66. {
  67. tileOrder = CalculateOrderedListOfTiles();
  68. // Calculate straightening properties
  69. shouldStraightenNextConnection = CalculateShouldStraightenNextConnection();
  70. if(shouldStraightenNextConnection)
  71. currentPathDirection = CalculateCurrentPathDirection();
  72. if (currentPathDirection == null)
  73. shouldStraightenNextConnection = false;
  74. List<DoorwayPair> potentialPairs;
  75. if (PreviousTile == null)
  76. potentialPairs = GetPotentialDoorwayPairsForFirstTile().ToList();
  77. else
  78. potentialPairs = GetPotentialDoorwayPairsForNonFirstTile().ToList();
  79. int count = potentialPairs.Count;
  80. if (maxCount.HasValue)
  81. count = Math.Min(count, maxCount.Value);
  82. Queue<DoorwayPair> pairs = new Queue<DoorwayPair>(
  83. OrderDoorwayPairs(potentialPairs)
  84. .Take(count));
  85. return pairs;
  86. }
  87. private bool CalculateShouldStraightenNextConnection()
  88. {
  89. PathStraighteningSettings straighteningSettings = null;
  90. if (PlacementParameters.Archetype != null)
  91. straighteningSettings = PlacementParameters.Archetype.StraighteningSettings;
  92. else if (PlacementParameters.Node != null)
  93. {
  94. straighteningSettings = PlacementParameters.Node.StraighteningSettings;
  95. // Until branch paths are supported on nodes, we should just set these manually
  96. // to avoid any potential situation where they were somehow set incorrectly
  97. straighteningSettings.CanStraightenMainPath = true;
  98. straighteningSettings.CanStraightenBranchPaths = false;
  99. }
  100. // Exit early if we have no settings to work with
  101. if (straighteningSettings == null)
  102. return false;
  103. // Apply any overrides to the global settings
  104. straighteningSettings = PathStraighteningSettings.GetFinalValues(straighteningSettings, DungeonFlow.GlobalStraighteningSettings);
  105. // Ignore main path based on user settings
  106. if (IsOnMainPath && !straighteningSettings.CanStraightenMainPath)
  107. return false;
  108. // Ignore branch paths based on user settings
  109. if (!IsOnMainPath && !straighteningSettings.CanStraightenBranchPaths)
  110. return false;
  111. // Random chance to straighten the connection
  112. return RandomStream.NextDouble() < straighteningSettings.StraightenChance;
  113. }
  114. private Vector3? CalculateCurrentPathDirection()
  115. {
  116. if (PreviousTile == null || !shouldStraightenNextConnection)
  117. return null;
  118. if(IsOnMainPath)
  119. {
  120. float pathDepth = PreviousTile.Placement.PathDepth;
  121. // Find the doorway we entered through and return its forward direction
  122. foreach (var doorway in PreviousTile.UsedDoorways)
  123. {
  124. var connectedTile = doorway.ConnectedDoorway.TileProxy;
  125. // We entered through this doorway if its connected Tile has a lower path depth than the current tile
  126. if (connectedTile.Placement.PathDepth < pathDepth)
  127. return -doorway.Forward;
  128. }
  129. }
  130. else
  131. {
  132. // We can't calculate a path direction for the first tile in a branch
  133. if (PreviousTile.Placement.IsOnMainPath)
  134. return null;
  135. float branchDepth = PreviousTile.Placement.BranchDepth;
  136. // Find the doorway we entered through and return its forward direction
  137. foreach (var doorway in PreviousTile.UsedDoorways)
  138. {
  139. var connectedTile = doorway.ConnectedDoorway.TileProxy;
  140. // We entered through this doorway if its connected Tile is on the main path or has a lower path depth than the current tile
  141. if (connectedTile.Placement.IsOnMainPath || connectedTile.Placement.BranchDepth < branchDepth)
  142. return -doorway.Forward;
  143. }
  144. }
  145. return null;
  146. }
  147. private IEnumerable<DoorwayPair> OrderDoorwayPairs(List<DoorwayPair> potentialPairs)
  148. {
  149. potentialPairs.Sort((a, b) =>
  150. {
  151. // First compare TileWeight (descending)
  152. int tileWeightComparison = b.TileWeight.CompareTo(a.TileWeight);
  153. // If TileWeights are equal, compare DoorwayWeight (descending)
  154. if (tileWeightComparison == 0)
  155. return b.DoorwayWeight.CompareTo(a.DoorwayWeight);
  156. return tileWeightComparison;
  157. });
  158. return potentialPairs;
  159. }
  160. private List<GameObjectChance> CalculateOrderedListOfTiles()
  161. {
  162. List<GameObjectChance> tiles = new List<GameObjectChance>(TileWeights.Count);
  163. GameObjectChanceTable table = new GameObjectChanceTable();
  164. table.Weights.AddRange(TileWeights);
  165. while (table.Weights.Any(x => x.Value != null && x.GetWeight(IsOnMainPath, NormalizedDepth) > 0.0f))
  166. tiles.Add(table.GetRandom(RandomStream, IsOnMainPath, NormalizedDepth, null, true, true));
  167. return tiles;
  168. }
  169. private IEnumerable<DoorwayPair> GetPotentialDoorwayPairsForNonFirstTile()
  170. {
  171. foreach (var previousDoor in PreviousTile.UnusedDoorways)
  172. {
  173. if (previousDoor.IsDisabled)
  174. continue;
  175. var validExits = PreviousTile.UnusedDoorways.Intersect(PreviousTile.Exits);
  176. var unusedDoorways = PreviousTile.UnusedDoorways.ToArray();
  177. bool requiresSpecificExit = validExits.Any();
  178. // If the previous tile must use a specific exit and this door isn't one of them, skip it
  179. if (requiresSpecificExit && !validExits.Contains(previousDoor))
  180. continue;
  181. foreach (var tileWeight in TileWeights)
  182. {
  183. // This tile wasn't even considered a possibility in the tile ordering phase, skip it
  184. if (!tileOrder.Contains(tileWeight))
  185. continue;
  186. var nextTile = GetTileTemplateDelegate(tileWeight.Value);
  187. float weight = tileOrder.Count - tileOrder.IndexOf(tileWeight);
  188. if (IsTileAllowedPredicate != null && !IsTileAllowedPredicate(PreviousTile, nextTile, ref weight))
  189. continue;
  190. foreach (var nextDoor in nextTile.Doorways)
  191. {
  192. bool requiresSpecificEntrance = nextTile.Entrances.Any();
  193. // If the next tile must use a specific entrance and this door isn't one of them, skip it
  194. if (requiresSpecificEntrance && !nextTile.Entrances.Contains(nextDoor))
  195. continue;
  196. // Skip this door if it's designated as an exit
  197. if (nextTile != null && nextTile.Exits.Contains(nextDoor))
  198. continue;
  199. float doorwayWeight = 0f;
  200. if (IsValidDoorwayPairing(previousDoor, nextDoor, PreviousTile, nextTile, ref doorwayWeight))
  201. yield return new DoorwayPair(PreviousTile, previousDoor, nextTile, nextDoor, tileWeight.TileSet, weight, doorwayWeight);
  202. }
  203. }
  204. }
  205. }
  206. private IEnumerable<DoorwayPair> GetPotentialDoorwayPairsForFirstTile()
  207. {
  208. foreach (var tileWeight in TileWeights)
  209. {
  210. // This tile wasn't even considered a possibility in the tile ordering phase, skip it
  211. if (!tileOrder.Contains(tileWeight))
  212. continue;
  213. var nextTile = GetTileTemplateDelegate(tileWeight.Value);
  214. float weight = tileWeight.GetWeight(IsOnMainPath, NormalizedDepth) * (float)RandomStream.NextDouble();
  215. if (IsTileAllowedPredicate != null && !IsTileAllowedPredicate(PreviousTile, nextTile, ref weight))
  216. continue;
  217. foreach (var nextDoorway in nextTile.Doorways)
  218. {
  219. var proposedConnection = new ProposedConnection(DungeonProxy, null, nextTile, null, nextDoorway);
  220. float doorwayWeight = CalculateConnectionWeight(proposedConnection);
  221. yield return new DoorwayPair(null, null, nextTile, nextDoorway, tileWeight.TileSet, weight, doorwayWeight);
  222. }
  223. }
  224. }
  225. private bool IsValidDoorwayPairing(DoorwayProxy previousDoorway, DoorwayProxy nextDoorway, TileProxy previousTile, TileProxy nextTile, ref float weight)
  226. {
  227. var proposedConnection = new ProposedConnection(DungeonProxy, previousTile, nextTile, previousDoorway, nextDoorway);
  228. // Enforce connection rules
  229. if (!DungeonFlow.CanDoorwaysConnect(proposedConnection))
  230. return false;
  231. // Enforce facing-direction
  232. Vector3? forcedDirection = null;
  233. // If AllowRotation has been set to false, or if the tile to be placed disallows rotation, we must force a connection from the correct direction
  234. bool disallowRotation = (AllowRotation.HasValue && !AllowRotation.Value) || (nextTile != null && !nextTile.PrefabTile.AllowRotation);
  235. // Always enforce facing direction for vertical doorways
  236. const float angleEpsilon = 1.0f;
  237. if (Vector3.Angle(previousDoorway.Forward, UpVector) < angleEpsilon)
  238. forcedDirection = -UpVector;
  239. else if (Vector3.Angle(previousDoorway.Forward, -UpVector) < angleEpsilon)
  240. forcedDirection = UpVector;
  241. else if (disallowRotation)
  242. forcedDirection = -previousDoorway.Forward;
  243. if (forcedDirection.HasValue)
  244. {
  245. float angleDiff = Vector3.Angle(forcedDirection.Value, nextDoorway.Forward);
  246. const float maxAngleDiff = 1.0f;
  247. if (angleDiff > maxAngleDiff)
  248. return false;
  249. }
  250. weight = CalculateConnectionWeight(proposedConnection);
  251. return weight > 0.0f;
  252. }
  253. private float CalculateConnectionWeight(ProposedConnection connection)
  254. {
  255. // Assign a random weight initially
  256. float weight = (float)RandomStream.NextDouble();
  257. bool straighten = shouldStraightenNextConnection &&
  258. currentPathDirection != null &&
  259. connection.PreviousDoorway != null;
  260. // Heavily weight towards doorways that keep the dungeon flowing in the same direction
  261. if (straighten)
  262. {
  263. // Compare exit doorway direction to the current path direction
  264. float dot = Vector3.Dot(currentPathDirection.Value, connection.PreviousDoorway.Forward);
  265. // If we're heading in the wrong direction, return a weight of 0
  266. if (dot < 0.99f)
  267. weight = 0.0f;
  268. }
  269. return weight;
  270. }
  271. }
  272. }