EasyCastleUNITY

드래곤 플라이트 모작 개발일지11 (보스 드래곤 패턴 구현) 본문

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

드래곤 플라이트 모작 개발일지11 (보스 드래곤 패턴 구현)

EasyCastleT 2023. 9. 20. 18:23

아이템 효과 구현을 마치고, 이제 가장 중요하다고 볼 수 있는 보스의 패턴을 만들어 보겠습니다. 

보스의 패턴은, 공격과 소환으로 나누어집니다. 

 

그중, 먼저 공격 패턴을 만들어 보겠습니다. 


공격 패턴

공격 패턴은 보스가 플레이어를 향해 총알을 발사합니다. 

왼쪽에서 2번, 오른쪽에서 2번, 가운데에서 한번에 총알 3개를 발사합니다. 

 

이러한 공격의 필요한 요소는 총알을 발사하는 위치들, 언제 발사가 되느냐, 어떻게 총알이 플레이어 쪽으로 날아오게 하는지 입니다.

먼저, 총알을 발사하는 위치들을 지정해 보겠습니다.

미리 만들어둔 보스 프리팹을 화면으로 가져옵니다.

화면 상으로 가져온 보스 프리팹

 

위의 3개의 사진 처럼, 빈 오브젝트를 이용하여, 총알의 발사위치를 정하고, 보스의 자식으로 만들어 주었습니다.

이렇게 보스는 총알을 발사하는 위치에 대한 정보를 가지고 있게 됩니다. 

 

그리고 이 위치들을 보스 스크립트의 배열의 형태로 저장하였습니다. 0이 왼쪽, 1이 중앙, 2가 오른쪽입니다. 

 

보스 총알 제작

이제 총알의 발사 위치가 지정되었으니, 총알을 만들어 보겠습니다.

총알은 위와 같은 스프라이트가 사용되며, 충돌처리를 위해, 콜라이더와 리지드바디를 가지고 있습니다. 

이 총알을 보스 드래곤의 프리팹으로써 저장합니다. 

그리고, 총알이 발사되는 타이밍의 Instantiate를 하도록 합니다. 

보스 드래곤 스크립트의 총알 프리팹 저장

총알이 플레이어 방향으로 날아감

이제 스크립트를 추가하여, 플레이어 방향으로 날아가도록 해보겠습니다. 

아래에 스크립트를 작성하여, 부착하였습니다.

EnemyBullet

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

public class EnemyBullet : MonoBehaviour
{
    private float moveSpeed;
    Player player;
    Vector3 playerPos;
    Vector3 direction;
    
    // Start is called before the first frame update
    void Start()
    {
        this.moveSpeed = 1.0f;
        this.player = GameObject.FindAnyObjectByType<Player>();
        if(this.player != null)
        {
            this.playerPos = this.player.transform.position;
        }
        else this.playerPos = Vector3.zero;
        
        this.direction = this.playerPos - this.transform.position;
    }

    // Update is called once per frame
    void Update()
    {
        this.transform.Translate(this.direction * Time.deltaTime * 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);
        }
    }
}

생성되면, Player 스크립트를 갖고 있는 플레이어를 찾고, 그 당시에 플레이어의 위치를 저장합니다. 

그리고, 플레이어의 위치와 현재 자신의 위치를 뺄셈하여, 방향을 찾습니다. 

 

그리고 Update에서 그 방향을 향해 날아가도록 지정해주면, 끝입니다. 

테스트 결과

그럼 이제 언제 발사하는지에 대해 다루겠습니다. 

 

저는 애니메이션 이벤트를 사용했습니다. 

미리 만들어둔 보스의 애니메이션의 애니메이션 이벤트를 등록하고, 보스 스크립트에서 

대리자를 통해, 애니메이션 이벤트를 호출하는 시점의 총알을 만들도록 했습니다. 

등록한 애니메이션 이벤트들

하단에 보이는 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 System.Action onSummon;
    
    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();
        }
    }

    public void OnSummon()
    {
        if(this.onSummon != null)
        {
            this.onSummon();
        }
    }
}

이렇게 보스가 플레이어를 향해 총알을 발사하는 공격 패턴을 만드는데 성공했습니다. 

 

공격 패턴 실행결과


이제 소환패턴을 만들어 보겠습니다.

소환패턴

소환 패턴도 공격패턴과 비슷합니다. 

먼저 해츨링들을 소환할 좌표를 보스에게 등록해주었습니다.

해츨링 소환좌표들

이렇게 소환좌표들을 저장받은 다음,

애니메이션 이벤트를 통해, 대리자를 호출하여 이벤트를 호출합니다. 

OnSummon 이벤트를 호출하고 

대리자로 전달하여, CallHatchling 함수를 호출합니다. 

저장한 소환좌표의 수만큼 반복하여, 오브젝트 풀링을 통해 만들어둔 해츨링들을 받아옵니다. 

그 다음, 지정된 좌표들에 해츨링을 배치시킵니다. 

해츨링 소환


패턴 전환

보스의 가장 중요한 점이, 패턴의 자연스러운 전환일 것입니다.

그래서 Idle이 되고, 일정 시간(3초)이 지나면, 소환과 공격 패턴 중 하나가 실행되도록 하였습니다. 

(공격은 90프로, 소환은 10프로의 확률로 실행된다.)

보스는 열거형의 상태를 가지고 있고, 해당 상태가 되면, 그에 맞는 애니메이션을 합니다. 

그리고 이 애니메이션에서 애니메이션 이벤트가 호출되고, 그에 맞는 함수가 실행되게 됩니다. 

 

아래는 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 = 1;
    private Animator anim;
    private float elapsedTime = 0.0f;

    public new int MaxHp
    {
        get { return base.maxHp; }
    }
    private BossAnimationReceiver receiver;
    [SerializeField] private GameObject bossBulletPrefab;
    [SerializeField] private GameObject hatchlingPrefab;

    [SerializeField] private Transform[] bulletInitPoses; //0이 왼쪽, 1이 중앙, 2가 오른쪽
    [SerializeField] private Transform[] hatchlingInintPoses;
    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;
        base.maxHp = 30;
    }
    private void OnEnable()
    {
        if (this.deathCount == 0)
        {
            base.maxHp = 30;
        }
        else base.maxHp = 30 * (deathCount-1);
        Debug.LogFormat("<color=red>Death Count:{0}</color>",this.deathCount);
        this.hp = base.maxHp;
        this.state = eState.Idle; 
    }
    private void OnDisable()
    {
        this.deathCount++;
    }
    // Start is called before the first frame update
    void Start()
    {
        this.hp = base.maxHp;
        this.receiver.onFinished = () => {
            this.state = eState.Idle;
            this.anim.SetTrigger("Idle");
            this.elapsedTime = 0.0f;
        };
        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;
            }
        };

        this.receiver.onSummon = () => {
            this.CallHatchling();
        };
    }

    private void CallHatchling()
    {
        for (int i = 0; i < hatchlingInintPoses.Length; i++)
        {
            GameObject hatchling = DragonPoolManager.instance.GetHatchlingInPool();
            hatchling.transform.SetParent(null); //매니저 밖으로 나옴
            hatchling.SetActive(true); // false였으니 true로 변환
           
            hatchling.transform.position = Vector2.Lerp(hatchling.transform.position,
            this.hatchlingInintPoses[i].position, 1.0f);
        }
    }
    // 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 > 3.0f)
        {
            this.elapsedTime = 0.0f;
            int rndState =Random.Range(1, 101); //1~2 즉, Attack이나 Summon 둘중하나
            if(rndState < 91) 
            {
                this.state = eState.Attack;
            }
            else
            {
                this.state = eState.Summon;
            }
        }

        
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Bullet"))
        {
            this.HitDamage();
        }
    }

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

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

이렇게 보스 패턴에 대한 구현을 마쳤습니다.