/// <summary> /// This method is called recursively to generate the level. Given a /// room that is partially initialized, in that only some of its doors /// are connected to other rooms, spawn new rooms for the remaining /// doors that do not have an associated room. It will then call /// SpawnChildRooms on those new rooms. /// </summary> /// <param name="room"> /// The RoomConnectionBehaviour of a partially initialized room. /// Partially initialized meaning it has some doors that are not yet /// connected to other rooms. /// </param> /// <param name="parent"> /// The transform of the parent gameobject, the instantiated rooms /// comprising the level will be a child of the given transform. /// </param> /// <param name="grid"> /// The RoomGrid to use while constructing the level. /// </param> /// <param name="depth"> /// The depth of the level to be generated. Depth should be a value /// larger than zero. The depth is used to limit the recursion of this /// method. /// </param> /// <param name="disableChildRooms"> /// Whether or not child-rooms (any room that is not the starting room) /// should be initially disabled. Normally rooms should only be enabled /// when the player enters them however it is sometimes useful for /// debugging to have all rooms enabled. /// </param> private static void SpawnChildRooms(RoomConnectionBehaviour room, Transform parent, RoomGrid grid, int depth, bool disableChildRooms) { bool enclosedPrefabRequired = depth == 1; bool DoorIsNotConnected(DoorConnectionBehaviour door) => door.ConnectingDoor == null; Debug.Assert(depth > 0 || room.Doors.Where(DoorIsNotConnected).Count() == 0, "depth reached zero however there are still rooms with unconnected doors."); foreach (DoorConnectionBehaviour currentDoor in room.Doors.Where(DoorIsNotConnected)) { // find room prefab Vector2Int newPosition = room.Position + currentDoor.Direction.ToVector2Int(); GameObject newRoomPrefab = grid.SelectPrefabFor(newPosition, enclosedPrefabRequired, depth); // create room using prefab GameObject newRoomInstance = InstanceFactory.InstantiateRoom(newRoomPrefab, parent, newPosition); RoomConnectionBehaviour newRoom = newRoomInstance.GetComponent <RoomConnectionBehaviour>(); if (disableChildRooms) { newRoom.gameObject.SetActive(false); } grid.Add(newRoom); // position room in scene newRoom.transform.position = new Vector3 { x = newPosition.x * RoomPlacementOffset.x, y = newPosition.y * RoomPlacementOffset.y, z = 0, }; // recursively spawn child-rooms SpawnChildRooms(newRoom, parent, grid, depth - 1, disableChildRooms); } }
/// <summary> /// Generate a new randomised level. This is an expensive method and /// should be called infrequently. /// </summary> /// <param name="startingRoomPrefab"> /// A room prefab to use as the starting room. /// </param> /// <param name="parent"> /// The transform of the parent gameobject, the instantiated rooms /// comprising the level will be a child of the given transform. /// </param> /// <param name="depth"> /// The depth of the level to be generated. Depth should be a value /// larger than zero. A larger depth value will result in a larger /// level being generated. /// </param> /// <param name="disableChildRooms"> /// Whether or not child-rooms (any room that is not the starting room) /// should be initially disabled. Normally rooms should only be enabled /// when the player enters them however it is sometimes useful for /// debugging to have all rooms enabled. /// </param> /// <returns> /// Returns the fully instantiated and initialized starting room. All /// rooms in the level are accessible via the starting room through the /// connected doors in each room which form a "level graph" of /// interconnected rooms. /// </returns> public static GameObject GenerateLevel(GameObject startingRoomPrefab, Transform parent, int depth, int?seed = null, bool disableChildRooms = true) { // NOTE: Due to how level generation is implemented, there is // always the small possibility that despite best efforts a // guaranteed room wont be able to find a viable position to spawn // while upholding all of the required constraints. // // To ensure the guaranteed rooms are included in the level, the // level will re-generate itself if it detects that not all the // guaranteed rooms were spawned. // set level generation seed if (!seed.HasValue) { seed = (int)DateTime.Now.Ticks; } UnityEngine.Random.InitState(seed.Value); Log.Info($"Generating level with depth {Log.Cyan(depth)} and seed {Log.Cyan(seed.Value)}", LogCategory.LevelGeneration); const int attempts = 25; for (int i = 0; i < attempts; i++) { RoomGrid grid = new RoomGrid(depth); Vector2Int centre = new Vector2Int(0, 0); GameObject startingRoomInstance = InstanceFactory.InstantiateRoom(startingRoomPrefab, parent, centre); RoomConnectionBehaviour startingRoom = startingRoomInstance.GetComponent <RoomConnectionBehaviour>(); grid.Add(startingRoom); SpawnChildRooms(startingRoom, parent, grid, depth, disableChildRooms); bool levelVerfied = grid.Verify(); if (!levelVerfied) { Log.Warning($"Generated level did not pass verification check. Regenerating... (attempt #{i+1})", LogCategory.LevelGeneration); foreach (Transform childTransform in parent.transform) { GameObject.Destroy(childTransform.gameObject); } continue; } return(startingRoomInstance); } throw new Exception("GenerateLevel failed. This is likely due to an invalid SpawnProbability configuration."); }