public void GenerateNextRoom() { Door targetDoor = openSet[0]; //grab the first door in the openset to process openSet.RemoveAt(0); Vector3 targetVoxel = targetDoor.position + targetDoor.direction; //offset one voxel in door dir so we work on the unoccupied voxel the door leads to Vector3 targetWorldVoxPos = GetVoxelWorldPos(targetVoxel, targetDoor.parent.rotation) + targetDoor.parent.transform.position; //need this for offset Vector3 targetWorldDoorDir = GetVoxelWorldDir(targetDoor.direction, targetDoor.parent.rotation); //the target voxel we're going to align to Door doorForProcessing = new Door(targetWorldVoxPos, targetWorldDoorDir, targetDoor.parent); //why do I do this instead of using targetDoor directly...? //AllDoorsData.Add(doorForProcessing); List <GameObject> roomsToTry = new List <GameObject>(generatorSettings.possibleRooms); //create a copy of the "all possible rooms list" so we can pick and remove from this list as we try different rooms //this ensures we don't try the same room over and over, and so we know when we have exhausted all the possiblities and just have to cap it off with a 1x1x1 vox room for (int i = 0; i < roomsToTry.Count; i++) //find the room template of the room we are trying to connect to, and remove that room template from the list of possible rooms to spawn { if (roomsToTry[i].GetComponent <RoomData>().roomTemplateID == targetDoor.parent.roomTemplateID) //this is to make it so we are less likely to spawn the same room type twice in a row { roomsToTry.RemoveAt(i); break; } } roomsToTry.Shuffle(rand); //shuffle this list so we dont always try the rooms in the same order. for (int i = 0; i < generatorSettings.possibleRooms.Count; i++) //add back the room type we removed to the end of the (now shuffled) list, so that we try every other room first and only use this room as a last choice { if (generatorSettings.possibleRooms[i].GetComponent <RoomData>().roomTemplateID == targetDoor.parent.roomTemplateID) { roomsToTry.Add(generatorSettings.possibleRooms[i]); } } RoomData instantiatedNewRoom = null; if (generatorSettings.useDeadendRooms) { if (AllRooms.Count + openSet.Count >= targetRooms) { roomsToTry.Clear(); //Debug.Log("Ending Gen, Target rooms hit"); } } else { if (AllRooms.Count >= targetRooms) { roomsToTry.Clear(); } } if (generatorSettings.useDeadendRooms) //append this to the end of the roomsToTryList { int ri = rand.Next(0, generatorSettings.deadendRooms.Count); //get a random deadend room roomsToTry.Add(generatorSettings.deadendRooms[ri]); //append the "singleVoxelRoom" as a last resort, this room will fit in ALL cases } //roomsToTry list MIGHT be empty, if so we just don't spawn a room, spawn a door and a halfempty connection bool makeRoomDeadend = false; if (roomsToTry.Count == 0) { makeRoomDeadend = true; } if (!makeRoomDeadend) //spawn rooms like normal { //data we will have once we find a room, used for spawning the room into the world RoomData newRoom = null; Vector3 computedRoomOffset = Vector3.zero; int doorIndex = 0; float computedRoomRotation = 0f; bool anyOverlaps = false; //start do { //it's possible that we try every room and none fit, especially if we don't have any deaded rooms that _should_ fit in any situation. //so if the list is empty, just break out if (roomsToTry.Count == 0) { makeRoomDeadend = true; break; } newRoom = roomsToTry[0].GetComponent <RoomData>(); roomsToTry.RemoveAt(0); List <int> doorsToTry = new List <int>(); for (int i = 0; i < newRoom.Doors.Count; i++) { doorsToTry.Add(i); } doorsToTry.Shuffle(rand); //same thing here with the doorors as with the rooms. Copy the list so we can exaust our options, shuffle it so we never try in the same order do //try all the different doors in different orientations { doorIndex = doorsToTry[0]; //get first doorIndex (has been shuffled) doorsToTry.RemoveAt(0); Door newDoor = newRoom.Doors[doorIndex]; Vector3 targetDoorDir = targetWorldDoorDir; computedRoomRotation = GetRoomRotation(targetDoorDir, newDoor); //computes the rotation of the room so that the door we've selected to try lines up properly to the door we are wanting to connect to Vector3 sDLocalWithRotation = GetVoxelWorldPos(newDoor.position, computedRoomRotation); computedRoomOffset = targetWorldVoxPos - sDLocalWithRotation; //the computed offset we need to apply to the room gameobject so that the doors align List <Vector3> worldVoxels = new List <Vector3>(); //check for overlaps with all of these. MUST BE Mathf.RoundToInt so that the vectors are not like 0.999999999 due to precision issues for (int i = 0; i < newRoom.LocalVoxels.Count; i++) { Vector3 v = GetVoxelWorldPos(newRoom.LocalVoxels[i].position, computedRoomRotation) + computedRoomOffset; //all the room voxels worldVoxels.Add(v); } for (int i = 0; i < newRoom.Doors.Count; i++) { if (i != doorIndex) //all the door voxels (except the one we're currently working on/linking up to another room). //We need to do this to so that we don't PLACE this room in a spot where the doors of this room have no space for at least a 1x1x1 room (eg, opening a door directly into a wall) { Vector3 v = GetVoxelWorldPos((newRoom.Doors[i].position + newRoom.Doors[i].direction), computedRoomRotation) + computedRoomOffset; worldVoxels.Add(v); } } //all room voxels addd. Get all open door voxels now.. as we don't want to block the exits to any doors not yet connected on both sides. List <Vector3> doorNeighbours = new List <Vector3>(); for (int i = 0; i < openSet.Count; i++) { Vector3 v = GetVoxelWorldPos((openSet[i].position + openSet[i].direction), openSet[i].parent.rotation) + openSet[i].parent.transform.position; doorNeighbours.Add(v); } anyOverlaps = false; for (int i = 0; i < worldVoxels.Count; i++) { Vector3 iV = new Vector3(Mathf.RoundToInt(worldVoxels[i].x), Mathf.RoundToInt(worldVoxels[i].y), Mathf.RoundToInt(worldVoxels[i].z)); bool result = IsVoxelOccupied(iV); //check this rooms volume (including the voxels this rooms doors lead into) against all occupied voxels to check for overlaps if (result) { anyOverlaps = true; break; } else { for (int j = 0; j < doorNeighbours.Count; j++) //also check this rooms volume against all the voxels openSet.doors lead into, prevents opening a door into a wall { Vector3 iD = new Vector3(Mathf.RoundToInt(doorNeighbours[j].x), Mathf.RoundToInt(doorNeighbours[j].y), Mathf.RoundToInt(doorNeighbours[j].z)); if (iD == iV) { anyOverlaps = true; break; } } } } } while(doorsToTry.Count > 0 && anyOverlaps); } while(roomsToTry.Count > 0 && anyOverlaps); if (anyOverlaps) //if we made it here, we either have a newRoom assigned and ready to spawn, or we have no rooms left to as they all didn't fit { makeRoomDeadend = true; } //Instantiate if (!makeRoomDeadend) { instantiatedNewRoom = AddRoom(newRoom, computedRoomOffset, computedRoomRotation); for (int i = 0; i < instantiatedNewRoom.Doors.Count; i++) { if (i != doorIndex) { openSet.Add(instantiatedNewRoom.Doors[i]); } } } } //spawn in door geometry int di = 0; GameObject doorToSpawn = null; if (!makeRoomDeadend) { di = rand.Next(0, generatorSettings.doors.Count); //get a random door from the list doorToSpawn = generatorSettings.doors[di]; } else { di = rand.Next(0, generatorSettings.deadendDoors.Count); //get a random door from the list doorToSpawn = generatorSettings.deadendDoors[di]; } Vector3 doorOffset = new Vector3(0f, 0.5f, 0f); //to offset it so the gameobject pivot is on the bottom edge of the voxel GameObject spawnedDoor = GameObject.Instantiate(doorToSpawn, doorForProcessing.position - (doorForProcessing.direction * 0.5f) - doorOffset, Quaternion.LookRotation(doorForProcessing.direction), this.transform); doorForProcessing.spawnedDoor = spawnedDoor; //need to link up the doors to the roomData's too? //AllDoors.Add(spawnedDoor); //instantiatedNewRoom.Doors[doorIndex].spawnedDoor = spawnedDoor; //build graph.. we know... //instantiatedNewRoom and doorForProcessing.parent are the only two rooms that share the connection we just made so... //we also know instantiedNewRoom is brand new and has no other connections, we we can use that directly, //however we need to search for doorForProcessing.parent in the roomsList first as it could have more connections already, if it does, we need to add the connection //betwen it and if (!makeRoomDeadend) { GraphNode newNode = new GraphNode(); newNode.data = instantiatedNewRoom; //connect it both ways, so we access the data from the node, and the node from the data... instantiatedNewRoom.node = newNode; //grab the node of the room we are connecting to GraphNode lastNode = doorForProcessing.parent.node; newNode.depth = lastNode.depth + 1; if (newNode.depth > highestDepth) { highestDepth = newNode.depth; //store the highest depth, used for debugging } //make a connection for the two of them GraphConnection con = new GraphConnection(); con.a = lastNode; //store the connections to the rooms con.b = newNode; con.open = true; con.doorRef = doorForProcessing; //this needs a reference to the door geometry, as we need to be able to...unlock it visually? We could hook it up to a data ref, which then has a ref to geometry too but that seems painfully overcomplicated lastNode.connections.Add(con); //store the connections both ways newNode.connections.Add(con); spawnedDoor.GetComponent <GeneratorDoor>().data = con; DungeonGraph.Add(newNode); } else { //want to add just a new door as we did not spawn a room! //GraphNode newNode = new GraphNode(); //newNode.data = instantiatedNewRoom; //connect it both ways, so we access the data from the node, and the node from the data... //instantiatedNewRoom.node = newNode; //grab the node of the room we are connecting to GraphNode lastNode = doorForProcessing.parent.node; //newNode.depth = lastNode.depth + 1; //if(newNode.depth > highestDepth) highestDepth = newNode.depth; //store the highest depth, used for debugging //make a connection for the two of them GraphConnection con = new GraphConnection(); con.a = lastNode; //store the connections to the rooms con.b = null; con.open = false; con.keyID = -1; con.doorRef = doorForProcessing; //this needs a reference to the door geometry, as we need to be able to...unlock it visually? We could hook it up to a data ref, which then has a ref to geometry too but that seems painfully overcomplicated lastNode.connections.Add(con); //store the connections both ways //newNode.connections.Add(con); spawnedDoor.GetComponent <GeneratorDoor>().data = con; //DungeonGraph.Add(newNode); } }
/// <summary> /// Finds a path between the start and end nodes. Interally, we don't need to use the path for anything, but we just need to make sure a path exsists, otherwise the graph is not solveable /// This is used for keys and locked doors and stuff like that /// </summary> /// <param name="start"></param> /// <param name="end"></param> /// <returns></returns> public bool HasPath(GraphNode start, GraphNode end) { //iterate through the graph to zero it out for (int i = 0; i < DungeonGraph.Count; i++) { DungeonGraph[i].pathfindingWeight = -1f; //-1f means "not processed" DungeonGraph[i].pathfindingProcessed = false; } List <GraphNode> nodesToProcess = new List <GraphNode>(); nodesToProcess.Add(start); start.pathfindingWeight = 0f; start.pathfindingProcessed = true; //do a flood fill starting from start room, increasing the depth every step //for(int i = 0; i < nodesToProcess.Count; i++) { //ColorChildren(start.data.gameObject.transform, Color.cyan); //this is infinite looping while (nodesToProcess.Count > 0) { GraphNode nodeToProcess = nodesToProcess[0]; nodesToProcess.RemoveAt(0); nodeToProcess.pathfindingProcessed = true; for (int j = 0; j < nodeToProcess.connections.Count; j++) { if (nodeToProcess.connections[j].open) { GraphConnection c = nodeToProcess.connections[j]; if (!c.a.pathfindingProcessed) { //a has not been processed, so add it to the "rooms to process" and set it's weight accordingly c.a.pathfindingWeight = nodeToProcess.pathfindingWeight + 1f; //c.a.pathfindingProcessed = true; nodesToProcess.Add(c.a); //ColorChildren(c.a.data.transform, Color.red); } if (!c.b.pathfindingProcessed) { //a has not been processed, so add it to the "rooms to process" and set it's weight accordingly c.b.pathfindingWeight = nodeToProcess.pathfindingWeight + 1f; //c.a.pathfindingProcessed = true; nodesToProcess.Add(c.b); //ColorChildren(c.b.data.transform, Color.green); } } else { //can't add this one as the way is closed! } } } bool foundPath = false; if (end.pathfindingWeight != -1) { foundPath = true; } return(foundPath); }
//Call this to start the generator. public void RunGenerator(int seed) { openSet = new List <Door>(); GlobalVoxelGrid = new Dictionary <Vector3, bool>(); AllRooms = new List <GameObject>(); AllDoorsData = new List <Door>(); DungeonGraph = new List <GraphNode>(); if (regenerateWithDifferentSeed) { seed = this.randomSeed; Debug.Log("Dungeon Generator:: Seed changed to [" + seed + "]"); attempts++; regenerateWithDifferentSeed = false; } //assign every possible room a unique template ID, used for identifying room types later int templateId = 0; for (int i = 0; i < generatorSettings.possibleRooms.Count; i++) { generatorSettings.possibleRooms[i].GetComponent <RoomData>().roomTemplateID = templateId; templateId++; } for (int i = 0; i < generatorSettings.spawnRooms.Count; i++) { generatorSettings.spawnRooms[i].GetComponent <RoomData>().roomTemplateID = templateId; templateId++; } for (int i = 0; i < generatorSettings.deadendRooms.Count; i++) { generatorSettings.deadendRooms[i].GetComponent <RoomData>().roomTemplateID = templateId; templateId++; } rand = new System.Random(seed); int ri = rand.Next(0, generatorSettings.spawnRooms.Count); //get a random start room RoomData startRoomPrefab = generatorSettings.spawnRooms[ri].GetComponent <RoomData>(); RoomData instantiatedDataStartRoom = AddRoom(startRoomPrefab, Vector3.zero, 0f); GraphNode firstNode = new GraphNode(); firstNode.data = instantiatedDataStartRoom; instantiatedDataStartRoom.node = firstNode; //cyclic yay DungeonGraph.Add(firstNode); for (int i = 0; i < instantiatedDataStartRoom.Doors.Count; i++) { openSet.Add(instantiatedDataStartRoom.Doors[i]); } if (generateInUpdate) { return; } while (openSet.Count > 0) { GenerateNextRoom(); //this is just isolated so we can tick this in update during testing } //generation is done, do any dungeon specific postprocessing here if (AllRooms.Count < generatorSettings.minRooms) { regenerateWithDifferentSeed = true; this.randomSeed++; Debug.Log("Dungeon Generator:: Generation failed to meet min rooms [" + AllRooms.Count + "/" + generatorSettings.minRooms + "] ... trying again with seed++ [ " + this.randomSeed + " ]"); return; } Debug.Log("Dungeon Generator:: Generation Complete in [" + DMDebugTimer.Lap() + "ms] and [" + attempts + "] attempts"); generationComplete = true; }