Always growing
Make yourself at home
← 모든 기록 / CS C#
C#
C#
C#
구현 영상
2765 words
14 minutes
[Algorithm] BSP 알고리즘(맵 생성) 유니티로 구현하기!(2D)
BSP algorithm with Unity, 자연스러운 스프라이트 배치로 멋있게 만들어보자!
여기 잠깐!!기본적인 알고리즘을 이해하는데 있어서 여기!를 참고해주세요!!
BSP 알고리즘 구현으로 해커톤 날먹이 된다고!?!
사실 이건 장난이고, 그냥 있어보이게 구현할 수 있도록 구조를 다 만들어둬서 그냥 가져다 쓰시면 됩니다.
물론 이 코드가 체계적으로 작성된 코드는 아니지만! 그래도 쓸만한 애들이라고 생각합니다(최적화는 안되어있는 편 ㅎㅎ)
일단 활용하기 편하도록 Scriptable Object로 다 설정하도록 만들어 뒀습니다.
Scriptable Object 주의이 Scriptable Object를 인게임 상에서 값을 수정하게 되면, editor 환경에서는 작동을 하나.. 나중에 build 이후로 가게되면 유니티에서 이제 최적화 과정을 거치면서 필요없는 prefabs 등 다른 리소스들을 다 없애는데 이때 Scriptable Object의 값이 변경되면 터집니다!
조심하십쇼!
기본적인 세팅
일단 저는 부가적으로 Layer를 사용합니다. 참고하실 분은 참고해주세용
1. 벽 세팅
기본적인 벽을 담을 스프라이트(2D Rect 정도?)를 기반으로 방향, 모서리 등의 위치에 배치할 스프라이트들을 넣으면 됩니다.
[CreateAssetMenu(fileName = "WallTileSettings", menuName = "Compy/Wall Tile Settings")]
public class WallTileSettings : ScriptableObject
{
[System.Serializable]
public class WallSprites
{
[Header("기본 벽 스프라이트")]
[Tooltip("주변에 아무 연결이 없는 기본적인 벽 스프라이트")]
public Sprite defaultWall;
[Header("벽 테두리")]
[Tooltip("아래쪽으로 연결된 상단 벽면")] // │ 의 윗부분
public Sprite wallTop;
[Tooltip("위쪽으로 연결된 하단 벽면")] // │ 의 아랫부분
public Sprite wallBottom;
[Tooltip("오른쪽으로 연결된 왼쪽 벽면")] // ─ 의 왼쪽
public Sprite wallLeft;
[Tooltip("왼쪽으로 연결된 오른쪽 벽면")] // ─ 의 오른쪽
public Sprite wallRight;
[Header("벽 모서리 - 외부")]
[Tooltip("오른쪽과 아래로 연결된 외부 모서리")] // ┌ 모양
public Sprite cornerOuterTopLeft;
[Tooltip("왼쪽과 아래로 연결된 외부 모서리")] // ┐ 모양
public Sprite cornerOuterTopRight;
[Tooltip("오른쪽과 위로 연결된 외부 모서리")] // └ 모양
public Sprite cornerOuterBottomLeft;
[Tooltip("왼쪽과 위로 연결된 외부 모서리")] // ┘ 모양
public Sprite cornerOuterBottomRight;
[Header("벽 모서리 - 내부")]
[Tooltip("위쪽과 왼쪽이 벽인 내부 모서리")] // ┘ 모양이지만 안쪽으로
public Sprite cornerInnerTopLeft;
[Tooltip("위쪽과 오른쪽이 벽인 내부 모서리")] // └ 모양이지만 안쪽으로
public Sprite cornerInnerTopRight;
[Tooltip("아래쪽과 왼쪽이 벽인 내부 모서리")] // ┐ 모양이지만 안쪽으로
public Sprite cornerInnerBottomLeft;
[Tooltip("아래쪽과 오른쪽이 벽인 내부 모서리")] // ┌ 모양이지만 안쪽으로
public Sprite cornerInnerBottomRight;
[Header("단독 벽")]
[Tooltip("주변에 어떤 연결도 없는 단독으로 서있는 벽")]
public Sprite singleWall;
[Header("끝부분")]
[Tooltip("위쪽 방향으로 돌출된 벽의 끝부분 (아래쪽만 연결)")] // ╽ 모양
public Sprite endTop;
[Tooltip("아래쪽 방향으로 돌출된 벽의 끝부분 (위쪽만 연결)")] // ╿ 모양
public Sprite endBottom;
[Tooltip("왼쪽 방향으로 돌출된 벽의 끝부분 (오른쪽만 연결)")] // ╾ 모양
public Sprite endLeft;
[Tooltip("오른쪽 방향으로 돌출된 벽의 끝부분 (왼쪽만 연결)")] // ╼ 모양
public Sprite endRight;
}
[Header("벽 스프라이트 설정")]
public WallSprites sprites;
[Header("벽 렌더링 설정")]
public string sortingLayerName = "Wall";
public int sortingOrder = 0;
}Bsp Generation 세팅
bsp로 맵 생성을 할 때 사용될 변수들
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "MapSettings", menuName = "Compy/Generator Settings")]
public class GeneratorSetting : ScriptableObject
{
[Header("Map Size")]
[Tooltip("전체 너비")]
public int mapWidth = 100;
[Tooltip("전체 높이")]
public int mapHeight = 100;
[Header("Room Generation")]
[Tooltip("방의 최소 크기")]
[Min(5)]
public int minRoomSize = 10;
[Tooltip("BSP 분할 최대 횟수")]
[Range(1, 10)]
public int maxIterations = 4;
[Header("Room Constraints")]
[Tooltip("방 사이의 최소 거리")]
[Min(1)]
public int minRoomDistance = 2;
[Tooltip("방의 최대 크기 (전체 공간 대비 비율)")]
[Range(0.3f, 0.8f)]
public float maxRoomSizeRatio = 0.6f;
[Header("Tile Settings")]
[Tooltip("벽 타일 설정")]
public WallTileSettings wallTileSettings;
[Tooltip("기본 타일 프리팹 (스프라이트 렌더러와 콜라이더가 있는 빈 게임오브젝트)")]
public GameObject tilePrefab;
[Header("Prefabs")]
[Tooltip("바닥 타일 프리팹")]
public GameObject floorPrefab;
[Header("Layer Settings")]
[Tooltip("벽 레이어의 이름")]
public string wallLayerName = "Wall";
[Header("Advanced Settings")]
[Tooltip("통로 너비")]
[Range(1, 5)]
public int corridorWidth = 3;
[Tooltip("방 생성 시도 최대 횟수")]
[Min(1)]
public int maxRoomPlacementAttempts = 10;
[Tooltip("추가 통로 생성 확률 (0-1)")]
[Range(0f, 1f)]
public float extraCorridorChance = 0.2f;
[Header("Structures")]
[Tooltip("맵에 배치할 구조물들의 정의")]
public List<CompyStructure> structures = new List<CompyStructure>();
[Tooltip("구조물 배치 최대 시도 횟수")]
[Min(1)]
public int maxPlacementAttempts = 100;
public void ValidateSettings()
{
mapWidth = Mathf.Max(mapWidth, minRoomSize * 2);
mapHeight = Mathf.Max(mapHeight, minRoomSize * 2);
minRoomSize = Mathf.Min(minRoomSize, Mathf.Min(mapWidth, mapHeight) / 2);
corridorWidth = Mathf.Min(corridorWidth, minRoomSize - 2);
minRoomDistance = Mathf.Max(minRoomDistance, corridorWidth);
ValidateStructures();
}
private void ValidateStructures()
{
if (structures == null) return;
foreach (var structure in structures)
{
if (structure == null) continue;
structure.width = Mathf.Min(structure.width, mapWidth - 4);
structure.height = Mathf.Min(structure.height, mapHeight - 4);
structure.minCount = Mathf.Min(structure.minCount, structure.maxCount);
if (structure.padding > 0)
structure.padding = Mathf.Min(structure.padding,
Mathf.Min(structure.width, structure.height) / 2);
}
}
public GeneratorSetting Clone()
{
var clone = CreateInstance<GeneratorSetting>();
clone.mapWidth = this.mapWidth;
clone.mapHeight = this.mapHeight;
clone.minRoomSize = this.minRoomSize;
clone.maxIterations = this.maxIterations;
clone.minRoomDistance = this.minRoomDistance;
clone.maxRoomSizeRatio = this.maxRoomSizeRatio;
clone.wallTileSettings = this.wallTileSettings;
clone.tilePrefab = this.tilePrefab;
clone.floorPrefab = this.floorPrefab;
clone.wallLayerName = this.wallLayerName;
clone.corridorWidth = this.corridorWidth;
clone.maxRoomPlacementAttempts = this.maxRoomPlacementAttempts;
clone.extraCorridorChance = this.extraCorridorChance;
clone.structures = new List<CompyStructure>(this.structures);
clone.maxPlacementAttempts = this.maxPlacementAttempts;
return clone;
}
#if UNITY_EDITOR
// 내가 한번 당하지 두번은 안 당할거임 ㄹㅇ
private void OnValidate()
{
ValidateSettings();
}
#endif
}BSP Algorithm
BSP Room
public class BSPRoom
{
public int x, y, width, height;
public BSPRoom(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
}BSP Generator
기본적인 생성만 가볍게 만들어본 버전입니다.
public class BSPGenerator : MonoBehaviour{
private enum WallType {
Single,
Top,
Bottom,
Left,
Right,
CornerOuterTopLeft,
CornerOuterTopRight,
CornerOuterBottomLeft,
CornerOuterBottomRight,
CornerInnerTopLeft,
CornerInnerTopRight,
CornerInnerBottomLeft,
CornerInnerBottomRight,
EndTop,
EndBottom,
EndLeft,
EndRight,
Default
}
private int mapWidth = 100;
private int mapHeight = 100;
private int minRoomSize = 10;
private int maxIterations = 4;
private string wallLayerName = "Wall";
private GameObject wallPrefab;
private GameObject floorPrefab;
private List<BSPRoom> rooms = new List<BSPRoom>();
private int[,] map;
private int wallLayerId;
private GeneratorSetting setting;
private const bool debug = false;
private bool generateDone;
public void GeneratorInit(GeneratorSetting setting)
{
generateDone = false;
InitWithSettings(setting);
wallLayerId = LayerMask.NameToLayer(wallLayerName);
if (wallLayerId == -1) {
Debug.LogError($"Layer '{wallLayerName}' not found! 이거 추가좀 이거 왜 안고쳐짐?");
wallLayerId = 0;
}
this.setting = setting;
}
private void InitWithSettings(GeneratorSetting setting) {
mapWidth = setting.mapWidth;
mapHeight = setting.mapHeight;
minRoomSize = setting.minRoomSize;
maxIterations = setting.maxIterations;
wallLayerName = setting.wallLayerName;
floorPrefab = setting.floorPrefab;
}
public void GenerateMap() {
map = new int[mapWidth, mapHeight];
for (int x = 0; x < mapWidth; x++) {
for (int y = 0; y < mapHeight; y++)
map[x, y] = 1;
}
SplitSpace(0, 0, mapWidth, mapHeight, 0);
ConnectRooms();
InstantiateTiles();
generateDone = true;
}
private void SplitSpace(int x, int y, int width, int height, int iteration)
{
if (iteration >= maxIterations || width < minRoomSize * 2 || height < minRoomSize * 2) {
int roomWidth = Random.Range(minRoomSize, width - 4);
int roomHeight = Random.Range(minRoomSize, height - 4);
int roomX = x + Random.Range(2, width - roomWidth - 2);
int roomY = y + Random.Range(2, height - roomHeight - 2);
CreateRoom(roomX, roomY, roomWidth, roomHeight);
rooms.Add(new BSPRoom(roomX, roomY, roomWidth, roomHeight));
return;
}
bool splitHorizontal = Random.Range(0f, 1f) > 0.5f;
if (width > height && width / height >= 1.25f) splitHorizontal = false;
else if (height > width && height / width >= 1.25f) splitHorizontal = true;
if (splitHorizontal) {
int splitAt = Random.Range(minRoomSize, height - minRoomSize);
SplitSpace(x, y, width, splitAt, iteration + 1);
SplitSpace(x, y + splitAt, width, height - splitAt, iteration + 1);
}
else {
int splitAt = Random.Range(minRoomSize, width - minRoomSize);
SplitSpace(x, y, splitAt, height, iteration + 1);
SplitSpace(x + splitAt, y, width - splitAt, height, iteration + 1);
}
}
private void CreateRoom(int x, int y, int width, int height)
{
for (int i = x; i < x + width; i++) {
for (int j = y; j < y + height; j++)
if (i >= 0 && i < mapWidth && j >= 0 && j < mapHeight) map[i, j] = 0;
}
}
private void ConnectRooms() {
for (int i = 0; i < rooms.Count - 1; i++) {
BSPRoom roomA = rooms[i];
BSPRoom roomB = rooms[i + 1];
Vector2Int pointA = new Vector2Int(
roomA.x + roomA.width / 2,
roomA.y + roomA.height / 2
);
Vector2Int pointB = new Vector2Int(
roomB.x + roomB.width / 2,
roomB.y + roomB.height / 2
);
CreateCorridor(pointA.x, pointA.y, pointB.x, pointA.y);
CreateCorridor(pointB.x, pointA.y, pointB.x, pointB.y);
}
}
private void CreateCorridor(int startX, int startY, int endX, int endY) {
int x = startX;
int y = startY;
while (x != endX || y != endY)
{
if (x < endX) x++;
else if (x > endX) x--;
if (y < endY) y++;
else if (y > endY) y--;
if (x >= 0 && x < mapWidth && y >= 0 && y < mapHeight) {
map[x, y] = 0;
for (int i = -1; i <= 1; i++)
{
for (int j = -1; j <= 1; j++)
{
int nx = x + i;
int ny = y + j;
if (nx >= 0 && nx < mapWidth && ny >= 0 && ny < mapHeight)
map[nx, ny] = 0;
}
}
}
}
}
private void InstantiateTiles()
{
GameObject wallParent = new GameObject("Walls");
GameObject floorParent = new GameObject("Floors");
wallParent.transform.parent = transform;
floorParent.transform.parent = transform;
wallParent.layer = LayerMask.NameToLayer("Wall");
floorParent.layer = LayerMask.NameToLayer("Floor");
for (int x = 0; x < mapWidth; x++)
{
for (int y = 0; y < mapHeight; y++)
{
Vector3 position = new Vector3(x, y, 0);
if (map[x, y] == 1)
{
GameObject wall = Instantiate(setting.tilePrefab, position, Quaternion.identity, wallParent.transform);
wall.layer = LayerMask.NameToLayer("Wall");
SpriteRenderer wallRenderer = wall.GetComponent<SpriteRenderer>();
if (wallRenderer != null)
{
WallType wallType = DetermineWallType(x, y);
ApplyWallSprite(wallRenderer, wallType);
wallRenderer.sortingLayerName = setting.wallTileSettings.sortingLayerName;
wallRenderer.sortingOrder = setting.wallTileSettings.sortingOrder;
}
Collider2D collider = wall.GetComponent<Collider2D>();
if (collider != null)
collider.isTrigger = false;
}
else
{
GameObject floor = Instantiate(setting.floorPrefab, position, Quaternion.identity, floorParent.transform);
floor.layer = LayerMask.NameToLayer("Floor");
SpriteRenderer floorRenderer = floor.GetComponent<SpriteRenderer>();
if (floorRenderer != null)
{
floorRenderer.sortingLayerName = "Floor";
floorRenderer.sortingOrder = 0;
}
}
}
}
}
private WallType DetermineWallType(int x, int y)
{
bool up = IsWall(x, y + 1);
bool down = IsWall(x, y - 1);
bool left = IsWall(x - 1, y);
bool right = IsWall(x + 1, y);
// 대각선 방향도 확인
bool upLeft = IsWall(x - 1, y + 1);
bool upRight = IsWall(x + 1, y + 1);
bool downLeft = IsWall(x - 1, y - 1);
bool downRight = IsWall(x + 1, y - 1);
// 단독 벽
if (!up && !down && !left && !right)
return WallType.Single;
// 내부 모서리 검사 (벽이 안쪽으로 꺾이는 경우)
if (up && right && !upRight) return WallType.CornerInnerTopRight;
if (up && left && !upLeft) return WallType.CornerInnerTopLeft;
if (down && right && !downRight) return WallType.CornerInnerBottomRight;
if (down && left && !downLeft) return WallType.CornerInnerBottomLeft;
// 외부 모서리 검사 (벽이 바깥쪽으로 꺾이는 경우)
if (!up && !left && right && down) return WallType.CornerOuterTopLeft;
if (!up && !right && left && down) return WallType.CornerOuterTopRight;
if (!down && !left && right && up) return WallType.CornerOuterBottomLeft;
if (!down && !right && left && up) return WallType.CornerOuterBottomRight;
// 끝부분 검사
if (up && !down && !left && !right) return WallType.EndBottom;
if (!up && down && !left && !right) return WallType.EndTop;
if (!up && !down && left && !right) return WallType.EndRight;
if (!up && !down && !left && right) return WallType.EndLeft;
// 테두리 검사
if (!up && down) return WallType.Top;
if (up && !down) return WallType.Bottom;
if (!left && right) return WallType.Left;
if (left && !right) return WallType.Right;
return WallType.Default;
}
private void ApplyWallSprite(SpriteRenderer renderer, WallType type)
{
var sprites = setting.wallTileSettings.sprites;
switch (type)
{
case WallType.Single:
renderer.sprite = sprites.singleWall;
break;
case WallType.Top:
renderer.sprite = sprites.wallTop;
break;
case WallType.Bottom:
renderer.sprite = sprites.wallBottom;
break;
case WallType.Left:
renderer.sprite = sprites.wallLeft;
break;
case WallType.Right:
renderer.sprite = sprites.wallRight;
break;
case WallType.CornerOuterTopLeft:
renderer.sprite = sprites.cornerOuterTopLeft;
break;
case WallType.CornerOuterTopRight:
renderer.sprite = sprites.cornerOuterTopRight;
break;
case WallType.CornerOuterBottomLeft:
renderer.sprite = sprites.cornerOuterBottomLeft;
break;
case WallType.CornerOuterBottomRight:
renderer.sprite = sprites.cornerOuterBottomRight;
break;
case WallType.CornerInnerTopLeft:
renderer.sprite = sprites.cornerInnerTopLeft;
break;
case WallType.CornerInnerTopRight:
renderer.sprite = sprites.cornerInnerTopRight;
break;
case WallType.CornerInnerBottomLeft:
renderer.sprite = sprites.cornerInnerBottomLeft;
break;
case WallType.CornerInnerBottomRight:
renderer.sprite = sprites.cornerInnerBottomRight;
break;
case WallType.EndTop:
renderer.sprite = sprites.endTop;
break;
case WallType.EndBottom:
renderer.sprite = sprites.endBottom;
break;
case WallType.EndLeft:
renderer.sprite = sprites.endLeft;
break;
case WallType.EndRight:
renderer.sprite = sprites.endRight;
break;
default:
renderer.sprite = sprites.defaultWall;
break;
}
}
private bool IsWall(int x, int y)
{
if (x < 0 || x >= mapWidth || y < 0 || y >= mapHeight)
return false;
return map[x, y] == 1;
}
public bool isDone()
{
return generateDone;
}
public BSPRoom GetRoomFromPosition(Vector2 position)
{
if (!generateDone)
{
Debug.LogWarning("Map generation is not complete. Cannot find room.");
return null;
}
return rooms.FirstOrDefault(room =>
position.x >= room.x && position.x < room.x + room.width &&
position.y >= room.y && position.y < room.y + room.height);
}
public BSPRoom GetRoomFromTransform(Transform transform)
{
if (transform == null) return null;
return GetRoomFromPosition(new Vector2(transform.position.x, transform.position.y));
}
private void OnDrawGizmos()
{
if (!debug) return;
if (map == null) return;
for (int x = 0; x < mapWidth; x++) {
for (int y = 0; y < mapHeight; y++) {
if (map[x, y] == 1)
Gizmos.color = Color.black;
else
Gizmos.color = Color.white;
Vector3 pos = new Vector3(x, y, 0);
Gizmos.DrawCube(pos, Vector3.one);
}
}
}
}여기까지 기본적인 맵 생성이였구. 다음 포스트에서 아이템 배치와 다른 지형 배치 등을 다루는 코드를 이어서 작성하도록 하겠습니다.
수고하셨습니다.
Reference
[Algorithm] BSP 알고리즘(맵 생성) 유니티로 구현하기!(2D)
https://compy07.github.io/Blog/posts/algorithms/bsp/unity/ Comments
© 2024 뒹굴뒹굴 이정훈 공부방. All Rights Reserved.
Powered by Fuwari
Powered by Fuwari