
Where Am I?생각보다 구현해야하는 기능이 너무 많아서 걱정이 되긴 하지만 조금씩 진행이 되는 것 같아서 다행이다.
이번 포스트에 작성할 스크립트는 이번에 미로 자동 생성에 사용한 알고리즘이다. 기본이 되는 알고리즘은 DFS와 Binary Tree이다.
MapDatapublic class MapData : MonoBehaviour
{
[Header("InputNumber")]
// 맵 크기 설정
[SerializeField] private int _mapSize;
public int MapSize => _mapSize;
// 안전 구역 크기 설정
[SerializeField] private int _safeZoneSize;
private int _safeZoneStart;
private int _safeZoneEnd;
// 맵 데이터를 담을 프로퍼티
private int[,] _map;
public int[,] Map => _map;
private void Awake()
{
Initialize();
}
// 맵 데이터 초기화
private void Initialize()
{
_safeZoneStart = (_mapSize - _safeZoneSize) / 2 + 1;
_safeZoneEnd = (_mapSize + _safeZoneSize) / 2 - 1;
// 맵 크기 데이터 입력
_map = new int[_mapSize, _mapSize];
// 맵의 모든 구역을 벽으로 설정
for (int x = 0; x < _mapSize; x++)
{
for (int y = 0; y < _mapSize; y++)
{
_map[x, y] = 1;
}
}
// 안전 구역 데이터 입력
for (int x = _safeZoneStart; x < _safeZoneEnd; x++)
{
for (int y = _safeZoneStart; y < _safeZoneEnd; y++)
{
_map[x, y] = 0;
}
}
_map[1, 1] = 0;
// 랜덤 미로 생성 메서드
GenerateMaze(1,1);
// 안전 구역에서 미로로 나가는 4개 방향의 문 설정
_map[_safeZoneStart - 1, _mapSize / 2] = 0;
_map[_safeZoneEnd, _mapSize / 2] = 0;
_map[_mapSize / 2, _safeZoneStart - 1] = 0;
_map[_mapSize / 2, _safeZoneEnd] = 0;
}
// 랜덤 방향 설정 메서드
private void GetRndDirection(List<Vector2Int> dirList)
{
for (int i = 0; i < dirList.Count; i++)
{
Vector2Int tempDir = dirList[i];
int rndIndex = Random.Range(i, dirList.Count);
dirList[i] = dirList[rndIndex];
dirList[rndIndex] = tempDir;
}
}
// 미로 생성 메서드
private void GenerateMaze(int x, int y)
{
List<Vector2Int> directions = new List<Vector2Int>
{
Vector2Int.up,
Vector2Int.down,
Vector2Int.right,
Vector2Int.left,
};
// 미로에 랜덤성 부여
GetRndDirection(directions);
foreach (Vector2Int direction in directions)
{
// 시작한 정점에서 랜덤 방향으로 한 칸 건너 뛴 정점 탐색(Binary Tree)
int nextX = x + direction.x * 2;
int nextY = y + direction.y * 2;
// 만약 해당 정점이 맵 영역을 벗어난 경우
if (nextX < 0 || nextX >= _mapSize - 1 || nextY < 0 || nextY >= _mapSize - 1)
{
continue; // 스킵
}
// 만약 해당 정점이 안전 구역 내부인 경우
if (nextX <= _safeZoneEnd && nextX >= _safeZoneStart
&& nextY <= _safeZoneEnd && nextY >= _safeZoneStart)
{
continue; // 스킵
}
// 만약 해당 정점이 벽인 경우
if (_map[nextX, nextY] == 1)
{
// 해당 정점을 통로로 바꾼다.
_map[nextX, nextY] = 0;
// 해당 정점과 기존 정점 사이 칸도 통로로 바꾼다.
_map[x + direction.x, y + direction.y] = 0;
// 해당 정점에서 다시 미로 생성(DFS)
GenerateMaze(nextX, nextY);
}
}
}
}
MapGenerator생성된 맵 데이터를 바탕으로 인게임에 벽과 통로로 이루어진 미로를 실제로 구현하는 클래스로, 간단히 벽인 곳에 벽 프리팹을 생성하고 통로인 곳에 통로 프리팹을 생성한다.
public class MapGenerator : MonoBehaviour
{
[Header("Drag&Drop")]
[SerializeField] private MapData_T _data;
[SerializeField] private GameObject _wallPrefab;
[SerializeField] private GameObject _tilePrefab;
private int[,] _map;
private int _mapSize;
private void Start()
{
Init();
GenerateMap();
}
private void GenerateMap()
{
for (int z = 0; z < _mapSize; z++)
{
for (int x = 0; x < _mapSize; x++)
{
Vector3 pos = new Vector3(x, 0, z);
if (_map[z, x] == 1)
{
Instantiate(_wallPrefab, pos, Quaternion.identity,transform);
}
else
{
Instantiate(_tilePrefab, pos, Quaternion.identity,transform);
}
}
}
}
private void Init()
{
_map = _data.Map;
_mapSize = _data.MapSize;
}
}
위처럼 구현했을 때 생기는 문제는 통로 간격이 1m로 고정된다는 것이다. 따라서 플레이어의 크기에 따라 통로에 낄수도 있고, 무엇보다 시야가 굉장히 답답해진다. 따라서 이를 해결하기 위해서는 offset을 통로 간격으로 설정해서 보정해줄 필요가 있다.
아래는 offset을 보정하여 작성한 실제 스크립트이다.
public class MapData_T : MonoBehaviour
{
[Header("InputNumber")]
[SerializeField] private int _mapSize;
public int MapSize => _mapSize;
[SerializeField] private int _safeZoneSize;
[SerializeField] private int _offset;
public int Offset => _offset;
private int _safeZoneStart;
private int _safeZoneEnd;
private int[,] _map;
public int[,] Map => _map;
private void Awake()
{
Initialize();
}
private void Initialize()
{
_safeZoneStart = (_mapSize - _safeZoneSize) / 2 + 1;
_safeZoneEnd = (_mapSize + _safeZoneSize) / 2 - 1;
// 맵 데이터
_map = new int[_offset * _mapSize, _offset * _mapSize];
// 벽 데이터
for (int x = 0; x < _mapSize; x++)
{
for (int y = 0; y < _mapSize; y++)
{
_map[_offset * x, _offset * y] = 1;
}
}
// 안전 구역 데이터
for (int x = _safeZoneStart; x < _safeZoneEnd; x++)
{
for (int y = _safeZoneStart; y < _safeZoneEnd; y++)
{
_map[_offset * x, _offset * y] = 0;
}
}
_map[_offset, _offset] = 0;
GenerateMaze(_offset,_offset);
_map[_offset * (_safeZoneStart - 1), _offset * _mapSize / 2] = 0;
_map[_offset * _safeZoneEnd, _offset * _mapSize / 2] = 0;
_map[_offset * _mapSize / 2, _offset * (_safeZoneStart - 1)] = 0;
_map[_offset * _mapSize / 2, _offset * _safeZoneEnd] = 0;
}
private void GetRndDirection(List<Vector2Int> dirList)
{
for (int i = 0; i < dirList.Count; i++)
{
Vector2Int tempDir = dirList[i];
int rndIndex = Random.Range(i, dirList.Count);
dirList[i] = dirList[rndIndex];
dirList[rndIndex] = tempDir;
}
}
private void GenerateMaze(int x, int y)
{
List<Vector2Int> directions = new List<Vector2Int>
{
Vector2Int.up,
Vector2Int.down,
Vector2Int.right,
Vector2Int.left,
};
GetRndDirection(directions);
foreach (Vector2Int direction in directions)
{
int nextX = x + direction.x * 2 * _offset;
int nextY = y + direction.y * 2 * _offset;
if (nextX < 0 || nextX >= (_mapSize - 1) * _offset
|| nextY < 0 || nextY >= (_mapSize - 1) * _offset)
{
continue;
}
if (nextX <= _safeZoneEnd * _offset && nextX >= _safeZoneStart * _offset
&& nextY <= _safeZoneEnd * _offset && nextY >= _safeZoneStart * _offset)
{
continue;
}
if (_map[nextX, nextY] == 1)
{
_map[nextX, nextY] = 0;
_map[ x + _offset * direction.x, y + _offset * direction.y] = 0;
GenerateMaze(nextX, nextY);
}
}
}
}
public class MapGenerator : MonoBehaviour
{
[Header("Drag&Drop")]
[SerializeField] private MapData_T _data;
[SerializeField] private GameObject _wallPrefab;
[SerializeField] private GameObject _tilePrefab;
private int[,] _map;
private int _mapSize;
private int _offset;
private void Start()
{
Init();
GenerateMap();
}
private void GenerateMap()
{
for (int z = 0; z < _mapSize; z++)
{
for (int x = 0; x < _mapSize; x++)
{
Vector3 pos = new Vector3(_offset * x, 0, _offset * z);
if (_map[_offset * z, _offset * x] == 1)
{
Instantiate(_wallPrefab, pos, Quaternion.identity,transform);
}
else
{
Instantiate(_tilePrefab, pos, Quaternion.identity,transform);
}
}
}
}
private void Init()
{
_map = _data.Map;
_mapSize = _data.MapSize;
_offset = _data.Offset;
}
}
이렇게 작성하게 되면 offset의 값에 따라 통로의 간격을 조절할 수 있고, 중요한 것은 실제 prefab들의 scale을 offset값만큼 곱해주어야 한다.