[유니티]탑다운 슈팅 따라하기 #12 맵 구성
- 게임 개발 - Unity3d
- 2020. 4. 6. 08:44
안녕하세요 유랑입니다.
실력향상을 위해서 오늘은 유튜브를 따라하면서 공부하겠습니다.
궁금하신점 있으시다면 댓글로 남겨주세요^^
1. 탑다운 슈팅 따라하기
이번 강의는 Sebastian Lague님께서 만든 예제이며,
유튜브를 보시면 자세한 내용을 배우실 수 있습니다.
유튜브 사이트 => 유튜브
1-1) 맵 구성
이번 시간에는 맵 구성을 해보겠습니다.
지금까지 했던 것들을 이용해 다양한 맵을 만들어 볼께요.
1-2) 스크립트 작성 -㉠MapGenerator
MapGenerator 스크립트의 내용을 재구성해보겠습니다.
맵의 정보를 클래스를 만들어 구성하고,
기존의 정보들은 이 클래스로 교체해 줄게요.
맵 클래스를 Serializable을 통해서 인스펙트에서 조정도 가능하답니다^^
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
public class MapGenerator : MonoBehaviour
{
public Map[] maps; // 맵들
public int mapIndex;
public Transform tilePrefab; // 타일 프리팹
public Transform obstaclePrefab; // 장애물 프리팹
public Transform navmeshFloor; // 네비게이션 맵
public Transform navmeshMaskPrefab; // 네비게이션 마스크용 프리팹
public Vector2 maxMapSize; // 최대맵 사이즈
[Range(0, 1)]
public float outlinePercent; // 테두리 영역
public float tileSize; // 타일 사이즈
List<Coord> allTileCoords; // 모든 좌표 리스트
Queue<Coord> shuffledTileCoords; // 셔플된 좌표 리스트
Map currentMap;
void Start()
{
GenerateMap();
}
// 맵 생성
public void GenerateMap()
{
currentMap = maps[mapIndex];
System.Random prng = new System.Random(currentMap.seed);
GetComponent<BoxCollider>().size = new Vector3(currentMap.mapSize.x * tileSize, .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));
// mapHolder 생성
string holderName = "Generated Map";
if (transform.Find(holderName))
{
DestroyImmediate(transform.Find(holderName).gameObject);
}
Transform mapHolder = new GameObject(holderName).transform;
mapHolder.parent = transform;
// 타일 생성
for (int x = 0; x < currentMap.mapSize.x; x++)
{
for (int y = 0; y < currentMap.mapSize.y; y++)
{
Vector3 tilePosition = CoordToPosition(x, y);
Transform newTile = Instantiate(tilePrefab, tilePosition, Quaternion.Euler(Vector3.right * 90));
newTile.localScale = Vector3.one * (1 - outlinePercent) * tileSize;
newTile.parent = mapHolder;
}
}
// 장애물 생성
bool[,] obstacleMap = new bool[(int)currentMap.mapSize.x, (int)currentMap.mapSize.y];
int obstacleCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y * currentMap.obstaclePercent);
int currentObstacleCount = 0;
for (int i = 0; i < obstacleCount; i++)
{
Coord randomCoord = GetRandomCoord();
obstacleMap[randomCoord.x, randomCoord.y] = true;
currentObstacleCount++;
if (randomCoord != currentMap.mapCentre && MapIsFullyAccessible(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 obstacleMaterial = new Material(obstacleRenderer.sharedMaterial);
float colourPercent = randomCoord.y / (float)currentMap.mapSize.y;
obstacleMaterial.color = Color.Lerp(currentMap.foregroundColour, currentMap.backgroundColour, colourPercent);
obstacleRenderer.sharedMaterial = obstacleMaterial;
}
else
{
obstacleMap[randomCoord.x, randomCoord.y] = false;
currentObstacleCount--;
}
}
// 네비메쉬 마스크 생성
Transform maskLeft = Instantiate(navmeshMaskPrefab, Vector3.left * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity);
maskLeft.parent = mapHolder;
maskLeft.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2f, 1, currentMap.mapSize.y) * tileSize;
Transform maskRight = Instantiate(navmeshMaskPrefab, Vector3.right * (currentMap.mapSize.x + maxMapSize.x) / 4f * tileSize, Quaternion.identity);
maskRight.parent = mapHolder;
maskRight.localScale = new Vector3((maxMapSize.x - currentMap.mapSize.x) / 2f, 1, currentMap.mapSize.y) * tileSize;
Transform maskTop = Instantiate(navmeshMaskPrefab, Vector3.forward * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity);
maskTop.parent = mapHolder;
maskTop.localScale = new Vector3(maxMapSize.x, 1, (maxMapSize.y - currentMap.mapSize.y) / 2f) * tileSize;
Transform maskBottom = Instantiate(navmeshMaskPrefab, Vector3.back * (currentMap.mapSize.y + maxMapSize.y) / 4f * tileSize, Quaternion.identity);
maskBottom.parent = mapHolder;
maskBottom.localScale = new Vector3(maxMapSize.x, 1, (maxMapSize.y - currentMap.mapSize.y) / 2f) * tileSize;
navmeshFloor.localScale = new Vector3(maxMapSize.x, maxMapSize.y) * tileSize;
}
// 맵 접근 가능 여부(Flood fill 알고리즘)
bool MapIsFullyAccessible(bool[,] obstacleMap, int currentObstacleCount)
{
bool[,] mapFlags = new bool[obstacleMap.GetLength(0), obstacleMap.GetLength(1)];
Queue<Coord> queue = new Queue<Coord>();
queue.Enqueue(currentMap.mapCentre);
mapFlags[currentMap.mapCentre.x, currentMap.mapCentre.y] = true;
int accessibleTileCount = 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;
int neighbourY = tile.y + y;
if (x == 0 || y == 0)
{
if (neighbourX >= 0 && neighbourX < obstacleMap.GetLength(0) && neighbourY >= 0 && neighbourY < obstacleMap.GetLength(1))
{
if (!mapFlags[neighbourX, neighbourY] && !obstacleMap[neighbourX, neighbourY])
{
mapFlags[neighbourX, neighbourY] = true;
queue.Enqueue(new Coord(neighbourX, neighbourY));
accessibleTileCount++;
}
}
}
}
}
}
int targetAccessibleTileCount = (int)(currentMap.mapSize.x * currentMap.mapSize.y - currentObstacleCount);
return targetAccessibleTileCount == accessibleTileCount;
}
// Coord를 Vector3로 변환
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;
}
// 랜덤 좌표 반환
public Coord GetRandomCoord()
{
Coord randomCoord = shuffledTileCoords.Dequeue();
shuffledTileCoords.Enqueue(randomCoord);
return randomCoord;
}
[System.Serializable]
public struct Coord
{
public int x;
public int 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);
}
}
[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 backgroundColour; // 후반부 색
public Coord mapCentre
{
get
{
return new Coord(mapSize.x / 2, mapSize.y / 2);
}
}
}
}
1-3) 스크립트 작성 -㉡MapEditor
MapEditor 스크립트에서는 "Generate Map" 버튼을 만들어주어,
다른 씬에서 편하게 적용해 주겠습니다.
using UnityEngine;
using System.Collections;
using UnityEditor;
// 커스텀 에디터(맵 설정)
[CustomEditor (typeof (MapGenerator))]
public class MapEditor : Editor
{
// 유니티가 인스펙터를 GUI로 그려주는 메소드
public override void OnInspectorGUI()
{
MapGenerator map = target as MapGenerator;
if (DrawDefaultInspector())
{
map.GenerateMap();
}
if (GUILayout.Button("Generate Map"))
{
map.GenerateMap();
}
}
}
간단히 2개의 맵 정보만 입력해 주겠습니다.
다르게 하셔도 되지만 저는 우선 이렇게 설정하였습니다.
그리고 콜라이더 컴포넌트도 붙여주세요.
어떤가요?
이제 그럴듯하죠ㅎㅎ
1-4) 맵 설정 -㉠복사
우리가 만든 맵 정보를 Main 씬에다가 적용해줄텐데요.
Map을 복사하여 옮겨줄게요.
Main씬에는 플레이어와 카메라, 라이트, 그리고 스포너의 정보만 남겨주고
나머지는 지워주겠습니다.
이렇게 복사해서 붙여주고, Generator Map 버튼을 클릭하여 맵을 생성해 주세요!!
1-5) 맵 설정 -㉡플레이어 위치
장애물에 가려지지 않도록 플레이어 위치를 옮겨줄게요.
1-6) 맵 설정 -㉢네비게이션
여기서도 네비게이션 설정을 해주어야겠죠?!
플레이어 크기를 고려하여 Agent Radius도 조정해 주겠습니다.
그리고 Bake 해줄게요ㅎㅎ
네비게이션 적용도 완료!!
1-7) 맵 설정 -㉣Map
맵 설정도 끝내고,
카메라 위치도 조정해 주고
Spawner 값도 조정하고 시작해보겠습니다.
이제 게임답네요.
따라란~~~
2. 마무리
오늘 강의는 여기까지입니다.
탑다운 슈팅을 따라하면서 맵 구성을 해보았습니다.
감사합니다.
수업자료: 탑다운 슈팅 따라하기 #12 맵 구성