내일배움캠프 37일차 TIL, 절차적생성

황오영·2024년 6월 10일
0

TIL

목록 보기
37/56
post-thumbnail

숙련주차 프로젝트 - 지형생성

  • 오늘은 프로젝트 마지막날이라 대부분의 기능을 구현을 했고 마지막으로 유니티 내부에서 자원(우리의 경우 나무,돌 등의 자원)을 지형내에 생성하는 로직에 대해 이것저것 찾아봤다.
  • 맨처음 찾은 방법이 절차적 지형 생성에 관한 얘기여서 정리를 좀 해보려고 한다.

절차적 지형생성 - 펄린노이즈

https://chipmunk.tistory.com/4?category=1011704 (출처)

  • 펄린노이즈란 연속적인 일련의 난수 값을 생성하는 알고리즘으로 유기적인 모양의 노이즈를 생성한다.
  • 우리가 사는 지구 역시 불규칙적이다. 섬, 대륙 해류등 대부분의 지형이 매우 불규칙적이므로 이러한 부분에 맞게 게임에서도 지형을 생성할 때 무작위성을 이용하게 되면 간편하다.
  • 하지만 이러한 지형생성에서 '완전하게' 무작위라면 생각보다 원하는 느낌이 안나오게 될 수도 있고 너무 인위적이라면 자연스럽지 못하기 때문에 약간의 무작위성은 유기적인 프로그래밍에 큰 도움이 될 것이다.
  • 그런 부분에서 나온 알고리즘이 펄린노이즈 알고맂므이다. 이는 좀더 자연스러운 결과를 유도할 수 있는 알고리즘이라고 보면 좋을것이다.
  • 펄린 노이즈는 난수가 이전 결과의 영향을 받기 때문에 부드러운 노이즈가 생성된다.

펄린 노이즈의 요소

  • 시드 : 노이즈의 무작위 생성을 위한 값
  • 주파수 : 노이즈의 간격 클수록 세밀해진다.
  • 옥타브 : 여러 주파수의 노이즈를 중첩 시킬때 사용, 노이즈의 진폭에 관련이 있고 여러번 중첩 시켜서 만든 노이즈를 프랙탈 노이즈라고 부른다.
  • 그렇기때문에 지형의 형태는 펄린노이즈를 이용하는데 옥타브를 적당히 높여서 여러 주파수를 합친 프렉탈 노이즈로 생성하는것이 좋다!

절차적 지형 생성 - BSP(이진분할)

https://1217pgy.tistory.com/5 (출처)

  • 이진분할알고리즘이란 정령된 리스트에서 특정한 KEY값을 찾는 알고리즘을 의미 하는데 여기서 BSP(Binary Space Partitioning)은 3D그래픽스와 게임엔진 많이 사용되는 공간 분할 알고리즘이다. 공간을 반복 분할하여 나누는 것을 의미한다.
  • 유니티에선 보통 로그라이류 게임의 던전 생성에 많이 사용되는 기술! 잘 알아두면 써먹을 곳이 많을 것 같다.
  • 개념보단 구현적으로 먼저 봐야한다.

구현

  • 1번과정에서 재귀적으로 공간을 사각형으로 분할한다.
  • 몇번을 할꺼고 얼마나 복잡한지를 설정후 1번과정을 반복
  • 그 이후 각각의 공간에 방을 생성하고
  • 방과 방을 넣어둔 트리에서 꺼내 연결해준다.

공간을 분할하는 로직(1번~2번과정)

private void DivideTree(TreeNode treeNode, int n) //재귀 함수
{
    if (n < maxNode) //0 부터 시작해서 노드의 최댓값에 이를 때 까지 반복
    {
        RectInt size = treeNode.treeSize; //이전 트리의 범위 값 저장, 사각형의 범위를 담기 위해 Rect 사용
        int length = size.width >= size.height ? size.width : size.height; //사각형의 가로와 세로 중 길이가 긴 축을, 트리를 반으로 나누는 기준선으로 사용
        int split = Mathf.RoundToInt(Random.Range(length * minDivideSize, length * maxDivideSize)); //기준선 위에서 최소 범위와 최대 범위 사이의 값을 무작위로 선택
        if (size.width >= size.height) //가로
        {
            treeNode.leftTree = new TreeNode(size.x, size.y, split, size.height); //기준선을 반으로 나눈 값인 split을 가로 길이로, 이전 트리의 height값을 세로 길이로 사용
            treeNode.rightTree = new TreeNode(size.x + split, size.y, size.width - split, size.height); //x값에 split값을 더해 좌표 설정, 이전 트리의 width값에 split값을 빼 가로 길이 설정
            OnDrawLine(new Vector2(size.x + split, size.y), new Vector2(size.x + split, size.y + size.height)); //기준선 렌더링
        }
        else //세로
        {
            treeNode.leftTree = new TreeNode(size.x, size.y, size.width, split);
            treeNode.rightTree = new TreeNode(size.x, size.y + split, size.width, size.height - split);
            OnDrawLine(new Vector2(size.x, size.y + split), new Vector2(size.x + size.width, size.y + split));
        }
        treeNode.leftTree.parentTree = treeNode; //분할한 트리의 부모 트리를 매개변수로 받은 트리로 할당
        treeNode.rightTree.parentTree = treeNode;
        DivideTree(treeNode.leftTree, n + 1); //재귀 함수, 자식 트리를 매개변수로 넘기고 노드 값 1 증가 시킴
        DivideTree(treeNode.rightTree, n + 1);
    }
}

분할된 공간에 방 생성

private RectInt GenerateDungeon(TreeNode treeNode, int n) //방 생성
{
    if (n == maxNode) //노드가 최하위일 때만 조건문 실행
    {
        RectInt size = treeNode.treeSize;
        int width = Mathf.Max(Random.Range(size.width / 2, size.width - 1)); //트리 범위 내에서 무작위 크기 선택, 최소 크기 : width / 2
        int height = Mathf.Max(Random.Range(size.height / 2, size.height - 1));
        int x = treeNode.treeSize.x + Random.Range(1, size.width - width); //최대 크기 : width / 2
        int y = treeNode.treeSize.y + Random.Range(1, size.height - height);
        OnDrawDungeon(x, y, width, height); //던전 렌더링
        return new RectInt(x, y, width, height); //리턴 값은 던전의 크기로 길을 생성할 때 크기 정보로 활용
    }
    treeNode.leftTree.dungeonSize = GenerateDungeon(treeNode.leftTree, n + 1); //리턴 값 = 던전 크기
    treeNode.rightTree.dungeonSize = GenerateDungeon(treeNode.rightTree, n + 1);
    return treeNode.leftTree.dungeonSize; //부모 트리의 던전 크기는 자식 트리의 던전 크기 그대로 사용
}

길 연결하기

private void GenerateRoad(TreeNode treeNode, int n) //길 연결
{
    if (n == maxNode) return; //노드가 최하위일 때는 길을 연결하지 않음, 최하위 노드는 자식 트리가 없기 때문
    int x1 = GetCenterX(treeNode.leftTree.dungeonSize); //자식 트리의 던전 중앙 위치를 가져옴
    int x2 = GetCenterX(treeNode.rightTree.dungeonSize);
    int y1 = GetCenterY(treeNode.leftTree.dungeonSize);
    int y2 = GetCenterY(treeNode.rightTree.dungeonSize);
    for (int x = Mathf.Min(x1, x2); x <= Mathf.Max(x1, x2); x++) //x1과 x2중 값이 작은 곳부터 값이 큰 곳까지 타일 생성
        tilemap.SetTile(new Vector3Int(x - mapSize.x / 2, y1 - mapSize.y / 2, 0), tile); //mapSize.x / 2를 빼는 이유는 화면 중앙에 맞추기 위함
    for (int y = Mathf.Min(y1, y2); y <= Mathf.Max(y1, y2); y++)
        tilemap.SetTile(new Vector3Int(x2 - mapSize.x / 2, y - mapSize.y / 2, 0), tile);
    GenerateRoad(treeNode.leftTree, n + 1);
    GenerateRoad(treeNode.rightTree, n + 1);
}

과제에서 적용한것

  • 방법은 찾아봤지만 막상 우리 프로젝트에 적용하기엔 복잡하기도 하고 시간도 좀 더 걸릴것같아서 단순하게 구현했다.
    public void Generate(Define.Resources resources)
    {
        int idx = Random.Range(0, 4);
        Transform selectPoisition = point[idx];
        Vector3 Spawn = selectPoisition.position;
        Spawn += SpwanPoint(selectPoisition.GetComponent<GenerateFloat>().radius);
        foreach (GameObject pos in spawnPos)
        {
            if (Vector3.Distance(pos.transform.position, Spawn) <= 4f)
            {
                Generate(resources);
                return;
            }
        }

        GameObject go = null;
        switch (resources)
        {
            case Define.Resources.Tree:
                go = Tree;
                break;
            case Define.Resources.Rock:
                go = Rock;
                break;
            case Define.Resources.NPC:
                go = NPC;
                break;
        }
        GameObject go1 = Managers.Resource.Instantiate(go);
        go1.transform.position = Spawn;
        spawnPos.Add(go1);
    }
    private Vector3 SpwanPoint(float radius)
    {
        Vector3 getPoint = Random.onUnitSphere;
        getPoint.y = 0.0f;
        float r = Random.Range(0.0f, radius);
        return getPoint * r;
    }
  • 단순하게 특점 범위내에 생성 되는 방식으로 구현했다. 위치 중심값을 유니티에서 오브젝트로 생성후 잡아 준뒤 그것을 중심으로 Random.onUnitSphere라는 함수로 단위구에서 특정 좌표를 받아오는것
  • 중간에 오브젝트를 리스트로 정리해주는건 오브젝트가 너무 가깝게 생성되는것을 막기 위해 랜덤으로 나온 좌표와 현재 생성된 오브젝트 중에 가까운 거리가 있다면 다시 탐색하는 방향으로 로직을 구성했다.
  • collider가 겹치나 물위에 생성되는것을 방지하기위에 Ray로 탐색하거나 하는 방식도 해봤는데 잘 안되서 결국 좀 더 간단하게 구현했다. 다른 방식이 있다면 나중에 한번 더 해봐야겠다.

오늘의 회고

  • 생각보다 팀프로젝트가 나쁘지않아서 기분이 좋다. 은근 간단하게 만든것 치고는 대부분의 기능을 잘 구현한기분?
  • 내일 발표가 끝나고는 조금 쉬고 마지막 주차 잘 해야겠다 벌써 최종 프로젝트를 진행해야해서 걱정도 앞서지만 재밌는 게임을 만들고 싶어서 살짝 가슴이 뛴다. 이번에도 잘 만들어봐야지
profile
게임개발을 꿈꾸는 개발자

0개의 댓글