본문 바로가기
개발/Unity 내일배움캠프 TIL

Unity 3D 타일맵 최적화

by 석시 2023. 9. 27.



지난 시간에 타일맵을 자동 생성하는 것을 다뤘다.

Unity 3D 타일맵 자동 생성
프로젝트 준비그리드 상에 배치하기랜덤한 맵 데이터 생성하기TilemapGenerator.cs절차적 맵 생성 (Procedural Map Generation)TileMapGenerator.cs 그리드에 다가 타일 브러쉬로 타일을 칠하는 것 까진 알겠는데, 스크립트로 동적 생성은 어떻게 할까 궁금했었다. 다음 내용은 그리드가 있을 때 그리드 상에 오브젝트들을 어떻게 배치할 지, 더 나아가 맵을 랜덤으로 생성하고 배치하는 법에 대해 다룬다. 프로젝트 준비 애셋은 다음 링크의 애셋을 사용하였다.Platformer Kit · KenneyDownload this package (110 assets) for free, CC0 licensed!https://www.kenney.nl/assets/platformer-ki..
https://seoksii.tistory.com/52

셀룰러 오토마타를 사용하여 맵의 데이터를 생성하는 것까진 좋은데, 맵의 데이터를 바탕으로 오브젝트를 깔 때, 모든 오브젝트를 한 번에 로드하는 방식은 사실 적절한 방식이 아니다.

위 프로젝트에서 생성하는 맵의 사이즈를 96x96정도로만 늘려봐도, 플레이 모드에 진입시 게임이 매우 버벅이는 것을 알 수 있다.

저런 그리드 형식의 맵에서는 사실 텍스쳐 로딩부터 다시 구현해야 한다.


마인크래프트의 청크 로딩 방식

Code a Game Like Minecraft in Unity
A tutorial series on how to create a Minecraft-like game in Unity 3D (using C#). The aim of this series is to present the video and code with an eye towards ...
https://youtube.com/playlist?list=PLVsTSlfj0qsWEJ-5eMtXsYp03Y9yF1dEn&si=_q3qE83wvaVGxUsi

위 재생목록은 유니티에서 만들어보는 마인크래프트 튜토리얼인데, 가장 중요한 방식은 바로 이 청크중심 로딩 방식이다.

마인크래프트를 하다보면, 가끔 천장 위로 머리를 뚫고 나가는 경험을 해본 적이 있을 것이다.

그 때 보이는 것이 매우 중요한데, 모든 오브젝트들의 모든 면이 보이는 것이 아니라, 덩어리(Chunk)를 이루고 있는 블럭들은 우리 눈에 보이는 면의 텍스쳐만 보인다!

즉, 한 번에 로드되어 있어야 하는 텍스쳐의 개수가 완전 급감하게 된다.


필요한 메시만 렌더링하기

그리드에다가 8면이 존재하는 육각기둥을 매번 로딩해줄 것이 아니라, 우리도 필요한 면만 로딩하는 작업을 해보도록 하자.

해당 파트는 다음의 영상을 토대로 작업하였다.

Procedurally Generating A Hexagon Grid in Unity
Hexagons are common in a lot of strategy games, so today let's look at how we can create them from scratch in Unity. Further Reading - https://www.redblobgames.com/grids/hexagons/ -------------------------------------------------------------------------------- Want to support the channel? ▶️ Help fund new episodes and get free project downloads by joining the Patreon - http://www.patreon.com/GameDevGuide 🛒Get GameDevGuide Merch! - http://www.gamedevguide.store 💡 Get One-Month Free of Skillshare Premium - https://skl.sh/MKR826 Use these links to grab some cool assets from the asset store: Get the Must Have Assets! - https://assetstore.unity.com/top-assets/top-download?aid=1101la6X4 Free Unity Assets! - https://assetstore.unity.com/top-assets/top-free?aid=1101la6X4 New on the Asset Store! - https://assetstore.unity.com/top-assets/top-new?aid=1101la6X4 Top Paid Asset Store Packages - https://assetstore.unity.com/top-assets/top-paid?aid=1101la6X4 Asset Store Partners - https://assetstore.unity.com/lists/asset-store-partners-6?aid=1101la6X4 -------------------------------------------------------------------------------- Socials and Other Stuff: • Subscribe - https://www.youtube.com/gamedevguide?sub_confirmation=1 • Join the Discord - http://www.discord.gg/yYcww7U • Twitter - http://www.twitter.com/GameDevGuideYT • Facebook - http://www.facebook.com/GameDevGuideYT • Instagram - http://www.instagram.com/GameDevGuideYT
https://youtu.be/EPaSmQ2vtek?si=x4CFZLObx4CQ2OA7

HexRenderer라는 이름의 스크립트를 하나 생성하여 다음과 같이 작성하자.

using UnityEngine;

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class HexRenderer : MonoBehaviour
{
    private Mesh m_mesh;
    private MeshFilter m_meshFilter;
    private MeshRenderer m_meshRenderer;

    public Material material;

    private void Awake()
    {
        m_meshFilter = GetComponent<MeshFilter>();
        m_meshRenderer = GetComponent<MeshRenderer>();

        m_mesh = new Mesh();
        m_mesh.name = "Hex";

        m_meshFilter.mesh = m_mesh;
        m_meshRenderer.material = material;
    }
}

메시 데이터 - Unity 매뉴얼
이 페이지에는 메시에 포함된 사항과 Unity가 메시 클래스에 해당 데이터를 저장하는 방법에 대한 정보가 있습니다.
https://docs.unity3d.com/kr/2021.3/Manual/AnatomyofaMesh.html

유니티에서 메시는 꼭짓점(버텍스)과 삼각형의 연속으로 이루어져 있다.

우리가 할 일은 이러한 메시에 메시 필터를 정해진 순서로 넣어주어 적절한 모양의 도형을 생성해주는 것이다.

이러한 정해진 순서를 매번 외우는 것은 굉장히 귀찮은 일이기 때문에 우리는 육각형 렌더링을 위한 구조체를 선언해줄 것이다.

HexRenderer.cs 스크립트 상단에 다음과 같이 작성해주자.

public struct Face
{
    public List<Vector3> vertices { get; private set; }
    public List<int> triangles { get; private set; }
    public List<Vector2> uvs { get; private set; }

    public Face(List<Vector3> vertices, List<int> triangles, List<Vector2> uvs)
    {
        this.vertices = vertices;
        this.triangles = triangles;
        this.uvs = uvs;
    }
}

우리는 해당 Face 구조체를 메쉬의 각 표면의 정보를 담는 컨테이너로 사용할 것이다.

vertices는 꼭짓점들의 위치정보를 담는 리스트이고, 마찬가지로 triangles는 삼각형들이 어떤 꼭짓점들로 이루어져 있는지 정의한다.

uvs는 UV 좌표를 담는 리스트인데, UV는 간단하게 설명하자면 텍스쳐를 그리기 위한 좌표이다.

참고) UV가 뭔가요?

텍스쳐는 2D 데이터가 아닌가?

uvs는 vertices와 같은 수의 벡터를 가지고 있으면서 꼭짓점 하나하나에 대한 텍스쳐 상의 위치에 매핑을 해준다.

왼쪽의 텍스쳐를 오른쪽의 3D 공간 상의 표면에 매핑해준다.

메쉬를 내가 직접 그릴 수 있으면 무엇이 좋은가?

바닥 면의 경우는 오브젝트의 윗면 메시만 그리면 렌더링해야하는 메시의 수가 급감하게 된다.

자동으로 생성되는 오브젝트의 대부분이 바닥 면이기 때문에 이 경우 많은 성능의 상승이 있을 것이다.

사용한 코드는 다음과 같다.

HexRenderer.cs
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Serialization;

public struct Face
{
    public List<Vector3> vertices { get; private set; }
    public List<int> triangles { get; private set; }
    public List<Vector2> uvs { get; private set; }

    public Face(List<Vector3> vertices, List<int> triangles, List<Vector2> uvs)
    {
        this.vertices = vertices;
        this.triangles = triangles;
        this.uvs = uvs;
    }
}

[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public class HexRenderer : MonoBehaviour
{
    private Mesh m_mesh;
    private MeshFilter m_meshFilter;
    private MeshRenderer m_meshRenderer;

    private List<Face> m_faces;

    public Material material;
    public float innerSize;
    public float outerSize;
    public float height;
    public bool isFlatTopped;

    private void Awake()
    {
        m_meshFilter = GetComponent<MeshFilter>();
        m_meshRenderer = GetComponent<MeshRenderer>();

        m_mesh = new Mesh();
        m_mesh.name = "Hex";
        
        m_meshFilter.mesh = m_mesh;
        m_meshRenderer.material = material;
    }

    private void OnEnable()
    {
        DrawMesh();
    }

    public void OnValidate()
    {
        if (Application.isPlaying)
        {
            DrawMesh();
        }
    }

    public void DrawMesh()
    {
        DrawFaces();
        CombineFaces();
    }

    private void DrawFaces()
    {
        m_faces = new List<Face>();
        
        // Top faces
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(innerSize, outerSize, height / 2f, height / 2f, point));
        }

        if (height <= 0) return;
        
        // Bottom faces
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(innerSize, outerSize, -height / 2f, -height / 2f, point, true));
        }
        
        // Outer faces
        for (int point = 0; point < 6; point++)
        {
            m_faces.Add(CreateFace(outerSize, outerSize, height / 2f, -height / 2f, point, true));
        }
        
        // Inner faces
        // for (int point = 0; point < 6; point++)
        // {
        //     m_faces.Add(CreateFace(innerSize, innerSize, height / 2f, -height / 2f, point, false));
        // }
    }

    private void CombineFaces()
    {
        List<Vector3> vertices = new List<Vector3>();
        List<int> tris = new List<int>();
        List<Vector2> uvs = new List<Vector2>();

        for (int i = 0; i < m_faces.Count; i++)
        {
            //Add the vertices
            vertices.AddRange(m_faces[i].vertices);
            uvs.AddRange(m_faces[i].uvs);
            
            //Offset the triangles
            int offset = (4 * i);
            foreach (int triangle in m_faces[i].triangles)
            {
                tris.Add(triangle + offset);
            }
        }

        m_mesh.vertices = vertices.ToArray();
        m_mesh.triangles = tris.ToArray();
        m_mesh.uv = uvs.ToArray();
        m_mesh.RecalculateNormals();
    }

    private Face CreateFace(float innerRad, float outerRad, float heightA, float heightB, int point, bool reverse = false)
    {
        Vector3 pointA = GetPoint(innerRad, heightB, point);
        Vector3 pointB = GetPoint(innerRad, heightB, (point < 5) ? point + 1 : 0);
        Vector3 pointC = GetPoint(outerRad, heightA, (point < 5) ? point + 1 : 0);
        Vector3 pointD = GetPoint(outerRad, heightA, point);

        List<Vector3> vertices = new List<Vector3>() { pointA, pointB, pointC, pointD };
        List<int> triangles = new List<int>() { 0, 1, 2, 2, 3, 0 };
        List<Vector2> uvs = new List<Vector2>() { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1) };
        if (reverse)
        {
            vertices.Reverse();
        }
        
        return new Face(vertices, triangles, uvs);
    }

    protected Vector3 GetPoint(float size, float height, int index)
    {
        float angle_deg = isFlatTopped ? 60 * index : 60 * index - 30;
        float angle_rad = Mathf.PI / 180f * angle_deg;
        return new Vector3((size * Mathf.Cos(angle_rad)), height, size * Mathf.Sin(angle_rad));
    }
}
TileMapGenerator.cs
using System;
using UnityEngine;

public class TileMapGenerator : MonoBehaviour
{
    [SerializeField] Grid grid;
    
    [SerializeField] GameObject[] tilePrefabs;

    public string seed;
    public bool useRandomSeed;
    
    [SerializeField] private Vector2Int size;
    [SerializeField, Range(0, 100)]
    private int normalBlockPercent;
    [SerializeField, Range(0, 100)]
    private int normalBiomePercent;

    [SerializeField] private int smoothingFactor;
    [SerializeField] private float height;
    
    public int[,] MapHeights;
    public int[,] MapBiomes;
    
    void Start()
    {
        GenerateMap();
        DrawMapTiles();
    }

    void GenerateMap()
    {
        MapHeights = new int[size.x, size.y];
        MapBiomes = new int[size.x, size.y];
        RandomFillMap();

        for (int i = 0; i < smoothingFactor; i++)
        {
            SmoothMap(MapHeights);
            SmoothMap(MapBiomes);
        }
    }

    void RandomFillMap()
    {
        if (useRandomSeed)
            seed = DateTime.Now.ToBinary().ToString();

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

        for (int x = 0; x < size.x; x++)
            for (int y = 0; y < size.y; y++)
            {
                if (x == 0 || x == size.x - 1 || y == 0 || y == size.y - 1)
                    MapHeights[x, y] = 1;
                else MapHeights[x, y] = psuedoRandom.Next(0, 100) < normalBlockPercent ? 0 : 1;
                MapBiomes[x, y] = psuedoRandom.Next(0, 100) < normalBiomePercent ? 0 : 1;
            }
    }

    void SmoothMap(int[,] map)
    {
        int[,] newMap = new int[size.x, size.y];
        
        for (int x = 0; x < size.x; x++)
            for (int y = 0; y < size.y; y++)
            {
                int neighbourDifferentTiles = GetSurroundingTiles(map, x, y);
                
                if (neighbourDifferentTiles > 4)
                    map[x, y] = 1;
                else if (neighbourDifferentTiles < 4)
                    map[x, y] = 0;
            }
    }

    int GetSurroundingTiles(int[,] map, int gridX, int gridY)
    {
        int wallCount = 0;
        for (int neighbourX = gridX - 1; neighbourX <= gridX + 1; neighbourX++)
            for (int neighbourY = gridY - 1; neighbourY <= gridY + 1; neighbourY++)
                if (neighbourX >= 0 && neighbourX < size.x && neighbourY >= 0 && neighbourY < size.y)
                {
                    if (neighbourX != gridX || neighbourY != gridY)
                        wallCount += map[neighbourX, neighbourY];
                }
                else wallCount++;

        return wallCount;
    }

    void DrawMapTiles()
    {
        for (int x = 0; x < size.x; x++)
            for (int y = 0; y < size.y; y++)
            {
                Vector3 pos = grid.CellToWorld(new Vector3Int(x, y, 0));
                GameObject generated = Instantiate(tilePrefabs[MapBiomes[x, y]], pos ,Quaternion.identity, gameObject.transform);
                generated.GetComponentInChildren<HexRenderer>().height = Convert.ToSingle(MapHeights[x, y]) * height;
                generated.SetActive(true);
            }
                
    }
}


Uploaded by N2T