본문 바로가기

Unity Engine/자료 번역

유니티 Procedural Cave Gerneration 랜덤 동굴 생성 1

728x90
728x90

유니티 버전

2021.3.9f1

들어가기 전 참고

이 포스트는 아래의 동영상의 내용을 한국어로 재구성한 자료이다. 어느 정도 영어 청취가 가능하다면 동영상을 봐도 좋다.

 

https://youtu.be/v7yyZZjF1z4

이 포스트는 유니티 엔진에서 셀룰러 오토마타 기반으로 동굴 지형을 생성하는 cave generation을 다룰 것이다.

 

에피소드 1에서는 직접 동굴을 만들기 전 기즈모를 이용해 대략적인 모습을 보여주는 것을 목표로 한다.

 

필요 변수 세팅

필요한 변수는 다음과 같다.

 

int width, height: 맵의 너비와 높이다.

bool useRandomSeed: 랜덤에 시드를 직접 부여할지 안 할지 고르는 변수다.

int randomFillPercent: 초기에 맵을 위와 같은 흑색을 어느 정도의 비율로 채울지 결정하는 변수다.

int[,] map: 기즈모로 칠할 맵이며 2차원 배열로 나타낸다.

 

MapGenerator 스크립트를 만들고 변수를 채워보는 걸로 시작하자.

 

RandomFillMap

그럼 바로 맵의 구역을 일정 확률로 흑색으로 칠하는 메서드를 구현해보자. 이 유튜브 채널에서는 useRandomSeed가 true일 경우 현재 시간을 문자열로 바꾼 값을 시드로 쓴다.

 

if (useRandomSeed)
{
    seed = Time.time.ToString();
}

 

유사 난수 객체를 생성하자. 시드 값은 GetHashCode로 시드 문자열을 해싱한 값이 들어간다.

 

System.Random pseudoRandom = new System.Random(seed.GetHashCode());

 

그리고 맵의 크기 width, height 만큼 이중 for 문을 돌면서 현재 격자가 맵의 구석이면 1, 그렇지 않으면 randomFillPercent의 값에 따라 1 또는 0을 부여한다.

 

RandomFillMap의 구현체는 다음과 같다.

 

void RandomFillMap()
{
    if (useRandomSeed)
    {
        seed = Time.time.ToString();
    }

    System.Random pseudoRandom = new System.Random(seed.GetHashCode());

    for (int x  = 0; x < width; x++)
    {
        for (int y = 0; y < height; y++)
        {
            if (x == 0 || x == width - 1 || y == 0 || y == height - 1)
            {
                map[x, y] = 1;
            }
            else
            {
                map[x, y] = pseudoRandom.Next(0, 100) < randomFillPercent ? 1 : 0;
            }
        }
    }
}

GenerateMap

map을 초기화하고 RandomFillMap을 호출할 메서드인 GenerateMap을 구현하자.

 

void GenerateMap()
{
    map = new int[width, height];
    RandomFillMap();
}

 

MapGenerator 오브젝트를 만들고 스크립트 컴포넌트를 추가하자. 그리고 변수에 적당한 값을 지정한다. 만약 Seed를 쓰지 않을 경우 꼭 UseRandomSeed를 추가하도록 하자.

 

 

Start에서 GenerateMap을 호출하는 것도 까먹지 말자.

 

OnDrawGizmos

생성한 맵을 시각적으로 확인하기 위해 map을 OnDrawGizmos로 그릴 필요가 있다.

 

자세한 설명은 공식 문서를 참조한다. 지금은 OnDrawGizmos에서 맵을 시각적으로 표현하도록 하자.

 

void OnDrawGizmos()
{
    if (map is null)
    {
        return;
    }

    for (int x  = 0; x < width; x++)
    {
        for (int y = 0; y < height; y++)
        {
            Gizmos.color = map[x, y] == 1 ? Color.black : Color.white;
            Vector3 pos = new Vector3(-width / 2 + x + 0.5f, 0, -height / 2 + y + 0.5f);
            Gizmos.DrawCube(pos, Vector3.one);
        }
    }
}

 

여기까지 하고 씬을 play 하면 다음과 같은 모습을 볼 수 있다.

 

 

뭔가 그럴듯한 그림이 나왔지만 "동굴"이라고 하기엔 부족하다. 여기서 셀룰러 오토마타를 사용하여 맵을 다듬을 것이다.

셀룰러 오토마타?

셀룰러 오토마타 또는 세포 자동자라고 불리는 모형은 쉽게 말해 검은 점을 세포라 하면 인접한 격자의 상태에 따라 사망하거나 생존, 또는 복제하도록 하여 어떤 시뮬레이션을 하는 것이라 볼 수 있다.

 

우리는 이 셀룰러 오토마타로 위 맵의 모습을 잘 다듬어 동굴처럼 보이게 할 것이다. 우리가 쓸 오토마타의 규칙은 다음과 같다.

 

인접 8방향의 셀의 개수가 4개면 현상 유지, 4개를 초과하면 1로 전환, 4개 미만이면 0으로 사망 처리를 한다. 인접 검사를 하는 도중 인덱스가 맵을 벗어나면 셀이 있는 것으로 간주한다.

 

인접 8방향의 셀 개수를 세는 메서드 GetAdjustCells를 만들자.

 

int GetAdjustCells(int currentX, int currentY)
{
    int cells = 0;

    for (int i = -1; i <= 1; i++)
    {
        for (int j = -1; j <= 1; j++)
        {
            if (i == 0 && j == 0) continue; // 현재 위치는 인접한 셀이 아니므로 건너뛴다.
            int adjX = currentX + i;
            int adjY = currentY + j;
            if (adjX < 0 || adjY < 0 || adjX >= width || adjY >= height) ++cells;
            else cells += map[adjX, adjY];
        }
    }

    return cells;
}

 

그리고 셀룰러 오토마타를 적용해 맵을 다듬는 SmoothMap 메서드를 구현하자. 모든 격자를 순회하면서 GetAdjustCells의 결과에 따라 map의 상태를 바꾸면 된다.

 

void SmoothMap()
{
    for (int x = 0; x < width; x++)
    {
        for (int y = 0; y < height; y++)
        {
            int neighbourWallTiles = GetAdjustCells(x, y);
            if (neighbourWallTiles > 4)
            {
                map[x, y] = 1;
            }
            else if (neighbourWallTiles < 4)
            {
                map[x, y] = 0;
            }
        }
    }
}

 

SmoothMap의 코드를 다 작성했으면 GenerateMap에서 호출해주도록 하자. 한 번만으로는 효과가 적을 것이므로 여러 번 호출하도록 한다.

 

void GenerateMap()
{
    map = new int[width, height];
    RandomFillMap();

    for (int i = 0; i < 5; i++)
    {
        SmoothMap();
    }
}

 

그리고 다시 실행하면 다음과 같은 모습을 볼 수 있다.

 

 

확실히 동굴 같은 그림이 나왔다. 하지만 실제 지형을 생성한 것은 아니고 갈 길이 아직 멀다.(이 시리즈는 에피소드 9까지 있다.) 이 로직을 가지고 다음 에피소드를 진행하도록 하자.

다음 에피소드

준비 중

참고

영상에서 사용한 규칙은 격자를 순회할 때 한계를 가지고 있다.

 

 

한쪽부터 채우기 때문에 대각선 방향으로의 편향이 발생한다고 하는데 일단 크게 중요한 부분은 아니므로(아직 우린 3D 형체를 보지도 못했다!) 고칠 사람은 자유롭게 고치도록 하자.

728x90
728x90