[졸업작품, Unity] 아이작 방 생성 알고리즘 구현

2025. 4. 3. 00:21·Unity

https://www.boristhebrave.com/2020/09/12/dungeon-generation-in-binding-of-isaac/

 

Dungeon Generation in Binding of Isaac

The Binding of Isaac, and its remake, Binding Of Isaac: Rebirth are one of my favourite games of all time. It’s a roguelite twin stick shooter, much like Enter the Gungeon. The dungeons it ge…

www.boristhebrave.com

먼저 이 문서을 참고하여 제작하였음을 알립니다.

 

 

알고리즘 소개

아이작의 방 생성 알고리즘을 간단하게 설명하자면 다음과 같다.

먼저 모든 방은 이차원 배열 처럼 배치 해둔다.(grid) 그리고 x,y 좌표로 표시를 하다.

플레이어는 항상 중앙 방(4,4)에서 시작한다.

그리고 4방향 중 하나로 무작위로 생성한다. (x + 1 or x - 1 or y + 1 or y - 1)(그리고 큐에 추가하는데 필자는 Stack을 사용했음)

예를 들어 5,4에 생성했다고 치자 그럼 이때 4가지 조건을 검사한다.

1. 현재 원하는 만큼 방을 생성하였나?

2. 생성하고자 하는 위치에 이미 방이 생성되어 있나?

3. 생성하고자 하는 위치 주위에 이웃하는 방이 2개 이상 있나?

4. 50퍼센트 확률로 포기한다.

 

3번의 경우에는 이해가 잘 안될 수 있는데

6,6에 생성한다고 가정해보자 그럼 6,6를 기준으로 4방향에 얼마나 방이 생성 되어 있는지 체크한다. 만일 2개 이상이라면 포기하는 것이다. 

 

이런 식으로 원하는 만큼 생성하는 건데 이것만 해서는 원하는 모양으로 방이 생성이 안돼서 몇가지 조건을 추가했다.

만약 생성하는 방의 갯수가 16개라고 가정하면 4,4를 기준으로 / 4를 해서 4방향에 나눠서 생성하기를 시도한다.

만일 시도하는 도중에 4,4로 돌아온다면 덜 생성 한 만큼 failureCount + 1를 하고 나머지를 전부 생성한 뒤에 failureCount 만큼 생성할 수 있는 곳에 방을 생성 해준다.

만일 이과정을 거치고도 전부 생성을 못했다면 다시 생성 해준다.

 

그리고 방과방을 연결해주면 끝난다.

알고리즘 구현

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

public enum SpecialRoomType
{
    Shop = 0, Bouns, Healing, Boss 
}

[Serializable]
public struct SpawnRoomInfo
{
    public int shopSpawnCount;
    public int bounsSpawnCount;
    public int healingSpawnCount;
    public int bossSpawnCount;
}

public struct MapInfo
{
    public int x;
    public int y;
}

public class RoomGenrator : MonoBehaviour
{
    [SerializeField] private int _gridX = 9;
    [SerializeField] private int _gridY = 9;
    
    [SerializeField] 
    private int _roomSpawnCount = 0;
    [SerializeField] private float _pathWidth = 5f;
    // [SerializeField] private SpawnRoomInfo _spawnInfo;

    [SerializeField] private GameObject _startRoom;
    [SerializeField] private RoomTableSO _roomTable;
    [SerializeField] private Material _pathMat;
    [SerializeField] private int _maxTryCount;
    [SerializeField] private GameObject _wallPrefab;

    private int[,] _map;
    private RoomNode[,] _roomArray; 
    private bool _isSpawnNow;

    private int currentX = 0;
    private int currentY = 0;
    private int _tryCount = 0;

    private Stack<MapInfo> _mapStack;
    private List<MapInfo> _failureList;
    private MeshRenderer _renderCompo;

    private void OnValidate()
    {
        _roomSpawnCount = Mathf.Clamp(_roomSpawnCount, 0, _gridX * _gridY);
    }
    
    private void Start()
    {
        _roomArray = new RoomNode[_gridX, _gridY];
    }

    private void Update()
    {
        #if UNITY_EDITOR
        if (Input.GetKeyDown(KeyCode.E))
        {
            if (_isSpawnNow) return;
            DefaultSetting();
        }
        #endif
    }
    public void DefaultSetting()
    {
        DestroyAll();
        Debug.Log("RoomGenrator Default Setting");
        _tryCount++;
        _isSpawnNow = true;
        int rand = 0;
        int currentSpawnCount = 0;
        int failureCount = 0;
        int mainRoomSpawnCount = 1;
        
        _map = new int[_gridX, _gridY];
        _mapStack = new Stack<MapInfo>(_roomSpawnCount);
        _failureList = new(_roomSpawnCount);
        
        _map[4, 4] = 1;
        currentX = 4;
        currentY = 4;
        _mapStack.Push(new MapInfo{ x = 4, y = 4 });
        rand = UnityEngine.Random.Range(0, 4);
        switch (rand)
        {
            case 0:
            {
                currentX += 1;
                _map[currentX, currentY] = 1;
                _mapStack.Push(new MapInfo{x = currentX, y = currentY});
            }
                break;
            case 1:
            {
                currentX -= 1;
                _map[currentX, currentY] = 1;
                _mapStack.Push(new MapInfo{x = currentX, y = currentY});
            }
                break;
            case 2:
            {
                currentY += 1;
                _map[currentX, currentY] = 1;
                _mapStack.Push(new MapInfo{x = currentX, y = currentY});
            }
                break;
            case 3:
            {
                currentY -= 1;
                _map[currentX, currentY] = 1;
                _mapStack.Push(new MapInfo{x = currentX, y = currentY});
            } 
                break;
        }
        Debug.Log("Start : " + currentX + " " + currentY);
        ++currentSpawnCount;
        int tryCount = 0;
        for (;;)
        {
            if(tryCount >= _maxTryCount) break;
            bool createSuccess = false;
            if(currentSpawnCount >= _roomSpawnCount)
            {
                Debug.Log(_tryCount);
                _tryCount = 0;
                CreateRoom();
                return;
            }
            
            if (currentSpawnCount / (_roomSpawnCount / 2) == mainRoomSpawnCount)
                // 4방향으로 생성 하기 위해서 
            {
                if (mainRoomSpawnCount == 4)
                {
                    mainRoomSpawnCount = 0;
                }
                while (_mapStack.Count > 1)
                {
                    _mapStack.Pop();
                }
                Debug.Log("Reset");
                currentX = _mapStack.Peek().x;
                currentY = _mapStack.Peek().y;
                mainRoomSpawnCount++;
            }
            
            for (int i = 0; i < 4; ++i)
            {
                int x = 0, y = 0;
                switch (i)
                {
                    case 0:
                        x = 1;
                        break;
                    case 1:
                        x = -1;
                        break;
                    case 2:
                        y = 1;
                        break;
                    case 3:
                        y = -1;   
                        break;
                }

                if (CanGenerateRoom(x, y) == true)
                {
                    createSuccess = true;
                    currentX += x;
                    currentY += y;
                    Debug.Log("Success : " + currentX + " " + currentY);
                    _map[currentX, currentY] = 1;
                    _mapStack.Push(new MapInfo{x = currentX, y = currentY});
                    currentSpawnCount++;
                    break;
                }
            }

            if (!createSuccess)
            {
                if (_mapStack.Count > 1)
                {
                    MapInfo currentMap = _mapStack.Pop();
                    _failureList.Add(currentMap);
                    failureCount++;
                    Debug.Log("Pop : " + currentMap.x + " " + currentMap.y);
                    currentX = _mapStack.Peek().x;
                    currentY = _mapStack.Peek().y;
                }
                else
                {
                    failureCount += (_roomSpawnCount / 4) - mainRoomSpawnCount;
                    mainRoomSpawnCount = 0;
                    currentX = 4;
                    currentY = 4;
                }
            }

            tryCount++;
        }
        
        int failureSpawnCount = 0;
        for (int j = 0; j < _failureList.Count; ++j)
        {
            if(failureSpawnCount > failureCount) break;
            
            currentX =  _failureList[j].x;
            currentY =  _failureList[j].y;
            
            for (int i = 0; i < 4; ++i)
            {
                int x = 0, y = 0;
                switch (i)
                {
                    case 0:
                        x = 1;
                        break;
                    case 1:
                        x = -1;
                        break;
                    case 2:
                        y = 1;
                        break;
                    case 3:
                        y = -1;   
                        break;
                }

                if (CanGenerateRoom(x, y, false) == true)
                {
                    currentX += x;
                    currentY += y;
                    _map[currentX, currentY] = 1;
                    Debug.Log("Success : " + currentX + " " + currentY);
                    failureSpawnCount++;
                    currentSpawnCount++;
                    break;
                }
            }
        }
        
        if(currentSpawnCount < _roomSpawnCount) DefaultSetting();
        else
        {
            _tryCount = 0;
            Debug.Log(_tryCount);
            CreateRoom();
        }
    }

    public void DestroyAll()
    {
        var roomCompoArray = transform.GetComponentsInChildren<RoomNode>();
        var pathCompoArray = transform.GetComponentsInChildren<RoomPath>();
        for (int i = 0; i < roomCompoArray.Length; i++)
        {
            Debug.Log("DestoryRoom");
            DestroyImmediate(roomCompoArray[i].gameObject);
        }
        for (int i = 0; i < pathCompoArray.Length; i++)
        {
            Debug.Log("DestoryPath");
            DestroyImmediate(pathCompoArray[i].gameObject);
        }
    }

    public bool CanGenerateRoom(int x, int y, bool randomFailure = true)
    {
        if (currentX + x >= _gridX || currentX + x < 0 
            || currentY + y >= _gridY || currentY + y < 0)
            return false;
        int sumX = currentX + x;
        int sumY = currentY + y;
        
        #region TestCase_1
        if (_map[sumX, sumY] == 1)
            return false;
        #endregion
        
        #region TestCase_2
        int count = 0;
        for (int i = 0; i < 4; ++i)
        {
            int countingX = 0;
            int countingY = 0;
            switch (i)
            {
                case 0:
                    countingX = 1;
                    break;
                case 1:
                    countingX = -1;
                    break;
                case 2:
                    countingY = +1;
                    break;
                case 3:
                    countingY = -1;   
                    break;
            }

            int allX = sumX + countingX;
            int allY = sumY + countingY;
            if(allX >= _gridX || allX < 0) continue;
            if(allY >= _gridY || allY < 0) continue;
            
            if (_map[allX, allY] == 1)
            {
                count++;
            }
            
            if (count >= 2) return false;
        }
        #endregion
        
        #region TestCase_3
        if(_mapStack.Count > 1 && randomFailure)
            if (UnityEngine.Random.Range(0, 2) == 0) return false;
        #endregion
        
        return true;
    }

    public void CreateRoom()
    {
        Debug.Log("CreateRoom");
        _roomArray = new RoomNode[_gridX, _gridY];
        GameObject obj = Instantiate(_startRoom, transform.position, Quaternion.identity);
        RoomNode room = obj.GetComponent<RoomNode>();
        obj.transform.SetParent(transform);
        obj.transform.localPosition 
            = new Vector3(40 * 4, 0, 40 * 4);
        obj.transform.rotation = Quaternion.Euler(0, 0, 0);
        _roomArray[4,4] = room;
        obj.transform.Find("Tile").GetComponent<TextMeshPro>().text = "4, 4";
        
        // Spawn
        for (int i = 0; i < _gridX; ++i)
        {
            for (int j = 0; j < _gridY; ++j)
            {
                if(i == 4 && j == 4) continue;
                if (_map[i, j] == 1)
                {
                    // todo : Change to Pool
                    obj = Instantiate(_roomTable.RandomReturn().gameObject, transform.position, Quaternion.identity);
                    room = obj.GetComponent<RoomNode>();
                    obj.transform.SetParent(transform);
                    obj.transform.localPosition 
                        = new Vector3(40 * i, 0, 40 * j);
                    obj.transform.rotation = Quaternion.Euler(0, 0, 0);
                    obj.transform.Find("Tile").GetComponent<TextMeshPro>().text = $"{i}, {j}";
                    _roomArray[i, j] = room;
                }
            }
        }
        
        // Link
        for (int i = 0; i < _gridX; ++i)
        {
            for (int j = 0; j < _gridY; ++j)
            {
                if(i == 4 && j == 4) continue;
                if (_roomArray[i, j] != null)
                {
                    LinkRoom(_roomArray[i, j], i, j);
                }
            }
        }
    }

    private void LinkRoom(RoomNode room, int x, int y)
    {
        for (int i = 0; i < 4; ++i)
        {
            switch ((EnterPoint.DIR)i)
            {
                case EnterPoint.DIR.Up:
                {
                    if (y - 1 < 0) break;
                    
                    if (_roomArray[x, y - 1] != null)
                    {
                        CreatePath(room.EnterPointList.Find(a => a.dir == EnterPoint.DIR.Up).transform,
                            _roomArray[x, y - 1].EnterPointList.
                                Find(a => a.dir == EnterPoint.DIR.Down).transform,
                            (EnterPoint.DIR)i);
                    }
                }
                    break;
                case EnterPoint.DIR.Down:
                {
                    if (y + 1 >= _gridY) break;
                    
                    if (_roomArray[x, y + 1] != null)
                    {
                        CreatePath(room.EnterPointList.Find(a => a.dir == EnterPoint.DIR.Down).transform,
                            _roomArray[x, y + 1].EnterPointList.
                                Find(a => a.dir == EnterPoint.DIR.Up).transform,
                            (EnterPoint.DIR)i);
                    }
                }
                    break;
                case EnterPoint.DIR.Left:
                {
                    if (x + 1 >= _gridX) break;
                    
                    if (_roomArray[x + 1, y] != null)
                    {
                        CreatePath(room.EnterPointList.Find(a => a.dir == EnterPoint.DIR.Left).transform,
                            _roomArray[x + 1, y].EnterPointList.
                                Find(a => a.dir == EnterPoint.DIR.Right).transform,
                            (EnterPoint.DIR)i);
                    }
                }
                    break;
                case EnterPoint.DIR.Right:
                {
                    if (x - 1 < 0) break;
                    
                    if (_roomArray[x - 1, y] != null)
                    {
                        CreatePath(room.EnterPointList.Find(a => a.dir == EnterPoint.DIR.Right).transform,
                            _roomArray[x - 1, y].EnterPointList.
                                Find(a => a.dir == EnterPoint.DIR.Left).transform
                            , (EnterPoint.DIR)i);
                    }
                }
                    break;
            }
        }
    }

    private void CreatePath(Transform start, Transform end, EnterPoint.DIR dir)
    {
        Mesh pathMesh = new Mesh();
        Vector3 topLeftV = Vector3.zero,
            topRightV = Vector3.zero,
            bottomLeftV = Vector3.zero,
            bottomRightV = Vector3.zero;
        
        switch (dir)
        {
            case EnterPoint.DIR.Left:
            {
                topLeftV = new Vector3(end.position.x, start.position.y, start.position.z - _pathWidth / 2);
                topRightV = new Vector3(start.position.x, start.position.y, start.position.z - _pathWidth / 2);
                bottomLeftV = new Vector3(end.position.x, start.position.y, start.position.z + _pathWidth / 2);
                bottomRightV = new Vector3(start.position.x, start.position.y, start.position.z + _pathWidth / 2);
            }
                break;
            case EnterPoint.DIR.Right:
            {
                topLeftV = new Vector3(start.position.x, start.position.y, start.position.z - _pathWidth / 2);
                topRightV = new Vector3(end.position.x, start.position.y, start.position.z - _pathWidth / 2);
                bottomLeftV = new Vector3(start.position.x, start.position.y, start.position.z + _pathWidth / 2);
                bottomRightV = new Vector3(end.position.x, start.position.y, start.position.z + _pathWidth / 2);
            }
                break;
            case EnterPoint.DIR.Up:
            {
                topLeftV = new Vector3(end.position.x + _pathWidth / 2, start.position.y, end.position.z);
                topRightV = new Vector3(end.position.x - _pathWidth / 2, start.position.y, end.position.z);
                bottomLeftV = new Vector3(start.position.x + _pathWidth / 2, start.position.y, start.position.z);
                bottomRightV = new Vector3(start.position.x - _pathWidth / 2, start.position.y, start.position.z);
            }
                break;
            case EnterPoint.DIR.Down:
            {
                topLeftV = new Vector3(start.position.x + _pathWidth / 2, start.position.y, start.position.z);
                topRightV = new Vector3(start.position.x - _pathWidth / 2, start.position.y, start.position.z);
                bottomLeftV = new Vector3(end.position.x + _pathWidth / 2, start.position.y, end.position.z);
                bottomRightV = new Vector3(end.position.x - _pathWidth / 2, start.position.y, end.position.z);
            }
                break;
        }

        Vector3[] vertices = new Vector3[]
        {
            topLeftV,
            topRightV,
            bottomLeftV,
            bottomRightV
        };
        
        Vector2[] uvs = new Vector2[vertices.Length];
        for (int i = 0; i < uvs.Length; i++)
        {
            uvs[i] = new Vector2(vertices[i].x, vertices[i].z);
        }

        int[] triangles = new int[]
        {
            0,
            1,
            2,
            2,
            1,
            3
        };
        
        pathMesh.vertices = vertices;
        pathMesh.uv = uvs;
        pathMesh.triangles = triangles;
        
        GameObject floor = new GameObject("Floor", 
            typeof(MeshFilter), typeof(MeshRenderer), typeof(RoomPath));
        floor.GetComponent<MeshFilter>().mesh = pathMesh;
        floor.GetComponent<MeshRenderer>().material = _pathMat;
        floor.GetComponent<RoomPath>().
            SetInfo(topLeftV, bottomLeftV, _wallPrefab);
        
        floor.transform.SetParent(transform);
        floor.transform.localPosition = Vector3.zero;
        floor.transform.rotation = Quaternion.identity;
    }
}

전체 코드다. 중요한 부분만 뜯어서 보겠다.

 

DestroyAll();
Debug.Log("RoomGenrator Default Setting");
_tryCount++;
_isSpawnNow = true;
int rand = 0;
int currentSpawnCount = 0;
int failureCount = 0;
int mainRoomSpawnCount = 1;

_map = new int[_gridX, _gridY];
_mapStack = new Stack<MapInfo>(_roomSpawnCount);
_failureList = new(_roomSpawnCount);

_map[4, 4] = 1;
currentX = 4;
currentY = 4;
_mapStack.Push(new MapInfo{ x = 4, y = 4 });

모든 방과 통로를 지워주고 초기화하는 과정이다. 아까 말했듯이 그리드 초기화 하고 4,4를 시작 시점으로 정해준다. ( = 1이 채운다는 의미임)

        rand = UnityEngine.Random.Range(0, 4);
        switch (rand)
        {
            case 0:
            {
                currentX += 1;
                _map[currentX, currentY] = 1;
                _mapStack.Push(new MapInfo{x = currentX, y = currentY});
            }
                break;
            case 1:
            {
                currentX -= 1;
                _map[currentX, currentY] = 1;
                _mapStack.Push(new MapInfo{x = currentX, y = currentY});
            }
                break;
            case 2:
            {
                currentY += 1;
                _map[currentX, currentY] = 1;
                _mapStack.Push(new MapInfo{x = currentX, y = currentY});
            }
                break;
            case 3:
            {
                currentY -= 1;
                _map[currentX, currentY] = 1;
                _mapStack.Push(new MapInfo{x = currentX, y = currentY});
            } 
                break;
        }
        Debug.Log("Start : " + currentX + " " + currentY);
        ++currentSpawnCount;

랜덤으로 방향 정해서 이동하고 Stack에 넣어준다.

        for (;;)
        {
            if(tryCount >= _maxTryCount) break;
            bool createSuccess = false;
            if(currentSpawnCount >= _roomSpawnCount)
            {
                Debug.Log(_tryCount);
                _tryCount = 0;
                CreateRoom();
                return;
            }
            
            if (currentSpawnCount / (_roomSpawnCount / 2) == mainRoomSpawnCount)
                // 4방향으로 생성 하기 위해서 
            {
                if (mainRoomSpawnCount == 4)
                {
                    mainRoomSpawnCount = 0;
                }
                while (_mapStack.Count > 1)
                {
                    _mapStack.Pop();
                }
                Debug.Log("Reset");
                currentX = _mapStack.Peek().x;
                currentY = _mapStack.Peek().y;
                mainRoomSpawnCount++;
            }
            
            for (int i = 0; i < 4; ++i)
            {
                int x = 0, y = 0;
                switch (i)
                {
                    case 0:
                        x = 1;
                        break;
                    case 1:
                        x = -1;
                        break;
                    case 2:
                        y = 1;
                        break;
                    case 3:
                        y = -1;   
                        break;
                }

                if (CanGenerateRoom(x, y) == true)
                {
                    createSuccess = true;
                    currentX += x;
                    currentY += y;
                    Debug.Log("Success : " + currentX + " " + currentY);
                    _map[currentX, currentY] = 1;
                    _mapStack.Push(new MapInfo{x = currentX, y = currentY});
                    currentSpawnCount++;
                    break;
                }
            }

            if (!createSuccess)
            {
                if (_mapStack.Count > 1)
                {
                    MapInfo currentMap = _mapStack.Pop();
                    _failureList.Add(currentMap);
                    failureCount++;
                    Debug.Log("Pop : " + currentMap.x + " " + currentMap.y);
                    currentX = _mapStack.Peek().x;
                    currentY = _mapStack.Peek().y;
                }
                else
                {
                    failureCount += (_roomSpawnCount / 4) - mainRoomSpawnCount;
                    mainRoomSpawnCount = 0;
                    currentX = 4;
                    currentY = 4;
                }
            }

            tryCount++;
        }

반복문 부분이다. 여기선 랜덤으로 방향을 정하고 방을 생성할 수 있는지 (아까 4가지 경우)를 확인해주고 된다면 생성 아니면 Stack에서 지우고 다른 후보 확인 이런식으로 A* 알고리즘과 비슷한 방식을 채용했다. 그리고 중간에 보면 / 4 한 값 만큼 나눠서 생성 하겠다는 코드도 있다.

 

아까 말했던 조건 함수

public bool CanGenerateRoom(int x, int y, bool randomFailure = true)
    {
        if (currentX + x >= _gridX || currentX + x < 0 
            || currentY + y >= _gridY || currentY + y < 0)
            return false;
        int sumX = currentX + x;
        int sumY = currentY + y;
        
        #region TestCase_1
        if (_map[sumX, sumY] == 1)
            return false;
        #endregion
        
        #region TestCase_2
        int count = 0;
        for (int i = 0; i < 4; ++i)
        {
            int countingX = 0;
            int countingY = 0;
            switch (i)
            {
                case 0:
                    countingX = 1;
                    break;
                case 1:
                    countingX = -1;
                    break;
                case 2:
                    countingY = +1;
                    break;
                case 3:
                    countingY = -1;   
                    break;
            }

            int allX = sumX + countingX;
            int allY = sumY + countingY;
            if(allX >= _gridX || allX < 0) continue;
            if(allY >= _gridY || allY < 0) continue;
            
            if (_map[allX, allY] == 1)
            {
                count++;
            }
            
            if (count >= 2) return false;
        }
        #endregion
        
        #region TestCase_3
        if(_mapStack.Count > 1 && randomFailure)
            if (UnityEngine.Random.Range(0, 2) == 0) return false;
        #endregion
        
        return true;
    }

 

나머지 생성해주고 만약 모든 경우의 수가 빗나갔을 경우를 생각해서 다시 반복해주는 부분 (거의 없다)

int failureSpawnCount = 0;
        for (int j = 0; j < _failureList.Count; ++j)
        {
            if(failureSpawnCount > failureCount) break;
            
            currentX =  _failureList[j].x;
            currentY =  _failureList[j].y;
            
            for (int i = 0; i < 4; ++i)
            {
                int x = 0, y = 0;
                switch (i)
                {
                    case 0:
                        x = 1;
                        break;
                    case 1:
                        x = -1;
                        break;
                    case 2:
                        y = 1;
                        break;
                    case 3:
                        y = -1;   
                        break;
                }

                if (CanGenerateRoom(x, y, false) == true)
                {
                    currentX += x;
                    currentY += y;
                    _map[currentX, currentY] = 1;
                    Debug.Log("Success : " + currentX + " " + currentY);
                    failureSpawnCount++;
                    currentSpawnCount++;
                    break;
                }
            }
        }
        
        if(currentSpawnCount < _roomSpawnCount) DefaultSetting();
        else
        {
            _tryCount = 0;
            Debug.Log(_tryCount);
            CreateRoom();
        }

 

= 1인 부분 찾아서 생성해주는 코드다.

public void CreateRoom()
    {
        Debug.Log("CreateRoom");
        _roomArray = new RoomNode[_gridX, _gridY];
        GameObject obj = Instantiate(_startRoom, transform.position, Quaternion.identity);
        RoomNode room = obj.GetComponent<RoomNode>();
        obj.transform.SetParent(transform);
        obj.transform.localPosition 
            = new Vector3(40 * 4, 0, 40 * 4);
        obj.transform.rotation = Quaternion.Euler(0, 0, 0);
        _roomArray[4,4] = room;
        obj.transform.Find("Tile").GetComponent<TextMeshPro>().text = "4, 4";
        
        // Spawn
        for (int i = 0; i < _gridX; ++i)
        {
            for (int j = 0; j < _gridY; ++j)
            {
                if(i == 4 && j == 4) continue;
                if (_map[i, j] == 1)
                {
                    // todo : Change to Pool
                    obj = Instantiate(_roomTable.RandomReturn().gameObject, transform.position, Quaternion.identity);
                    room = obj.GetComponent<RoomNode>();
                    obj.transform.SetParent(transform);
                    obj.transform.localPosition 
                        = new Vector3(40 * i, 0, 40 * j);
                    obj.transform.rotation = Quaternion.Euler(0, 0, 0);
                    obj.transform.Find("Tile").GetComponent<TextMeshPro>().text = $"{i}, {j}";
                    _roomArray[i, j] = room;
                }
            }
        }

RoomNode는 간단한 정보만 가지고 있음(구조체가 아닌 이유는 나중에 기능을 더 추가할 예정이기 때문이고 상속을 사용할 것이기 때문이다.)

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

[Serializable]
public struct EnterPoint
{
    public enum DIR
    {
        Up = 0, Down, Left, Right
    }
    public DIR dir;
    public Transform transform;
}

public class RoomNode : MonoBehaviour
{
    public List<EnterPoint> EnterPointList = new();
    public BoxCollider Width;
    public BoxCollider Height;
}

 

 

그리고 방 끼리 연결해주는 코드

for (int i = 0; i < _gridX; ++i)
        {
            for (int j = 0; j < _gridY; ++j)
            {
                if(i == 4 && j == 4) continue;
                if (_roomArray[i, j] != null)
                {
                    LinkRoom(_roomArray[i, j], i, j);
                }
            }
        }
    private void LinkRoom(RoomNode room, int x, int y)
    {
        for (int i = 0; i < 4; ++i)
        {
            switch ((EnterPoint.DIR)i)
            {
                case EnterPoint.DIR.Up:
                {
                    if (y - 1 < 0) break;
                    
                    if (_roomArray[x, y - 1] != null)
                    {
                        CreatePath(room.EnterPointList.Find(a => a.dir == EnterPoint.DIR.Up).transform,
                            _roomArray[x, y - 1].EnterPointList.
                                Find(a => a.dir == EnterPoint.DIR.Down).transform,
                            (EnterPoint.DIR)i);
                    }
                }
                    break;
                case EnterPoint.DIR.Down:
                {
                    if (y + 1 >= _gridY) break;
                    
                    if (_roomArray[x, y + 1] != null)
                    {
                        CreatePath(room.EnterPointList.Find(a => a.dir == EnterPoint.DIR.Down).transform,
                            _roomArray[x, y + 1].EnterPointList.
                                Find(a => a.dir == EnterPoint.DIR.Up).transform,
                            (EnterPoint.DIR)i);
                    }
                }
                    break;
                case EnterPoint.DIR.Left:
                {
                    if (x + 1 >= _gridX) break;
                    
                    if (_roomArray[x + 1, y] != null)
                    {
                        CreatePath(room.EnterPointList.Find(a => a.dir == EnterPoint.DIR.Left).transform,
                            _roomArray[x + 1, y].EnterPointList.
                                Find(a => a.dir == EnterPoint.DIR.Right).transform,
                            (EnterPoint.DIR)i);
                    }
                }
                    break;
                case EnterPoint.DIR.Right:
                {
                    if (x - 1 < 0) break;
                    
                    if (_roomArray[x - 1, y] != null)
                    {
                        CreatePath(room.EnterPointList.Find(a => a.dir == EnterPoint.DIR.Right).transform,
                            _roomArray[x - 1, y].EnterPointList.
                                Find(a => a.dir == EnterPoint.DIR.Left).transform
                            , (EnterPoint.DIR)i);
                    }
                }
                    break;
            }
        }
    }

 

그리고 길 이어주는 통로를 생성하는 로직이다.

private void CreatePath(Transform start, Transform end, EnterPoint.DIR dir)
{
    Mesh pathMesh = new Mesh();
    Vector3 topLeftV = Vector3.zero,
        topRightV = Vector3.zero,
        bottomLeftV = Vector3.zero,
        bottomRightV = Vector3.zero;

    switch (dir)
    {
        case EnterPoint.DIR.Left:
        {
            topLeftV = new Vector3(end.position.x, start.position.y, start.position.z - _pathWidth / 2);
            topRightV = new Vector3(start.position.x, start.position.y, start.position.z - _pathWidth / 2);
            bottomLeftV = new Vector3(end.position.x, start.position.y, start.position.z + _pathWidth / 2);
            bottomRightV = new Vector3(start.position.x, start.position.y, start.position.z + _pathWidth / 2);
        }
            break;
        case EnterPoint.DIR.Right:
        {
            topLeftV = new Vector3(start.position.x, start.position.y, start.position.z - _pathWidth / 2);
            topRightV = new Vector3(end.position.x, start.position.y, start.position.z - _pathWidth / 2);
            bottomLeftV = new Vector3(start.position.x, start.position.y, start.position.z + _pathWidth / 2);
            bottomRightV = new Vector3(end.position.x, start.position.y, start.position.z + _pathWidth / 2);
        }
            break;
        case EnterPoint.DIR.Up:
        {
            topLeftV = new Vector3(end.position.x + _pathWidth / 2, start.position.y, end.position.z);
            topRightV = new Vector3(end.position.x - _pathWidth / 2, start.position.y, end.position.z);
            bottomLeftV = new Vector3(start.position.x + _pathWidth / 2, start.position.y, start.position.z);
            bottomRightV = new Vector3(start.position.x - _pathWidth / 2, start.position.y, start.position.z);
        }
            break;
        case EnterPoint.DIR.Down:
        {
            topLeftV = new Vector3(start.position.x + _pathWidth / 2, start.position.y, start.position.z);
            topRightV = new Vector3(start.position.x - _pathWidth / 2, start.position.y, start.position.z);
            bottomLeftV = new Vector3(end.position.x + _pathWidth / 2, start.position.y, end.position.z);
            bottomRightV = new Vector3(end.position.x - _pathWidth / 2, start.position.y, end.position.z);
        }
            break;
    }

    Vector3[] vertices = new Vector3[]
    {
        topLeftV,
        topRightV,
        bottomLeftV,
        bottomRightV
    };

    Vector2[] uvs = new Vector2[vertices.Length];
    for (int i = 0; i < uvs.Length; i++)
    {
        uvs[i] = new Vector2(vertices[i].x, vertices[i].z);
    }

    int[] triangles = new int[]
    {
        0,
        1,
        2,
        2,
        1,
        3
    };

    pathMesh.vertices = vertices;
    pathMesh.uv = uvs;
    pathMesh.triangles = triangles;

    GameObject floor = new GameObject("Floor", 
        typeof(MeshFilter), typeof(MeshRenderer), typeof(RoomPath));
    floor.GetComponent<MeshFilter>().mesh = pathMesh;
    floor.GetComponent<MeshRenderer>().material = _pathMat;
    floor.GetComponent<RoomPath>().
        SetInfo(topLeftV, bottomLeftV, _wallPrefab);

    floor.transform.SetParent(transform);
    floor.transform.localPosition = Vector3.zero;
    floor.transform.rotation = Quaternion.identity;
}

Mesh를 직접 생성해서 이어주고 있는 모습이다.

 

 

결과물 & 소감

매우 잘되는 모습..... 처음에는 저 글만 보고 만들 수 있을까 했는데 생각보다 쉽게 되었다 소요 시간은 대략 2일이다.

 

구조를 손을 좀 봤다. dx, dy를 사용해서 가독성을 높혔다. roof와 Wall 자동생성도 구현 중이다.

https://github.com/ljs1206/GraduationWork_Script/tree/main/Scripts/01_Script/RandomRoomGenrateSystem

'Unity' 카테고리의 다른 글

[Unity, Editor]두개의 EditorWindw 창을 Split, Dock하기  (0) 2025.04.11
[Unity]UniTask란?  (0) 2025.03.31
[Unity] 2D 환경에서 점프 구현 (y축 이동이 구현되어 있을 때)  (0) 2025.03.17
[졸업작품, Unity] ItemSystem 구조 수정  (0) 2025.03.14
[졸업작품, Unity]Assembly definition를 사용하는 방법  (1) 2025.03.12
'Unity' 카테고리의 다른 글
  • [Unity, Editor]두개의 EditorWindw 창을 Split, Dock하기
  • [Unity]UniTask란?
  • [Unity] 2D 환경에서 점프 구현 (y축 이동이 구현되어 있을 때)
  • [졸업작품, Unity] ItemSystem 구조 수정
HK1206
HK1206
고3 게임 개발자의 개발 일지
  • HK1206
    GGM-LJS
    HK1206
  • 전체
    오늘
    어제
    • 분류 전체보기 (25)
      • Unity (16)
      • Shader (1)
      • Editor (8)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
HK1206
[졸업작품, Unity] 아이작 방 생성 알고리즘 구현
상단으로

티스토리툴바