K-digital traning/Final Project

Gazzlers 개발일지 - Enemy 관리

내꺼블로그 2023. 12. 21. 12:58

이번에는 Enemy의 생성 및 관리를 구현해보려고 한다.

원작에서는 한 화면 안에서 Enemy가 최대 2마리씩 나오며 각 웨이브마다 생성되는 최대 Enemy 수량이 존재한다.

이 수량을 다 채울 때까지 Enemy를 생성 및 spawn하는 부분을 구현하려고 한다.

 

 

구현계획

1. 일정 시간간격을 두고 enemy 2마리 생성

2. ui 버튼을 만들어서 버튼을 클릭할 때마다 enemy Die

3. enemy 최대 수량을 채우지 못했을 경우 enemy 생성, 그렇지 않을 경우 enemy 생성 중단.

 

 

 

 

1. 일정 시간간격을 두고 enemy 2마리 생성(기존 TestEnemy1MoveMain 스크립트 참고)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


    public class TestManageEnemyMain : MonoBehaviour
    {
        private List<Enemy1Move> listEnemys = new List<Enemy1Move>();
        private int maxEnemyCount = 2;
        public float delayGenerateEnemy = 5f;

        void Start()
        {
            EnemyPoolManager.Instance.GenerateEnemy();
            this.StartWave();
        }

        //void Update()
        //{

        //}

        private void StartWave()
        {
            Debug.Log("Start Wave!");
            this.StartCoroutine(this.CoStartWave());
        }

        private IEnumerator CoStartWave()
        {
            for(int i=0;i<maxEnemyCount;i++)
            {
                this.GenerateAndSpawnEnemy();
                yield return new WaitForSeconds(this.delayGenerateEnemy);
            }
        }

        private void GenerateAndSpawnEnemy()
        {
            float x = Random.Range(-3f, 3f);
            float z = Random.Range(10f, 15f);
            Vector3 pos = Vector3.right * x + Vector3.forward * z;
            while (true)
            {
                int layer = 1 << 3 | 1 << 6 | 1 << 7 |1<<9;
                Collider[] hit = new Collider[10];
                int num = Physics.OverlapSphereNonAlloc(pos, 2f, hit, layer);
                Debug.LogFormat("num:{0}", num);
                if (num == 0)
                    break;
                x = Random.Range(-3f, 3f);
                z = Random.Range(10f, 20f);
                pos = Vector3.right * x + Vector3.forward * z;
            }
            Enemy1Move enemy = EnemyPoolManager.Instance.EnableEnemy();
            Debug.Log(enemy);
            enemy.Init(pos);
            listEnemys.Add(enemy);
            Debug.LogFormat("{0} pos: {1}", enemy, pos);
        }
    }

 

 

 

 

 

 

 

 

 

 

2. ui 버튼을 만들어서 버튼을 클릭할 때마다 enemy Die

 

 

btnEnemyDie 버튼 생성

 

 

 

 

TestManageEnemyMain 스크립트에 btnEnemyDie 할당.

btnEnemyDie를 클릭할 때마다 스크립트 내 구현한 EnemyDie()메서드 호출

(현재 구현된 EnemyDie 메서드는 화면 내에 있는 enemy 중 하나를 랜덤으로 골라 처치)

 

 

 

결과

 

 

 

 

 

 

3. enemy 최대 수량을 채우지 못했을 경우 enemy 생성, 그렇지 않을 경우 enemy 생성 중단.

 

 

(1) 최대수량 무시하고 enemy가 처치될 때마다 다른 enemy 생성

 

Invoke를 사용하여 enemy가 죽을 때마다 몇 초 후 GenerateAndSpawnEnemy를 호출.

 

 

 

 

 

 

(2)최대 수량 생성된 상태면 더이상 생성되지 않도록 조절

 

웨이브 내 최대 enemy 수량과 생성되어온 enemy 수량 선언

 

enemy를 생성할 때마다(GenerateAndSpawnEnemy를 호출할 때마다) generatedEnemyCount 값을 +1함.

 

 

EnemyDie 메서드에서 Invoke로 GenerateAndSpawnEnemy를 호출하려 할 때 생성되어온 enemy가 wave에서 최대로 생성되어야 하는 enemy수량을 넘겼는지 아닌지를 체크.

수량을 넘기지 않았을 경우에만 enemy를 생성

 

 

 

 

영상

 

 

 

 

 

코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

    public class TestManageEnemyMain : MonoBehaviour
    {
        [SerializeField] private Button btnEnemyDie;
        private List<Enemy1Move> listEnemys = new List<Enemy1Move>();
        private int maxEnemyCount = 2;
        private int maxWaveEnemyCount = 3;
        private int generatedEnemyCount = 0;
        public float delayGenerateEnemy = 3f;

        void Start()
        {
            this.btnEnemyDie.onClick.AddListener(() =>
            {
                this.EnemyDie();
            });
            EnemyPoolManager.Instance.GenerateEnemy();
            this.StartWave();
        }

        private void StartWave()
        {
            Debug.Log("Start Wave!");
            this.StartCoroutine(this.CoStartWave());
        }

        private IEnumerator CoStartWave()
        {
            for(int i=0;i<maxEnemyCount;i++)
            {
                this.GenerateAndSpawnEnemy();
                yield return new WaitForSeconds(this.delayGenerateEnemy);
            }
        }

        private void GenerateAndSpawnEnemy()
        {
            float x = Random.Range(-3f, 3f);
            float z = Random.Range(10f, 15f);
            Vector3 pos = Vector3.right * x + Vector3.forward * z;
            while (true)
            {
                int layer = 1 << 3 | 1 << 6 | 1 << 7 |1<<9;
                Collider[] hit = new Collider[10];
                int num = Physics.OverlapSphereNonAlloc(pos, 2f, hit, layer);
                Debug.LogFormat("num:{0}", num);
                if (num == 0)
                    break;
                x = Random.Range(-3f, 3f);
                z = Random.Range(10f, 20f);
                pos = Vector3.right * x + Vector3.forward * z;
            }
            Enemy1Move enemy = EnemyPoolManager.Instance.EnableEnemy();
            //Debug.Log(enemy);
            enemy.Init(pos);
            listEnemys.Add(enemy);
            //Debug.LogFormat("{0} pos: {1}", enemy, pos);
            this.generatedEnemyCount++;
        }

        private void EnemyDie()
        {
            int idx = Random.Range(0, this.listEnemys.Count);
            Enemy1Move deadEnemy = this.listEnemys[idx];
            EnemyPoolManager.Instance.DisableEnemy(deadEnemy);
            this.listEnemys.Remove(deadEnemy);
            if (this.generatedEnemyCount < this.maxWaveEnemyCount)
                Invoke("GenerateAndSpawnEnemy", this.delayGenerateEnemy);
        }
    }

 

 

 

 

 

 

+추가구현

4. 웨이브 별 enemy 최대 생성 수량 구현

5. 웨이브 start, completed 구분

6. 웨이브 별 출현하는 enemy 종류까지 고려해서 구현

 

 

 

 

 

 

 

4. 웨이브 별 enemy 최대 생성 수량 구현

 

enum으로 wave 표현

 

 

 

 

 

5. 웨이브 start, completed 구분

 

 

 

 

 

 

 

 

 

오류 발생

 

 

이유: generatedEnemyCount를 GenerateAndSpawnEnemy가 호출될 때 증가시키므로 GenerateAndSpawnEnemy가 호출되기 전에 EnemyDie가 호출되면서 generatedEnemyCount가 증가되기 전 값으로 if문이 돌아가기 때문.


해결방안 : 생성된 enemy 수량을 기준으로 하지 말고 처치하고 남은 enemy 수량을 기준으로 하쟈.

 

 

 

 

 

 

generatedEnemyCount를 remainEnemyCount로 변경하고 enemy가 죽을 때마다 remainEnemyCount를 감소시킨다.

기존에 만들어놓은 maxWaveEnemyCount를 웨이브 별 처치해야 하는 enemy수량으로 간주하여 remainEnemyCount를 웨이브 별 처치해야 되는 enemy 수량으로 초기화시키고 EnemyDie가 호출 될 때 화면에 나온 enemy가 2(maxEnemyCount)보다는 반드시 적음을 인지하여 남아있는 수량(remainEnemyCount)이 2보다 크거나 같을 경우 enemy를 생성하도록 한다.

그렇지 않을 경우에는 enemy를 생성하지 않되 남아있는 enemy 수량이 없을 경우 wave는 complete된다.

 

 

 

 

 

 

 

 

결과 영상

 

 

 

 

 

 

 

코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;


    public class TestManageEnemyMain : MonoBehaviour
    {
        public enum eWave
        {
            WAVE1, WAVE2, WAVE3
        }
        private eWave wave;
        [SerializeField] private Button btnEnemyDie;
        [SerializeField] private Button btnStartWave;
        private List<Enemy1Move> listEnemys = new List<Enemy1Move>();
        private int maxEnemyCount = 2;
        private int[] maxWaveEnemyCount = new int[3] { 3, 9, 15 };
        private int remainEnemyCount;
        public float delayGenerateEnemy = 1f;

        void Start()
        {
            this.btnStartWave.onClick.AddListener(() =>
            {
                this.btnEnemyDie.gameObject.SetActive(true);
                this.StartWave();
            });
            this.btnEnemyDie.onClick.AddListener(() =>
            {
                this.EnemyDie();
            });
            EnemyPoolManager.Instance.GenerateEnemy();
        }

        private void StartWave()
        {
            Debug.LogFormat("{0} Start", this.wave.ToString());
            this.remainEnemyCount = maxWaveEnemyCount[(int)this.wave];
            Debug.LogFormat("remain Enemy: {0}", this.remainEnemyCount);
            this.btnStartWave.gameObject.SetActive(false);
            this.StartCoroutine(this.CoStartWave());
        }

        private IEnumerator CoStartWave()
        {
            for(int i=0;i<maxEnemyCount;i++)
            {
            	yield return new WaitForSeconds(this.delayGenerateEnemy);
                this.GenerateAndSpawnEnemy();
            }
        }

        private void GenerateAndSpawnEnemy()
        {
            float x = Random.Range(-3f, 3f);
            float z = Random.Range(10f, 15f);
            Vector3 pos = Vector3.right * x + Vector3.forward * z;
            while (true)
            {
                int layer = 1 << 3 | 1 << 6 | 1 << 7 |1<<9;
                Collider[] hit = new Collider[10];
                int num = Physics.OverlapSphereNonAlloc(pos, 2f, hit, layer);
                //Debug.LogFormat("num:{0}", num);
                if (num == 0)
                    break;
                x = Random.Range(-3f, 3f);
                z = Random.Range(10f, 20f);
                pos = Vector3.right * x + Vector3.forward * z;
            }
            Enemy1Move enemy = EnemyPoolManager.Instance.EnableEnemy();
            //Debug.Log(enemy);
            enemy.Init(pos);
            listEnemys.Add(enemy);
            //Debug.LogFormat("{0} pos: {1}", enemy, pos);
        }

        private void EnemyDie()
        {
            this.remainEnemyCount--;
            Debug.LogFormat("remain Enemy: {0}", this.remainEnemyCount);
            int idx = Random.Range(0, this.listEnemys.Count);
            Enemy1Move deadEnemy = this.listEnemys[idx];
            EnemyPoolManager.Instance.DisableEnemy(deadEnemy);
            this.listEnemys.Remove(deadEnemy);
            if (this.remainEnemyCount >= this.maxEnemyCount)
            {
                Invoke("GenerateAndSpawnEnemy", this.delayGenerateEnemy);
            }
            else
            {
                if (this.remainEnemyCount == 0)
                {
                    Debug.LogFormat("{0} completed", this.wave.ToString());
                    if (this.wave == eWave.WAVE3) return;
                    this.btnEnemyDie.gameObject.SetActive(false);
                    this.btnStartWave.gameObject.SetActive(true);
                    this.wave++;
                }
            }
        }
    }

 

 

 

 

 

 

6. 웨이브 별 출현하는 enemy 종류까지 고려해서 구현

 

(1) EnemyPoolManager 스크립트 수정

arrEnemyPrefabs에다 type별 enemy의 prefab들을 할당한다.

enemyPool 이중배열을 두어 row로 원하는 enemy의 type을 접근할 수 있도록 구현한다.

 

enemy 생성 및 외부에서 enemy를 얻는 함수 구현 부분.

기존 오브젝트풀링과 크게 다른 점은 X

GenerateEnemy는 이중포문을 사용하여 enemy type별로 2마리씩 생성하도록 구현.

 

 

 

enemy type 순서대로 할당.

enemy2 : 오토바이

enemy3 : 비행체

 

 

 

(2)TestManageEnemyMain 스크립트 수정

 

현재 Wave에 따라 나올 수 있는 enemy의 type에 변화를 주기 위해 작성된 코드.

exclusive max값을 wave+1로 두어 Wave에 따라 index의 Range에 제한을 둠.

(ex. wave1이면 0~1 즉 enemy1만, wave2면 0~2, enemy1과 enemy2가 생성되도록 구현)

 

 

결과

 

 

 

 

 

 

 

코드

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;


    public class TestManageEnemyMain : MonoBehaviour
    {
        public enum eWave
        {
            WAVE1, WAVE2, WAVE3
        }
        private eWave wave;
        [SerializeField] private Button btnEnemyDie;
        [SerializeField] private Button btnStartWave;
        private List<Enemy1Move> listEnemys = new List<Enemy1Move>();
        private int maxEnemyCount = 2;
        private int[] maxWaveEnemyCount = new int[3] { 3, 9, 15 };
        private int remainEnemyCount;
        public float delayGenerateEnemy = 1f;

        void Start()
        {
            this.btnStartWave.onClick.AddListener(() =>
            {
                this.btnEnemyDie.gameObject.SetActive(true);
                this.StartWave();
            });
            this.btnEnemyDie.onClick.AddListener(() =>
            {
                this.EnemyDie();
            });
            EnemyPoolManager.Instance.GenerateEnemy();
        }

        private void StartWave()
        {
            Debug.LogFormat("{0} Start", this.wave.ToString());
            this.remainEnemyCount = maxWaveEnemyCount[(int)this.wave];
            Debug.LogFormat("remain Enemy: {0}", this.remainEnemyCount);
            this.btnStartWave.gameObject.SetActive(false);
            this.StartCoroutine(this.CoStartWave());
        }

        private IEnumerator CoStartWave()
        {
            for(int i=0;i<maxEnemyCount;i++)
            {
                yield return new WaitForSeconds(this.delayGenerateEnemy);
                this.GenerateAndSpawnEnemy();
            }
        }

        private void GenerateAndSpawnEnemy()
        {
            float x = Random.Range(-3f, 3f);
            float z = Random.Range(10f, 15f);
            Vector3 pos = Vector3.right * x + Vector3.forward * z;
            while (true)
            {
                int layer = 1 << 3 | 1 << 6 | 1 << 7 |1<<9;
                Collider[] hit = new Collider[10];
                int num = Physics.OverlapSphereNonAlloc(pos, 2f, hit, layer);
                //Debug.LogFormat("num:{0}", num);
                if (num == 0)
                    break;
                x = Random.Range(-3f, 3f);
                z = Random.Range(10f, 20f);
                pos = Vector3.right * x + Vector3.forward * z;
            }
            int idx = Random.Range(0, (int)this.wave + 1);
            Debug.LogFormat("idx: {0}", idx);
            Enemy1Move enemy = EnemyPoolManager.Instance.EnableEnemy(idx);
            //Debug.Log(enemy);
            enemy.Init(pos);
            listEnemys.Add(enemy);
            //Debug.LogFormat("{0} pos: {1}", enemy, pos);
        }

        private void EnemyDie()
        {
            this.remainEnemyCount--;
            Debug.LogFormat("remain Enemy: {0}", this.remainEnemyCount);
            int idx = Random.Range(0, this.listEnemys.Count);
            Enemy1Move deadEnemy = this.listEnemys[idx];
            EnemyPoolManager.Instance.DisableEnemy(deadEnemy);
            this.listEnemys.Remove(deadEnemy);
            if (this.remainEnemyCount >= this.maxEnemyCount)
            {
                Invoke("GenerateAndSpawnEnemy", this.delayGenerateEnemy);
            }
            else
            {
                if (this.remainEnemyCount == 0)
                {
                    Debug.LogFormat("{0} completed", this.wave.ToString());
                    if (this.wave == eWave.WAVE3) return;
                    this.btnEnemyDie.gameObject.SetActive(false);
                    this.btnStartWave.gameObject.SetActive(true);
                    this.wave++;
                }
            }
        }
    }

 

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;


    public class EnemyPoolManager : MonoBehaviour
    {
        public static EnemyPoolManager Instance;

        [SerializeField] private GameObject[] arrEnemyPrefabs;
        private List<List<Enemy1Move>> enemyPool = new List<List<Enemy1Move>>();
        // Start is called before the first frame update
        void Awake()
        {
            Instance = this;
        }

        public void GenerateEnemy()
        {
            for (int i = 0; i < this.arrEnemyPrefabs.Length; i++)
            {
                List<Enemy1Move> list = new List<Enemy1Move>();
                for(int j=0;j<2;j++)
                {
                    GameObject go = Instantiate(arrEnemyPrefabs[i], this.transform);
                    go.SetActive(false);
                    list.Add(go.GetComponent<Enemy1Move>());
                }
                enemyPool.Add(list);
            }
        }

        public Enemy1Move EnableEnemy(int idx)
        {
            Enemy1Move result = null;
            for(int i = 0; i < enemyPool[idx].Count;i++) {
                if (enemyPool[idx][i].gameObject.activeSelf == false)
                {
                    result = enemyPool[idx][i];
                    //result.gameObject.SetActive(true);
                    result.transform.SetParent(null);
                    break;
                }
            }
            if(result == null)
            {
                result = Instantiate(arrEnemyPrefabs[idx]).GetComponent<Enemy1Move>();
                enemyPool[idx].Add(result);
            }
            return result;
        }

        public void DisableEnemy(Enemy1Move enemy)
        {
            enemy.gameObject.SetActive(false);
            enemy.transform.SetParent(this.transform);
        }
    }