그리드에 다가 타일 브러쉬로 타일을 칠하는 것 까진 알겠는데, 스크립트로 동적 생성은 어떻게 할까 궁금했었다.
다음 내용은 그리드가 있을 때 그리드 상에 오브젝트들을 어떻게 배치할 지, 더 나아가 맵을 랜덤으로 생성하고 배치하는 법에 대해 다룬다.
프로젝트 준비
애셋은 다음 링크의 애셋을 사용하였다.
또한 Tilemap을 위한 패키지를 설치해야 한다.
다음 링크를 참고하여 2D Tilemap Editor
를 설치하자.
그리드 상에 배치하기
남들이 좀 더 안하는 내용으로 하기 위해, 나는 육각형 그리드에 오브젝트를 배치하겠다.
제일 먼저 해야할 것은, 오브젝트의 Pivot 설정이다.
모든 오브젝트들의 Pivot을 균일하게 맞춰줘야 한다.
다음과 같이 Pivot의 위치가 바닥의 중앙에 있는지 확인하자.
Instantiate를 할 때 이 Pivot을 기준점으로 오브젝트가 생성된다.
TileMapGenerator라는 이름의 스크립트를 생성하여 다음과 같이 작성해주자.
sing UnityEngine;
public class TileMapGenerator : MonoBehaviour
{
[SerializeField] Grid grid;
[SerializeField] GameObject[] tilePrefabs;
void Start()
{
for (int x = 0; x < 32; x++)
{
Vector3 pos = grid.CellToWorld(Vector3Int.right * x);
GameObject newObject = Instantiate(tilePrefabs[0], pos, Quaternion.identity, gameObject.transform);
}
}
}
다음과 같이 구성하고 생성을 원하는 오브젝트를 넣은 뒤 실행해보자.
랜덤한 맵 데이터 생성하기
이제 랜덤하게 맵이 생성되게 하고 싶으면, 맵에 대한 데이터를 담은 2차원 배열을 생성해서 그 2차원 배열대로 Grid에 오브젝트를 생성해주기만 하면 되는 것이다.
일단 무작위 랜덤으로 맵을 생성해보자.
다음과 같이 코드를 작성했다.
TilemapGenerator.cs
using System;
using UnityEngine;
public class TileMapGenerator : MonoBehaviour
{
[SerializeField] Grid grid;
[SerializeField] Blocks[] 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;
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();
}
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 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));
Instantiate(tilePrefabs[MapBiomes[x, y]][MapHeights[x, y]], pos ,Quaternion.identity, gameObject.transform);
}
}
}
[Serializable]
public class Blocks
{
public GameObject normalBlock;
public GameObject highBlock;
public GameObject this[int i]
{
get { return (i == 0) ? normalBlock : highBlock; }
}
생성해보면 다음과 같이 지형이 생성되는 걸 볼 수 있는데, 이런 것은 너무 지저분해서 맵으로 쓰기가 힘들다….
우리는 어느정도는 깔끔하고, 블럭들이 어느정도 군집을 이루는 맵을 생성하고 싶다.
절차적 맵 생성 (Procedural Map Generation)
여러 방법이 있지만, 모두 절차적 맵 생성이라는 하나의 카테고리로 묶인다.
따라서 검색해볼 때는 절차적 맵 생성 또는 Procedural Map Generation으로 찾아보자.
대표적으로 다음 방법들이 있다.
- 펄린 노이즈
- 셀룰러 오토마타
- BSP 알고리즘
이번에는 셀룰러 오토마타를 써보고자 한다.
관련해서 참고하기 아주 좋은 영상이 있었기 때문이다.
다음과 같은 코드들을 추가해줬다.
먼저 셀룰러 오토마타를 기반으로 한 Smooth 함수이다.
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;
}
}
근처 타일 중 1인 타일의 개수를 세서 비율이 반반이면 현상유지, 나머지는 더 큰 쪽의 타일을 따라가는 것이다.
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;
}
근처 타일을 체크하는 건.. 그냥 열심히 돌렸다.
[SerializeField] private int smoothingFactor;
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);
}
}
그 뒤 이제 남은건 맵을 생성할 때 스무딩을 열심히 해주는 것이다.
여러번 할 수록 더욱더 군집을 형성하게 되는데, 다른 예제들도 그렇고 한 5번정도 하니깐 딱 적절한 모양인듯 하다.
나머지는 직접 수치를 조정해가면서 확인해보자.
다음은 전체 코드이다.
TileMapGenerator.cs
using System;
using UnityEngine;
public class TileMapGenerator : MonoBehaviour
{
[SerializeField] Grid grid;
[SerializeField] Blocks[] 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;
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));
Instantiate(tilePrefabs[MapBiomes[x, y]][MapHeights[x, y]], pos ,Quaternion.identity, gameObject.transform);
}
}
}
[Serializable]
public class Blocks
{
public GameObject normalBlock;
public GameObject highBlock;
public GameObject this[int i]
{
get { return (i == 0) ? normalBlock : highBlock; }
}
}
Uploaded by N2T
'개발 > Unity 내일배움캠프 TIL' 카테고리의 다른 글
Unity3D 프로빌더 사용해보기 (0) | 2023.10.02 |
---|---|
Unity 3D 타일맵 최적화 (0) | 2023.09.27 |
Unity 3D 타일맵 만들기 (0) | 2023.09.25 |
Unity 어트리뷰트 (Attribute) (0) | 2023.09.22 |
Unity2D 궁수의 전설 UI 따라만들기 (3) (0) | 2023.09.21 |