Unity의 절차적 세계 생성

Unity의 월드 생성은 Unity 게임 엔진 내에서 가상 세계, 지형, 풍경 또는 환경을 생성하거나 절차적으로 생성하는 프로세스를 의미합니다. 이 기술은 오픈 월드 게임, RPG, 시뮬레이션 등 다양한 유형의 게임에서 일반적으로 사용되어 방대하고 다양한 게임 세계를 동적으로 생성합니다.

Unity 이러한 세계적 기술을 구현하기 위한 유연한 프레임워크와 광범위한 도구 및 API를 제공합니다. C#를 사용하여 사용자 정의 스크립트를 작성하여 게임 세계를 생성 및 조작하거나 지형 시스템, 노이즈 기능 및 스크립팅 인터페이스와 같은 Unity 내장 기능을 활용하여 원하는 결과를 얻을 수 있습니다. 또한 Unity Asset Store에는 세계 생성 작업을 지원할 수 있는 타사 자산과 플러그인도 있습니다.

Unity에는 세계 생성에 대한 여러 접근 방식이 있으며, 선택은 게임의 특정 요구 사항에 따라 달라집니다. 다음은 일반적으로 사용되는 몇 가지 방법입니다.

  • Perlin 노이즈를 사용한 절차적 지형 생성
  • 셀룰러 오토마타
  • 보로노이 다이어그램
  • 절차적 개체 배치

Perlin 노이즈를 사용한 절차적 지형 생성

Unity의 절차적 지형 생성은 다양한 알고리즘과 기술을 사용하여 달성할 수 있습니다. 널리 사용되는 접근 방식 중 하나는 Perlin 노이즈를 사용하여 높이 맵을 생성한 다음 다양한 텍스처링 및 폴리지 기술을 적용하여 현실적이거나 양식화된 지형을 만드는 것입니다.

Perlin 노이즈는 Ken Perlin이 개발한 그래디언트 노이즈의 한 유형입니다. 이는 무작위로 보이지만 일관된 구조를 갖는 부드럽고 연속적인 값 패턴을 생성합니다. Perlin 노이즈는 자연스러운 지형, 구름, 텍스처 및 기타 유기적 모양을 만드는 데 널리 사용됩니다.

Unity에서는 'Mathf.PerlinNoise()' 함수를 사용하여 Perlin 노이즈를 생성할 수 있습니다. 두 개의 좌표를 입력으로 사용하고 0과 1 사이의 값을 반환합니다. 서로 다른 주파수와 진폭에서 Perlin 노이즈를 샘플링하면 절차 콘텐츠에 서로 다른 수준의 세부 사항과 복잡성을 생성할 수 있습니다.

Unity에서 이를 구현하는 방법의 예는 다음과 같습니다.

  • Unity 편집기에서 "GameObject -> 3D Object -> Terrain"으로 이동합니다. 그러면 장면에 기본 지형이 생성됩니다.
  • "TerrainGenerator"라는 새로운 C# 스크립트를 생성하고 지형 개체에 연결합니다. 다음은 Perlin 노이즈를 사용하여 절차적 지형을 생성하는 예제 스크립트입니다.
using UnityEngine;

public class TerrainGenerator : MonoBehaviour
{
    public int width = 512;       // Width of the terrain
    public int height = 512;      // Height of the terrain
    public float scale = 10f;     // Scale of the terrain
    public float offsetX = 100f;  // X offset for noise
    public float offsetY = 100f;  // Y offset for noise
    public float noiseIntensity = 0.1f; //Intensity of the noise

    private void Start()
    {
        Terrain terrain = GetComponent<Terrain>();

        // Create a new instance of TerrainData
        TerrainData terrainData = new TerrainData();

        // Set the heightmap resolution and size of the TerrainData
        terrainData.heightmapResolution = width;
        terrainData.size = new Vector3(width, 600, height);

        // Generate the terrain heights
        float[,] heights = GenerateHeights();
        terrainData.SetHeights(0, 0, heights);

        // Assign the TerrainData to the Terrain component
        terrain.terrainData = terrainData;
    }

    private float[,] GenerateHeights()
    {
        float[,] heights = new float[width, height];

        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                // Generate Perlin noise value for current position
                float xCoord = (float)x / width * scale + offsetX;
                float yCoord = (float)y / height * scale + offsetY;
                float noiseValue = Mathf.PerlinNoise(xCoord, yCoord);

                // Set terrain height based on noise value
                heights[x, y] = noiseValue * noiseIntensity;
            }
        }

        return heights;
    }
}
  • Unity 편집기의 지형 개체에 "TerrainGenerator" 스크립트를 첨부합니다.
  • 지형 개체에 대한 검사기 창에서 너비, 높이, 배율, 오프셋 및 노이즈 강도를 조정하여 생성된 지형의 모양을 조정합니다.
  • Unity 편집기에서 Play 버튼을 누르면 절차적 지형이 Perlin 노이즈 알고리즘을 기반으로 생성되어야 합니다.

Perlin 노이즈를 사용한 Unity 지형 생성.

참고: 이 스크립트는 Perlin 노이즈를 사용하여 기본 지형 높이 맵을 생성합니다. 더 복잡한 지형을 만들려면 스크립트를 수정하여 추가 노이즈 알고리즘을 통합하거나, 침식 또는 평활화 기술을 적용하거나, 텍스처링을 추가하거나, 지형의 특징에 따라 나뭇잎과 개체를 배치하십시오.

셀룰러 오토마타

셀룰러 오토마타는 셀 그리드로 구성된 계산 모델로, 각 셀은 미리 정의된 규칙 세트와 인접 셀의 상태에 따라 진화합니다. 컴퓨터 과학, 수학, 물리학 등 다양한 분야에서 사용되는 강력한 개념입니다. 셀룰러 오토마타는 단순한 규칙에서 나오는 복잡한 행동 패턴을 나타낼 수 있으므로 자연 현상을 시뮬레이션하고 절차적 콘텐츠를 생성하는 데 유용합니다.

셀룰러 오토마타의 기본 이론에는 다음 요소가 포함됩니다.

  1. 그리드: 그리드는 정사각형 또는 육각형 격자와 같은 규칙적인 패턴으로 배열된 셀의 모음입니다. 각 셀은 유한한 수의 상태를 가질 수 있습니다.
  2. 이웃: 각 셀에는 일반적으로 바로 인접한 셀인 이웃 셀이 있습니다. 이웃은 폰 노이만(위, 아래, 왼쪽, 오른쪽) 또는 무어(대각선 포함) 이웃과 같은 다양한 연결 패턴을 기반으로 정의할 수 있습니다.
  3. 규칙: 각 셀의 동작은 현재 상태와 인접 셀의 상태를 기반으로 진화하는 방식을 지정하는 일련의 규칙에 의해 결정됩니다. 이러한 규칙은 일반적으로 조건문이나 조회 테이블을 사용하여 정의됩니다.
  4. 업데이트: 셀룰러 오토마톤은 규칙에 따라 각 셀의 상태를 동시에 업데이트하여 진화합니다. 이 프로세스는 반복적으로 반복되어 일련의 세대를 만듭니다.

셀룰러 오토마타에는 다음과 같은 다양한 실제 응용 프로그램이 있습니다.

  1. 자연 현상 시뮬레이션: 셀룰러 오토마타는 유체 역학, 산불, 교통 흐름, 인구 역학 등 물리적 시스템의 동작을 시뮬레이션할 수 있습니다. 적절한 규칙을 정의함으로써 셀룰러 오토마타는 실제 시스템에서 관찰되는 새로운 패턴과 역학을 포착할 수 있습니다.
  2. 절차적 콘텐츠 생성: 셀룰러 오토마타를 사용하여 게임 및 시뮬레이션에서 절차적 콘텐츠를 생성할 수 있습니다. 예를 들어 지형, 동굴 시스템, 식생 분포 및 기타 유기적 구조를 만드는 데 사용할 수 있습니다. 세포의 성장과 상호작용을 관리하는 규칙을 지정하여 복잡하고 현실적인 환경을 생성할 수 있습니다.

다음은 인생 게임을 시뮬레이션하기 위해 Unity에 기본 세포 자동 장치를 구현하는 간단한 예입니다.

using UnityEngine;

public class CellularAutomaton : MonoBehaviour
{
    public int width = 50;
    public int height = 50;
    public float cellSize = 1f;
    public float updateInterval = 0.1f;
    public Renderer cellPrefab;

    private bool[,] grid;
    private Renderer[,] cells;
    private float timer = 0f;
    private bool[,] newGrid;

    private void Start()
    {
        InitializeGrid();
        CreateCells();
    }

    private void Update()
    {
        timer += Time.deltaTime;

        if (timer >= updateInterval)
        {
            UpdateGrid();
            UpdateCells();
            timer = 0f;
        }
    }

    private void InitializeGrid()
    {
        grid = new bool[width, height];
        newGrid = new bool[width, height];

        // Initialize the grid randomly
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                grid[x, y] = Random.value < 0.5f;
            }
        }
    }

    private void CreateCells()
    {
        cells = new Renderer[width, height];

        // Create a GameObject for each cell in the grid
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Vector3 position = new Vector3(x * cellSize, 0f, y * cellSize);
                Renderer cell = Instantiate(cellPrefab, position, Quaternion.identity);
                cell.material.color = Color.white;
                cells[x, y] = cell;
            }
        }
    }

    private void UpdateGrid()
    {
        // Apply the rules to update the grid
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                int aliveNeighbors = CountAliveNeighbors(x, y);

                if (grid[x, y])
                {
                    // Cell is alive
                    if (aliveNeighbors < 2 || aliveNeighbors > 3)
                        newGrid[x, y] = false; // Die due to underpopulation or overpopulation
                    else
                        newGrid[x, y] = true; // Survive
                }
                else
                {
                    // Cell is dead
                    if (aliveNeighbors == 3)
                        newGrid[x, y] = true; // Revive due to reproduction
                    else
                        newGrid[x, y] = false; // Remain dead
                }
            }
        }

        grid = newGrid;
    }

    private void UpdateCells()
    {
        // Update the visual representation of cells based on the grid
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Renderer renderer = cells[x, y];
                renderer.sharedMaterial.color = grid[x, y] ? Color.black : Color.white;
            }
        }
    }

    private int CountAliveNeighbors(int x, int y)
    {
        int count = 0;

        for (int i = -1; i <= 1; i++)
        {
            for (int j = -1; j <= 1; j++)
            {
                if (i == 0 && j == 0)
                    continue;

                int neighborX = x + i;
                int neighborY = y + j;

                if (neighborX >= 0 && neighborX < width && neighborY >= 0 && neighborY < height)
                {
                    if (grid[neighborX, neighborY])
                        count++;
                }
            }
        }

        return count;
    }
}
  • "CellularAutomaton" 스크립트를 Unity 장면의 GameObject에 연결하고 인스펙터의 'cellPrefab' 필드에 셀 프리팹을 할당합니다.

Unity의 셀룰러 자동 장치.

이 예에서 셀 그리드는 부울 배열로 표현됩니다. 여기서 'true'은 살아있는 셀을 나타내고 'false'은 죽은 셀을 나타냅니다. 그리드를 업데이트하기 위해 인생 게임의 규칙이 적용되고 그에 따라 세포의 시각적 표현이 업데이트됩니다. 'CreateCells()' 메서드는 각 셀에 대한 GameObject를 생성하고, 'UpdateCells()' 메서드는 그리드 상태에 따라 각 GameObject의 색상을 업데이트합니다.

참고: 이는 단지 기본적인 예일 뿐이며 탐색할 수 있는 셀룰러 오토마타에는 다양한 변형과 ​​확장이 있습니다. 규칙, 셀 동작 및 그리드 구성을 수정하여 다양한 시뮬레이션을 생성하고 다양한 패턴과 동작을 생성할 수 있습니다.

보로노이 다이어그램

보로노이 테셀레이션 또는 보로노이 파티션이라고도 알려진 보로노이 다이어그램은 시드 또는 사이트라고 불리는 일련의 점에 대한 근접성을 기준으로 공간을 여러 영역으로 나누는 기하학적 구조입니다. 보로노이 다이어그램의 각 영역은 다른 시드보다 특정 시드에 더 가까운 공간의 모든 점으로 구성됩니다.

보로노이 다이어그램의 기본 이론에는 다음 요소가 포함됩니다.

  1. 시드/사이트: 시드 또는 사이트는 공간의 점 집합입니다. 이러한 포인트는 무작위로 생성되거나 수동으로 배치될 수 있습니다. 각 시드는 보로노이 지역의 중심점을 나타냅니다.
  2. 보로노이 셀/영역: 각 보로노이 셀 또는 영역은 다른 시드보다 특정 시드에 더 가까운 공간 영역에 해당합니다. 영역의 경계는 인접한 시드를 연결하는 선분의 ​​수직 이등분선으로 형성됩니다.
  3. Delaunay 삼각분할: Voronoi 다이어그램은 Delaunay 삼각분할과 밀접한 관련이 있습니다. 들로네 삼각측량(Delaunay Triangulation)은 시드가 삼각형의 외접원 내부에 없도록 시드 점을 삼각측량하는 것입니다. Delaunay 삼각분할은 보로노이 다이어그램을 구성하는 데 사용할 수 있으며 그 반대의 경우도 마찬가지입니다.

Voronoi 다이어그램에는 다음을 포함하여 다양한 실제 응용 프로그램이 있습니다.

  1. 절차적 콘텐츠 생성: Voronoi 다이어그램은 절차적 지형, 자연 경관 및 유기적 형태를 생성하는 데 사용할 수 있습니다. 시드를 제어점으로 사용하고 보로노이 셀에 속성(예: 고도 또는 생물군계 유형)을 할당함으로써 현실적이고 다양한 환경을 만들 수 있습니다.
  2. 게임 디자인: Voronoi 다이어그램은 게임 디자인에서 게임 플레이 목적으로 공간을 분할하는 데 사용될 수 있습니다. 예를 들어, 전략 게임에서 보로노이 다이어그램을 사용하면 게임 맵을 여러 세력이 제어하는 ​​영토나 구역으로 나눌 수 있습니다.
  3. 길 찾기 및 AI: 보로노이 다이어그램은 가장 가까운 시드 또는 영역을 효율적으로 계산할 수 있는 공간 표현을 제공하여 길 찾기 및 AI 탐색에 도움을 줄 수 있습니다. 탐색 메시를 정의하거나 AI 에이전트에 대한 영향 맵을 정의하는 데 사용할 수 있습니다.

Unity에는 보로노이 다이어그램을 생성하고 활용하는 여러 가지 방법이 있습니다.

  1. 절차적 생성: 개발자는 Unity의 시드 포인트 세트에서 보로노이 다이어그램을 생성하는 알고리즘을 구현할 수 있습니다. Fortune 알고리즘이나 Lloyd 완화 알고리즘과 같은 다양한 알고리즘을 사용하여 보로노이 다이어그램을 구성할 수 있습니다.
  2. 지형 생성: 보로노이 다이어그램은 지형 생성에 활용되어 다양하고 사실적인 풍경을 만들 수 있습니다. 각 보로노이 셀은 산, 계곡, 평야 등 다양한 지형 특징을 나타낼 수 있습니다. 고도, 수분, 초목과 같은 속성을 각 셀에 할당하여 다양하고 시각적으로 매력적인 지형을 만들 수 있습니다.
  3. 맵 분할: Voronoi 다이어그램을 사용하여 게임 플레이 목적으로 게임 맵을 영역으로 나눌 수 있습니다. 각 지역에 서로 다른 속성이나 속성을 할당하여 고유한 게임플레이 영역을 생성할 수 있습니다. 이는 전략 게임, 영토 제어 메커니즘 또는 레벨 디자인에 유용할 수 있습니다.

Voronoi 다이어그램 기능을 제공하는 Unity 패키지와 자산이 있으므로 Voronoi 기반 기능을 Unity 프로젝트에 더 쉽게 통합할 수 있습니다. 이러한 패키지에는 Voronoi 다이어그램 생성 알고리즘, 시각화 도구 및 Unity 렌더링 시스템과의 통합이 포함되는 경우가 많습니다.

다음은 Fortune 알고리즘을 사용하여 Unity에 2D 보로노이 다이어그램을 생성하는 예입니다.

using UnityEngine;
using System.Collections.Generic;

public class VoronoiDiagram : MonoBehaviour
{
    public int numSeeds = 50;
    public int diagramSize = 50;
    public GameObject seedPrefab;

    private List<Vector2> seeds = new List<Vector2>();
    private List<List<Vector2>> voronoiCells = new List<List<Vector2>>();

    private void Start()
    {
        GenerateSeeds();
        GenerateVoronoiDiagram();
        VisualizeVoronoiDiagram();
    }

    private void GenerateSeeds()
    {
        // Generate random seeds within the diagram size
        for (int i = 0; i < numSeeds; i++)
        {
            float x = Random.Range(0, diagramSize);
            float y = Random.Range(0, diagramSize);
            seeds.Add(new Vector2(x, y));
        }
    }

    private void GenerateVoronoiDiagram()
    {
        // Compute the Voronoi cells based on the seeds
        for (int i = 0; i < seeds.Count; i++)
        {
            List<Vector2> cell = new List<Vector2>();
            voronoiCells.Add(cell);
        }

        for (int x = 0; x < diagramSize; x++)
        {
            for (int y = 0; y < diagramSize; y++)
            {
                Vector2 point = new Vector2(x, y);
                int closestSeedIndex = FindClosestSeedIndex(point);
                voronoiCells[closestSeedIndex].Add(point);
            }
        }
    }

    private int FindClosestSeedIndex(Vector2 point)
    {
        int closestIndex = 0;
        float closestDistance = Vector2.Distance(point, seeds[0]);

        for (int i = 1; i < seeds.Count; i++)
        {
            float distance = Vector2.Distance(point, seeds[i]);
            if (distance < closestDistance)
            {
                closestDistance = distance;
                closestIndex = i;
            }
        }

        return closestIndex;
    }

    private void VisualizeVoronoiDiagram()
    {
        // Visualize the Voronoi cells by instantiating a sphere for each cell point
        for (int i = 0; i < voronoiCells.Count; i++)
        {
            List<Vector2> cell = voronoiCells[i];
            Color color = Random.ColorHSV();

            foreach (Vector2 point in cell)
            {
                Vector3 position = new Vector3(point.x, 0, point.y);
                GameObject sphere = Instantiate(seedPrefab, position, Quaternion.identity);
                sphere.GetComponent<Renderer>().material.color = color;
            }
        }
    }
}
  • 이 코드를 사용하려면 구형 프리팹을 생성하고 Unity 인스펙터의 SeedPrefab 필드에 할당하세요. numSeeds 및 DiagramSize 변수를 조정하여 시드 수와 다이어그램 크기를 제어합니다.

Unity의 보로노이 다이어그램.

이 예에서 VoronoiDiagram 스크립트는 지정된 다이어그램 크기 내에 시드 포인트를 무작위로 배치하여 Voronoi 다이어그램을 생성합니다. 'GenerateVoronoiDiagram()' 메소드는 시드 포인트를 기반으로 보로노이 셀을 계산하고, 'VisualizeVoronoiDiagram()' 메소드는 보로노이 셀의 각 포인트에서 구체 GameObject를 인스턴스화하여 다이어그램을 시각화합니다.

참고: 이 예는 보로노이 다이어그램의 기본 시각화를 제공하지만 셀 포인트를 선으로 연결하거나 지형 생성 또는 게임플레이 목적을 위해 각 셀에 서로 다른 속성을 할당하는 등 추가 기능을 추가하여 이를 더욱 확장할 수 있습니다.

전반적으로 Voronoi 다이어그램은 절차적 콘텐츠 생성, 공간 분할, Unity에서 흥미롭고 다양한 환경 생성을 위한 다재다능하고 강력한 도구를 제공합니다.

절차적 개체 배치

Unity의 절차적 개체 배치에는 수동으로 개체를 배치하는 대신 알고리즘을 통해 장면에 개체를 생성하고 배치하는 작업이 포함됩니다. 자연스럽고 역동적인 방식으로 나무, 바위, 건물 또는 기타 물체로 환경을 채우는 등 다양한 목적으로 사용되는 강력한 기술입니다.

다음은 Unity의 절차적 객체 배치 예입니다.

using UnityEngine;

public class ObjectPlacement : MonoBehaviour
{
    public GameObject objectPrefab;
    public int numObjects = 50;
    public Vector3 spawnArea = new Vector3(10f, 0f, 10f);

    private void Start()
    {
        PlaceObjects();
    }

    private void PlaceObjects()
    {
        for (int i = 0; i < numObjects; i++)
        {
            Vector3 spawnPosition = GetRandomSpawnPosition();
            Quaternion spawnRotation = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f);
            Instantiate(objectPrefab, spawnPosition, spawnRotation);
        }
    }

    private Vector3 GetRandomSpawnPosition()
    {
        Vector3 center = transform.position;
        Vector3 randomPoint = center + new Vector3(
            Random.Range(-spawnArea.x / 2, spawnArea.x / 2),
            0f,
            Random.Range(-spawnArea.z / 2, spawnArea.z / 2)
        );
        return randomPoint;
    }
}
  • 이 스크립트를 사용하려면 Unity 장면에 빈 GameObject를 만들고 여기에 attach "ObjectPlacement" 스크립트를 추가하세요. 객체 프리팹을 할당하고 요구사항에 맞게 인스펙터에서 'numObjects''spawnArea' 매개변수를 조정합니다. 장면을 실행할 때 객체는 정의된 생성 영역 내에 절차적으로 배치됩니다.

Unity의 절차적 객체 배치.

이 예에서 'ObjectPlacement' 스크립트는 장면에 객체를 절차적으로 배치하는 역할을 합니다. 'objectPrefab' 필드에는 배치할 객체의 프리팹이 할당되어야 합니다. 'numObjects' 변수는 배치할 객체의 수를 결정하고, 'spawnArea' 변수는 객체가 무작위로 배치될 영역을 정의합니다.

'PlaceObjects()' 메서드는 원하는 개체 수를 반복하고 정의된 생성 영역 내에서 임의의 생성 위치를 생성합니다. 그런 다음 무작위 회전을 통해 각 무작위 위치에서 객체 프리팹을 인스턴스화합니다.

참고: 프로젝트의 특정 요구 사항에 따라 그리드 기반 배치, 밀도 기반 배치 또는 규칙 기반 배치와 같은 다양한 배치 알고리즘을 통합하여 이 코드를 더욱 향상시킬 수 있습니다.

결론

Unity의 절차적 생성 기술은 역동적이고 몰입감 있는 경험을 만들기 위한 강력한 도구를 제공합니다. Perlin 노이즈 또는 프랙탈 알고리즘을 사용하여 지형을 생성하거나, Voronoi 다이어그램을 사용하여 다양한 환경을 생성하거나, 셀룰러 오토마타를 사용하여 복잡한 동작을 시뮬레이션하거나, 절차적으로 배치된 객체로 장면을 채우는 등 이러한 기술은 콘텐츠 생성을 위한 유연성, 효율성 및 무한한 가능성을 제공합니다. 이러한 알고리즘을 활용하고 이를 Unity 프로젝트에 통합함으로써 개발자는 현실적인 지형 생성, 실제와 같은 시뮬레이션, 시각적으로 매력적인 환경 및 매력적인 게임플레이 메커니즘을 달성할 수 있습니다. 절차적 생성은 시간과 노력을 절약할 뿐만 아니라 플레이어를 사로잡고 가상 세계에 생명을 불어넣는 독특하고 끊임없이 변화하는 경험을 생성할 수 있게 해줍니다.

추천 기사
Unity 게임 개발에서 스토리텔링의 중요성
Unity에서 지형에 나무를 그리는 방법
불법 복제로부터 Unity 게임을 보호하기 위한 전략
Unity의 2D 및 3D 개발 환경 비교
Unity의 Transform 컴포넌트 마스터하기
Unity 시네머신 및 타임라인 튜토리얼
Unity로 애니메이션을 가져오는 방법