지난번에 이어 게임을 만들어보자.
이번에 참고할 강의는 아래 강의이다.
https://www.youtube.com/watch?v=YaLMri-h1JQ&list=PLFt_AvWsXl0ctd4dgE1F8g3uec4zKNRV0&index=15
이번 강의에서는 게임오버, 웨이브 별 스테이지 재구성 등을 추가해본다.
시작해보자.
모든 적을 쓰러뜨리고 다음 웨이브가 시작되면 맵을 변경하는 등 스테이지를 재구성해보자.
Spawner 클래스와 MapGenerator 스크립트를 수정한다.
| Spawner 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Spawner : MonoBehaviour
{
public Wave[] waves; // 웨이브들을 저장할 배열 생성
public Enemy enemy; // 스폰할 적 레퍼런스
LivingEntity playerEntity; // 플레이어 레퍼런스
Transform playerT; // 플레이어 위치
Wave currentWave; // 현재 웨이브 레퍼런스
int currentWaveNumber; // 현재 웨이브 번호
int enemiesRemainingToSpawn; // 남아있는 스폰 할 적 수
int enemiesRemainingAlive; // 살아있는 적의 수
float nextSpawnTime; // 다음 스폰까지의 시간
MapGenerator map; // 맵 생성기 레퍼런스
float timeBetweenCampingChecks = 2; // 캠핑 방지 체크 딜레이
float nextCampingCheckTime; // 다음 캠핑 체크 시간
float campThresholdDistance = 1.5f; // 캠핑 방지를 위해 이동해야 하는 거리
Vector3 campPositionOld; // 이전 플레이어 위치
bool isCamping; // 캠핑중인지
bool isDisable; // 플레이어 생존 여부
public event System.Action<int> OnNewWave; // 웨이브 번호를 매개변수로 갖는 웨이브 이벤트 추가
void Start() {
playerEntity = FindObjectOfType<Player>(); // 플레이어 오브젝트 할당
playerT = playerEntity.transform; // 플레이어 위치
nextCampingCheckTime = Time.time + timeBetweenCampingChecks;
// 다음 캠핑 체크 시간 설정
campPositionOld = playerT.position;
// 플레이어 위치 설정
map = FindObjectOfType<MapGenerator>(); // 맵 생성기
NextWave(); // 시작 시 웨이브 실행
playerEntity.OnDeath += OnPlayerDeath; // 플레이어가 죽을 때 위 메소드를 추가
}
void Update()
{
if (!isDisable) { // 비활성화(플레이어 죽음)가 아닌 경우
if (Time.time > nextCampingCheckTime)
{ // 캠핑 체크 시간이 지난 경우
nextCampingCheckTime = Time.time + timeBetweenCampingChecks;
// 다음 캠핑 체크 시간 설정
isCamping = (Vector3.Distance(playerT.position, campPositionOld) < campThresholdDistance);
// 캠핑중인지 확인
campPositionOld = playerT.position;
// 위치 갱신
}
if (enemiesRemainingToSpawn > 0 && Time.time >= nextSpawnTime)
// 적 수가 남아있고 현재 시간이 다음 스폰 시간보다 큰 경우
{
enemiesRemainingToSpawn--; // 적 수를 하나 줄임
nextSpawnTime = Time.time + currentWave.timeBetweenSpawns;
// 다음 스폰 시간을 현재시간 + 스폰 간격 으로 저장
StartCoroutine(SpawnEnemy()); // 적 스폰 코루틴 시작
}
}
}
// ■ 적 스폰 코루틴
IEnumerator SpawnEnemy() {
float spawnDelay = 1;
// 적 스폰 대기시간
float tileFlashSpeed = 4;
// 초당 몇번 타일이 깜빡일지 설정
Transform spawnTile = map.GetRandomOpenTile();
// 랜덤 오픈 타일 가져오기
if (isCamping) { // 플레이어가 캠핑중인 경우
spawnTile = map.GetTileFromPosition(playerT.position);
// 타일 위치를 플레이어 위치로 설정
isCamping = false;
// 캠핑 해제
}
Material tileMat = spawnTile.GetComponent<Renderer>().material;
// 가져온 타일의 마테리얼 가져오기
Color initialColour = tileMat.color;
// 기존 타일 색상 저장
Color flashColour = Color.red;
// 변경할 색상 저장
float spawnTimer = 0;
// 적 소환 시간 타이머
while(spawnTimer < spawnDelay) { // 적 스폰 대기시간이 안된 경우
tileMat.color = Color.Lerp(initialColour, flashColour, Mathf.PingPong(spawnTimer * tileFlashSpeed, 1));
// 보간값을 통해 색상을 설정하여 깜빡이도록, PingPong 함수로 설정한 횟수만큼 깜빡이도록 속도를 설정.
spawnTimer += Time.deltaTime; // 타이머에 시간 추가
yield return null; // 한 프레임 대기
}
tileMat.color = initialColour; // 타일 색상 초기화
Enemy spawnedEnemy = Instantiate(enemy, spawnTile.position + Vector3.up, Quaternion.identity) as Enemy;
// 적을 인스턴스화를 통해 생성, 랜덤 타일 위치에 회전값 없이 배치
spawnedEnemy.OnDeath += OnEnemyDeath;
// 적이 죽을 때 위 메소드를 추가
}
// ■ 플레이어가 죽을 때 처리하는 메소드
void OnPlayerDeath() {
isDisable = true;
// 생존 여부 죽음(비활성화)으로 설정.
}
void OnEnemyDeath()
/* 적이 죽을 때 처리하는 메소드
적이 죽으면 LivingEntity 에서 OnDeath 를 호출하고,
OnDeath 는 이 메소드를 호출해서 적이 죽을 때 알려준다. */
{
enemiesRemainingAlive--; // 적의 수를 1 줄임
if(enemiesRemainingAlive == 0) // 적의 수가 0이 되면
{
NextWave(); // 다음 웨이브 실행
}
}
// ■ 플레이어 위치 리셋 메소드
void ResetPlayerPosition() {
playerT.position = map.GetTileFromPosition(Vector3.zero).position + Vector3.up * 1.5f;
// 플레이어 위치를 가운데 타일 위치로 설정
}
// ■ 다음 웨이브 실행 메소드
void NextWave()
{
currentWaveNumber++; // 웨이브 숫자 증가
if(currentWaveNumber - 1 < waves.Length) { // 배열 인덱스 예외 없도록 처리
currentWave = waves[currentWaveNumber - 1];
/* 현재 웨이브 레퍼런스 참조
(웨이브 숫자는 1부터 시작 할 것이므로 -1 하여 배열의 인덱스에 맞게 참조) */
enemiesRemainingToSpawn = currentWave.enemyCount;
// 이번 웨이브의 적 수 저장
enemiesRemainingAlive = enemiesRemainingToSpawn;
// 살아있는 적의 수를 스폰 할 적의 수로 저장
if (OnNewWave != null) { // 이벤트 구독자가 있는 경우
OnNewWave(currentWaveNumber); // 웨이브 번호 전달
}
ResetPlayerPosition(); // 플레이어 위치 초기화
}
}
[System.Serializable]
/* 스크립트 직렬화, 직렬화를 통해 객체, 변수 등을 선언 할 때의
접근 제한(private 등)은 유지되지만 인스펙터에서 값을 변경 가능하게 함 */
public class Wave
// 적의 주기, 스폰 주기 등 웨이브 정보를 저장 할 클래스 생성
{
public int enemyCount; // 적의 수
public float timeBetweenSpawns; // 적의 스폰 주기
}
}
| 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; // 셔플된 좌표값을 저장할 큐 생성
Queue<Coord> shuffledOpenTileCoords; // 셔플된 오픈 타일 좌표값을 저장할 큐 생성
Transform[,] tileMap; // 생성한 타일맵 배열
Map currentMap; // 현재 맵
void Start() {
FindObjectOfType<Spawner>().OnNewWave += OnNewWave; // 이벤트 구독
}
// ■ 웨이브 이벤트 구독자 메소드
void OnNewWave(int waveNumber) {
mapIndex = waveNumber - 1; // 맵 배열 인덱스 설정
GeneratorMap(); // 맵 생성
}
// ■ 맵 생성 메소드
public void GeneratorMap() {
currentMap = maps[mapIndex]; // 맵 설정
tileMap = new Transform[currentMap.mapSize.x, currentMap.mapSize.y]; // 타일맵 배열 크기 설정
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;
// 타일의 부모 오브젝트 설정
tileMap[x, y] = newTile;
// 타일맵 배열에 타일 저장
}
}
// ■ 장애물들을 생성
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); // 지정한 비율에 따른 장애물 개수
List<Coord> allOpenCoords = new List<Coord>(allTileCoords); // 오픈 타일 배열, 일단 모든 타일 좌표로 초기화.
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;
// 장애물의 셰어드 마테리얼 설정
allOpenCoords.Remove(randomCoord);
// 오픈 타일 좌표만 남기기 위해 모든 타일 좌표에서 장애물이 있는 타일 좌표를 빼준다.
}
else { // 장애물 생성 조건이 맞지 않는 경우
obstaclemap[randomCoord.x, randomCoord.y] = false; // 해당 랜덤 위치 비활성화
currentObstacleCount--; // 장애물 개수 감소
}
}
shuffledOpenTileCoords = new Queue<Coord>(Utility.ShuffleArray(allOpenCoords.ToArray(), currentMap.seed));
// 새 큐 생성, 셔플된 오픈 타일 좌표값 배열을 저장함
// ■ 내브메쉬 마스크 생성
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, 3, 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, 3, 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, 3, (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, 3, (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 Transform GetTileFromPosition(Vector3 position) {
int x = Mathf.RoundToInt(position.x / tileSize + (currentMap.mapSize.x - 1) / 2f);
// 타일 x 좌표 계산, int 형변환시 내림하기 때문에 메소드를 통해 정수로 반올림한다.
int y = Mathf.RoundToInt(position.z / tileSize + (currentMap.mapSize.y - 1) / 2f);
// 타일 y 좌표 계산.
x = Mathf.Clamp(x, 0, tileMap.GetLength(0) - 1);
// 타일맵 배열 인덱스 초과 오류 방지를 위해 x 값 제한.
y = Mathf.Clamp(y, 0, tileMap.GetLength(1) - 1);
// 타일맵 배열 인덱스 초과 오류 방지를 위해 y 값 제한.
return tileMap[x, y];
// 타일 위치 반환
}
// ■ 큐에 저장된 좌표를 가져오는 메소드
public Coord GetRandomCoord() {
Coord randomCoord = shuffledTileCoords.Dequeue(); // 큐의 첫 번째 값을 가져온다.
shuffledTileCoords.Enqueue(randomCoord); // 가져온 값을 큐의 맨 뒤로 넣는다.
return randomCoord;
}
// ■ 랜덤한 오픈 타일을 가져오는 메소드
public Transform GetRandomOpenTile() {
Coord randomCoord = shuffledOpenTileCoords.Dequeue(); // 큐의 첫 번째 값을 가져온다.
shuffledOpenTileCoords.Enqueue(randomCoord); // 가져온 값을 큐의 맨 뒤로 넣는다.
return tileMap[randomCoord.x, randomCoord.y]; // 큐에 저장된 오픈 타일을 반환
}
// ■ 타일 좌표 구조체
[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); // 중앙 좌표 지정 후 리턴
}
}
}
}
Spawner 에서는 웨이브를 담당하는 이벤트를 생성한다.
매개변수로 정수(웨이브 번호)를 받도록 한다.
이벤트를 만들고 구독을 하면 이벤트가 발생했을 때 모든 구독자에게 알림을 보낸다고 보면 된다.
따라서 MapGenerator 스크립트에서 이벤트를 구독,
Spawner 스크립트의 다음 웨이브 실행 메소드에서 이벤트를 실행.
MapGenerator 에서는 알림을 받고 타일맵을 변경하도록 한다.
웨이브가 넘어가고 맵 크기와 플레이어 위치에 따라
맵 재생성시 플레이어가 맵 밖에 위치하게 되는 경우가 발생한다.
이를 해결하기 위해 Spawner 스크립트에서 플레이어 위치 리셋 메소드 ResetPlayerPosition 을 추가하고,
다음 웨이브 실행 메소드에서 호출해 웨이브가 넘어가면 플레이어 위치를 초기화한다.
이제 실행해보자.
웨이브가 끝나면 맵이 변경되고 플레이어 위치가 초기화된다.
이제 게임이 끝나면 UI 를 띄워 게임 오버를 표시하고 다시 시작할지 선택하도록 해보자.
Canvas 오브젝트를 생성하자.
스케일 모드(사이즈를 자동으로 화면 비율에 맞추도록)와 해상도를 설정한다.
이제 캔버스에 이미지를 생성하자.
이름을 Fade 로 하고 사이즈와 색상을 조정한다.
게임이 끝나면 이 이미지를 배경으로 UI가 나타날 것이다.
이제 UI에 나타날 글자와 버튼을 만들어보자.
Game Over UI 라는 빈 오브젝트를 만들고 그 아래에
텍스트와 버튼을 생성하고 위치와 사이즈를 조정해준다.
이제 이 UI를 조작하기 위해 GameUI 라는 스크립트를 만들고 Canvas 에 넣어준다.
GameUI 에서는 플레이어가 죽으면 UI 를 띄우도록 한다.
이를 위해 플레이어가 죽을 때를 알아와야 하는데, LivingEntitiy 에서 생성한 OnDeath 이벤트를 구독해
알림을 받을 수 있다.
| GameUI 스크립트
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameUI : MonoBehaviour {
public Image fadePlane; // 페이드 이미지(UI 배경) 오브젝트 레퍼런스
public GameObject gameOverUI; // 게임 오버 텍스트, 버튼 오브젝트 레퍼런스
void Start() {
FindObjectOfType<Player>().OnDeath += OnGameOver; // 플레이어 사망 이벤트 구독
}
// ■ 플레이어 사망(게임 오버) 시 UI 처리 메소드
void OnGameOver() {
StartCoroutine(Fade(Color.clear, Color.black, 1)); // 배경 이미지 페이드 인 효과 코루틴 시작
gameOverUI.SetActive(true); // 게임 오버 텍스트, 버튼 오브젝트 활성화
}
// ■ 이미지 페이드 효과 코루틴
IEnumerator Fade(Color from, Color to, float time) {
float speed = 1 / time; // 페이드 효과 속도
float percent = 0; // 이미지 색상(투명도) 변화 퍼센트
while(percent < 1) { // 색상(투명도) 변화가 1(100%)미만일 때
percent += Time.deltaTime * speed; // 색상(투명도) 변화율 계산
fadePlane.color = Color.Lerp(from, to, percent); // 색상(투명도) 지정
yield return null; // 다음 프레임으로 건너뜀
}
}
// UI Input 부분
// ■ 새 게임 시작 메소드
public void startNewGame() {
Application.LoadLevel("Game"); // 게임 씬, 게임을 다시 로드
}
}
게임 플레이 중 플레이어가 죽기까지 매번 기다리기는 지루하다.
플레이어 오브젝트를 자체적으로 빠르게 제거할 수 있도록
LivingEntity 스크립트의 Die 메소드에 코드를 추가했다.
| Die 메소드
[ContextMenu("Self-Destruct")]
// 인스펙터에서 스크립트 컴포넌트 우클릭 시 자체 파괴 버튼 추가
protected void Die() // 죽음 메소드
{
dead = true; // 죽은 판정으로 변경.
if(OnDeath != null) // 이벤트가 있는 경우
{
OnDeath(); // 메소드를 호출
}
GameObject.Destroy(gameObject); // 현재 오브젝트 파괴
}
이제 Canvas 오브젝트로 가서 GameUI 스크립트의
Fade Plane 는 Fade 오브젝트를, Game Over UI 는 Game Over UI 오브젝트를 할당해주고
버튼으로 이동해 클릭 시 실행 될 메소드를 Canvas 오브젝트 GameUI 의 startNewGame 으로 설정해준다.
이제 실행해보자.
플레이어가 사망(오브젝트 삭제)하면 게임 오버 UI가 나타나고,
버튼을 누르면 게임이 다시 시작된다.
이제 다음 강의부터는 총 변경, 이펙트 틍 게임에 부가적인 요소들을 추가하여
게임을 좀 더 재밌게 만들어 본다.
'Unity > 게임개발' 카테고리의 다른 글
[Unity] 게임 개발 일지 | 탑다운 슈팅 16 (0) | 2023.12.07 |
---|---|
[Unity] 게임 개발 일지 | 탑다운 슈팅 15 (2) | 2023.12.06 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 13 (2) | 2023.12.05 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 12 (0) | 2023.12.01 |
[Unity] 게임 개발 일지 | 탑다운 슈팅 11 (1) | 2023.11.30 |