unity/기술개발일지

Unity - 절차적 동굴 생성 (셀룰러 오토마타)

rimugiri 2024. 10. 26. 14:41
728x90

전에 절차적 지향 맵 생성을 제작해 보았는데 이번에는 다른 알고리즘으로 동굴느낌의 맵을 생성하려고한다.

 

Unity2D 절차 지향적 맵 생성

2D 타일맵을 검색해 보면 BSP 알고리즘을 이용한 생성과 간단한 절차 지향적 알고리즘을 이용한 생성을 많이 소개합니다그중 절차 지향적으로 랜덤맵을 생성하는 방법을 시도해 보았습니다 결

rimugiri.tistory.com

 

셀룰러 오토마타라는 알고리즘을 약간 변형하여 사용한 것인데 유전자 알고리즘과 비슷하게 현재 세대가 다음세대에 영향을 미치고 변형하는 원리의 알고리즘이다.

 

i번째 세대에 어떤 셀이 빈공간이고 그 주변의 8칸중 5칸 이상이면 벽으로 3칸 이하면 빈공간 4칸이면 그대로 둔다 이렇게 되면 세대를 거듭 할때마다 최종적으로 특정 모양으로 이뤄진 공간이 남고 이 공간이 이어져 있으면 거대한 동굴이 형성되는 것이다

특정 모양

1. 생성 코드

- 전체적인 흐름만 이해하면 된다 초기에 가장자리를 벽으로 채워주고 랜덤으로 공간을 채워준다

- 다음으로는 step이 진행할때마다 위에 설명한 알고리즘으로 공간을 변형시켜간다

using System;
using UnityEngine;

namespace PKW_Tilemap
{
    enum CaveTileType
    {
        EMPTY = 0,
        WALL = 1,
    }
    public class RandomCaveGenerator : DungenGeneratorBase
    {
        [SerializeField] private RandomSquareSO _randomSquareSO;

        [Range(0, 100)]
        [SerializeField] private int _randomFillPercent;
        public int wallLimit = 4;
        public bool temp = false;
        int[,] maps = null;
        protected override void RunGenerator()
        {
            if (temp)
            {
                ResetMap();
                temp = false;
            }

            if (maps == null)
            {
                Init();
            }
            else
            {
                SmoothMap();
            }
        }

        private void SmoothMap()
        {
            for (int x = 0; x < _randomSquareSO.maxWidth; x++)
            {
                for (int y = 0; y < _randomSquareSO.maxHeight; y++)
                {
                    int wallCount = GetSurroundingWallCount(x, y);

                    if (wallCount > wallLimit)
                    {
                        maps[x, y] = (int)CaveTileType.WALL;
                    }
                    else if (wallCount < wallLimit)
                    {
                        maps[x, y] = (int)CaveTileType.EMPTY;
                    }

                    PaintTile(x, y, maps[x, y]);
                }
            }

            Debug.Log("Smooth Complete");
        }

        private int GetSurroundingWallCount(int x, int y)
        {
            int wallCount = 0;
            for (int neighborX = x - 1; neighborX <= x + 1; neighborX++)
            {
                for (int neighborY = y - 1; neighborY <= y + 1; neighborY++)
                {
                    // 범위 이상과 자기 자신은 제외한다
                    if (x == neighborX && y == neighborY) { continue; }
                    if (neighborX < 0 || neighborX >= _randomSquareSO.maxWidth || neighborY < 0 || neighborY >= _randomSquareSO.maxHeight)
                    {
                        wallCount++;
                        continue;
                    }

                    wallCount += maps[neighborX, neighborY];
                }
            }

            return wallCount;
        }

        public void ResetMap()
        {
            maps = null;
        }

        private void Init()
        {
            int randomSeed = Time.time.GetHashCode();
            System.Random randomNumber = new System.Random(randomSeed);

            maps = new int[_randomSquareSO.maxWidth, _randomSquareSO.maxHeight];

            for (int x = 0; x < _randomSquareSO.maxWidth; x++)
            {
                for (int y = 0; y < _randomSquareSO.maxHeight; y++)
                {
                    // 가장자리는 벽으로 채워준다
                    if (x == 0 || x == _randomSquareSO.maxWidth - 1 || y == 0 || y == _randomSquareSO.maxHeight - 1)
                    {
                        maps[x, y] = (int)CaveTileType.WALL;
                    }
                    // 나머지는 설정한 랜덤 값으로 설정해 준다
                    else
                    {
                        maps[x, y] = (randomNumber.Next(0, 100) < _randomFillPercent) ? (int)CaveTileType.WALL :
                            (int)CaveTileType.EMPTY;
                    }

                    PaintTile(x, y, maps[x, y]);
                }
            }

            Debug.Log("Init Complete");
        }

        void PaintTile(int x, int y, int tileType)
        {
            switch (tileType)
            {
                case (int)CaveTileType.EMPTY:
                    tilemapVisualizer.PaintEmptyTile(new Vector2Int(x, y));
                    break;
                case (int)CaveTileType.WALL:
                    tilemapVisualizer.PaintCaveTile(new Vector2Int(x, y));
                    break;
            }
        }
    }
}

 

2. 결과

 

728x90