유니티/실습

바인딩오브아이작 만들기 - 그리드맵 생성 - 1

파란색까마귀 2022. 4. 19. 20:25

간단하게 만들 생각이었는데 막상 알고리즘을 짜보려고 하니까

첫 방부터 퍼져나가는 방식, 그리고 원하는 갯수만큼 제한하는 방식등이 예상외로 까다로웠다

 

다행이 구글링을 통해 강좌들을 찾아보니 유용하게 쓸만한 아이디어가 있어서 이를 응용해봤다

 

https://www.youtube.com/watch?v=qAf9axsyijY&ab_channel=Blackthornprod

 

딱 원하던 방식으로 임의 맵을 생성하는 알고리즘이었다

 

핵심은 간단하다

 

 

바로 방의 '방향'을 미리 정해두고 '이전' 방에 맞춰 '다음'방을 임의로 정해주면 그 '다음'방이 '다다음'방을 임의로 골라서 그 다다음방을 생성하면서 뻗어나가는 방식이다

 
 

 

특이한점은, 영상에서는 각 방은 '다음 방의 방향'을 정하는 점을 따로 만들고

해당 '문'이 다음 방을 생성하는 방식이다

 

위 예시 대로라면, 4방향의 '첫 방'이 생성되면 각각 보라색 마름모 포인트는 '방 생성을 시작'해서

아까의 9가지 (처음 방 빼고) 방 패턴에 맞춰 '연결되는 방'에서 골라서 생성해준다

 

예를들어 12시 방향에 있는 점은 '아래에 문이 있는 방들' 중에서 하나를 골라서 해당 위치에 생성하면

첫 방과 연결되는 두번째방이 생성되는 방식이다

 

위 알고리즘에 따라 방은 점점 뻗어나가게되는데

 

만약 생성하고자 하는 위치에 '이미 보라색 포인트가 있으면' 방을 생성하지 않는다

여기서 다시한번 특이한점이 각 보라색 마름모 포인트는 'rigidbody2D' 컴포넌트와 collider를 가지고있으며

유니티의 '충돌' 시스템을 활용해서 '보라색 포인트가 있는지' 확인을 하고 진행을 한다

 

 

하지만.. 위 알고리즘에 몇가지 오류가 있었다

먼저 위 스샷에서 표시한대로 '옆으로 가려는데 옆방의 벽이 막혀있다!' 라는 문제다

 

또 하나는 각각 오브젝트가 가지는 '맵 생성 포인트'에서 맵이 생성되기때문에

맵의 순서 정보나 좌표정보를 따로 관리할 수 없고

오로지 생성되기만 하는 문제가 발생한다

 

첫번째 문제의 경우엔 해당 유튜브 튜토리얼 #3 버전에서 따로 후속처리를 통해서 해결했지만

두번째 경우 해당 알고리즘을 기반으로 '실제 플레이블 맵'을 구현해야하는 목표와 조금 달라지게되서

 

여기서 해당 알고리즘을 응용해서 다른 방식으로 맵을 구현하기로 했다

 

핵심은 비슷하다

1. 각 맵은 자신의 타입과 생성포인트를 가진다

2. 맵의 타입은 따로 인스펙터상으로 지정되고 맵 생성 매니저 스크립트에서 관리한다

 

처음 할일은 여러가지 타입의 맵 프리펩을 미리 생성해줘야한다

 

스프라이트를 가진 오브젝트를 하나 만들어서 RoomSpawner 스크립트를 생성해서 붙여준다

 

public Vector2 m_currentPos = new Vector2(0, 0);

	public int m_roomType = -1;
	public bool m_isRommGenerateComplete;
	public GameObject[] m_spawnPoint;

	private RoomTemplate m_template;
	private RoomManager m_roomManger;

	private int m_rand;

	public int m_myTopIndex = -1;
	public int m_myBottomIndex = -1;
	public int m_myLeftIndex = -1;
	public int m_myRightIndex = -1;

 

 

각 맵의 담길 정보는 위와 같다

접근자에 대한건 아직 최적화가 덜되서.. 이후 수정예정....

 

먼저 중복 확인을 위한 '맵 상 위치'의 좌표값과

방의 타입을 구분해줄 변수와 맵생성을 완료했는지 확인하는 변수

 

그리고 아까의 '보라색 생성 포인트'인 방 생성할 방향과 위치를 담아둔 변수

생성할 방의 타입을 선택할수있도록 해둔 RoomTemplate와

생성된 맵을 관리할 RoomManger

 

마지막으로 '상하좌우에 맵이 존재하는지' 확인하기위한 변수다

(처음엔 index로 구분하기위해 int형으로 잡았는데, bool로 처리하는게 깔끔할것같다)

 

각 변수를 초기화해준다음 선택된 프리펩의 타입 인덱스를 지정해주고,

아까 스샷대로 '문이 있는 방향'에 있는 스폰포인트를 지정해준다

 
이런식으로 각 방에 대해서 모두 오브젝트 생성 후 스크립트도 붙여주고 안쪽 속성도 잡아주는 작업을 반복한다
 
 

그렇게 각 10가지 방에 대한 설정을 모두 잡아준다

(위에서부터 1,0,2,3,4,... 이렇게 잡아줬다. D가 0인 이유는 첫 기본방인 Default라서 그렇다)

 

이제 각 방의 타입을 인스펙터상에서 관리하기 위한 스크립트 "RoomTemplate"를 만들어서

"Template"라는 이름의 오브젝트에 붙여준다

 

사실 위 방식은 굳이 필요없다고 판단했다

어차피 각 방의 타입을 인덱스로 관리하니까 굳이 이런식으로 나눌필요 없다고 생각했는데

만들면서 생각해보니 인스펙터창에서 이런식으로 구분해두면 나중에 타입을 관리하거나 방을 추가할때

나름 한눈에 보기 좋은 방식인것같아서 그대로 사용하기로 했다

 
public class RoomTemplate : MonoBehaviour
{
	public GameObject[] m_bottomRooms;
	public GameObject[] m_topRooms;
	public GameObject[] m_leftRooms;
	public GameObject[] m_rightRoom;
}

 

 

코드는 단순히 각각 public으로 생성된 배열만 존재한다

각 변수에 아까 만들어둔 '방 프리펩'을 넣어주면되는데

 

여기서 말하는 '방향'은 '문이 있는 방향'에 해당한다는것만 주의하면 된다

즉, '아래, 왼쪽'에 문이 있는 방은 'BottomRooms'과 'LeftRooms' 배열에 둘다 들어가있어야 된다

 

이제 다시 'RoomSpawner' 스크립트로 돌아가자

 

	public void Start()
	{
		m_template = GameObject.Find("RoomTemplate").GetComponent<RoomTemplate>();
		m_roomManger = GameObject.Find("RoomManager").GetComponent<RoomManager>();

		if (m_roomManger.IsDuplicate(m_currentPos) == true)
			Destroy(gameObject);

		m_roomManger.RoomList().Add(this);

		Invoke("SpawnRoom", 0.1f);
		//SpawnRoom();
	}

 

먼저 RoomTemplate와 RoomManager를 먼저 찾아서 할당해준다

다른방식을 써도 되지만.. 임시로 쓰기엔 Find만한게 없다

(그리고 SendMessage도!)

이런식으로 정확한 구조를 짜두기보다.. 유연하게 만들어두고 정리하는방식을 선호한다

 

그리고 '방이 생성되었을때' 중복 확인 후 스스로를 삭제하는 코드도 넣어둔다

IsDuplicate() 함수는 나중에 설명하겠다

중복 확인을 하는 방법은 Vector2로 된 '맵 좌표'를 사용한다

 

그리고 생성이 성공하면 관리 스크립트RoomManager의 리스트에 넣어주고

'다음 방' 생성의 시작한다 (SpawnRoom)

여기서 Invoke함수를 사용하면 '방이 순서대로' 생성되는 효과를 낼수있다!

 

	void SpawnRoom()
	{
		m_isRommGenerateComplete = false;

		for (int i = 0; i < m_spawnPoint.Length; i++)
		{
			GameObject obj = null;
			Vector2 addPos = Vector2.zero;

			if (m_spawnPoint[i].GetComponent<RoomType>().roomType == 0)
			{//right
				if (m_roomManger.IsOverRoomCount())
					break;

				addPos = new Vector2(-1, 0) + this.m_currentPos;
				if (m_roomManger.IsDuplicate(addPos) == false)
				{
					if (IsDirectRoomCountUpper(addPos))
						break;

					m_rand = Random.Range(0, m_template.m_rightRoom.Length);
					obj = Instantiate(m_template.m_rightRoom[m_rand], m_spawnPoint[i].transform.position, Quaternion.identity);
				}
			}
			else if (m_spawnPoint[i].GetComponent<RoomType>().roomType == 1)
			{//left
				if (m_roomManger.IsOverRoomCount())
					break;

				addPos = new Vector2(1, 0) + this.m_currentPos;
				if (m_roomManger.IsDuplicate(addPos) == false)
				{
					if (IsDirectRoomCountUpper(addPos))
						break;

					m_rand = Random.Range(0, m_template.m_leftRooms.Length);
					obj = Instantiate(m_template.m_leftRooms[m_rand], m_spawnPoint[i].transform.position, Quaternion.identity);
				}
			}
			else if (m_spawnPoint[i].GetComponent<RoomType>().roomType == 2)
			{//bottom
				if (m_roomManger.IsOverRoomCount())
					break;

				addPos = new Vector2(0, -1) + this.m_currentPos;
				if (m_roomManger.IsDuplicate(addPos) == false)
				{
					if (IsDirectRoomCountUpper(addPos))
						break;

					m_rand = Random.Range(0, m_template.m_bottomRooms.Length);
					obj = Instantiate(m_template.m_bottomRooms[m_rand], m_spawnPoint[i].transform.position, Quaternion.identity);
				}
			}
			else if (m_spawnPoint[i].GetComponent<RoomType>().roomType == 3)
			{//top(위쪽방향인것들. 아래에 생김)
				if (m_roomManger.IsOverRoomCount())
					break;

				addPos = new Vector2(0, 1) + this.m_currentPos;
				if (m_roomManger.IsDuplicate(addPos) == false)
				{
					if (IsDirectRoomCountUpper(addPos))
						break;

					m_rand = Random.Range(0, m_template.m_topRooms.Length);

					obj = Instantiate(m_template.m_topRooms[m_rand], m_spawnPoint[i].transform.position, Quaternion.identity);
				}
			}

			if (obj != null)
			{ 
				obj.transform.SetParent(m_roomManger.transform);
				obj.GetComponent<RoomSpawner>().m_currentPos = addPos;
			}
		}

		m_isRommGenerateComplete = true;
	}

 

 

생성함수는 조금 반복적이다

함수를 하나로 뭉쳐보려다가.. 이렇게 펼처두는게 추후 관리하기도 편하고 수정도 용이하기에 일단 그대로 놔뒀다

 

먼저 생성 시작 전, '다음 방'의 생성완료를 확인할 수 있는 변수를 false로 해주고

for문을 통해 미리 선언해뒀던 변수 중 '다음 방 생성 위치'를 모두 확인한다

 

 

즉, 위 스샷처럼 다음 방을 생성되어야 할 포인트가 4개가 있으면 각 포인트를 전부 검사하고

각 포인트의 맞는 '방향'을 가진 방을 찾고, 중복검사를 한 뒤 모든 조건에 만족하면 생성하는 방식이다

 

위 코드를 분석해보자면

1. 각 '생성 포인트'의 방향을 확인하고 시작한다

2. 'RoomManager'를 통해 '방 생성 숫자'를 확인해서 추가로 방을 더 만들어야되는지 확인한다

3. '다음 방'의 좌표를 확인하고 (현재 좌표에서 상하좌우..x축 y축 기준으로 ±1)

각 좌표에 '이미 방이 있는지' 중복 확인을 한다

4. 방 생성 조건을 모두 확인한 다음

RoomTemplate에서 확인한 타입에 맞춘 프리펩을 생성해준다

 

이후 관리를 위해 생성된 프리펩의 부모를 잡아주고

각 프리펩의 '본인 좌표'를 아까 3번에서 확인한 좌표로 지정해준다

 

모든 작업이 끝나면 '나는 만들 수 있는 방은 다 만들었다' 라는 의미로 bool값을 갱신해준다

 

여기서 추가로 3번과 4번사이에 하나 더 확인해야할게..

새로 추가되는 조건

'주변 방이 2개 이상이면 생성하지 않는다' 라는 조건이다

해당 조건이 있으면 방이 뭉쳐서 생성되는걸 방지할 수 있는데,

디테일한 생성 조건이나 기타 수치값은 테스트를 위해 조정해할 예정이지만 일단 만들어뒀다

	private bool IsDirectRoomCountUpper(Vector2 pos)
	{
		int directRoomCount = 0;

		if (m_roomManger.FindIndex(pos + new Vector2(1,0)) != null)
		{
			directRoomCount++;
		}
		if (m_roomManger.FindIndex(pos + new Vector2(-1, 0)) != null)
		{
			directRoomCount++;
		}
		if (m_roomManger.FindIndex(pos + new Vector2(0, 1)) != null)
		{
			directRoomCount++;
		}
		if (m_roomManger.FindIndex(pos + new Vector2(0, -1)) != null)
		{
			directRoomCount++;
		}

		if (directRoomCount >= 2)
			return true;
		else
			return false;
	}

 

 

코드는 단순히 해당 좌표의 주변좌표값에 이미 방이 존재하는지 확인해서 갯수확인을 하는 방식이다

(쓰다보니 발견한건데 그냥 IsDuplicate 함수를 그대로 써도 됬을꺼같다..)

 

이런식으로 몇가지 조건을 넣어서 방 생성 알고리즘을 세부적으로 조절할 수 있다

 

여기까지만 해도 각 방들이 '알아서' 다음방을 고르고 확인해서 생성하기때문에 상관없지만

생성에 제약조건을 달지 않으면 무한으로 방을 만들기때문에 관리 스크립트를 추가해준다

 

public class RoomManager : MonoBehaviour
{

	private List<RoomSpawner> m_roomList = new List<RoomSpawner>();

	public FieldManager m_fieldManager;
	public GameObject m_fieldPrefabs;
	public GameObject m_startRoom;

	private int m_maxRoomCount = 14;

	public List<RoomSpawner> RoomList() { return m_roomList; }

	public bool IsDuplicate(Vector2 pos)
	{
		bool isDup = false;
		for (int i = 0; i < m_roomList.Count; i++)
		{
			if (m_roomList[i].m_currentPos == pos)
			{
				isDup = true;
			}
		}

		return isDup;
	}


	public RoomSpawner FindIndex(Vector2 pos)
	{
		RoomSpawner spawner = null;
		for (int i = 0; i < m_roomList.Count; i++)
		{
			if (m_roomList[i].m_currentPos == pos)
			{
				spawner = m_roomList[i];
			}
		}

		return spawner;
	}

	public bool RoomGenerateComplete()
	{
		int completeCount = 0;
		if (m_roomList.Count <= 1)
			return false;

		for (int i = 0; i < m_roomList.Count; i++)
		{
			if (m_roomList[i].m_isRommGenerateComplete == true)
				completeCount++;
		}

		if (completeCount == m_roomList.Count)
			return true;
		else
			return false;
	}

	public bool IsOverRoomCount()
	{
		if (m_roomList.Count >= m_maxRoomCount)
			return true;
		else
			return false;
	}

 

RoomManager 스크립트다

실제 플레이어블 '필드' 생성도 하고 맵의 관리도 하는 스크립트다

먼저 설정된 field관련 변수는 다음 포스트에서 설명하기로 하고,

 

각각 '최대 방 갯수'와 '방' 리스트 등등을 만들어주고

중복 확인 함수인 IsDuplicate()와 좌표값을 통해 원하는 '방'을 찾을 수 있는 FindIndex()

그리고 모든 맵이 '생성 완료'했는지 확인할 수 있는 RoomGenerateComplete()함수와

생성된 방이 최대 갯수에 도달했는지 확인할 수 잇는 IsOverRoomCount()함수도 만들어준다

 

전부 RoomSpawner.cs에서 사용했던 함수들이다

 

public void Update()
	{
		if (RoomGenerateComplete())
		{
			//Debug.Log("DONE" + m_roomList.Count);
			if (m_roomList.Count < m_maxRoomCount)
			{
				//Debug.Log("RESET");
				int count = this.transform.childCount;
				for (int i = 1; i < count; i++)
				{
					Destroy(this.transform.GetChild(i).gameObject);
				}

				m_roomList.Clear();
				m_roomList = new List<RoomSpawner>();
				m_startRoom.GetComponent<RoomSpawner>().Start();
			}
			else
			{
				if(m_complete == false)
					SetRoomConnection();
			}
		}
	}

 

그리고 update()함수를 통해 '모든 방 생성이 완료됬는지' 확인해서

모든 방이 완료되었을때 방 갯수가 원하는 갯수(최대 값)에 만족하는지 확인해서

만족하지 않을 경우 모두 파괴하고 처음 방부터 새로 시작한다..!

 

방이 생성되다보면 생각보다 빠르게 방이 막히는 경우도 있고 예상보다 더 많은 방이 생성되는 경우도 있어서

이런식으로 모든 조건을 만족하는 맵이 나올때까지 반복하는 방법이다

 

근데 이 방법은.. 방 갯수가 많아질 경우 모든 조건에 맞는 맵 생성에만 한참 걸리다보니

더 빠르게 확인할 수 있는 다른 방법을 찾는중이다...

 

(m_compelete함수는 '필드 생성'을 확인하는 함수다 현재 포스팅에서는 빼도된다)

 

private void SetRoomConnection()
	{
		for (int i = 0; i < m_roomList.Count; i++)
		{
			if (FindIndex(m_roomList[i].m_currentPos + new Vector2(1,0)) != null)
			{
				m_roomList[i].m_myRightIndex = 0;
			}
			if (FindIndex(m_roomList[i].m_currentPos + new Vector2(-1, 0)) != null)
			{
				m_roomList[i].m_myLeftIndex = 0;
			}
			if (FindIndex(m_roomList[i].m_currentPos + new Vector2(0, 1)) != null)
			{
				m_roomList[i].m_myBottomIndex = 0;
			}
			if (FindIndex(m_roomList[i].m_currentPos + new Vector2(0, -1)) != null)
			{
				m_roomList[i].m_myTopIndex = 0;
			}
		}
	}

 

그리고 모든 방 생성이 확인되면

각 방들의 '상하좌우' 좌표를 확인해서 해당 좌표에 방이 있는지 여부를 확인해서 각 방의 정보를 갱신해준다

이는 이후 필드 생성시 '문' 오브젝트를 배치할때 사용하는 정보다

 

 

 

이상으로 맵 생성 알고리즘에 맞춰서 오브젝트를 생성하는 방법을 정리해봤다

매우매우 최적화가 필요한 부분이나 리펙토링할 부분이 많지만...

 

일단은 개발 도중 디테일한 정리를 하기 어렵기에 기본적인 알고리즘의 원리와 해당 원리에 맞는 생성방식 위주로 정리했다

 

 

728x90