├── .gitattributes ├── AgentPlacer.cs ├── DungeonData.cs ├── LICENSE ├── Prop.cs ├── PropPlacementManager.cs ├── README.md ├── RoomDataExtractor.cs └── SimpleDungeonGenerator.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /AgentPlacer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEngine; 6 | using Cinemachine; 7 | 8 | public class AgentPlacer : MonoBehaviour 9 | { 10 | [SerializeField] 11 | private GameObject enemyPrefab, playerPrefab; 12 | 13 | [SerializeField] 14 | private int playerRoomIndex; 15 | [SerializeField] 16 | private CinemachineVirtualCamera vCamera; 17 | 18 | [SerializeField] 19 | private List roomEnemiesCount; 20 | 21 | DungeonData dungeonData; 22 | 23 | [SerializeField] 24 | private bool showGizmo = false; 25 | 26 | private void Awake() 27 | { 28 | dungeonData = FindObjectOfType(); 29 | } 30 | 31 | public void PlaceAgents() 32 | { 33 | if (dungeonData == null) 34 | return; 35 | 36 | //Loop for each room 37 | for (int i = 0; i < dungeonData.Rooms.Count; i++) 38 | { 39 | //TO place eneies we need to analyze the room tiles to find those accesible from the path 40 | Room room = dungeonData.Rooms[i]; 41 | RoomGraph roomGraph = new RoomGraph(room.FloorTiles); 42 | 43 | //Find the Path inside this specific room 44 | HashSet roomFloor = new HashSet(room.FloorTiles); 45 | //Find the tiles belonging to both the path and the room 46 | roomFloor.IntersectWith(dungeonData.Path); 47 | 48 | //Run the BFS to find all the tiles in the room accessible from the path 49 | Dictionary roomMap = roomGraph.RunBFS(roomFloor.First(), room.PropPositions); 50 | 51 | //Positions that we can reach + path == positions where we can place enemies 52 | room.PositionsAccessibleFromPath = roomMap.Keys.OrderBy(x => Guid.NewGuid()).ToList(); 53 | 54 | //did we add this room to the roomEnemiesCount list? 55 | if(roomEnemiesCount.Count > i) 56 | { 57 | PlaceEnemies(room, roomEnemiesCount[i]); 58 | } 59 | 60 | //Place the player 61 | if(i==playerRoomIndex) 62 | { 63 | GameObject player = Instantiate(playerPrefab); 64 | player.transform.localPosition = dungeonData.Rooms[i].RoomCenterPos + Vector2.one*0.5f; 65 | //Make the camera follow the player 66 | vCamera.Follow = player.transform; 67 | vCamera.LookAt = player.transform; 68 | dungeonData.PlayerReference = player; 69 | } 70 | } 71 | } 72 | 73 | /// 74 | /// Places enemies in the positions accessible from the path 75 | /// 76 | /// 77 | /// 78 | private void PlaceEnemies(Room room, int enemysCount) 79 | { 80 | for (int k = 0; k < enemysCount; k++) 81 | { 82 | if (room.PositionsAccessibleFromPath.Count <= k) 83 | { 84 | return; 85 | } 86 | GameObject enemy = Instantiate(enemyPrefab); 87 | enemy.transform.localPosition = (Vector2)room.PositionsAccessibleFromPath[k] + Vector2.one*0.5f; 88 | room.EnemiesInTheRoom.Add(enemy); 89 | } 90 | } 91 | 92 | private void OnDrawGizmosSelected() 93 | { 94 | if (dungeonData == null || showGizmo == false) 95 | return; 96 | foreach (Room room in dungeonData.Rooms) 97 | { 98 | Color color = Color.green; 99 | color.a = 0.3f; 100 | Gizmos.color = color; 101 | 102 | foreach (Vector2Int pos in room.PositionsAccessibleFromPath) 103 | { 104 | Gizmos.DrawCube((Vector2)pos + Vector2.one * 0.5f, Vector2.one); 105 | } 106 | } 107 | } 108 | } 109 | 110 | public class RoomGraph 111 | { 112 | public static List fourDirections = new() 113 | { 114 | Vector2Int.up, 115 | Vector2Int.right, 116 | Vector2Int.down, 117 | Vector2Int.left 118 | }; 119 | 120 | Dictionary> graph = new Dictionary>(); 121 | 122 | public RoomGraph(HashSet roomFloor) 123 | { 124 | foreach (Vector2Int pos in roomFloor) 125 | { 126 | List neighbours = new List(); 127 | foreach (Vector2Int direction in fourDirections) 128 | { 129 | Vector2Int newPos = pos + direction; 130 | if (roomFloor.Contains(newPos)) 131 | { 132 | neighbours.Add(newPos); 133 | } 134 | } 135 | graph.Add(pos, neighbours); 136 | } 137 | } 138 | 139 | /// 140 | /// Creates a map of reachable tiles in our dungeon. 141 | /// 142 | /// Door position or tile position on the path between rooms inside this room 143 | /// 144 | /// 145 | public Dictionary RunBFS(Vector2Int startPos, HashSet occupiedNodes) 146 | { 147 | //BFS related variuables 148 | Queue nodesToVisit = new Queue(); 149 | nodesToVisit.Enqueue(startPos); 150 | 151 | HashSet visitedNodes = new HashSet(); 152 | visitedNodes.Add(startPos); 153 | 154 | //The dictionary that we will return 155 | Dictionary map = new Dictionary(); 156 | map.Add(startPos, startPos); 157 | 158 | while (nodesToVisit.Count > 0) 159 | { 160 | //get the data about specific position 161 | Vector2Int node = nodesToVisit.Dequeue(); 162 | List neighbours = graph[node]; 163 | 164 | //loop through each neighbour position 165 | foreach (Vector2Int neighbourPosition in neighbours) 166 | { 167 | //add the neighbour position to our map if it is valid 168 | if (visitedNodes.Contains(neighbourPosition) == false && 169 | occupiedNodes.Contains(neighbourPosition) == false) 170 | { 171 | nodesToVisit.Enqueue(neighbourPosition); 172 | visitedNodes.Add(neighbourPosition); 173 | map[neighbourPosition] = node; 174 | } 175 | } 176 | } 177 | 178 | return map; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /DungeonData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | 6 | /// 7 | /// Stores all the data about our dungeon. 8 | /// Useful when creating a save / load system 9 | /// 10 | public class DungeonData : MonoBehaviour 11 | { 12 | public List Rooms { get; set; } = new List(); 13 | public HashSet Path { get; set; } = new HashSet(); 14 | 15 | public GameObject PlayerReference { get; set; } 16 | public void Reset() 17 | { 18 | foreach (Room room in Rooms) 19 | { 20 | foreach (var item in room.PropObjectReferences) 21 | { 22 | Destroy(item); 23 | } 24 | foreach (var item in room.EnemiesInTheRoom) 25 | { 26 | Destroy(item); 27 | } 28 | } 29 | Rooms = new(); 30 | Path = new(); 31 | Destroy(PlayerReference); 32 | } 33 | 34 | public IEnumerator TutorialCoroutine(Action code) 35 | { 36 | yield return new WaitForSeconds(1); 37 | code(); 38 | } 39 | } 40 | 41 | 42 | /// 43 | /// Holds all the data about the room 44 | /// 45 | public class Room 46 | { 47 | public Vector2 RoomCenterPos { get; set; } 48 | public HashSet FloorTiles { get; private set; } = new HashSet(); 49 | 50 | public HashSet NearWallTilesUp { get; set; } = new HashSet(); 51 | public HashSet NearWallTilesDown { get; set; } = new HashSet(); 52 | public HashSet NearWallTilesLeft { get; set; } = new HashSet(); 53 | public HashSet NearWallTilesRight { get; set; } = new HashSet(); 54 | public HashSet CornerTiles { get; set; } = new HashSet(); 55 | 56 | public HashSet InnerTiles { get; set; } = new HashSet(); 57 | 58 | public HashSet PropPositions { get; set; } = new HashSet(); 59 | public List PropObjectReferences { get; set; } = new List(); 60 | 61 | public List PositionsAccessibleFromPath { get; set; } = new List(); 62 | 63 | public List EnemiesInTheRoom { get; set; } = new List(); 64 | 65 | public Room(Vector2 roomCenterPos, HashSet floorTiles) 66 | { 67 | this.RoomCenterPos = roomCenterPos; 68 | this.FloorTiles = floorTiles; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Peter 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Prop.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | [CreateAssetMenu] 6 | public class Prop : ScriptableObject 7 | { 8 | [Header("Prop data:")] 9 | public Sprite PropSprite; 10 | /// 11 | /// Affects the collider size of the prop 12 | /// 13 | public Vector2Int PropSize = Vector2Int.one; 14 | 15 | [Space, Header("Placement type:")] 16 | public bool Corner = true; 17 | public bool NearWallUP = true; 18 | public bool NearWallDown = true; 19 | public bool NearWallRight = true; 20 | public bool NearWallLeft = true; 21 | public bool Inner = true; 22 | [Min(1)] 23 | public int PlacementQuantityMin = 1; 24 | [Min(1)] 25 | public int PlacementQuantityMax = 1; 26 | 27 | [Space, Header("Group placement:")] 28 | public bool PlaceAsGroup = false; 29 | [Min(1)] 30 | public int GroupMinCount = 1; 31 | [Min(1)] 32 | public int GroupMaxCount = 1; 33 | 34 | } 35 | -------------------------------------------------------------------------------- /PropPlacementManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using UnityEngine; 6 | using UnityEngine.Events; 7 | 8 | public class PropPlacementManager : MonoBehaviour 9 | { 10 | DungeonData dungeonData; 11 | 12 | [SerializeField] 13 | private List propsToPlace; 14 | 15 | [SerializeField, Range(0, 1)] 16 | private float cornerPropPlacementChance = 0.7f; 17 | 18 | [SerializeField] 19 | private GameObject propPrefab; 20 | 21 | public UnityEvent OnFinished; 22 | 23 | private void Awake() 24 | { 25 | dungeonData = FindObjectOfType(); 26 | } 27 | 28 | public void ProcessRooms() 29 | { 30 | if (dungeonData == null) 31 | return; 32 | foreach (Room room in dungeonData.Rooms) 33 | { 34 | //Place props place props in the corners 35 | List cornerProps = propsToPlace.Where(x => x.Corner).ToList(); 36 | PlaceCornerProps(room, cornerProps); 37 | 38 | //Place props near LEFT wall 39 | List leftWallProps = propsToPlace 40 | .Where(x => x.NearWallLeft) 41 | .OrderByDescending(x => x.PropSize.x * x.PropSize.y) 42 | .ToList(); 43 | 44 | PlaceProps(room, leftWallProps, room.NearWallTilesLeft, PlacementOriginCorner.BottomLeft); 45 | 46 | //Place props near RIGHT wall 47 | List rightWallProps = propsToPlace 48 | .Where(x => x.NearWallRight) 49 | .OrderByDescending(x => x.PropSize.x * x.PropSize.y) 50 | .ToList(); 51 | 52 | PlaceProps(room, rightWallProps, room.NearWallTilesRight, PlacementOriginCorner.TopRight); 53 | 54 | //Place props near UP wall 55 | List topWallProps = propsToPlace 56 | .Where(x => x.NearWallUP) 57 | .OrderByDescending(x => x.PropSize.x * x.PropSize.y) 58 | .ToList(); 59 | 60 | PlaceProps(room, topWallProps, room.NearWallTilesUp, PlacementOriginCorner.TopLeft); 61 | 62 | //Place props near DOWN wall 63 | List downWallProps = propsToPlace 64 | .Where(x => x.NearWallDown) 65 | .OrderByDescending(x => x.PropSize.x * x.PropSize.y) 66 | .ToList(); 67 | 68 | PlaceProps(room, downWallProps, room.NearWallTilesDown, PlacementOriginCorner.BottomLeft); 69 | 70 | //Place inner props 71 | List innerProps = propsToPlace 72 | .Where(x => x.Inner) 73 | .OrderByDescending(x => x.PropSize.x * x.PropSize.y) 74 | .ToList(); 75 | PlaceProps(room, innerProps, room.InnerTiles, PlacementOriginCorner.BottomLeft); 76 | } 77 | 78 | //OnFinished?.Invoke(); 79 | Invoke("RunEvent", 1); 80 | 81 | } 82 | 83 | public void RunEvent() 84 | { 85 | OnFinished?.Invoke(); 86 | } 87 | 88 | private IEnumerator TutorialCoroutine(Action code) 89 | { 90 | yield return new WaitForSeconds(3); 91 | code(); 92 | } 93 | 94 | /// 95 | /// Places props near walls. We need to specify the props anw the placement start point 96 | /// 97 | /// 98 | /// Props that we should try to place 99 | /// Tiles that are near the specific wall 100 | /// How to place bigger props. Ex near top wall we want to start placemt from the Top corner and find if there are free spaces below 101 | private void PlaceProps( 102 | Room room, List wallProps, HashSet availableTiles, PlacementOriginCorner placement) 103 | { 104 | //Remove path positions from the initial nearWallTiles to ensure the clear path to traverse dungeon 105 | HashSet tempPositons = new HashSet(availableTiles); 106 | tempPositons.ExceptWith(dungeonData.Path); 107 | 108 | //We will try to place all the props 109 | foreach (Prop propToPlace in wallProps) 110 | { 111 | //We want to place only certain quantity of each prop 112 | int quantity 113 | = UnityEngine.Random.Range(propToPlace.PlacementQuantityMin, propToPlace.PlacementQuantityMax +1); 114 | 115 | for (int i = 0; i < quantity; i++) 116 | { 117 | //remove taken positions 118 | tempPositons.ExceptWith(room.PropPositions); 119 | //shuffel the positions 120 | List availablePositions = tempPositons.OrderBy(x => Guid.NewGuid()).ToList(); 121 | //If placement has failed there is no point in trying to place the same prop again 122 | if (TryPlacingPropBruteForce(room, propToPlace, availablePositions, placement) == false) 123 | break; 124 | } 125 | 126 | } 127 | } 128 | 129 | /// 130 | /// Tries to place the Prop using brute force (trying each available tile position) 131 | /// 132 | /// 133 | /// 134 | /// 135 | /// 136 | /// False if there is no space. True if placement was successful 137 | private bool TryPlacingPropBruteForce( 138 | Room room, Prop propToPlace, List availablePositions, PlacementOriginCorner placement) 139 | { 140 | //try placing the objects starting from the corner specified by the placement parameter 141 | for (int i = 0; i < availablePositions.Count; i++) 142 | { 143 | //select the specified position (but it can be already taken after placing the corner props as a group) 144 | Vector2Int position = availablePositions[i]; 145 | if (room.PropPositions.Contains(position)) 146 | continue; 147 | 148 | //check if there is enough space around to fit the prop 149 | List freePositionsAround 150 | = TryToFitProp(propToPlace, availablePositions, position, placement); 151 | 152 | //If we have enough spaces place the prop 153 | if (freePositionsAround.Count == propToPlace.PropSize.x * propToPlace.PropSize.y) 154 | { 155 | //Place the gameobject 156 | PlacePropGameObjectAt(room, position, propToPlace); 157 | //Lock all the positions recquired by the prop (based on its size) 158 | foreach (Vector2Int pos in freePositionsAround) 159 | { 160 | //Hashest will ignore duplicate positions 161 | room.PropPositions.Add(pos); 162 | } 163 | 164 | //Deal with groups 165 | if (propToPlace.PlaceAsGroup) 166 | { 167 | PlaceGroupObject(room, position, propToPlace, 1); 168 | } 169 | return true; 170 | } 171 | } 172 | 173 | return false; 174 | } 175 | 176 | /// 177 | /// Checks if the prop will fit (accordig to it size) 178 | /// 179 | /// 180 | /// 181 | /// 182 | /// 183 | /// 184 | private List TryToFitProp( 185 | Prop prop, 186 | List availablePositions, 187 | Vector2Int originPosition, 188 | PlacementOriginCorner placement) 189 | { 190 | List freePositions = new(); 191 | 192 | //Perform the specific loop depending on the PlacementOriginCorner 193 | if (placement == PlacementOriginCorner.BottomLeft) 194 | { 195 | for (int xOffset = 0; xOffset < prop.PropSize.x; xOffset++) 196 | { 197 | for (int yOffset = 0; yOffset < prop.PropSize.y; yOffset++) 198 | { 199 | Vector2Int tempPos = originPosition + new Vector2Int(xOffset, yOffset); 200 | if (availablePositions.Contains(tempPos)) 201 | freePositions.Add(tempPos); 202 | } 203 | } 204 | } 205 | else if (placement == PlacementOriginCorner.BottomRight) 206 | { 207 | for (int xOffset = -prop.PropSize.x + 1; xOffset <= 0; xOffset++) 208 | { 209 | for (int yOffset = 0; yOffset < prop.PropSize.y; yOffset++) 210 | { 211 | Vector2Int tempPos = originPosition + new Vector2Int(xOffset, yOffset); 212 | if (availablePositions.Contains(tempPos)) 213 | freePositions.Add(tempPos); 214 | } 215 | } 216 | } 217 | else if (placement == PlacementOriginCorner.TopLeft) 218 | { 219 | for (int xOffset = 0; xOffset < prop.PropSize.x; xOffset++) 220 | { 221 | for (int yOffset = -prop.PropSize.y + 1; yOffset <= 0; yOffset++) 222 | { 223 | Vector2Int tempPos = originPosition + new Vector2Int(xOffset, yOffset); 224 | if (availablePositions.Contains(tempPos)) 225 | freePositions.Add(tempPos); 226 | } 227 | } 228 | } 229 | else 230 | { 231 | for (int xOffset = -prop.PropSize.x + 1; xOffset <= 0; xOffset++) 232 | { 233 | for (int yOffset = -prop.PropSize.y + 1; yOffset <= 0; yOffset++) 234 | { 235 | Vector2Int tempPos = originPosition + new Vector2Int(xOffset, yOffset); 236 | if (availablePositions.Contains(tempPos)) 237 | freePositions.Add(tempPos); 238 | } 239 | } 240 | } 241 | 242 | return freePositions; 243 | } 244 | 245 | /// 246 | /// Places props in the corners of the room 247 | /// 248 | /// 249 | /// 250 | private void PlaceCornerProps(Room room, List cornerProps) 251 | { 252 | float tempChance = cornerPropPlacementChance; 253 | 254 | foreach (Vector2Int cornerTile in room.CornerTiles) 255 | { 256 | if (UnityEngine.Random.value < tempChance) 257 | { 258 | Prop propToPlace 259 | = cornerProps[UnityEngine.Random.Range(0, cornerProps.Count)]; 260 | 261 | PlacePropGameObjectAt(room, cornerTile, propToPlace); 262 | if (propToPlace.PlaceAsGroup) 263 | { 264 | PlaceGroupObject(room, cornerTile, propToPlace, 2); 265 | } 266 | } 267 | else 268 | { 269 | tempChance = Mathf.Clamp01(tempChance + 0.1f); 270 | } 271 | } 272 | } 273 | 274 | /// 275 | /// Helps to find free spaces around the groupOriginPosition to place a prop as a group 276 | /// 277 | /// 278 | /// 279 | /// 280 | /// The search offset ex 1 = we will check all tiles withing the distance of 1 unity away from origin position 281 | private void PlaceGroupObject( 282 | Room room, Vector2Int groupOriginPosition, Prop propToPlace, int searchOffset) 283 | { 284 | //*Can work poorely when placing bigger props as groups 285 | 286 | //calculate how many elements are in the group -1 that we have placed in the center 287 | int count = UnityEngine.Random.Range(propToPlace.GroupMinCount, propToPlace.GroupMaxCount) - 1; 288 | count = Mathf.Clamp(count, 0, 8); 289 | 290 | //find the available spaces around the center point. 291 | //we use searchOffset to limit the distance between those points and the center point 292 | List availableSpaces = new List(); 293 | for (int xOffset = -searchOffset; xOffset <= searchOffset; xOffset++) 294 | { 295 | for (int yOffset = -searchOffset; yOffset <= searchOffset; yOffset++) 296 | { 297 | Vector2Int tempPos = groupOriginPosition + new Vector2Int(xOffset, yOffset); 298 | if (room.FloorTiles.Contains(tempPos) && 299 | !dungeonData.Path.Contains(tempPos) && 300 | !room.PropPositions.Contains(tempPos)) 301 | { 302 | availableSpaces.Add(tempPos); 303 | } 304 | } 305 | } 306 | 307 | //shuffle the list 308 | availableSpaces.OrderBy(x => Guid.NewGuid()); 309 | 310 | //place the props (as many as we want or if there is less space fill all the available spaces) 311 | int tempCount = count < availableSpaces.Count ? count : availableSpaces.Count; 312 | for (int i = 0; i < tempCount; i++) 313 | { 314 | PlacePropGameObjectAt(room, availableSpaces[i], propToPlace); 315 | } 316 | 317 | } 318 | 319 | /// 320 | /// Place a prop as a new GameObject at a specified position 321 | /// 322 | /// 323 | /// 324 | /// 325 | /// 326 | private GameObject PlacePropGameObjectAt(Room room, Vector2Int placementPostion, Prop propToPlace) 327 | { 328 | //Instantiat the prop at this positon 329 | GameObject prop = Instantiate(propPrefab); 330 | SpriteRenderer propSpriteRenderer = prop.GetComponentInChildren(); 331 | 332 | //set the sprite 333 | propSpriteRenderer.sprite = propToPlace.PropSprite; 334 | 335 | //Add a collider 336 | CapsuleCollider2D collider 337 | = propSpriteRenderer.gameObject.AddComponent(); 338 | collider.offset = Vector2.zero; 339 | if(propToPlace.PropSize.x > propToPlace.PropSize.y) 340 | { 341 | collider.direction = CapsuleDirection2D.Horizontal; 342 | } 343 | Vector2 size 344 | = new Vector2(propToPlace.PropSize.x*0.8f, propToPlace.PropSize.y*0.8f); 345 | collider.size = size; 346 | 347 | prop.transform.localPosition = (Vector2)placementPostion; 348 | //adjust the position to the sprite 349 | propSpriteRenderer.transform.localPosition 350 | = (Vector2)propToPlace.PropSize * 0.5f; 351 | 352 | //Save the prop in the room data (so in the dunbgeon data) 353 | room.PropPositions.Add(placementPostion); 354 | room.PropObjectReferences.Add(prop); 355 | return prop; 356 | } 357 | } 358 | 359 | /// 360 | /// Where to start placing the prop ex. start at BottomLeft corner and search 361 | /// if there are free space to the Right and Up in case of placing a biggex prop 362 | /// 363 | public enum PlacementOriginCorner 364 | { 365 | BottomLeft, 366 | BottomRight, 367 | TopLeft, 368 | TopRight 369 | } 370 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Procedural Props and Enemies Placement in Unity 2 | 3 | -------------------------------------------------------------------------------- /RoomDataExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.Events; 6 | 7 | public class RoomDataExtractor : MonoBehaviour 8 | { 9 | private DungeonData dungeonData; 10 | 11 | [SerializeField] 12 | private bool showGizmo = false; 13 | 14 | public UnityEvent OnFinishedRoomProcessing; 15 | 16 | private void Awake() 17 | { 18 | dungeonData = FindObjectOfType(); 19 | } 20 | public void ProcessRooms() 21 | { 22 | if (dungeonData == null) 23 | return; 24 | 25 | foreach (Room room in dungeonData.Rooms) 26 | { 27 | //find corener, near wall and inner tiles 28 | foreach (Vector2Int tilePosition in room.FloorTiles) 29 | { 30 | int neighboursCount = 4; 31 | 32 | if(room.FloorTiles.Contains(tilePosition+Vector2Int.up) == false) 33 | { 34 | room.NearWallTilesUp.Add(tilePosition); 35 | neighboursCount--; 36 | } 37 | if (room.FloorTiles.Contains(tilePosition + Vector2Int.down) == false) 38 | { 39 | room.NearWallTilesDown.Add(tilePosition); 40 | neighboursCount--; 41 | } 42 | if (room.FloorTiles.Contains(tilePosition + Vector2Int.right) == false) 43 | { 44 | room.NearWallTilesRight.Add(tilePosition); 45 | neighboursCount--; 46 | } 47 | if (room.FloorTiles.Contains(tilePosition + Vector2Int.left) == false) 48 | { 49 | room.NearWallTilesLeft.Add(tilePosition); 50 | neighboursCount--; 51 | } 52 | 53 | //find corners 54 | if (neighboursCount <= 2) 55 | room.CornerTiles.Add(tilePosition); 56 | 57 | if (neighboursCount == 4) 58 | room.InnerTiles.Add(tilePosition); 59 | } 60 | 61 | room.NearWallTilesUp.ExceptWith(room.CornerTiles); 62 | room.NearWallTilesDown.ExceptWith(room.CornerTiles); 63 | room.NearWallTilesLeft.ExceptWith(room.CornerTiles); 64 | room.NearWallTilesRight.ExceptWith(room.CornerTiles); 65 | } 66 | 67 | //OnFinishedRoomProcessing?.Invoke(); 68 | Invoke("RunEvent", 1); 69 | } 70 | 71 | public void RunEvent() 72 | { 73 | OnFinishedRoomProcessing?.Invoke(); 74 | } 75 | 76 | private void OnDrawGizmosSelected() 77 | { 78 | if (dungeonData == null || showGizmo == false) 79 | return; 80 | foreach (Room room in dungeonData.Rooms) 81 | { 82 | //Draw inner tiles 83 | Gizmos.color = Color.yellow; 84 | foreach (Vector2Int floorPosition in room.InnerTiles) 85 | { 86 | if (dungeonData.Path.Contains(floorPosition)) 87 | continue; 88 | Gizmos.DrawCube(floorPosition + Vector2.one * 0.5f, Vector2.one); 89 | } 90 | //Draw near wall tiles UP 91 | Gizmos.color = Color.blue; 92 | foreach (Vector2Int floorPosition in room.NearWallTilesUp) 93 | { 94 | if (dungeonData.Path.Contains(floorPosition)) 95 | continue; 96 | Gizmos.DrawCube(floorPosition + Vector2.one * 0.5f, Vector2.one); 97 | } 98 | //Draw near wall tiles DOWN 99 | Gizmos.color = Color.green; 100 | foreach (Vector2Int floorPosition in room.NearWallTilesDown) 101 | { 102 | if (dungeonData.Path.Contains(floorPosition)) 103 | continue; 104 | Gizmos.DrawCube(floorPosition + Vector2.one * 0.5f, Vector2.one); 105 | } 106 | //Draw near wall tiles RIGHT 107 | Gizmos.color = Color.white; 108 | foreach (Vector2Int floorPosition in room.NearWallTilesRight) 109 | { 110 | if (dungeonData.Path.Contains(floorPosition)) 111 | continue; 112 | Gizmos.DrawCube(floorPosition + Vector2.one * 0.5f, Vector2.one); 113 | } 114 | //Draw near wall tiles LEFT 115 | Gizmos.color = Color.cyan; 116 | foreach (Vector2Int floorPosition in room.NearWallTilesLeft) 117 | { 118 | if (dungeonData.Path.Contains(floorPosition)) 119 | continue; 120 | Gizmos.DrawCube(floorPosition + Vector2.one * 0.5f, Vector2.one); 121 | } 122 | //Draw near wall tiles CORNERS 123 | Gizmos.color = Color.magenta; 124 | foreach (Vector2Int floorPosition in room.CornerTiles) 125 | { 126 | if (dungeonData.Path.Contains(floorPosition)) 127 | continue; 128 | Gizmos.DrawCube(floorPosition + Vector2.one * 0.5f, Vector2.one); 129 | } 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /SimpleDungeonGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using UnityEngine; 5 | using UnityEngine.Events; 6 | using UnityEngine.InputSystem; 7 | using UnityEngine.Tilemaps; 8 | 9 | public class SimpleDungeonGenerator : MonoBehaviour 10 | { 11 | [SerializeField] 12 | private Vector2Int roomSize = new Vector2Int(10, 10); 13 | 14 | [SerializeField] 15 | private Tilemap roomMap, colliderMap; 16 | [SerializeField] 17 | private TileBase roomFloorTile, pathFloorTile; 18 | 19 | [SerializeField] 20 | private InputActionReference generate; 21 | 22 | public UnityEvent OnFinishedRoomGeneration; 23 | 24 | public static List fourDirections = new() 25 | { 26 | Vector2Int.up, 27 | Vector2Int.right, 28 | Vector2Int.down, 29 | Vector2Int.left 30 | }; 31 | 32 | private DungeonData dungeonData; 33 | 34 | private void Awake() 35 | { 36 | dungeonData = FindObjectOfType(); 37 | if (dungeonData == null) 38 | dungeonData = gameObject.AddComponent(); 39 | 40 | generate.action.performed += Generate; 41 | } 42 | 43 | private void Generate(InputAction.CallbackContext obj) 44 | { 45 | dungeonData.Reset(); 46 | 47 | dungeonData.Rooms.Add( 48 | GenerateRectangularRoomAt(Vector2.zero, roomSize)); 49 | dungeonData.Rooms.Add( 50 | GenerateRectangularRoomAt(Vector2Int.zero + Vector2Int.right * 15, roomSize)); 51 | dungeonData.Rooms.Add( 52 | GenerateRectangularRoomAt(Vector2Int.zero + Vector2Int.down * 15, roomSize)); 53 | 54 | dungeonData.Path.UnionWith( 55 | CreateStraightCorridor(Vector2Int.zero, Vector2Int.zero + Vector2Int.right * 15)); 56 | dungeonData.Path.UnionWith( 57 | CreateStraightCorridor(Vector2Int.zero, Vector2Int.zero + Vector2Int.down * 15)); 58 | 59 | GenerateDungeonCollider(); 60 | 61 | OnFinishedRoomGeneration?.Invoke(); 62 | } 63 | 64 | private void GenerateDungeonCollider() 65 | { 66 | //create a hahset that contains all the tiles that represent the dungeon 67 | HashSet dungeonTiles = new HashSet(); 68 | foreach (Room room in dungeonData.Rooms) 69 | { 70 | dungeonTiles.UnionWith(room.FloorTiles); 71 | } 72 | dungeonTiles.UnionWith(dungeonData.Path); 73 | 74 | //Find the outline of the dungeon that will be our walls / collider aound the dungeon 75 | HashSet colliderTiles = new HashSet(); 76 | foreach (Vector2Int tilePosition in dungeonTiles) 77 | { 78 | foreach (Vector2Int direction in fourDirections) 79 | { 80 | Vector2Int newPosition = tilePosition + direction; 81 | if(dungeonTiles.Contains(newPosition) == false) 82 | { 83 | colliderTiles.Add(newPosition); 84 | continue; 85 | } 86 | } 87 | } 88 | 89 | foreach (Vector2Int pos in colliderTiles) 90 | { 91 | colliderMap.SetTile((Vector3Int)pos, roomFloorTile); 92 | } 93 | } 94 | 95 | private Room GenerateRectangularRoomAt(Vector2 roomCenterPosition, Vector2Int roomSize) 96 | { 97 | Vector2Int half = roomSize / 2; 98 | 99 | HashSet roomTiles = new(); 100 | 101 | //Generate the room around the roomCenterposition 102 | for (int x = -half.x; x < half.x; x++) 103 | { 104 | for (int y = -half.y; y < half.y; y++) 105 | { 106 | Vector2 position = roomCenterPosition + new Vector2(x, y); 107 | Vector3Int positionInt = roomMap.WorldToCell(position); 108 | roomTiles.Add((Vector2Int)positionInt); 109 | roomMap.SetTile(positionInt, roomFloorTile); 110 | } 111 | } 112 | return new Room(roomCenterPosition, roomTiles); 113 | } 114 | 115 | private HashSet CreateStraightCorridor(Vector2Int startPostion, 116 | Vector2Int endPosition) 117 | { 118 | //Create a hashset and add start and end positions to it 119 | HashSet corridorTiles = new(); 120 | corridorTiles.Add(startPostion); 121 | roomMap.SetTile((Vector3Int)startPostion, pathFloorTile); 122 | corridorTiles.Add(endPosition); 123 | roomMap.SetTile((Vector3Int)endPosition, pathFloorTile); 124 | 125 | //Find the direction of the straight line 126 | Vector2Int direction 127 | = Vector2Int.CeilToInt(((Vector2)endPosition - startPostion).normalized); 128 | Vector2Int currentPosition = startPostion; 129 | 130 | //Add all tiles until we reach the end position 131 | while (Vector2.Distance(currentPosition, endPosition) > 1) 132 | { 133 | currentPosition += direction; 134 | corridorTiles.Add(currentPosition); 135 | roomMap.SetTile((Vector3Int)currentPosition, pathFloorTile); 136 | } 137 | 138 | return corridorTiles; 139 | } 140 | } 141 | 142 | --------------------------------------------------------------------------------