And here’s all of the code. If you happen to dig around in here criticism would be appreciated. It’s a bit of a mess. I’m pretty sure I made the mistake in the code above but this might help give a bit of context.
This is the script that manages the whole thing.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// This is the base class of our world generator. It holds all of the critical information and functions about our world generation.
/// </summary>
public class WorldGrid : MonoBehaviour
{
[Header("World Settings")]
//How many UnityUnitys wide and tall a segment is. This is set in the editor
[Tooltip("How many unity units our WorldSegments are. Each tile should be 1 unity unit")]
public int segmentSize;
//Word width in segments. This is set in the editor
[Tooltip("The width of the world in WorldSegments")]
public int worldWidth;
//World height in world segments. This is set in the editor.
[Tooltip("The height of the world in WorldSegments")]
public int worldHeight;
//Total number of rooms. Set in editor.
[Tooltip("How many rooms to generate.")]
public int roomCount;
//2d array of WorldSegment objects. This represents the world. This is generated through code.
WorldSegment[,] WorldTiles;
[Space(1)]
[Header("Room Controls")]
//Array of our rooms we want to build the dungeon from. This is set in the editor,
[Tooltip("A list of rooms used in creating the dungeon. Each room needs to be a gameobject with a Room script attached.")]
public Room[] BlueprintList;
//The connections that are on the outside of a room. Used in placing room
private List<Connection> exteriorConnections = new List<Connection>();
// Use this for initialization
void Start ()
{
//Set up the 2d array for the world and populate it with
WorldTiles = new WorldSegment[worldWidth, worldHeight];
for(int x = 0; x < worldWidth; x++)
{
for(int y = 0; y < worldHeight; y++)
{
WorldTiles[x,y] = new WorldSegment();
}
}
//Generate the dungeon
GenerateDungeon();
}
void GenerateDungeon()
{
//Place the seed room.
//TODO: MAKE THIS SPAWN A SPECIFIC "ENTRANCE" ROOM
PlaceRoom(BlueprintList[0], worldWidth/2, worldHeight/2);
//Fill dungeon. Subtract 1 because of the seed room.
FillDungeon(roomCount -1);
//Connect the dungeon
}
void FillDungeon(int roomNumber)
{
int roomCounter = 0;
for(int x = 0; x <= roomNumber; x++)
{
Room newRoom;
int placeX;
int placeZ;
//Get the rooms position.
//Find one connection.
Connection placementConnection = exteriorConnections[Random.Range(0,exteriorConnections.Count)];
Room newRoomBlueprint = BlueprintList[Random.Range(0, BlueprintList.Length)];
//Pick a random room
//TODO: This only respects the connection we're building off and doesn't make sure there's a connection on the object we're placing that will match up.
//Check to see if we're on the leftside of a segment. The subtraction is for a buffer zone.
if ((placementConnection.gameObject.transform.position.x % segmentSize < (segmentSize/2)-(segmentSize*0.10)))
{
placeX = (int)placementConnection.gameObject.transform.position.x/segmentSize - 1 /* - newRoomBlueprint.width*/;
placeX = placeX - newRoomBlueprint.width;
placeZ = (int)placementConnection.gameObject.transform.position.z/segmentSize;
}
//Check if we're off the right side.
if((placementConnection.gameObject.transform.position.x % segmentSize > (segmentSize/2)+(segmentSize*0.10)))
{
placeX = (int)placementConnection.gameObject.transform.position.x / segmentSize + 1;
placeZ = (int)placementConnection.gameObject.transform.position.z/segmentSize;
}
//Check if we're off the bottom side.
if((placementConnection.gameObject.transform.position.z % segmentSize < (segmentSize/2)-(segmentSize*0.10)))
{
placeX = (int)placementConnection.gameObject.transform.position.x/segmentSize;
placeZ = (int)placementConnection.gameObject.transform.position.z/segmentSize - 1;
placeZ = placeZ - newRoomBlueprint.height;
}
//we're off the top side.
{
placeX = (int)placementConnection.gameObject.transform.position.x/segmentSize;
//Take the objects z depth, turn it into a segment position, move the position up by one and then move it up by the room height
placeZ = (int)placementConnection.gameObject.transform.position.z/segmentSize + 1;
}
//place the room
newRoom = PlaceRoom(newRoomBlueprint, placeX,placeZ);
if(newRoom == null) //Our placement didn't work, don't increment the counter.
{
x--;
}
else
roomCounter++;
if(newRoom != null)
{
newRoom.UpdateConnections(GetAdjacentRooms(newRoom));
}
//Call the new room check connections here. We'll need to sort through the tiles to find adjacent rooms.
}
Debug.Log("We have " + roomCounter + " rooms.");
}
//This function will place a room on the worldgrid adjacent to one of our attachment points
Room PlaceRoom(Room room, int xPos, int zPos)
{
//We can't place a room, the spot is occupied.
if (CheckPlacement(room, xPos, zPos) != true)
{
return null;
}
//Place the room. Take it's x position on the grid and multiply it by the size of a segment to get it's world position, then add half the room size so it places relative to the lower left corner.
GameObject roomObject = Object.Instantiate(room.gameObject, new Vector3(xPos*segmentSize + (0.5f*room.gameObject.transform.position.x),0,zPos* segmentSize + (0.5f * room.gameObject.transform.position.z)), room.gameObject.transform.rotation);
roomObject.transform.parent = this.gameObject.transform;
Room newRoom = roomObject.GetComponent<Room>();
newRoom.x = xPos;
newRoom.z = zPos;
//Fill in the grid where we want to place ourselves.
for(int x = 0; x < room.width; x++)
{
for(int z= 0; z < room.height; z++)
{
WorldTiles[(xPos + x), (zPos + z)].room = newRoom;
WorldTiles[xPos + x, zPos + z].full = true;
}
}
//Add all of the connections from our new room to the exterior connections list.
//We'll remove the ones that are adjacent to another room when we update our connections.
foreach(Connection connection in newRoom.connections)
{
exteriorConnections.Add(connection);
}
//Break from the function. We're done.
return newRoom.GetComponent<Room>();
}
//TODO: FIXME! GetAdjacentRooms returning a list containing null references
//Returns all rooms adjacent on the X/Y axis to this room. It shouldn't return rooms in the corners.
//DEBUG: Currently causing problems and giving NullReferenceException: Object reference not set to an instance of an object.
//DEBUG2: Seems like the rooms connection list is getting mangled somehow
public List<Room> GetAdjacentRooms(Room room)
{
List<Room> adjacentRooms = new List<Room>();
//Check the tiles above and below our given room
for(int x = 0; x < room.width; x++)
{
//Check to see if the index exists above
if(room.height + room.z + 1 < worldHeight)
{
//Check to see if the room hasn't been added to our list and make sure we're not adding a null room to our list.
if(!adjacentRooms.Contains(WorldTiles[x,room.height + room.z + 1].room) && WorldTiles[x, room.height + room.z + 1].room != null)
{
adjacentRooms.Add(WorldTiles[x,room.height + room.z + 1].room);
}
}
if(room.z - 1 > 0)
{
//Check to see if the room hasn't been added to our list and make sure we're not adding a null room to our list.
if (!adjacentRooms.Contains(WorldTiles[x,room.z - 1].room) && WorldTiles[x, room.z - 1].room != null)
{
adjacentRooms.Add(WorldTiles[x,room.z - 1].room);
}
}
Debug.Log("Adjacent Rooms: " + adjacentRooms.Count);
if(adjacentRooms.Count != 0)
Debug.Log(adjacentRooms[0]); //Null Reference exception in this test code.
}
//Check the tiles to our right and left to find adjacent rooms.
for(int z = 0; z < room.height; z++)
{
//Check to see if the index exists
if(room.width + room.x + 1 < worldWidth)
{
//Check to see if the room hasn't been added to our list.
if (!adjacentRooms.Contains(WorldTiles[room.width + room.x + 1, z].room) && WorldTiles[room.width + room.x + 1, z].room != null) ;
{
adjacentRooms.Add(WorldTiles[room.width + room.x + 1,z].room);
}
}
if(room.x - 1 > 0)
{
if(!adjacentRooms.Contains(WorldTiles[room.x - 1,z].room) && WorldTiles[room.x - 1, z].room != null)
{
adjacentRooms.Add(WorldTiles[room.x - 1,z].room);
}
}
}
//TODO: DELETE ME. THIS CODE IS JUST FOR TESTING.
foreach (Room testRoom in adjacentRooms)
{
Debug.Log("Room:" + testRoom); //This is fine and happily prints "Room:"
Debug.Log("Room" + testRoom.connections.Count); //This also gives a null reference exception
foreach (Connection connection in testRoom.connections)
{
Debug.Log("Connection:" + connection); //This gives a null reference exception if the other problematic code is removed
}
}
return adjacentRooms;
}
//Check to see if the placement of a room is clear.
//xPos/yPos should correspond to the lower left corner of the room.
//TODO: Rooms are sometimes being spawned on eachother. I suspect the problem is in check placement.
bool CheckPlacement(Room room, int xPos, int zPos)
{
for(int x = 0; x < room.width; x++)
{
for(int z = 0; z < room.height; z++)
{
//Check to see if any of the position is filled or sits outside our desired dungeon. If so we know we can't place a room here
if(xPos + x > worldWidth || zPos + z > worldHeight ||xPos + x < 0 || zPos + z < 0 )
{
//Room placement is bad.
return false;
}
else //Check to see if the spot is full
if(WorldTiles[xPos + x, zPos + z].full == true)
{
return false;
}
}
}
//None of the spots were filled so the spot must be clear.
return true;
}
public void removeExteriorConnection(Connection connection)
{
exteriorConnections.Remove(connection);
}
}
Here’s the room code, it’s the next most important piece of code.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//Lets us use Concat for our lists.
using System.Linq;
/// <summary>
/// The Room Data model holds basic information about rooms and is used to differentiate between different rooms when they've been placed onto the grid.
/// </summary>
public class Room : MonoBehaviour
{
[Tooltip("The width of the room in the X direction measured in WorldSegments")]
public int width; //In WorldSegments. This is set in the editor.
[Tooltip("The height of the room in the Z direction measured in WorldSegments")]
public int height; //In WorldSegments. This is set in the editor.
//List of connections to other rooms. This is set in the editor.
[Tooltip("Connection GameObjects with a Connection script attached. Used to toggle walls on and off.")]
public List<Connection> connections = new List<Connection>();
//Position on the
public int x; //X position of the lower left segment on the grid
public int z; //Y position of the lower left segment on the grid
//Update the connections
public void UpdateConnections(List<Room> adjacentRooms)
{
if(adjacentRooms.Count != 0 )
{
Debug.Log("Adjacent Rooms:" + adjacentRooms.Count);
foreach (Room room in adjacentRooms)
{
Debug.Log("Room:" + room); //Returns a value. This statement prints out "Room:"
//It looks to me like the rooms connections are being lost.
Debug.Log("Room Connections" + room.connections); //This is giving me a null reference exception. If I remove it the next line gives me a null reference exception.
//Iterate through every connection in every adjacent room
foreach(Connection connection in room.connections)
{
//Iterate through every one of the connections on this object
foreach (Connection myConnection in connections)
{
//If we're within 1 unit in the X/Y direction we want to connect our objects.
//IF possible make this value non hardcoded.
if(Mathf.Abs(myConnection.gameObject.transform.position.x - connection.gameObject.transform.position.x) < 1)
{
if(Mathf.Abs(myConnection.gameObject.transform.position.x - connection.gameObject.transform.position.x) < 1)
{
Connection oldConnection = connection;
//Connect the two game objects and then discard them.
Connect(myConnection,ref oldConnection);
}
}
}
}
}
}
}
//Merge the objects the connections represent so we can turn on off both sets of connections just by toggling this one connection.
//Then when we're finished delete one of the connections and set the saved connection as the other connection.
void Connect(Connection savedConnection,ref Connection destroyedConnection)
{
//Merge the object lists so we know what to turn on/off when we open or close our connection.
savedConnection.openObjects = savedConnection.openObjects.Concat(destroyedConnection.openObjects).ToArray();
savedConnection.closeObjects = savedConnection.closeObjects.Concat(destroyedConnection.closeObjects).ToArray();
//Our connections have found eachother. We need to remove both the connected and destroyed connection from the exterior connections list.
GameObject[] controllerObjects = GameObject.FindGameObjectsWithTag("GameController");
foreach(GameObject gameObject in controllerObjects)
{
if(gameObject.name == "WorldController")
{
gameObject.GetComponent<WorldGrid>().removeExteriorConnection(savedConnection);
gameObject.GetComponent<WorldGrid>().removeExteriorConnection(destroyedConnection);
}
}
//overwrite the old connection
Destroy(destroyedConnection.gameObject);
destroyedConnection = savedConnection;
}
}
Here we have the humble Connection object code. This is attached to an empty object in Unity and placed in the center of spots where rooms could possibly connect to eachother. It also holds information about what objects to turn on/off if I want to make to rooms connect.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// Connection classes are used to hold information about where rooms can connect to other rooms
/// Connection classes also hold a list of objects used in opening/closing those connections.
/// </summary>
public class Connection : MonoBehaviour
{
public bool canOpen = true;
bool isOpen = false;
public GameObject[] openObjects;
public GameObject[] closeObjects;
void Start()
{
//Every connection should start closed.
CloseConnection();
}
void OpenConnection()
{
if(canOpen == true)
{
isOpen = true;
foreach(GameObject activatedObject in openObjects)
{
activatedObject.SetActive(true);
}
foreach(GameObject deactivatedObject in closeObjects)
{
deactivatedObject.SetActive(false);
}
}
}
void CloseConnection()
{
isOpen = false;
foreach(GameObject activatedObject in openObjects)
{
activatedObject.SetActive(false);
}
foreach(GameObject deactivatedObject in closeObjects)
{
deactivatedObject.SetActive(true);
}
}
}
Here’s the WorldSegment code. Not a lot magical happening in here and I’m considering axing the entire class after I get everything working.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// World segment.
/// This is a datamodel.
/// </summary>
public class WorldSegment
{
public bool full = false;
public Room room;
}
Thank you for bearing with my everyone. I’ve been struggling with this world generator for a while now.