지난번에 이어 게임을 만들어보자.
이번에 참고할 강의는 아래 강의이다.
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 | 
