EasyCastleUNITY

드래곤 플라이트 모작 개발일지 8 (보스 등장, 보스 패턴 생성) 본문

2D 프로젝트 개발 일지(드래곤 플라이트 모작)

드래곤 플라이트 모작 개발일지 8 (보스 등장, 보스 패턴 생성)

EasyCastleT 2023. 9. 15. 18:21

이전 개발일지에서 보스의 애니메이션을 완성했습니다. 

그래서 이번에는 보스가 토벌점수가 3만점을 넘어가면 나오도록 해보겠습니다.

보스 오브젝트 풀링

보스를 계속 생성하는 것이 아니라, 시작할때, 미리 만들어 두고, 활용하는 방법으로 구현해보았습니다. 

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

public class DragonPoolManager : MonoBehaviour
{
    //화이트 드래곤을 미리 생성해 저장할 리스트 
    [SerializeField] private List<GameObject> whitePool = new List<GameObject>();
    //골드 드래곤을 미리 생성해 저장할 리스트 
    [SerializeField] private List<GameObject> goldPool = new List<GameObject>();
    //보스 드래곤을 미리 생성해 저장할 리스트 
    [SerializeField] private List<GameObject> bossPool = new List<GameObject>();
    //오브젝트 풀에 생성할 드래곤의 최대 수 
    [SerializeField] private int maxDragons = 10;
    [SerializeField] private int maxBossDragons = 3;
    //싱글톤 인스턴스 선언
    public static DragonPoolManager instance=null;
    //드래곤 프리팹들 
    [SerializeField] private GameObject[] dragonPrefabs; //0이 화이트, 1이 골드 
    [SerializeField] private GameObject bossDragonPrefab;

    public List<GameObject> WhitePool { get => whitePool;  }
    public List<GameObject> GoldPool { get => goldPool;  }

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
        }
        else if (instance != null)
        {
            Destroy(this.gameObject);
        }
        // 아직 게임 씬만 구성하지만
        // 추후에 새로운 씬도 추가할 수 있도록 사용 
        DontDestroyOnLoad(gameObject);
        this.CreateDragonPool();
    }
    // Start is called before the first frame update
    void Start()
    {
       
    }

    private void CreateDragonPool()
    {
        for(int i=0; i<maxDragons; i++)
        {
            GameObject whiteGo = Instantiate(this.dragonPrefabs[0]);
            whiteGo.SetActive(false);
            whiteGo.transform.SetParent(this.transform);
            whitePool.Add(whiteGo);

            GameObject goldGo = Instantiate(this.dragonPrefabs[1]);
            goldGo.SetActive(false);
            goldGo.transform.SetParent(this.transform);
            goldPool.Add(goldGo);
        }

        for(int i=0; i<maxBossDragons; i++)
        {
            GameObject bossGo = Instantiate(this.bossDragonPrefab);
            bossGo.SetActive(false);
            bossGo.transform.SetParent (this.transform);
            bossPool.Add(bossGo);
        }
    }
    //풀에서 드래곤 받아가기 
    public GameObject GetWhiteDragonInPool()
    {
        foreach(var white in this.whitePool)
        {
            if(white.activeSelf == false)
            {
                return white;
            }
        }
        return null;
    }

    public GameObject GetGoldDragonInPool()
    {
        foreach (var gold in this.goldPool)
        {
            if (gold.activeSelf == false)
            {
                return gold;
            }
        }
        return null;
    }

    public GameObject GetBossDragonInPool()
    {
        foreach (var boss in this.bossPool)
        {
            if (boss.activeSelf == false)
            {
                return boss;
            }
        }
        return null;
    }
    //돌아오기 
    public void ReleaseDragon(GameObject dragonGo)
    {
        dragonGo.SetActive(false);
        dragonGo.transform.SetParent(this.transform);
    }
}

같은 드래곤이기에, 전에 작성한 DragonPoolManager를 수정하여, 보스 또한 만들도록 했습니다. 

DragonGenerator에 해당 메서드 추가

위와 같은 메서드를 추가하여, 풀에서 보스 드래곤을 받아오도록 했고

메인의 일부분 Update 문 안

메인에서, 토벌점수와 함께 올라가는 bossSummonPoint라는 변수를 만들고, 이 변수의 값이 30,000 이 넘어가면 

보스가 소환되도록 하였습니다. 

 

그리고 보스가 소환되고, 보스가 살아있으면, 다른 드래곤들은 소환되면 안되기에,

stateAliveBossDragon이라는 bool 변수를 통해, 보스가 살아있으면, true로, 죽으면, false로 바꾸는 방식으로 

일반 드래곤의 소환을 제어합니다.

일반 드래곤 소환

이렇게 오브젝트 풀링을 통해, 보스를 소환해 보는 방식이 구현되었습니다.


보스 패턴

보스의 패턴은 총 3가지로, Idle,Attack,Summon 입니다. 

Idle은 아무것도 하지 않는 상태이고, Attack은 총알을 발사하는 공격을, Summon은 작은 드래곤, 해츨링을 소환합니다. 

보스의 애니메이터

보스의 애니메이션은 트리거 변수에 의하여 제어하고 있습니다. 

작성한 BossDragon 스크립트 입니다.

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

public class BossDragon : Dragon
{
    private enum eState
    {
        Idle,Attack,Summon
    }
    private eState state;
    private int deathCount = 0;
    private Animator anim;
    private float elapsedTime = 0.0f;
    private BossAnimationReceiver receiver;
    [SerializeField] private GameObject bossBulletPrefab;

    [SerializeField] private Transform[] bulletInitPoses; //0이 왼쪽, 1이 중앙, 2가 오른쪽
    private void Awake()
    {
        Debug.Log("BossDragon 실행");
        GameMain gameMain = GameObject.FindAnyObjectByType<GameMain>();
        gameMain.BossDragon = this;
        this.anim = this.GetComponent<Animator>();
        this.receiver = this.GetComponent<BossAnimationReceiver>();
        this.anim.SetTrigger("Idle");
        this.state = eState.Idle;
    }
    private void OnEnable()
    {
        Debug.LogFormat("<color=red>Death Count:{0}</color>",this.deathCount);
        this.hp = 30 * deathCount;
    }
    private void OnDisable()
    {
        this.deathCount++;
    }
    // Start is called before the first frame update
    void Start()
    {
        this.receiver.onFinished = () => {
            this.state = eState.Idle;
            this.anim.SetTrigger("Idle");
        };
        this.receiver.onAttackLeft = () =>
        {
            GameObject bullet=Instantiate(this.bossBulletPrefab);
            bullet.transform.position = this.bulletInitPoses[0].position;
        };

        this.receiver.onAttackRight = () =>
        {
            GameObject bullet = Instantiate(this.bossBulletPrefab);
            bullet.transform.position = this.bulletInitPoses[2].position;
        };

        this.receiver.onAttackFront = () =>
        {
            for(int i = 0; i < 3; i++)
            {
                GameObject bullet = Instantiate(this.bossBulletPrefab);
                bullet.transform.position = this.bulletInitPoses[1].position;
            }
        };
    }

    // Update is called once per frame
    void Update()
    {
        if(this.state == eState.Idle) //Idle 상태일때만 시간 계산
        {
            this.elapsedTime += Time.deltaTime;
            
        }
        else if(this.state == eState.Attack)
        {
            this.anim.SetTrigger("Attack");
        }
        else if(this.state == eState.Summon)
        {
            this.anim.SetTrigger("Summon");
        }

        if(this.elapsedTime > 2.0f)
        {
            this.elapsedTime = 0.0f;
            int rndState =Random.Range(1, 3); //1~2 즉, Attack이나 Summon 둘중하나
            this.state = (eState)rndState;
        }

        
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Bullet"))
        {
            Debug.Log("<color=red>Boss Dragon이 총알과 충돌함</color>");
            this.HitDamage();
        }
    }

    private void HitDamage()
    {
        this.hp -= 1;
        if (this.hp <= 0)
        {
            this.Die();
        }
    }

    private void Die()
    {
        this.deadPos = this.transform.position; //죽었을 때의 좌표를 저장
        onDie(this.deadPos);
        DragonPoolManager.instance.ReleaseDragon(this.gameObject);
    }
}

이 코드를 읽다보면, 아실 수 있는데 바로, BossAnimationReceiver의 존재입니다. 

이 스크립트는 애니메이션 이벤트를 활용하기 위해 작성하였습니다. 

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

[RequireComponent(typeof(Animator))] //이게 붙어있으면, 해당 요소는 제거하지 못함

public class BossAnimationReceiver : MonoBehaviour
{
    public System.Action onFinished;
    public System.Action onAttackRight;
    public System.Action onAttackLeft;
    public System.Action onAttackFront;
    
    public void OnFinished()
    {
        if(this.onFinished != null)
        {
            this.onFinished();
        }
    }
    public void OnAttackRight()
    {
        if (this.onAttackRight != null)
        {
            this.onAttackRight();
        }
    }

    public void OnAttackLeft()
    {
        if (this.onAttackLeft != null)
        {
            this.onAttackLeft();
        }
    }

    public void OnAttackFront()
    {
        if(this.onAttackFront != null)
        {
            this.onAttackFront();
        }
    }
}

BossAnimationReceiver는 각각의 메서드를 가지고 있고, BossDragon내의 대리자와 연결되어 있습니다. 

그러므로 이 메서드들을 호출하는 것이 관건이 되는데, 애니메이션의 애니메이션 이벤트를 통해, 

호출이 되게 됩니다. 

만들어둔 Attack 애니메이션

위에 사진에서 OnAttackRight라고 보이는 부분이 애니메이션 이벤트 부분으로, 

애니메이션이 실행되며, 해당 프레임이 되면, 그 프레임에 연결된 이벤트가 호출이 되게 하는 방법입니다. 

그래서, BossDragon의 Start 부분의 대리자 이벤트들이 실행되게 됩니다. 

BossDragon의 Start 부분

그리고 BossDragon 스크립트를 보면, 열거형을 이용하여, 현재 상태를 저장하고 있습니다. 

이에 따라, 해당되는 애니메이션이 실행되게 됩니다. 

지금은 각각 50% 확률로, Attack을 할지 Summon을 할지 결정됩니다. 

Attack 상태가 되면, Attack 애니메이션을 하는데, 그 애니메이션에서 애니메이션 이벤트를 통해, 

대리자가 호출되며, 호출된 대리자가 실행되며, 보스 드래곤이 총알을 발사 합니다. 

발사되는 총알은 아래와 같은 스크립트를 가지고 있습니다.

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

public class EnemyBullet : MonoBehaviour
{
    private float moveSpeed;

    // Start is called before the first frame update
    void Start()
    {
        this.moveSpeed = 5.0f;
    }

    // Update is called once per frame
    void Update()
    {
        this.transform.Translate(Vector2.down * Time.deltaTime * this.moveSpeed);
        this.SelfDestory();
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if(collision.CompareTag("Player")){
            Destroy(this.gameObject);
        }
    }

    private void SelfDestory()
    {
        if (this.transform.position.y < -5.7f)
        {
            Destroy(this.gameObject);
        }
    }
}

이러한 과정을 거쳐 아래와 같은 결과가 나오게 됩니다. 

애니메이션의 맞추어 총알을 발사하는 모습

여기서 총알이 플레이어가 있는 방향으로 발사되도록 해야 하지만, 아직은 구현하지 못했습니다.

하지만, 그 부분만 구현되면, 보스의 Attack 패턴은 구현이 완료됩니다. 

보스의 Summon 패턴