mafiaf
mafiaf

Reputation: 99

Game levels are unexpectedly generating on top of each other

I have been working on procedural generating levels. I have created boxes who have a spawn depending on their opening.

If a box has a left spawn. the left spawn will know that it needs to create at least 1 door on the right. This seems to be working, but for some reason, after a while, the rooms start stacking on top of each other. even tho my code does not allow this?

Could this be because the walls are not in the perfect symmetry of eachother? Since I want to have broader and different kind of levels I thought having only the spawn points align would be enough?

This is how the level starts 4 different pathways. Starting image

Still going good Starting image

Still good Starting image

As you can see every entrance from the Starting floor have been blocked. After this they keep stacking op top of eachother giving no end to the generation of levels. Starting image

Room Spawner

public class RoomSpawner : MonoBehaviour
{
    public int openingDirection;
    // 1 --> need bottom door
    // 2 --> need top door
    // 3 --> need left door
    // 4 --> need right door

    private RoomTemplates templates;
    private int rand;
    private bool spawned = false;

    void Start(){
      templates = GameObject.FindGameObjectWithTag("Rooms").GetComponent<RoomTemplates>();
      Invoke("Spawn", 0.5f);
    }

    void Spawn(){
      if(spawned == false){
        if(openingDirection == 1){
            // Need to spawn a room with a BOTTOM door.
            rand = Random.Range(0, templates.bottomRooms.Length);
            Instantiate(templates.bottomRooms[rand], transform.position, templates.bottomRooms[rand].transform.rotation);
          } else if(openingDirection == 2){
            // Need to spawn a room with a TOP door.
            rand = Random.Range(0, templates.topRooms.Length);
            Instantiate(templates.topRooms[rand], transform.position, templates.topRooms[rand].transform.rotation);
          } else if(openingDirection == 3){
            // Need to spawn a room with a LEFT door.
            rand = Random.Range(0, templates.leftRooms.Length);
            Instantiate(templates.leftRooms[rand], transform.position, templates.leftRooms[rand].transform.rotation);
          } else if(openingDirection == 4){
            // Need to spawn a room with a RIGHT door.
            rand = Random.Range(0, templates.rightRooms.Length);
            Instantiate(templates.rightRooms[rand], transform.position, templates.rightRooms[rand].transform.rotation);
          }
          spawned = true;
      }

      void OnTriggerEnter2D(Collider2D other){
        if(other.CompareTag("SpawnPoint")){
          if(other.GetComponent<RoomSpawner>().spawned == false && spawned == false){
            // spawns walls blocking off any opening !
            Instantiate(templates.closedRoom, transform.position, Quaternion.identity);
            Destroy(gameObject);
          }
          spawned = true;
        }
    }
  }
}

Destroyer tag

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Destroyer : MonoBehaviour
{
    void OnTriggerEnter2D(Collider2D other  ){
      Destroy(other.gameObject);
    }
}

Roomtemplates


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class RoomTemplates : MonoBehaviour
{
    public GameObject[] bottomRooms;
    public GameObject[] topRooms;
    public GameObject[] leftRooms;
    public GameObject[] rightRooms;

    public GameObject closedRoom;

    public List<GameObject> rooms;
}

Upvotes: 2

Views: 89

Answers (1)

derHugo
derHugo

Reputation: 90669

So I came up with the following solution:

(Limit - if a room is already spawned it is still possible that it might get surrounded by other spawning rooms so its doors get blocked)

  1. Have a proper enum flag type

    #if UNITY_EDITOR // exclude this from a build
    using Unity.Editor;
    #endif
    
    [Flags]
    public enum DoorType
    {
        Top = 0x01,
        Right = 0x02,
        Bottom = 0x04,
        Left = 0x08
    }
    
    public class EnumFlagsAttribute : PropertyAttribute
    {
        public EnumFlagsAttribute() { }
    }
    
    #if UNITY_EDITOR // exclude this from a build
    [CustomPropertyDrawer(typeof(EnumFlagsAttribute))]
    public class EnumFlagsAttributeDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect _position, SerializedProperty _property, GUIContent _label)
        {
            _property.intValue = EditorGUI.MaskField(_position, _label, _property.intValue, _property.enumNames);
        }
    }
    #endif
    

    this allows you to choose one or multiple values from the flag via the Inspector.

    enter image description here

  2. Change your RoomTemplate script like

    public class RoomTemplates : MonoBehaviour
    {
        public RoomSpawner[] bottomRooms;
        public RoomSpawner[] topRooms;
        public RoomSpawner[] leftRooms;
        public RoomSpawner[] rightRooms;
    
        [Space]
    
        public RoomSpawner closedRoomTop;
        public RoomSpawner closedRoomRight;
        public RoomSpawner closedRoomBottom;
        public RoomSpawner closedRoomLeft;
    
        [Space]
    
        public List<GameObject> rooms;
    }
    

    this gives direct access to the values of RoomSpawner on the prefabs.

    enter image description here

  3. Use the flag instead of the int for defining the next door direction on the prefabs.

    Then everytime spawing a new room it ads its own positions to the occupiedPositions so no other room can be spawned here anymore.

    Additionally check in which directions the room that is about to be added can even go and only pick a random room from that list using Linq Where.

    If there is no way left to go use the closed Room instead. (You could ofcourse still add it to the prefab lists if you also want the possibility of a randomly picked closed room)

    public class RoomSpawner : MonoBehaviour
    {
        [EnumFlags] public DoorType openingDirections;
    
        // Keep track of already used positions
        private static List<Vector2Int> occupiedPositions = new List<Vector2Int>();
    
        // store own room position
        private Vector2Int roomFieldPosition;
    
        private RoomTemplates templates;
        private bool spawned = false;
    
        private void Start()
        {
            templates = FindObjectOfType<RoomTemplates>();
    
            roomFieldPosition = new Vector2Int(Mathf.RoundToInt(transform.localPosition.x), Mathf.RoundToInt(transform.localPosition.z));
    
            occupiedPositions.Add(roomFieldPosition);
    
            Invoke("Spawn", 0.5f);
        }
    
        private static DoorType GetPossibleDirections(Vector2Int position)
        {
            DoorType output = 0;
    
            if (!occupiedPositions.Contains(new Vector2Int(position.x, position.y + 1))) output |= DoorType.Top;
            if (!occupiedPositions.Contains(new Vector2Int(position.x, position.y - 1))) output |= DoorType.Bottom;
    
            if (!occupiedPositions.Contains(new Vector2Int(position.x + 1, position.y))) output |= DoorType.Right;
            if (!occupiedPositions.Contains(new Vector2Int(position.x - 1, position.y))) output |= DoorType.Left;
    
            return output;
        }
    
        private void SpawnRoom(DoorType type)
        {
            Vector2Int nextPosition;
            RoomSpawner[] templateArray;
            RoomSpawner closedRoom;
    
            switch (type)
            {
                case DoorType.Top:
                    nextPosition = new Vector2Int(roomFieldPosition.x, roomFieldPosition.y + 1);
                    templateArray = templates.topRooms;
                    closedRoom = templates.closedRoomTop;
                    break;
    
                case DoorType.Right:
                    nextPosition = new Vector2Int(roomFieldPosition.x + 1, roomFieldPosition.y);
                    templateArray = templates.rightRooms;
                    closedRoom = templates.closedRoomRight;
                    break;
    
                case DoorType.Bottom:
                    nextPosition = new Vector2Int(roomFieldPosition.x, roomFieldPosition.y - 1);
                    templateArray = templates.bottomRooms;
                    closedRoom = templates.closedRoomBottom;
                    break;
    
                case DoorType.Left:
                    nextPosition = new Vector2Int(roomFieldPosition.x - 1, roomFieldPosition.y);
                    templateArray = templates.leftRooms;
                    closedRoom = templates.closedRoomLeft;
                    break;
    
                default:
                    return;
            }
    
            if (occupiedPositions.Contains(nextPosition)) return;
    
            var directions = GetPossibleDirections(nextPosition);
    
            var prefabs = new List<RoomSpawner>();
            foreach (var doorType in (DoorType[])Enum.GetValues(typeof(DoorType)))
            {
                if (!directions.HasFlag(doorType)) continue;
    
                prefabs.AddRange(templateArray.Where(r => r.openingDirections.HasFlag(doorType)));
            }
    
            if (prefabs.Count == 0)
            {
                prefabs.Add(closedRoom);
            }
    
            // Need to spawn a room with a BOTTOM door.
            var rand = Random.Range(0, prefabs.Count);
            Instantiate(prefabs[rand], new Vector3(nextPosition.x, 0, nextPosition.y), Quaternion.identity);
        }
    
        private void Spawn()
        {
            if (spawned) return;
    
            if (openingDirections.HasFlag(DoorType.Top)) SpawnRoom(DoorType.Top);
            if (openingDirections.HasFlag(DoorType.Bottom)) SpawnRoom(DoorType.Bottom);
    
            if (openingDirections.HasFlag(DoorType.Right)) SpawnRoom(DoorType.Right);
            if (openingDirections.HasFlag(DoorType.Left)) SpawnRoom(DoorType.Left);
    
            spawned = true;
        }
    }
    

    enter image description here

Upvotes: 3

Related Questions