DoorwayRule.cs 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. using DunGen.Graph;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEngine;
  5. namespace DunGen.Editor.Validation.Rules
  6. {
  7. sealed class DoorwayRule : IValidationRule
  8. {
  9. #region Nested Types
  10. private sealed class DoorwayInfo
  11. {
  12. public Doorway Doorway { get; private set; }
  13. public GameObject TilePrefab { get; private set; }
  14. public DoorwayInfo(Doorway doorway, GameObject tilePrefab)
  15. {
  16. Doorway = doorway;
  17. TilePrefab = tilePrefab;
  18. }
  19. }
  20. #endregion
  21. private const float AngleThreshold = 0.5f;
  22. public void Validate(DungeonFlow flow, DungeonValidator validator)
  23. {
  24. var tiles = flow.GetUsedTileSets()
  25. .SelectMany(ts => ts.TileWeights.Weights.Select(w => w.Value))
  26. .Where(t => t != null);
  27. var tileDoorways = new Dictionary<GameObject, Doorway[]>();
  28. foreach (var tile in tiles)
  29. tileDoorways[tile] = tile.GetComponentsInChildren<Doorway>(true);
  30. CheckDoorwayCount(flow, validator);
  31. CheckDoorwayUpVectors(flow, validator, tileDoorways);
  32. CheckDoorwayForwardVectorsAndPositionAlongBounds(flow, validator, tileDoorways);
  33. CheckDoorwaySockets(flow, validator, tileDoorways);
  34. }
  35. private void CheckDoorwayCount(DungeonFlow flow, DungeonValidator validator)
  36. {
  37. var pathTileSets = new List<TileSet>();
  38. foreach (var node in flow.Nodes)
  39. if (node.NodeType != NodeType.Start && node.NodeType != NodeType.Goal)
  40. pathTileSets.AddRange(node.TileSets);
  41. foreach (var line in flow.Lines)
  42. foreach (var archetype in line.DungeonArchetypes)
  43. pathTileSets.AddRange(archetype.TileSets);
  44. var pathTiles = pathTileSets.SelectMany(ts => ts.TileWeights.Weights.Select(w => w.Value)).Where(t => t != null);
  45. foreach (var tile in pathTiles)
  46. {
  47. int doorwayCount = tile.GetComponentsInChildren<Doorway>(true).Count();
  48. if (doorwayCount < 2)
  49. validator.AddError("Tile '{0}' does not have enough doorways. Two doorways are required for all tiles except those that appear exclusively as a start node, goal node, or branch cap", tile, tile.name);
  50. }
  51. }
  52. private void CheckDoorwayUpVectors(DungeonFlow flow, DungeonValidator validator, Dictionary<GameObject, Doorway[]> tileDoorways)
  53. {
  54. var doorwaysByUpVector = new Dictionary<Vector3, List<DoorwayInfo>>();
  55. foreach(var pair in tileDoorways)
  56. {
  57. foreach(var doorway in pair.Value)
  58. {
  59. Vector3 upVector = doorway.transform.up;
  60. List<DoorwayInfo> doorwaySet = null;
  61. foreach(var existingPair in doorwaysByUpVector)
  62. if(Vector3.Angle(upVector, existingPair.Key) <= AngleThreshold)
  63. doorwaySet = existingPair.Value;
  64. if(doorwaySet == null)
  65. {
  66. doorwaySet = new List<DoorwayInfo>();
  67. doorwaysByUpVector[upVector] = doorwaySet;
  68. }
  69. doorwaySet.Add(new DoorwayInfo(doorway, pair.Key));
  70. }
  71. }
  72. if(doorwaysByUpVector.Count > 1)
  73. {
  74. Vector3 mostCommonUpVector = doorwaysByUpVector.OrderByDescending(x => x.Value.Count).First().Key;
  75. if (!UnityUtil.IsVectorAxisAligned(mostCommonUpVector))
  76. validator.AddError("The most common doorway up vector is not axis-aligned");
  77. foreach(var pair in doorwaysByUpVector)
  78. {
  79. if (pair.Key == mostCommonUpVector)
  80. continue;
  81. foreach (var info in pair.Value)
  82. validator.AddError("Doorway '{0}' in tile '{1}' has an invalid rotation. The most common up-vector among doorways is {2}", info.TilePrefab, info.Doorway.name, info.TilePrefab.name, mostCommonUpVector);
  83. }
  84. }
  85. }
  86. private void CheckDoorwayForwardVectorsAndPositionAlongBounds(DungeonFlow flow, DungeonValidator validator, Dictionary<GameObject, Doorway[]> tileDoorways)
  87. {
  88. foreach(var pair in tileDoorways)
  89. {
  90. var tilePrefab = pair.Key;
  91. var tileComponent = tilePrefab.GetComponent<Tile>();
  92. // If there's no tile component, we can't validate the doorways
  93. if (tileComponent == null)
  94. continue;
  95. foreach (var doorway in pair.Value)
  96. {
  97. doorway.ValidateTransform(out _, out bool isAxisAligned, out bool isEdgePositioned);
  98. if (!isAxisAligned)
  99. validator.AddError("Doorway '{0}' in tile '{1}' has an invalid rotation. the forward vector is not axis-aligned", tilePrefab, doorway.name, tilePrefab.name);
  100. else if (!isEdgePositioned)
  101. validator.AddWarning("Doorway '{0}' in tile '{1}' is not positioned at the edge of the tile's bounding box. This could also indicate that the doorway is facing the wrong way - a doorway should be rotated such that its local z-axis is facing away from the tile", tilePrefab, doorway.name, tilePrefab.name);
  102. }
  103. }
  104. }
  105. private void CheckDoorwaySockets(DungeonFlow flow, DungeonValidator validator, Dictionary<GameObject, Doorway[]> tileDoorways)
  106. {
  107. foreach (var pair in tileDoorways)
  108. {
  109. var tile = pair.Key;
  110. foreach (var doorway in pair.Value)
  111. {
  112. if (!doorway.HasSocketAssigned)
  113. validator.AddWarning("Doorway '{0}' in tile '{1}' has no socket assigned. The default socket will be used.", tile, doorway.name, tile.name);
  114. }
  115. }
  116. }
  117. }
  118. }