private static string GetMesssage(string propertyName, CellMetadata existingMetadata, CellMetadata newMetadata) { return (string.Format( "{0} property has its metadata set to '{1}'. Can't apply the new metadata '{2}' to {0} property!", propertyName, existingMetadata, newMetadata)); }
/// <summary> /// Add entrance/exit stairs in rooms furthest from one-another /// </summary> private void BuildEntranceAndExit() { // This'll be lazy for now - we scan for the first hit of a ROOM_FLOOR // and make that the entrance. We then continue that scan to find the // furthest ROOM_FLOOR for an exit. float lastDistance = -1; float distance; int x; int z; entranceCell = null; exitCell = null; // Search for a dungeon entrance for (x = 0; x < size.x && entranceCell == null; x++) { for (z = 0; z < size.z && entranceCell == null; z++) { // If it's a room floor that isn't next to any walls, select it if (cells[x, z].Type == CellType.ROOM_FLOOR && GetAdjacentCells(cells[x, z], false, CellType.ROOM_FLOOR).Count == 4 ) { entranceCell = cells[x, z]; entranceCell.Type = CellType.ENTRANCE; } } } // Search for a possible exit furthest from the entrance for (x = 0; x < size.x; x++) { for (z = 0; z < size.z; z++) { if (cells[x, z].Type == CellType.ROOM_FLOOR && GetAdjacentCells(cells[x, z], false, CellType.ROOM_FLOOR).Count == 4 ) { // If it's further than the last exit, use distance = Vector2.Distance( cells[x, z].Position.ToVector2(), entranceCell.Position.ToVector2() ); if (distance > lastDistance) { exitCell = cells[x, z]; } } } } // Finalize new cell types exitCell.Type = CellType.EXIT; }
/// <summary> /// Determine if a cell is considered isolated (walls on all sides) /// </summary> /// <param name="position"></param> /// <returns></returns> private bool IsIsolatedCell(CellMetadata cell) { foreach (CellMetadata adjacent in GetAdjacentCells(cell, true)) { if (adjacent.Type != CellType.WALL) { return(false); } } return(true); }
/// <summary> /// Perform a Tree Growing maze algorithm originating from the given (x, z) /// to fill all unoccupied (walled) space s.t. all halls are walled in /// </summary> /// <param name="start"></param> private void TreeGrowingFloodFill(CellMetadata start) { List <CellMetadata> frontier = new List <CellMetadata>(); List <IntVector2> validDirections; IntVector2? lastDirection = null; CellMetadata cell; // Use a new region for this flood fill maze currentRegion += 1; // Carve out starting point CarveHall(start); frontier.Add(start); // Loop until we run out of cells we can carve into while (frontier.Count > 0) { // Pick one at "random" (this implementation uses the last // in the stack to continue digging until it's at a dead end) cell = frontier[frontier.Count - 1]; validDirections = GetCarveableDirections(cell); if (validDirections.Count > 0) { // Either walk in our last direction (if it is a valid direction) // or walk in a random direction if the RNG god says so if (!lastDirection.HasValue || !validDirections.Contains(lastDirection.Value) || Random.Range(0.0f, 1.0f) < hallwayRandomness ) { lastDirection = validDirections[ Random.Range(0, validDirections.Count) ]; } // Carve into the cell at the given direction CarveHall(GetCell(cell.Position + lastDirection.Value)); // Carve into the cell after the cell at the given direction // and add that cell as another possible branching path cell = GetCell(cell.Position + lastDirection.Value * 2); CarveHall(cell); frontier.Add(cell); } else // Can't go anywhere from this cell, we're done using it { frontier.Remove(cell); } } }
/// <summary> /// Fill in dead end paths (uncarving) /// </summary> private void FillDeadEnds() { CellMetadata cell = FindDeadEnd(); while (cell != null) { // Refill the cell cell.Type = CellType.WALL; // Search for the next one cell = FindDeadEnd(); } }
/// <summary> /// Get all unique region IDs of adjacent cells, excluding any /// that don't have a region ID /// </summary> /// <param name="cell"></param> /// <returns></returns> private HashSet <int> GetAdjacentRegions(CellMetadata cell) { HashSet <int> regions = new HashSet <int>(); foreach (CellMetadata adjacent in GetAdjacentCells(cell, false)) { regions.Add(adjacent.Region); } // Remove any "no region" results regions.Remove(0); return(regions); }
/// <summary> /// Performs a Tree Growing Maze flood fill for every location /// we identify as fillable entry points (any isolated cell) /// </summary> private void BuildMazes() { CellMetadata cell = FindIsolatedCell(); // lazy safety net until I can figure out unity coroutines int iterations = 0; while (cell != null && iterations < 100) { TreeGrowingFloodFill(cell); cell = FindIsolatedCell(); iterations++; } }
/// <summary> /// Get all directions around the given cell that we can carve a path. /// </summary> /// <param name="cell"></param> /// <returns></returns> private List <IntVector2> GetCarveableDirections(CellMetadata cell) { List <IntVector2> directions = new List <IntVector2>(); foreach (IntVector2 dir in IntVector2.Cardinals) { if (Contains(cell.Position + dir * 3)) { if (GetCell(cell.Position + dir * 2).Type == CellType.WALL) { directions.Add(dir); } } } return(directions); }
/// <summary> /// Get all cells adjacent in the cardinal (and intercardinal) directions of the given cell /// </summary> /// <param name="cell"></param> /// <param name="includeIntercardinals"></param> /// <returns></returns> private List <CellMetadata> GetAdjacentCells( CellMetadata cell, bool includeIntercardinals, CellType?typeFilter = null ) { List <CellMetadata> adjacencyList = new List <CellMetadata>(); CellMetadata adjacent; IntVector2 position; // Grab cells in all cardinal directions foreach (IntVector2 direction in IntVector2.Cardinals) { position = cell.Position + direction; if (Contains(position)) { adjacent = GetCell(position); if (!typeFilter.HasValue || adjacent.Type == typeFilter.Value) { adjacencyList.Add(adjacent); } } } // If they ask for intercardinals, grab those too if (includeIntercardinals) { foreach (IntVector2 direction in IntVector2.Intercardinals) { position = cell.Position + direction; if (Contains(position)) { adjacent = GetCell(position); if (!typeFilter.HasValue || adjacent.Type == typeFilter.Value) { adjacencyList.Add(adjacent); } } } } return(adjacencyList); }
/// <summary> /// Run through the full generator process /// </summary> public void Generate() { // Run sanity checks before we do anything CheckConfigurations(); currentRegion = 0; if (randomSeed != 0) { Random.InitState(randomSeed); } cells = new CellMetadata[size.x, size.z]; openCells = new List <CellMetadata>(); // Initialize everything in the cell metadata array for (int x = 0; x < size.x; x++) { for (int z = 0; z < size.z; z++) { cells[x, z] = new CellMetadata(new IntVector2(x, z)); } } // Populate the dungeon with open rooms BuildRooms(); // In open spaces between rooms, generate perfect mazes BuildMazes(); // Bridge rooms and mazes with doors BuildDoors(); // Fill in dead ends of the maze halls, if chosen if (fillDeadEnds) { FillDeadEnds(); } // Build an entrance and exit point BuildEntranceAndExit(); // Finally, generate prefabs for every cell BuildPrefabs(); }
/// <summary> /// Place a prefab from the cell metadata in the given position /// </summary> /// <param name="position"></param> private void CreatePrefab(IntVector2 position) { CellMetadata cell = cells[position.x, position.z]; DungeonCell gameObject; // Load a different prefab based on the cell type switch (cell.Type) { case CellType.DOOR: gameObject = Instantiate(doorPrefab) as DungeonCell; break; case CellType.HALL_FLOOR: gameObject = Instantiate(hallFloorPrefab) as DungeonCell; break; case CellType.ROOM_FLOOR: gameObject = Instantiate(roomFloorPrefab) as DungeonCell; break; case CellType.ENTRANCE: gameObject = Instantiate(entrancePrefab) as DungeonCell; break; case CellType.EXIT: gameObject = Instantiate(exitPrefab) as DungeonCell; break; default: // Assume to be a wall gameObject = Instantiate(wallPrefab) as DungeonCell; break; } gameObject.LoadMetadata(cell); // Transform cell to a local space position relative to its (x, z) // Ensuring the parent Dungeon is centered in the generated cells gameObject.transform.parent = transform; gameObject.transform.localPosition = new Vector3( position.x - size.x * 0.5f + 0.5f, 0f, position.z - size.z * 0.5f + 0.5f ); }
/// <summary> /// Returns true if the cell has only one exit /// </summary> /// <param name="cell"></param> /// <returns></returns> private bool IsDeadEnd(CellMetadata cell) { // Ignore walls that are surrounded by walls and rooms that are 1x1 if (cell.Type == CellType.WALL || cell.Type == CellType.ROOM_FLOOR) { return(false); } int walls = 0; foreach (CellMetadata adjacent in GetAdjacentCells(cell, false)) { if (adjacent.Type == CellType.WALL) { walls++; } } return(walls > 2); }
/// <summary> /// Return the spawn point for a player entering this dungeon /// </summary> /// <returns></returns> public Vector3 GetSpawnPoint() { // Spawn in a cell adjacent to the entrance List <CellMetadata> adjacent = GetAdjacentCells(entranceCell, true); CellMetadata selected = null; foreach (CellMetadata cell in adjacent) { if (cell.Type != CellType.WALL) { selected = cell; break; } } return(new Vector3( selected.Position.x - size.x * 0.5f + 0.5f, 0.2f, selected.Position.z - size.z * 0.5f + 0.5f )); }
public void AddCellMetadata(string propertyName, string fileName, string sheetName, string columnLetter, int rowNumber) { if (string.IsNullOrWhiteSpace(propertyName)) { throw new ArgumentException("Value cannot be null or whitespace.", nameof(propertyName)); } var metadataToAdd = new CellMetadata( fileName, sheetName, columnLetter, rowNumber ); if (_metadataDictionary.ContainsKey(propertyName)) { throw new PropertyCanHaveOnlyOneCellMetadataException(propertyName, _metadataDictionary[propertyName], metadataToAdd); } _metadataDictionary.Add(propertyName, metadataToAdd); }
/// <summary> /// Mark a cell as a floor for the current hall /// </summary> /// <param name="cell"></param> private void CarveHall(CellMetadata cell) { cell.Type = CellType.HALL_FLOOR; cell.Region = currentRegion; }
public PropertyCanHaveOnlyOneCellMetadataException(string propertyName, CellMetadata existingMetadata, CellMetadata newMetadata) : base(GetMesssage(propertyName, existingMetadata, newMetadata)) { }
/// <summary> /// Mark a cell as a door that can be passed through /// </summary> /// <param name="cell"></param> private void CarveDoor(CellMetadata cell) { cell.Type = CellType.DOOR; }