https://1217pgy.tistory.com/5 (출처)
- 이진분할알고리즘이란 정령된 리스트에서 특정한 KEY값을 찾는 알고리즘을 의미 하는데 여기서 BSP(Binary Space Partitioning)은 3D그래픽스와 게임엔진 많이 사용되는 공간 분할 알고리즘이다. 공간을 반복 분할하여 나누는 것을 의미한다.
- 유니티에선 보통 로그라이류 게임의 던전 생성에 많이 사용되는 기술! 잘 알아두면 써먹을 곳이 많을 것 같다.
- 개념보단 구현적으로 먼저 봐야한다.
구현
공간을 분할하는 로직(1번~2번과정)
private void DivideTree(TreeNode treeNode, int n) //재귀 함수
{
if (n < maxNode) //0 부터 시작해서 노드의 최댓값에 이를 때 까지 반복
{
RectInt size = treeNode.treeSize; //이전 트리의 범위 값 저장, 사각형의 범위를 담기 위해 Rect 사용
int length = size.width >= size.height ? size.width : size.height; //사각형의 가로와 세로 중 길이가 긴 축을, 트리를 반으로 나누는 기준선으로 사용
int split = Mathf.RoundToInt(Random.Range(length * minDivideSize, length * maxDivideSize)); //기준선 위에서 최소 범위와 최대 범위 사이의 값을 무작위로 선택
if (size.width >= size.height) //가로
{
treeNode.leftTree = new TreeNode(size.x, size.y, split, size.height); //기준선을 반으로 나눈 값인 split을 가로 길이로, 이전 트리의 height값을 세로 길이로 사용
treeNode.rightTree = new TreeNode(size.x + split, size.y, size.width - split, size.height); //x값에 split값을 더해 좌표 설정, 이전 트리의 width값에 split값을 빼 가로 길이 설정
OnDrawLine(new Vector2(size.x + split, size.y), new Vector2(size.x + split, size.y + size.height)); //기준선 렌더링
}
else //세로
{
treeNode.leftTree = new TreeNode(size.x, size.y, size.width, split);
treeNode.rightTree = new TreeNode(size.x, size.y + split, size.width, size.height - split);
OnDrawLine(new Vector2(size.x, size.y + split), new Vector2(size.x + size.width, size.y + split));
}
treeNode.leftTree.parentTree = treeNode; //분할한 트리의 부모 트리를 매개변수로 받은 트리로 할당
treeNode.rightTree.parentTree = treeNode;
DivideTree(treeNode.leftTree, n + 1); //재귀 함수, 자식 트리를 매개변수로 넘기고 노드 값 1 증가 시킴
DivideTree(treeNode.rightTree, n + 1);
}
}
분할된 공간에 방 생성
private RectInt GenerateDungeon(TreeNode treeNode, int n) //방 생성
{
if (n == maxNode) //노드가 최하위일 때만 조건문 실행
{
RectInt size = treeNode.treeSize;
int width = Mathf.Max(Random.Range(size.width / 2, size.width - 1)); //트리 범위 내에서 무작위 크기 선택, 최소 크기 : width / 2
int height = Mathf.Max(Random.Range(size.height / 2, size.height - 1));
int x = treeNode.treeSize.x + Random.Range(1, size.width - width); //최대 크기 : width / 2
int y = treeNode.treeSize.y + Random.Range(1, size.height - height);
OnDrawDungeon(x, y, width, height); //던전 렌더링
return new RectInt(x, y, width, height); //리턴 값은 던전의 크기로 길을 생성할 때 크기 정보로 활용
}
treeNode.leftTree.dungeonSize = GenerateDungeon(treeNode.leftTree, n + 1); //리턴 값 = 던전 크기
treeNode.rightTree.dungeonSize = GenerateDungeon(treeNode.rightTree, n + 1);
return treeNode.leftTree.dungeonSize; //부모 트리의 던전 크기는 자식 트리의 던전 크기 그대로 사용
}
길 연결하기
private void GenerateRoad(TreeNode treeNode, int n) //길 연결
{
if (n == maxNode) return; //노드가 최하위일 때는 길을 연결하지 않음, 최하위 노드는 자식 트리가 없기 때문
int x1 = GetCenterX(treeNode.leftTree.dungeonSize); //자식 트리의 던전 중앙 위치를 가져옴
int x2 = GetCenterX(treeNode.rightTree.dungeonSize);
int y1 = GetCenterY(treeNode.leftTree.dungeonSize);
int y2 = GetCenterY(treeNode.rightTree.dungeonSize);
for (int x = Mathf.Min(x1, x2); x <= Mathf.Max(x1, x2); x++) //x1과 x2중 값이 작은 곳부터 값이 큰 곳까지 타일 생성
tilemap.SetTile(new Vector3Int(x - mapSize.x / 2, y1 - mapSize.y / 2, 0), tile); //mapSize.x / 2를 빼는 이유는 화면 중앙에 맞추기 위함
for (int y = Mathf.Min(y1, y2); y <= Mathf.Max(y1, y2); y++)
tilemap.SetTile(new Vector3Int(x2 - mapSize.x / 2, y - mapSize.y / 2, 0), tile);
GenerateRoad(treeNode.leftTree, n + 1);
GenerateRoad(treeNode.rightTree, n + 1);
}
public void Generate(Define.Resources resources)
{
int idx = Random.Range(0, 4);
Transform selectPoisition = point[idx];
Vector3 Spawn = selectPoisition.position;
Spawn += SpwanPoint(selectPoisition.GetComponent<GenerateFloat>().radius);
foreach (GameObject pos in spawnPos)
{
if (Vector3.Distance(pos.transform.position, Spawn) <= 4f)
{
Generate(resources);
return;
}
}
GameObject go = null;
switch (resources)
{
case Define.Resources.Tree:
go = Tree;
break;
case Define.Resources.Rock:
go = Rock;
break;
case Define.Resources.NPC:
go = NPC;
break;
}
GameObject go1 = Managers.Resource.Instantiate(go);
go1.transform.position = Spawn;
spawnPos.Add(go1);
}
private Vector3 SpwanPoint(float radius)
{
Vector3 getPoint = Random.onUnitSphere;
getPoint.y = 0.0f;
float r = Random.Range(0.0f, radius);
return getPoint * r;
}