지난 시간에 타일맵을 자동 생성하는 것을 다뤘다.
셀룰러 오토마타를 사용하여 맵의 데이터를 생성하는 것까진 좋은데, 맵의 데이터를 바탕으로 오브젝트를 깔 때, 모든 오브젝트를 한 번에 로드하는 방식은 사실 적절한 방식이 아니다.
위 프로젝트에서 생성하는 맵의 사이즈를 96x96정도로만 늘려봐도, 플레이 모드에 진입시 게임이 매우 버벅이는 것을 알 수 있다.
저런 그리드 형식의 맵에서는 사실 텍스쳐 로딩부터 다시 구현해야 한다.
마인크래프트의 청크 로딩 방식
위 재생목록은 유니티에서 만들어보는 마인크래프트 튜토리얼인데, 가장 중요한 방식은 바로 이 청크중심 로딩 방식이다.
마인크래프트를 하다보면, 가끔 천장 위로 머리를 뚫고 나가는 경험을 해본 적이 있을 것이다.
그 때 보이는 것이 매우 중요한데, 모든 오브젝트들의 모든 면이 보이는 것이 아니라, 덩어리(Chunk)를 이루고 있는 블럭들은 우리 눈에 보이는 면의 텍스쳐만 보인다!
즉, 한 번에 로드되어 있어야 하는 텍스쳐의 개수가 완전 급감하게 된다.
필요한 메시만 렌더링하기
그리드에다가 8면이 존재하는 육각기둥을 매번 로딩해줄 것이 아니라, 우리도 필요한 면만 로딩하는 작업을 해보도록 하자.
해당 파트는 다음의 영상을 토대로 작업하였다.
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;
}
}
유니티에서 메시는 꼭짓점(버텍스)과 삼각형의 연속으로 이루어져 있다.
우리가 할 일은 이러한 메시에 메시 필터를 정해진 순서로 넣어주어 적절한 모양의 도형을 생성해주는 것이다.
이러한 정해진 순서를 매번 외우는 것은 굉장히 귀찮은 일이기 때문에 우리는 육각형 렌더링을 위한 구조체를 선언해줄 것이다.
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와 같은 수의 벡터를 가지고 있으면서 꼭짓점 하나하나에 대한 텍스쳐 상의 위치에 매핑을 해준다.
메쉬를 내가 직접 그릴 수 있으면 무엇이 좋은가?
바닥 면의 경우는 오브젝트의 윗면 메시만 그리면 렌더링해야하는 메시의 수가 급감하게 된다.
자동으로 생성되는 오브젝트의 대부분이 바닥 면이기 때문에 이 경우 많은 성능의 상승이 있을 것이다.
사용한 코드는 다음과 같다.
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
'개발 > Unity 내일배움캠프 TIL' 카테고리의 다른 글
Unity Universal Render Pipeline (URP) (0) | 2023.10.04 |
---|---|
Unity3D 프로빌더 사용해보기 (0) | 2023.10.02 |
Unity 3D 타일맵 자동 생성 (0) | 2023.09.26 |
Unity 3D 타일맵 만들기 (0) | 2023.09.25 |
Unity 어트리뷰트 (Attribute) (0) | 2023.09.22 |