지난번에 이어 게임을 만들어보자.
이번에 참고할 강의는 아래 강의이다.
https://www.youtube.com/watch?v=HuQITd3epIU&list=PLctzObGsrjfyevwpeEVQ9pxGVwZtS7gZK&index=13
이번 강의에서는 맵 생성기에 기능을 추가하고, 생성한 맵에 적들이 움직이도록 적용해보자.
시작해보자.
맵 생성기에 장애물 높이, 색상 설정 기능을 추가하자.
그리고 지금까지 만든 맵 생성기를 게임에 적용하기 위해
래핑(Wrapping, 클래스를 사용하기 쉽도록 적절한 인터페이스 등의 형태로 감싸주는 것) 해주자.
이를 위해 Map 클래스를 생성하여 속성들을 정리해주자.
이제 스테이지별로 맵을 다르게 하기 위해 Map 배열을 생성하여 배열에서 선택된 맵을 생성하도록 한다.
| MapGenerator 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapGenerator : MonoBehaviour
{
public Map[] maps; // 맵 배열
public int mapIndex; // 맵 배열 참조용 인덱스
public Transform tilePrefab; // 인스턴스화 할 타일 프리팹
public Transform obstaclePrefab; // 인스턴스화 할 장애물 프리팹
public Transform navmeshFloor; // 내브메쉬를 위한 바닥 사이즈
public Transform navemeshMaskPrefab; // 맵 바깥쪽 마스킹 프리팹
public Vector2 maxMapSize; // 최대 맵 크기
[Range(0, 1)] // 범위 지정
public float outlinePercent; // 테두리 두께
public float tileSize; // 타일 사이즈
List<Coord> allTileCoords; // 모든 좌표값을 저장할 리스트 생성
Queue<Coord> shuffledTileCoords; // 셔플된 좌표값을 저장할 큐 생성
Map currentMap; // 현재 맵
void Start() {
GeneratorMap();
}
// ■ 맵 생성 메소드
public void GeneratorMap() {
System.Random prng = new System.Random(currentMap.seed); // 난수 생성
currentMap = maps[mapIndex]; // 맵 설정
// ■ 좌표(Coord)들을 생성
allTileCoords = new List<Coord>(); // 새 리스트 생성
for (int x = 0; x < currentMap.mapSize.x; x++) {
for (int y = 0; y < currentMap.mapSize.y; y++) { // 지정한 맵 크기만큼 루프
allTileCoords.Add(new Coord(x, y)); // 리스트에 타일 좌표 추가
}
}
shuffledTileCoords = new Queue<Coord>(Utility.ShuffleArray(allTileCoords.ToArray(), currentMap.seed));
// 새 큐 생성, 셔플된 좌표값 배열을 저장함
// ■ 맵 홀더(부모 오브젝트) 생성
string holderName = "Generator Map"; // 타일 오브젝트를 가질 부모 오브젝트 이름
if (transform.Find(holderName)) // 위 오브젝트에 자식 오브젝트가 있다면
// * 강의에서는 FindChild 를 사용하는데, 버전이 바뀌면서 사용하지 않고 Find 로 변경되었다.
{
DestroyImmediate(transform.Find(holderName).gameObject);
/* 오브젝트의 자식 게임 오브젝트를 제거한다.
에디터에서 호출 할 것이기 때문에 DestroyImmediate 를 사용한다. */
}
Transform mapHolder = new GameObject(holderName).transform;
// 타일을 자식 오브젝트로 가질 새 오브젝트를 생성
mapHolder.parent = transform;
// 현재 오브젝트를 mapHolder 오브젝트의 부모 오브젝트로 설정.
// ■ 타일들을 생성
for (int x = 0; x < currentMap.mapSize.x; x++) {
for (int y = 0; y < currentMap.mapSize.y; y++) { // 지정한 맵 크기만큼 루프
Vector3 tilePosition = CoordToPosition(x, y);
/* 타일이 생성될 위치 저장.
-지정한 맵 가로 길이/2 를 설정하면 0에서 가로길이의 절반만큼 왼쪽으로 이동한 위치가 된다.
이를 활용하여 z 좌표에도 적용해 화면의 왼쪽 위 모서리부터 맵이 생성되도록 한다. */
Transform newTile = Instantiate(tilePrefab, tilePosition, Quaternion.Euler(Vector3.right * 90)) as Transform;
/* 타일을 인스턴스화 하여 생성하고 위치, 각도를 설정
위치는 위에서 생성한 값을 전달하고 각도는 Quaternion(사원수) 메소드를 사용,
오일러 각을 기준으로 회전시킨다. */
newTile.localScale = Vector3.one * (1 - outlinePercent) * tileSize;
/* localScale 은 오브젝트의 상대적 크기, Vector3.one 은 Vector3(1,1,1) 이다.
크기를 지정한 테두리 두께만큼 줄여서 지정한다. */
newTile.parent = mapHolder;
// 타일의 부모 오브젝트 설정
}
}
// ■ 장애물들을 생성
bool[,] obstaclemap = new bool[(int)currentMap.mapSize.x, (int)currentMap.mapSize.y]; // 장애물 위치 확인 배열
int currentObstacleCount = 0; // 현재 올바른 장애물 생성 개수
int obstacleCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y * currentMap.obstaclePercent); // 지정한 비율에 따른 장애물 개수
for (int i = 0; i < obstacleCount; i++) { // 장애물 갯수만큼 루프
Coord randomCoord = GetRandomCoord(); // 랜덤한 좌표를 받아옴
obstaclemap[randomCoord.x, randomCoord.y] = true; // 해당 랜덤 위치 활성화
currentObstacleCount++; // 장애물 개수 증가
if (randomCoord != currentMap.mapCenter && MaplsFullyAccessible(obstaclemap, currentObstacleCount)) {
// 랜덤 위치가 맵 중앙이 아니고 막힌 곳을 만들지 않을 때 아래 코드 실행
float obstacleHeight = Mathf.Lerp(currentMap.minObstacleHeight, currentMap.maxObstacleHeight, (float)prng.NextDouble());
// 장애물 높이 설정
Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);
// 좌표 변환
Transform newObstacle = Instantiate(obstaclePrefab, obstaclePosition + Vector3.up * obstacleHeight / 2, Quaternion.identity);
// 장애물 인스턴스화 하여 생성
newObstacle.parent = mapHolder;
// 장애물의 부모 오브젝트 설정
newObstacle.localScale = new Vector3((1 - outlinePercent) * tileSize, obstacleHeight, (1 - outlinePercent) * tileSize);
// 장애물 크기를 지정한 테두리 두께만큼 줄여서 타일 사이즈와 맞게 지정하고 높이를 지정한다.
}
else { // 장애물 생성 조건이 맞지 않는 경우
obstaclemap[randomCoord.x, randomCoord.y] = false; // 해당 랜덤 위치 비활성화
currentObstacleCount--; // 장애물 개수 감소
}
}
// ■ 내브메쉬 마스크 생성
Transform maskLeft = Instantiate(navemeshMaskPrefab, Vector3.left * (currentMap.mapSize.x + maxMapSize.x) / 4 * tileSize, Quaternion.identity) as Transform;
// 왼쪽 맵 바깥 마스킹 오브젝트 생성
maskLeft.parent = mapHolder;
// 부모 오브젝트 설정
maskLeft.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2 * tileSize, 1, currentMap.mapSize.y * tileSize);
// 왼쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskRight = Instantiate(navemeshMaskPrefab, Vector3.right * (currentMap.mapSize.x + maxMapSize.x) / 4 * tileSize, Quaternion.identity) as Transform;
// 오른쪽 맵 바깥 마스킹 오브젝트 생성
maskRight.parent = mapHolder;
// 부모 오브젝트 설정
maskRight.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2 * tileSize, 1, currentMap.mapSize.y * tileSize);
// 오른쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskTop = Instantiate(navemeshMaskPrefab, Vector3.forward * (currentMap.mapSize.y + maxMapSize.y) / 4 * tileSize, Quaternion.identity) as Transform;
// 위쪽 맵 바깥 마스킹 오브젝트 생성
maskTop.parent = mapHolder;
// 부모 오브젝트 설정
maskTop.localScale = new Vector3(maxMapSize.x * tileSize, 1, (maxMapSize.y - currentMap.mapSize.y) / 2 * tileSize);
// 위쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskBottom = Instantiate(navemeshMaskPrefab, Vector3.back * (currentMap.mapSize.y + maxMapSize.y) / 4 * tileSize, Quaternion.identity) as Transform;
// 아래쪽 맵 바깥 마스킹 오브젝트 생성
maskBottom.parent = mapHolder;
// 부모 오브젝트 설정
maskBottom.localScale = new Vector3(maxMapSize.x * tileSize, 1, (maxMapSize.y - currentMap.mapSize.y) / 2 * tileSize);
// 아래쪽 맵 바깥 마스킹 오브젝트 크기 설정
navmeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y) * tileSize;
// 지정한 최대 맵 사이즈에 맞게 내브메쉬 바닥 크기 조정
}
// 맵 확인 메소드
bool MaplsFullyAccessible(bool[,] obstacleMap, int currentObstacleCount) {
bool[,] mapFlag = new bool[obstacleMap.GetLength(0), obstacleMap.GetLength(1)];
// 지나온 비어있는 타일을 체크할 배열을 생성
Queue<Coord> queue = new Queue<Coord>(); // 큐 생성
queue.Enqueue(currentMap.mapCenter); // 맵 중앙 위치를 큐에 넣음
mapFlag[currentMap.mapCenter.x, currentMap.mapCenter.y] = true; // 맵 중앙을 비어있는 타일로 체크
int accessibleTileCount = 1; // 접근 가능한 타일 개수(맵 중앙 포함이므로 기본 1)
while (queue.Count > 0) { // 큐에 들어있는 값이 있는 경우
Coord tile = queue.Dequeue(); // 큐에 저장된 맨 앞 타일 위치를 빼서 가져옴
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) { // 주변 타일 루프
int neighbourX = tile.x + x; // 주변 타일의 x 좌표
int neighbourY = tile.y + y; // 주변 타일의 y 좌표
if (x == 0 || y == 0) { // 주변 타일 중 대각선상에 위치하지 않은 경우
if (neighbourX >= 0 && neighbourX < obstacleMap.GetLength(0) && neighbourY >= 0 && neighbourY < obstacleMap.GetLength(1)) {
// 체크 중 맵 크기를 벗어나지 않는 경우
if (!mapFlag[neighbourX, neighbourY] && !obstacleMap[neighbourX, neighbourY]) {
// 체크된 타일이 아니고, 장애물이 아닌 경우
mapFlag[neighbourX, neighbourY] = true; // 타일 체크
queue.Enqueue(new Coord(neighbourX, neighbourY)); // 해당 타일 위치를 큐에 삽입
accessibleTileCount++; // 접근 가능한 타일 수 증가
}
}
}
}
}
}
int targetAccessibleTileCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y - currentObstacleCount);
// 현재 접근 가능해야 하는 타일 개수
return targetAccessibleTileCount == accessibleTileCount;
// 개수가 같다면(막힌 곳 없이 모든 타일에 접근 가능) true, 아니면 false 반환
}
Vector3 CoordToPosition(int x, int y) { // 좌표 변환 메소드
return new Vector3(-currentMap.mapSize.x / 2 + 0.5f + x, 0, -currentMap.mapSize.y / 2 + 0.5f + y) * tileSize;
// 입력받은 x, y 좌표로 Vector3 상의 타일 위치 설정
}
public Coord GetRandomCoord() { // 큐에 저장된 좌표를 가져오는 메소드
Coord randomCoord = shuffledTileCoords.Dequeue(); // 큐의 첫 번째 값을 가져온다.
shuffledTileCoords.Enqueue(randomCoord); // 가져온 값을 큐의 맨 뒤로 넣는다.
return randomCoord;
}
[System.Serializable] // 인스펙터에서 보이도록 설정
public struct Coord { // 타일 좌표 구조체
public int x, y; // x, y 좌표값
public Coord(int _x, int _y) { // 생성자로 좌표값 초기화
x = _x;
y = _y;
}
public static bool operator ==(Coord c1, Coord c2) { // 구조체 비교 연산자 정의
return c1.x == c2.x && c1.y == c2.y;
}
public static bool operator !=(Coord c1, Coord c2) { // 구조체 비교 연산자 정의
return !(c1 == c2);
}
public override bool Equals(object obj) { // 비교 메소드 재정의
return base.Equals(obj);
}
public override int GetHashCode() { // GetHashCode 메소드 재정의
return base.GetHashCode();
}
}
[System.Serializable] // 인스펙터에서 보이도록 설정
public class Map { // 맵 속성들을 저장 할 클래스 생성
public Coord mapSize; // 맵 크기
[Range(0, 1)] // 장애물 비율 범위 설정
public float obstaclePercent; // 맵의 장애물 비율
public int seed; // 장애물 랜덤 생성 시드
public float minObstacleHeight; // 장애물 최소 높이
public float maxObstacleHeight; // 장애물 최대 높이
public Color foregroundColour; // 장애물 전면부 색상
public Color backgroungColour; // 장애물 후면부 색상
public Coord mapCenter { // 맵 중앙 좌표
get {
return new Coord(mapSize.x / 2, mapSize.y / 2); // 중앙 좌표 지정 후 리턴
}
}
}
}
맵에서 장애물의 높이를 랜덤하게 조정하기 위해
장애물 생성 과정에서 랜덤한 높이값을 주도록 했다.
실행해보자,
최소, 최대 높이 설정값에 따라 장애물들의 높이가 랜덤하게 조정된다.
지금 문제점이 하나 있다.
스크립트에서 정수를 사용하면서 문제가 하나 생겼는데, 맵 크기를 홀수로 지정하면 내비메쉬 마스크
오브젝트와 맵의 위치가 맞지않게 된다.
이를 위해 맵의 좌표를 구하는 과정에서 나누는 값을 실수형으로 명시하자.
숫자의 경우 뒤에 f 를 붙여 float 형으로 명시할 수 있다.
그리고 위에서 추가해놓은 foregroundColour 와 backgroundColour 를 사용하여
장애물의 색상 조절 기능도 추가하자.
| MapGenerator 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapGenerator : MonoBehaviour
{
public Map[] maps; // 맵 배열
public int mapIndex; // 맵 배열 참조용 인덱스
public Transform tilePrefab; // 인스턴스화 할 타일 프리팹
public Transform obstaclePrefab; // 인스턴스화 할 장애물 프리팹
public Transform navmeshFloor; // 내브메쉬를 위한 바닥 사이즈
public Transform navemeshMaskPrefab; // 맵 바깥쪽 마스킹 프리팹
public Vector2 maxMapSize; // 최대 맵 크기
[Range(0, 1)] // 범위 지정
public float outlinePercent; // 테두리 두께
public float tileSize; // 타일 사이즈
List<Coord> allTileCoords; // 모든 좌표값을 저장할 리스트 생성
Queue<Coord> shuffledTileCoords; // 셔플된 좌표값을 저장할 큐 생성
Map currentMap; // 현재 맵
void Start() {
GeneratorMap();
}
// ■ 맵 생성 메소드
public void GeneratorMap() {
currentMap = maps[mapIndex]; // 맵 설정
System.Random prng = new System.Random(currentMap.seed); // 난수 생성
// ■ 좌표(Coord)들을 생성
allTileCoords = new List<Coord>(); // 새 리스트 생성
for (int x = 0; x < currentMap.mapSize.x; x++) {
for (int y = 0; y < currentMap.mapSize.y; y++) { // 지정한 맵 크기만큼 루프
allTileCoords.Add(new Coord(x, y)); // 리스트에 타일 좌표 추가
}
}
shuffledTileCoords = new Queue<Coord>(Utility.ShuffleArray(allTileCoords.ToArray(), currentMap.seed));
// 새 큐 생성, 셔플된 좌표값 배열을 저장함
// ■ 맵 홀더(부모 오브젝트) 생성
string holderName = "Generator Map"; // 타일 오브젝트를 가질 부모 오브젝트 이름
if (transform.Find(holderName)) // 위 오브젝트에 자식 오브젝트가 있다면
// * 강의에서는 FindChild 를 사용하는데, 버전이 바뀌면서 사용하지 않고 Find 로 변경되었다.
{
DestroyImmediate(transform.Find(holderName).gameObject);
/* 오브젝트의 자식 게임 오브젝트를 제거한다.
에디터에서 호출 할 것이기 때문에 DestroyImmediate 를 사용한다. */
}
Transform mapHolder = new GameObject(holderName).transform;
// 타일을 자식 오브젝트로 가질 새 오브젝트를 생성
mapHolder.parent = transform;
// 현재 오브젝트를 mapHolder 오브젝트의 부모 오브젝트로 설정.
// ■ 타일들을 생성
for (int x = 0; x < currentMap.mapSize.x; x++) {
for (int y = 0; y < currentMap.mapSize.y; y++) { // 지정한 맵 크기만큼 루프
Vector3 tilePosition = CoordToPosition(x, y);
/* 타일이 생성될 위치 저장.
-지정한 맵 가로 길이/2 를 설정하면 0에서 가로길이의 절반만큼 왼쪽으로 이동한 위치가 된다.
이를 활용하여 z 좌표에도 적용해 화면의 왼쪽 위 모서리부터 맵이 생성되도록 한다. */
Transform newTile = Instantiate(tilePrefab, tilePosition, Quaternion.Euler(Vector3.right * 90)) as Transform;
/* 타일을 인스턴스화 하여 생성하고 위치, 각도를 설정
위치는 위에서 생성한 값을 전달하고 각도는 Quaternion(사원수) 메소드를 사용,
오일러 각을 기준으로 회전시킨다. */
newTile.localScale = Vector3.one * (1 - outlinePercent) * tileSize;
/* localScale 은 오브젝트의 상대적 크기, Vector3.one 은 Vector3(1,1,1) 이다.
크기를 지정한 테두리 두께만큼 줄여서 지정한다. */
newTile.parent = mapHolder;
// 타일의 부모 오브젝트 설정
}
}
// ■ 장애물들을 생성
bool[,] obstaclemap = new bool[(int)currentMap.mapSize.x, (int)currentMap.mapSize.y]; // 장애물 위치 확인 배열
int currentObstacleCount = 0; // 현재 올바른 장애물 생성 개수
int obstacleCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y * currentMap.obstaclePercent); // 지정한 비율에 따른 장애물 개수
for (int i = 0; i < obstacleCount; i++) { // 장애물 갯수만큼 루프
Coord randomCoord = GetRandomCoord(); // 랜덤한 좌표를 받아옴
obstaclemap[randomCoord.x, randomCoord.y] = true; // 해당 랜덤 위치 활성화
currentObstacleCount++; // 장애물 개수 증가
if (randomCoord != currentMap.mapCenter && MaplsFullyAccessible(obstaclemap, currentObstacleCount)) {
// 랜덤 위치가 맵 중앙이 아니고 막힌 곳을 만들지 않을 때 아래 코드 실행
float obstacleHeight = Mathf.Lerp(currentMap.minObstacleHeight, currentMap.maxObstacleHeight, (float)prng.NextDouble());
// 장애물 높이 설정
Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);
// 좌표 변환
Transform newObstacle = Instantiate(obstaclePrefab, obstaclePosition + Vector3.up * obstacleHeight / 2, Quaternion.identity);
// 장애물 인스턴스화 하여 생성
newObstacle.parent = mapHolder;
// 장애물의 부모 오브젝트 설정
newObstacle.localScale = new Vector3((1 - outlinePercent) * tileSize, obstacleHeight, (1 - outlinePercent) * tileSize);
// 장애물 크기를 지정한 테두리 두께만큼 줄여서 타일 사이즈와 맞게 지정하고 높이를 지정한다.
Renderer obstacleRenderer = newObstacle.GetComponent<Renderer>();
// 인스턴스화 하여 생성한 장애물의 렌더러 레퍼런스 생성
Material obstacleMatetial = new Material(obstacleRenderer.sharedMaterial);
// 장애물의 마테리얼 생성, 위 레퍼런스를 통해 장애물의 셰어드 마테리얼 저장
float colourPercent = randomCoord.y / (float)currentMap.mapSize.y;
// 색상 비율 설정
obstacleMatetial.color = Color.Lerp(currentMap.foregroundColour, currentMap.backgroungColour, colourPercent);
// 장애물 색상 설정
obstacleRenderer.sharedMaterial = obstacleMatetial;
// 장애물의 셰어드 마테리얼 설정
}
else { // 장애물 생성 조건이 맞지 않는 경우
obstaclemap[randomCoord.x, randomCoord.y] = false; // 해당 랜덤 위치 비활성화
currentObstacleCount--; // 장애물 개수 감소
}
}
// ■ 내브메쉬 마스크 생성
Transform maskLeft = Instantiate(navemeshMaskPrefab, Vector3.left * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity) as Transform;
// 왼쪽 맵 바깥 마스킹 오브젝트 생성
maskLeft.parent = mapHolder;
// 부모 오브젝트 설정
maskLeft.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2f * tileSize, 1, currentMap.mapSize.y * tileSize);
// 왼쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskRight = Instantiate(navemeshMaskPrefab, Vector3.right * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity) as Transform;
// 오른쪽 맵 바깥 마스킹 오브젝트 생성
maskRight.parent = mapHolder;
// 부모 오브젝트 설정
maskRight.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2f * tileSize, 1, currentMap.mapSize.y * tileSize);
// 오른쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskTop = Instantiate(navemeshMaskPrefab, Vector3.forward * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity) as Transform;
// 위쪽 맵 바깥 마스킹 오브젝트 생성
maskTop.parent = mapHolder;
// 부모 오브젝트 설정
maskTop.localScale = new Vector3(maxMapSize.x * tileSize, 1, (maxMapSize.y - currentMap.mapSize.y) / 2f * tileSize);
// 위쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskBottom = Instantiate(navemeshMaskPrefab, Vector3.back * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity) as Transform;
// 아래쪽 맵 바깥 마스킹 오브젝트 생성
maskBottom.parent = mapHolder;
// 부모 오브젝트 설정
maskBottom.localScale = new Vector3(maxMapSize.x * tileSize, 1, (maxMapSize.y - currentMap.mapSize.y) / 2f * tileSize);
// 아래쪽 맵 바깥 마스킹 오브젝트 크기 설정
navmeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y) * tileSize;
// 지정한 최대 맵 사이즈에 맞게 내브메쉬 바닥 크기 조정
}
// ■ 맵 확인 메소드 (Flood-fill Algorithm)
bool MaplsFullyAccessible(bool[,] obstacleMap, int currentObstacleCount) {
bool[,] mapFlag = new bool[obstacleMap.GetLength(0), obstacleMap.GetLength(1)];
// 지나온 비어있는 타일을 체크할 배열을 생성
Queue<Coord> queue = new Queue<Coord>(); // 큐 생성
queue.Enqueue(currentMap.mapCenter); // 맵 중앙 위치를 큐에 넣음
mapFlag[currentMap.mapCenter.x, currentMap.mapCenter.y] = true; // 맵 중앙을 비어있는 타일로 체크
int accessibleTileCount = 1; // 접근 가능한 타일 개수(맵 중앙 포함이므로 기본 1)
while (queue.Count > 0) { // 큐에 들어있는 값이 있는 경우
Coord tile = queue.Dequeue(); // 큐에 저장된 맨 앞 타일 위치를 빼서 가져옴
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) { // 주변 타일 루프
int neighbourX = tile.x + x; // 주변 타일의 x 좌표
int neighbourY = tile.y + y; // 주변 타일의 y 좌표
if (x == 0 || y == 0) { // 주변 타일 중 대각선상에 위치하지 않은 경우
if (neighbourX >= 0 && neighbourX < obstacleMap.GetLength(0) && neighbourY >= 0 && neighbourY < obstacleMap.GetLength(1)) {
// 체크 중 맵 크기를 벗어나지 않는 경우
if (!mapFlag[neighbourX, neighbourY] && !obstacleMap[neighbourX, neighbourY]) {
// 체크된 타일이 아니고, 장애물이 아닌 경우
mapFlag[neighbourX, neighbourY] = true; // 타일 체크
queue.Enqueue(new Coord(neighbourX, neighbourY)); // 해당 타일 위치를 큐에 삽입
accessibleTileCount++; // 접근 가능한 타일 수 증가
}
}
}
}
}
}
int targetAccessibleTileCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y - currentObstacleCount);
// 현재 접근 가능해야 하는 타일 개수
return targetAccessibleTileCount == accessibleTileCount;
// 개수가 같다면(막힌 곳 없이 모든 타일에 접근 가능) true, 아니면 false 반환
}
// ■ 좌표 변환 메소드
Vector3 CoordToPosition(int x, int y) {
return new Vector3(-currentMap.mapSize.x / 2f + 0.5f + x, 0, -currentMap.mapSize.y / 2f + 0.5f + y) * tileSize;
// 입력받은 x, y 좌표로 Vector3 상의 타일 위치 설정
}
// ■ 큐에 저장된 좌표를 가져오는 메소드
public Coord GetRandomCoord() {
Coord randomCoord = shuffledTileCoords.Dequeue(); // 큐의 첫 번째 값을 가져온다.
shuffledTileCoords.Enqueue(randomCoord); // 가져온 값을 큐의 맨 뒤로 넣는다.
return randomCoord;
}
// ■ 타일 좌표 구조체
[System.Serializable] // 인스펙터에서 보이도록 설정
public struct Coord {
public int x, y; // x, y 좌표값
public Coord(int _x, int _y) { // 생성자로 좌표값 초기화
x = _x;
y = _y;
}
public static bool operator ==(Coord c1, Coord c2) { // 구조체 비교 연산자 정의
return c1.x == c2.x && c1.y == c2.y;
}
public static bool operator !=(Coord c1, Coord c2) { // 구조체 비교 연산자 정의
return !(c1 == c2);
}
public override bool Equals(object obj) { // 비교 메소드 재정의
return base.Equals(obj);
}
public override int GetHashCode() { // GetHashCode 메소드 재정의
return base.GetHashCode();
}
}
// ■ 맵 속성들을 저장 할 클래스
[System.Serializable] // 인스펙터에서 보이도록 설정
public class Map {
public Coord mapSize; // 맵 크기
[Range(0, 1)] // 장애물 비율 범위 설정
public float obstaclePercent; // 맵의 장애물 비율
public int seed; // 장애물 랜덤 생성 시드
public float minObstacleHeight; // 장애물 최소 높이
public float maxObstacleHeight; // 장애물 최대 높이
public Color foregroundColour; // 장애물 전면부 색상
public Color backgroungColour; // 장애물 후면부 색상
public Coord mapCenter { // 맵 중앙 좌표
get {
return new Coord(mapSize.x / 2, mapSize.y / 2); // 중앙 좌표 지정 후 리턴
}
}
}
}
이제 실행해보자.
지정한 색상에 따라 앞에서부터 뒤까지 그라데이션으로 색상이 설정된다.
또한 맵의 사이즈를 홀수로 지정하여도 내비메쉬 마스크 오브젝트와 잘 맞게 생성된다.
이제 해야 할 일은 플레이어와 적이 위에서 걸어다닐 수 있도록
Map 오브젝트에 박스 콜라이더를 추가해주자.
그리고 MapGenerator 스크립트를 수정해준다.
| MapGenerator 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MapGenerator : MonoBehaviour
{
public Map[] maps; // 맵 배열
public int mapIndex; // 맵 배열 참조용 인덱스
public Transform tilePrefab; // 인스턴스화 할 타일 프리팹
public Transform obstaclePrefab; // 인스턴스화 할 장애물 프리팹
public Transform navmeshFloor; // 내브메쉬를 위한 바닥 사이즈
public Transform navemeshMaskPrefab; // 맵 바깥쪽 마스킹 프리팹
public Vector2 maxMapSize; // 최대 맵 크기
[Range(0, 1)] // 범위 지정
public float outlinePercent; // 테두리 두께
public float tileSize; // 타일 사이즈
List<Coord> allTileCoords; // 모든 좌표값을 저장할 리스트 생성
Queue<Coord> shuffledTileCoords; // 셔플된 좌표값을 저장할 큐 생성
Map currentMap; // 현재 맵
void Start() {
GeneratorMap();
}
// ■ 맵 생성 메소드
public void GeneratorMap() {
currentMap = maps[mapIndex]; // 맵 설정
System.Random prng = new System.Random(currentMap.seed); // 난수 생성
GetComponent<BoxCollider>().size = new Vector3(currentMap.mapSize.x * tileSize, 0.05f, currentMap.mapSize.y * tileSize);
// 박스 콜라이더 맵 크기로 설정
// ■ 좌표(Coord)들을 생성
allTileCoords = new List<Coord>(); // 새 리스트 생성
for (int x = 0; x < currentMap.mapSize.x; x++) {
for (int y = 0; y < currentMap.mapSize.y; y++) { // 지정한 맵 크기만큼 루프
allTileCoords.Add(new Coord(x, y)); // 리스트에 타일 좌표 추가
}
}
shuffledTileCoords = new Queue<Coord>(Utility.ShuffleArray(allTileCoords.ToArray(), currentMap.seed));
// 새 큐 생성, 셔플된 좌표값 배열을 저장함
// ■ 맵 홀더(부모 오브젝트) 생성
string holderName = "Generator Map"; // 타일 오브젝트를 가질 부모 오브젝트 이름
if (transform.Find(holderName)) // 위 오브젝트에 자식 오브젝트가 있다면
// * 강의에서는 FindChild 를 사용하는데, 버전이 바뀌면서 사용하지 않고 Find 로 변경되었다.
{
DestroyImmediate(transform.Find(holderName).gameObject);
/* 오브젝트의 자식 게임 오브젝트를 제거한다.
에디터에서 호출 할 것이기 때문에 DestroyImmediate 를 사용한다. */
}
Transform mapHolder = new GameObject(holderName).transform;
// 타일을 자식 오브젝트로 가질 새 오브젝트를 생성
mapHolder.parent = transform;
// 현재 오브젝트를 mapHolder 오브젝트의 부모 오브젝트로 설정.
// ■ 타일들을 생성
for (int x = 0; x < currentMap.mapSize.x; x++) {
for (int y = 0; y < currentMap.mapSize.y; y++) { // 지정한 맵 크기만큼 루프
Vector3 tilePosition = CoordToPosition(x, y);
/* 타일이 생성될 위치 저장.
-지정한 맵 가로 길이/2 를 설정하면 0에서 가로길이의 절반만큼 왼쪽으로 이동한 위치가 된다.
이를 활용하여 z 좌표에도 적용해 화면의 왼쪽 위 모서리부터 맵이 생성되도록 한다. */
Transform newTile = Instantiate(tilePrefab, tilePosition, Quaternion.Euler(Vector3.right * 90)) as Transform;
/* 타일을 인스턴스화 하여 생성하고 위치, 각도를 설정
위치는 위에서 생성한 값을 전달하고 각도는 Quaternion(사원수) 메소드를 사용,
오일러 각을 기준으로 회전시킨다. */
newTile.localScale = Vector3.one * (1 - outlinePercent) * tileSize;
/* localScale 은 오브젝트의 상대적 크기, Vector3.one 은 Vector3(1,1,1) 이다.
크기를 지정한 테두리 두께만큼 줄여서 지정한다. */
newTile.parent = mapHolder;
// 타일의 부모 오브젝트 설정
}
}
// ■ 장애물들을 생성
bool[,] obstaclemap = new bool[(int)currentMap.mapSize.x, (int)currentMap.mapSize.y]; // 장애물 위치 확인 배열
int currentObstacleCount = 0; // 현재 올바른 장애물 생성 개수
int obstacleCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y * currentMap.obstaclePercent); // 지정한 비율에 따른 장애물 개수
for (int i = 0; i < obstacleCount; i++) { // 장애물 갯수만큼 루프
Coord randomCoord = GetRandomCoord(); // 랜덤한 좌표를 받아옴
obstaclemap[randomCoord.x, randomCoord.y] = true; // 해당 랜덤 위치 활성화
currentObstacleCount++; // 장애물 개수 증가
if (randomCoord != currentMap.mapCenter && MaplsFullyAccessible(obstaclemap, currentObstacleCount)) {
// 랜덤 위치가 맵 중앙이 아니고 막힌 곳을 만들지 않을 때 아래 코드 실행
float obstacleHeight = Mathf.Lerp(currentMap.minObstacleHeight, currentMap.maxObstacleHeight, (float)prng.NextDouble());
// 장애물 높이 설정
Vector3 obstaclePosition = CoordToPosition(randomCoord.x, randomCoord.y);
// 좌표 변환
Transform newObstacle = Instantiate(obstaclePrefab, obstaclePosition + Vector3.up * obstacleHeight / 2, Quaternion.identity);
// 장애물 인스턴스화 하여 생성
newObstacle.parent = mapHolder;
// 장애물의 부모 오브젝트 설정
newObstacle.localScale = new Vector3((1 - outlinePercent) * tileSize, obstacleHeight, (1 - outlinePercent) * tileSize);
// 장애물 크기를 지정한 테두리 두께만큼 줄여서 타일 사이즈와 맞게 지정하고 높이를 지정한다.
Renderer obstacleRenderer = newObstacle.GetComponent<Renderer>();
// 인스턴스화 하여 생성한 장애물의 렌더러 레퍼런스 생성
Material obstacleMatetial = new Material(obstacleRenderer.sharedMaterial);
// 장애물의 마테리얼 생성, 위 레퍼런스를 통해 장애물의 셰어드 마테리얼 저장
float colourPercent = randomCoord.y / (float)currentMap.mapSize.y;
// 색상 비율 설정
obstacleMatetial.color = Color.Lerp(currentMap.foregroundColour, currentMap.backgroungColour, colourPercent);
// 장애물 색상 설정
obstacleRenderer.sharedMaterial = obstacleMatetial;
// 장애물의 셰어드 마테리얼 설정
}
else { // 장애물 생성 조건이 맞지 않는 경우
obstaclemap[randomCoord.x, randomCoord.y] = false; // 해당 랜덤 위치 비활성화
currentObstacleCount--; // 장애물 개수 감소
}
}
// ■ 내브메쉬 마스크 생성
Transform maskLeft = Instantiate(navemeshMaskPrefab, Vector3.left * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity) as Transform;
// 왼쪽 맵 바깥 마스킹 오브젝트 생성
maskLeft.parent = mapHolder;
// 부모 오브젝트 설정
maskLeft.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2f * tileSize, 1, currentMap.mapSize.y * tileSize);
// 왼쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskRight = Instantiate(navemeshMaskPrefab, Vector3.right * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity) as Transform;
// 오른쪽 맵 바깥 마스킹 오브젝트 생성
maskRight.parent = mapHolder;
// 부모 오브젝트 설정
maskRight.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2f * tileSize, 1, currentMap.mapSize.y * tileSize);
// 오른쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskTop = Instantiate(navemeshMaskPrefab, Vector3.forward * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity) as Transform;
// 위쪽 맵 바깥 마스킹 오브젝트 생성
maskTop.parent = mapHolder;
// 부모 오브젝트 설정
maskTop.localScale = new Vector3(maxMapSize.x * tileSize, 1, (maxMapSize.y - currentMap.mapSize.y) / 2f * tileSize);
// 위쪽 맵 바깥 마스킹 오브젝트 크기 설정
Transform maskBottom = Instantiate(navemeshMaskPrefab, Vector3.back * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity) as Transform;
// 아래쪽 맵 바깥 마스킹 오브젝트 생성
maskBottom.parent = mapHolder;
// 부모 오브젝트 설정
maskBottom.localScale = new Vector3(maxMapSize.x * tileSize, 1, (maxMapSize.y - currentMap.mapSize.y) / 2f * tileSize);
// 아래쪽 맵 바깥 마스킹 오브젝트 크기 설정
navmeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y) * tileSize;
// 지정한 최대 맵 사이즈에 맞게 내브메쉬 바닥 크기 조정
}
// ■ 맵 확인 메소드 (Flood-fill Algorithm)
bool MaplsFullyAccessible(bool[,] obstacleMap, int currentObstacleCount) {
bool[,] mapFlag = new bool[obstacleMap.GetLength(0), obstacleMap.GetLength(1)];
// 지나온 비어있는 타일을 체크할 배열을 생성
Queue<Coord> queue = new Queue<Coord>(); // 큐 생성
queue.Enqueue(currentMap.mapCenter); // 맵 중앙 위치를 큐에 넣음
mapFlag[currentMap.mapCenter.x, currentMap.mapCenter.y] = true; // 맵 중앙을 비어있는 타일로 체크
int accessibleTileCount = 1; // 접근 가능한 타일 개수(맵 중앙 포함이므로 기본 1)
while (queue.Count > 0) { // 큐에 들어있는 값이 있는 경우
Coord tile = queue.Dequeue(); // 큐에 저장된 맨 앞 타일 위치를 빼서 가져옴
for (int x = -1; x <= 1; x++) {
for (int y = -1; y <= 1; y++) { // 주변 타일 루프
int neighbourX = tile.x + x; // 주변 타일의 x 좌표
int neighbourY = tile.y + y; // 주변 타일의 y 좌표
if (x == 0 || y == 0) { // 주변 타일 중 대각선상에 위치하지 않은 경우
if (neighbourX >= 0 && neighbourX < obstacleMap.GetLength(0) && neighbourY >= 0 && neighbourY < obstacleMap.GetLength(1)) {
// 체크 중 맵 크기를 벗어나지 않는 경우
if (!mapFlag[neighbourX, neighbourY] && !obstacleMap[neighbourX, neighbourY]) {
// 체크된 타일이 아니고, 장애물이 아닌 경우
mapFlag[neighbourX, neighbourY] = true; // 타일 체크
queue.Enqueue(new Coord(neighbourX, neighbourY)); // 해당 타일 위치를 큐에 삽입
accessibleTileCount++; // 접근 가능한 타일 수 증가
}
}
}
}
}
}
int targetAccessibleTileCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y - currentObstacleCount);
// 현재 접근 가능해야 하는 타일 개수
return targetAccessibleTileCount == accessibleTileCount;
// 개수가 같다면(막힌 곳 없이 모든 타일에 접근 가능) true, 아니면 false 반환
}
// ■ 좌표 변환 메소드
Vector3 CoordToPosition(int x, int y) {
return new Vector3(-currentMap.mapSize.x / 2f + 0.5f + x, 0, -currentMap.mapSize.y / 2f + 0.5f + y) * tileSize;
// 입력받은 x, y 좌표로 Vector3 상의 타일 위치 설정
}
// ■ 큐에 저장된 좌표를 가져오는 메소드
public Coord GetRandomCoord() {
Coord randomCoord = shuffledTileCoords.Dequeue(); // 큐의 첫 번째 값을 가져온다.
shuffledTileCoords.Enqueue(randomCoord); // 가져온 값을 큐의 맨 뒤로 넣는다.
return randomCoord;
}
// ■ 타일 좌표 구조체
[System.Serializable] // 인스펙터에서 보이도록 설정
public struct Coord {
public int x, y; // x, y 좌표값
public Coord(int _x, int _y) { // 생성자로 좌표값 초기화
x = _x;
y = _y;
}
public static bool operator ==(Coord c1, Coord c2) { // 구조체 비교 연산자 정의
return c1.x == c2.x && c1.y == c2.y;
}
public static bool operator !=(Coord c1, Coord c2) { // 구조체 비교 연산자 정의
return !(c1 == c2);
}
public override bool Equals(object obj) { // 비교 메소드 재정의
return base.Equals(obj);
}
public override int GetHashCode() { // GetHashCode 메소드 재정의
return base.GetHashCode();
}
}
// ■ 맵 속성들을 저장 할 클래스
[System.Serializable] // 인스펙터에서 보이도록 설정
public class Map {
public Coord mapSize; // 맵 크기
[Range(0, 1)] // 장애물 비율 범위 설정
public float obstaclePercent; // 맵의 장애물 비율
public int seed; // 장애물 랜덤 생성 시드
public float minObstacleHeight; // 장애물 최소 높이
public float maxObstacleHeight; // 장애물 최대 높이
public Color foregroundColour; // 장애물 전면부 색상
public Color backgroungColour; // 장애물 후면부 색상
public Coord mapCenter { // 맵 중앙 좌표
get {
return new Coord(mapSize.x / 2, mapSize.y / 2); // 중앙 좌표 지정 후 리턴
}
}
}
}
그리고 지금은 인스펙터에서 스크립트를 불러올 때 마다 맵이 재생성 되는데,
큰 맵을 만들때 효율이 떨어지게 된다.
이를 해결하기 위해 MapEditor 스크립트도 수정해준다.
| MapEditor 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor; // 유니티 에디터 사용
[CustomEditor(typeof(MapGenerator))]
// 에디터가 사용(설정)할 클래스 명시
public class MapEditor : Editor
{ // 에디터 상속
public override void OnInspectorGUI() // 인스펙터 메소드
{
MapGenerator map = target as MapGenerator;
/* 유니티에서 위에 CuntomEditor 로 명시해둔 클래스를 target 으로 지정해준다.
이를 형변환하여 MapGenerator map 으로 가져오자. */
if (DrawDefaultInspector())
{ // bool 값을 반환, 인스펙터에서 값을 변경한 경우에만 true 를 반환한다.
map.GeneratorMap();
/* GUI 가 그려지는 매 프레임, 즉 GUI 에서 우리가 값을 설정하며 바꿀때마다
메소드를 호출하여 타일을 재생성한다. */
}
if (GUILayout.Button("Generate Map")) { // 버튼을 누르면 코드 실행
map.GeneratorMap();
}
}
}
자 이렇게 맵 생성기도 어느정도 완성했다.
이 맵 생성기를 게임에 적용해보자.
Map 오브젝트를 복사하고 Game 씬으로 이동하자.
만들어두었던 Plane 과 Obstacle 오브젝트를 삭제하고 Map 오브젝트를 붙여넣자.
왜 핑크색인지..
Generate Map 버튼을 눌러 맵을 다시 생성한다.
플레이어 위치도 조금 조정해서 맵의 중앙쪽에 오도록 하자.
장애물 수도 좀 줄이고 맵도 좀 키워야 해볼만 할 것 같다..
설정하고 실행해보자.
총알이 장애물을 뚫고 적을 맞추긴 하지만..
일단 지금까지 만든 기능들이 잘 작동하는 것 같다.
다음 강의에서는 적이 랜덤 위치에서 스폰되도록 해본다.
'Unity > 게임개발' 카테고리의 다른 글
[Unity] 게임 개발 일지 | 탑다운 슈팅 14 (0) | 2023.12.06 |
---|---|
[Unity] 게임 개발 일지 | 탑다운 슈팅 13 (2) | 2023.12.05 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 11 (1) | 2023.11.30 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 10 (1) | 2023.11.30 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 9 (0) | 2023.11.29 |