본문 바로가기

알고리즘/알고리즘 In game

[유니티] BSP 알고리즘을 이용해서 랜덤한 게임 맵 생성하기 [구현(2)]

https://sharp2studio.tistory.com/45

 

[유니티] BSP 알고리즘을 이용해서 랜덤한 게임 맵 생성하기 [구현(1)]

https://sharp2studio.tistory.com/44 [유니티] BSP 알고리즘을 이용해서 랜덤한 게임 맵 생성하기 [이론] BSP 알고리즘 (Binary Space Partitioning)은 한국어로 이진 공간 분할법이라는 뜻이다. 말 그대로 한 공..

sharp2studio.tistory.com

이전 포스팅으로 BSP알고리즘을 통해서 맵을 나누고, 그 안에서 방을 생성하며 각 방끼리 연결을 어떤식으로 할 지에 대해서 알아봤다.

이번에는, 이전에 나눴던 영역에 타일을 깔고 collider를 넣는 작업을 해볼것이다.

우선, 적당한 타일 이미지를 받아온다.

 

https://assetstore.unity.com/packages/2d/environments/backyard-top-down-tileset-53854

 

Backyard Top-Down Tileset | 2D 주변환경 | Unity Asset Store

Elevate your workflow with the Backyard Top-Down Tileset asset from Kittens and Elves at Work. Find this & more 주변환경 on the Unity Asset Store.

assetstore.unity.com

나는 유니티 에셋스토어의 무료 에셋을 사용하였다.

먼저, Hierarchy창에 들어가 우클릭-2D Object-Tilemap 을 눌러서 tilemap을 생성해준다.

tilemap이 생성되면, Scene뷰에서 그리드와 같은 모습이 됨을 볼 수 있다.

 

그 후, Window-2D-Tile palette 을 눌러준다.

Create new palette를 눌러 경로를 지정해준 후,받아왔던 이미지를 드래그앤 드롭 해준다.

그러면 Project창에서 tile형태의 파일이 생기게 된다.

 

이제 코드로 넘어가면,

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

public class MapGenerator :MonoBehaviour
{
    [SerializeField] Vector2Int mapSize;
    [SerializeField] float minimumDevideRate; //공간이 나눠지는 최소 비율
    [SerializeField] float maximumDivideRate; //공간이 나눠지는 최대 비율
    [SerializeField] private GameObject line; //lineRenderer를 사용해서 공간이 나눠진걸 시작적으로 보여주기 위함
    [SerializeField] private GameObject map; //lineRenderer를 사용해서 첫 맵의 사이즈를 보여주기 위함
    [SerializeField] private GameObject roomLine; //lineRenderer를 사용해서 방의 사이즈를 보여주기 위함
    [SerializeField] private int maximumDepth; //트리의 높이, 높을 수록 방을 더 자세히 나누게 됨
    [SerializeField] Tilemap tileMap; 
    [SerializeField] Tile roomTile; //방을 구성하는 타일
    [SerializeField] Tile wallTile; //방과 외부를 구분지어줄 벽 타일
    [SerializeField] Tile outTile; //방 외부의 타일
    
    void Start()
    {   
        FillBackground();//신 로드 시 전부다 바깥타일로 덮음
        Node root = new Node(new RectInt(0, 0, mapSize.x, mapSize.y));
        Divide(root, 0);
        GenerateRoom(root, 0);
        GenerateLoad(root, 0);
        FillWall(); //바깥과 방이 만나는 지점을 벽으로 칠해주는 함수
    }
  
    void Divide(Node tree,int n)
    {
        if (n == maximumDepth) return; //내가 원하는 높이에 도달하면 더 나눠주지 않는다.
        //그 외의 경우에는
        
        int maxLength = Mathf.Max(tree.nodeRect.width, tree.nodeRect.height);
        //가로와 세로중 더 긴것을 구한후, 가로가 길다면 위 좌, 우로 세로가 더 길다면 위, 아래로 나눠주게 될 것이다.
        int split = Mathf.RoundToInt(Random.Range(maxLength * minimumDevideRate, maxLength * maximumDivideRate));
        //나올 수 있는 최대 길이와 최소 길이중에서 랜덤으로 한 값을 선택
        if (tree.nodeRect.width >= tree.nodeRect.height) //가로가 더 길었던 경우에는 좌 우로 나누게 될 것이며, 이 경우에는 세로 길이는 변하지 않는다.
        {
          
            tree.leftNode = new Node(new RectInt(tree.nodeRect.x,tree.nodeRect.y,split,tree.nodeRect.height));
            //왼쪽 노드에 대한 정보다 
            //위치는 좌측 하단 기준이므로 변하지 않으며, 가로 길이는 위에서 구한 랜덤값을 넣어준다.
            tree.rightNode= new Node(new RectInt(tree.nodeRect.x+split, tree.nodeRect.y, tree.nodeRect.width-split, tree.nodeRect.height));
            //우측 노드에 대한 정보다 
            //위치는 좌측 하단에서 오른쪽으로 가로 길이만큼 이동한 위치이며, 가로 길이는 기존 가로길이에서 새로 구한 가로값을 뺀 나머지 부분이 된다. 
        }
        else
        {
          
            tree.leftNode = new Node(new RectInt(tree.nodeRect.x, tree.nodeRect.y, tree.nodeRect.width,split));
            tree.rightNode = new Node(new RectInt(tree.nodeRect.x, tree.nodeRect.y + split, tree.nodeRect.width , tree.nodeRect.height-split));
            //DrawLine(new Vector2(tree.nodeRect.x , tree.nodeRect.y+ split), new Vector2(tree.nodeRect.x + tree.nodeRect.width, tree.nodeRect.y  + split));
        }
        tree.leftNode.parNode = tree; //자식노드들의 부모노드를 나누기전 노드로 설정
        tree.rightNode.parNode = tree;
        Divide(tree.leftNode, n + 1); //왼쪽, 오른쪽 자식 노드들도 나눠준다.
        Divide(tree.rightNode, n + 1);//왼쪽, 오른쪽 자식 노드들도 나눠준다.
    }
    private RectInt GenerateRoom(Node tree,int n)
    {
        RectInt rect;
        if (n == maximumDepth) //해당 노드가 리프노드라면 방을 만들어 줄 것이다.
        {
           rect = tree.nodeRect;
           int width = Random.Range(rect.width/2,rect.width-1); 
           //방의 가로 최소 크기는 노드의 가로길이의 절반, 최대 크기는 가로길이보다 1 작게 설정한 후 그 사이 값중 랜덤한 값을 구해준다.
           int height=Random.Range(rect.height / 2,rect.height - 1);  
           //높이도 위와 같다.
           int x = rect.x + Random.Range(1, rect.width - width);
           //방의 x좌표이다. 만약 0이 된다면 붙어 있는 방과 합쳐지기 때문에,최솟값은 1로 해주고, 최댓값은 기존 노드의 가로에서 방의 가로길이를 빼 준 값이다.
           int y = rect.y + Random.Range(1, rect.height - height);        
           //y좌표도 위와 같다.
           rect = new RectInt(x, y, width, height);
           FillRoom(rect); 
        }
        else
        {
            tree.leftNode.roomRect = GenerateRoom(tree.leftNode,n+1);
            tree.rightNode.roomRect = GenerateRoom(tree.rightNode, n + 1);
            rect = tree.leftNode.roomRect;
        }
        return rect;
    }
    private void GenerateLoad(Node tree,int n)
    {
        if (n == maximumDepth) //리프 노드라면 이을 자식이 없다.
            return;
        Vector2Int leftNodeCenter = tree.leftNode.center;
        Vector2Int rightNodeCenter = tree.rightNode.center;      
       
           for (int i=Mathf.Min(leftNodeCenter.x, rightNodeCenter.x);i<=Mathf.Max(leftNodeCenter.x, rightNodeCenter.x); i++)
         {
             tileMap.SetTile(new Vector3Int(i - mapSize.x / 2, leftNodeCenter.y - mapSize.y / 2, 0), roomTile);
         }

         for (int j = Mathf.Min(leftNodeCenter.y, rightNodeCenter.y); j <= Mathf.Max(leftNodeCenter.y, rightNodeCenter.y); j++)
         {
             tileMap.SetTile(new Vector3Int(rightNodeCenter.x - mapSize.x / 2, j - mapSize.y / 2, 0), roomTile);
         }  
         //이전 포스팅에서 선으로 만들었던 부분을 room tile로 채우는 과정
        GenerateLoad(tree.leftNode, n + 1); //자식 노드들도 탐색
        GenerateLoad(tree.rightNode, n + 1);
    }

    void FillBackground() //배경을 채우는 함수, 씬 load시 가장 먼저 해준다.
    {
        for(int i = -10; i < mapSize.x+10; i++) //바깥타일은 맵 가장자리에 가도 어색하지 않게
        //맵 크기보다 넓게 채워준다.
        {
            for(int j = -10; j < mapSize.y+10; j++)
            {
                tileMap.SetTile(new Vector3Int(i - mapSize.x / 2, j - mapSize.y / 2, 0), outTile);
            }
        }
    }
    void FillWall() //룸 타일과 바깥 타일이 만나는 부분
    {      
        for (int i = 0; i < mapSize.x; i++) //타일 전체를 순회
        {
            for (int j = 0; j < mapSize.y; j++)
            {
               if(tileMap.GetTile(new Vector3Int(i - mapSize.x / 2, j - mapSize.y / 2, 0)) == outTile)
                {
      //바깥타일 일 경우
                    for(int x = -1; x <= 1; x++)
                    {
                        for(int y = -1; y <= 1; y++)
                        {
                            if (x == 0 && y == 0) continue;//바깥 타일 기준 8방향을 탐색해서 room tile이 있다면 wall tile로 바꿔준다.
                            if(tileMap.GetTile(new Vector3Int(i - mapSize.x / 2+x, j - mapSize.y / 2+y, 0)) == roomTile)
                            {
                                tileMap.SetTile(new Vector3Int(i - mapSize.x / 2, j - mapSize.y / 2, 0) , wallTile);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
    private void FillRoom(RectInt rect) { //room의 rect정보를 받아서 tile을 set해주는 함수
    for(int i = rect.x; i< rect.x + rect.width; i++)
        {
            for(int j = rect.y; j < rect.y + rect.height; j++)
            {
                tileMap.SetTile(new Vector3Int(i - mapSize.x / 2, j - mapSize.y / 2, 0), roomTile);
            }
        }
    }
    
}

이전 포스팅의 Draw부분을, tile을 채우는 함수들로 변경한 부분이다.

MapGenerator.cs를 위와 같이 하고 실행을 하게 되면,

우리가 원하는 랜덤한 형태의 맵이 나오게 된다 !

추가적으로, 지나갈 수 없는 길과 지나갈 수 있는 길을 구분하고 싶다면,

 

처음에 만들었던 tilemap에, tilemap collider2d 컴포넌트를 추가해준다.

충돌이 되야 하는 타일의 collider type은 Sprite(형태대로) or Grid(그리드 대로) 로 해주고,

길로 만들 예정이여서 충돌이 없어야 되는 타일의 collider type은 None으로 해주면, 같은 타일이여도 collider가 있고 없고가 바뀌게 된다.

길은 길이요 벽은 벽이다.


완성

위처럼 같은 코드여도, 타일에 따라서 다른 느낌의 맵 생성또한 가능하다 !!

 

참고 블로그 : https://velog.io/@1217pgy/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%A0%88%EC%B0%A8%EC%A0%81-%EC%83%9D%EC%84%B1%EC%9D%84-%EC%9C%84%ED%95%9C-%EB%8D%98%EC%A0%84-%EC%83%9D%EC%84%B1-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98

 

https://github.com/raetic/BSP-algorhitm-With-Unity

 

GitHub - raetic/BSP-algorhitm-With-Unity

Contribute to raetic/BSP-algorhitm-With-Unity development by creating an account on GitHub.

github.com

프로젝트는 깃허브에 업로드 해 놨습니다!