소소한 나의 하루들

2d 종스크롤 슈팅(11) - 탄막을 뿜어대는 보스 만들기 본문

개발/유니티

2d 종스크롤 슈팅(11) - 탄막을 뿜어대는 보스 만들기

소소한 나의 하루 2024. 2. 26. 17:41

출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared

 

📚 유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com

드디어 슈팅게임의 보스를 구현해보도록 한다.


#1. 준비하기

주어진 에셋의 sprite를 준비하고, 설정을 한다. (Pixels per Unit은 24, 애니메이션이 있기 때문에 Sprite Mode는 Multiple, Filter Mode는 Point (no filter), Compression은 None) Sprite Editor에서 Automatic으로 Slice해준다.

sprites 중 하나를 갖다가 Scene에 드래그하여 Enemy Boss 오브젝트를 생성한다.

 

그리고 애니메이션을 적용하기 위해 적절한 프레임을 다중선택해서 적용시킨다. (Boss_Idle 애니메이션 생성) 마지막 sprite도 다중선택해서 적용시키고 Boss_Hit 애니메이션을 생성하고, 적용시킨다.

 

피격 애니메이션 (Boss_Hit)은 어느때나 실행할 수 있게 Any State에다 transition(화살표)로 연결해주고, 다시 Boss_Idle 애니메이션으로 transition(화살표)를 연결해준다. 그리고 피격을 위한 트리거 매개변수 OnHit를 생성한다. Any State에서 Boss_Hit 애니메이션으로 이동하는 transition에는 Has Exit Time 체크해제, Transition Duration은 0으로 하고, OnHit 트리거 매개변수를 추가해준다. 다시 Boss_Idle 애니메이션으로 이동하는 화살표에는 Has Exit Time은 체크하고, Transition Duration은 0으로 두고 매개변수는 추가하지 않는다. (Condition이 없기 때문에 Has Exit Time은 체크한다)

애니메이션 설정은 끝났다.

 

이제 Capsule Collider 컴포넌트를 추가하는데, Direction(방향)을 Vertical이 아니라 Horizontal(가로)로 설정한다. 적절하게 캡슐 콜라이더의 모양을 설정한다. (x2.7 y1.5) Is Trigger도 체크해준다. 그리고 Rigidbody 2D 컴포넌트도 추가하고 Gravity Scale은 0으로 설정한다. 태그는 Enemy로 설정한다.

이렇게 기본적인 설정도 끝났다.


#2. 패턴 흐름

보스가 쏠 총알도 따로 만들도록 한다. 이미 만들어져있는 총알 프리펩을 복사, 붙여넣기(ctrl+D)한다. 그리고 프리펩을 열어서 이미지도 바꾸고, 보스가 위에서 아래로 쏘는 총알이기 때문에 이미지가 한쪽 방향으로 그려져있다면 y축 Flip도 체크해서 아래방향으로 이미지를 바꿔주고, 태그도 적절히 설정해준다. 콜라이더도 설정해준다. (x0.2 y0.35) 다시한번 버 ctrl+D해서 총알 프리펩을 생성한다. (Enemy Bullet C, Enemy Bullet D)

*꼭 Is Trigger을 체크해준다.

 

이렇게 만든 보스 총알 프리펩을 오브젝트 풀링에 등록한다. (bulletBossC[] - bulletBossCPrefab, bulletBossD[] - bulletBossDPrefab)

 

보스 패턴을 만들기 위해 앞에서 만들어놓았던 적 비행기 총알 로직을 활용해본다. 여기서 해볼 것은 '스스로 알아서 회전하는 총알'을 만들어보도록 한다. 이전에 만들었던 Bullet 스크립트에서 bool형 변수 isRotate를 생성한다.

Update()를 만들고, 회전하는 로직을 작성한다.

*이때 회전하는 총알은 이미지에 방향성이 있는 Bullet Enemy C로 설정한다. (모든 방향이 동일해서 구분이 없으면 총알이 회전하더라도 잘 드러나지 않기 때문)

//Bullet
public class Bullet : MonoBehaviour
{
    public int damage;
    public bool isRotate;

    public void Update()
    {
        if (isRotate)
            transform.Rotate(Vector3.forward * 10);
    }
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.gameObject.tag == "BorderBullet")
        {
            gameObject.SetActive(false);
        }
    }
}

2D에서 회전하는 것은 z축을 이용해 회전한다. 이때 방향벡터는 Vector3.forward 또는 Vector3.back을 활용한다. 만약 IsRotate가 체크되어있으면 총알이 회전한다.


#3. 보스 기본 로직

이제 남은 것은 보스 프리펩이다. 이전에 만들어놓은 Enemy 로직을 그대로 재활용하기위해 Enemy Boss 오브젝트에 드래그하여 적용시킨다. 그리고 enemy Name은 "B"로, speed, health와 score는 적절하게 설정해준다. 

void Update()
{
    if (enemyName == "B")
        return;

    Fire();
    Reload();
}

보스와 일반 적 비행기는 로직이 살짝 달라서 그 부분을 다듬어주기위해 Enemy 스크립트에서 로직을 작성한다. 보스는 Fire()와 Reload()를 사용하지 않는다.

 

하지만 OnHit()은 공통 로직이다. 여기서 일반 적 비행기는 기존 로직(피격 시 sprite가 순간적 변하는)을 사용하고, 보스는 다른 로직을 사용해준다.

//Enemy
Animator anime;

private void Awake()
{
    spriteRenderer = GetComponent<SpriteRenderer>();

    if (enemyName == "B")
        anime = GetComponent<Animator>();
}

public void OnHit(int damage)
{
    if (health <= 0)
        return;

    health -= damage;

    if (enemyName == "B")
    {
        anime.SetTrigger("OnHit");
    }

    else
    {
        spriteRenderer.sprite = sprites[1];
        Invoke("ReturnSprite", 0.1f);
    }
    

    if (health <= 0)
    {
        Player playerLogic = player.GetComponent<Player>();
        playerLogic.score += enemyScore;

        //Random Ratio Item Drop
        int ran = enemyName == "B" ? 0 : Random.Range(0, 10);
        if(ran < 5) //Nothing 0.5
        {
            Debug.Log("Not Item");
        }
        else if (ran < 8) //Coin 0.3
        {
            GameObject itemCoin = objectManager.MakeObj("ItemCoin");
            itemCoin.transform.position = transform.position;
        }

        else if (ran < 9) //Power 0.1
        {
            GameObject itemPower = objectManager.MakeObj("ItemPower");
            itemPower.transform.position = transform.position;
        }

        else if (ran < 10) //Boom 0.1
        {
            GameObject itemBoom = objectManager.MakeObj("ItemBoom");
            itemBoom.transform.position = transform.position;
        }

        gameObject.SetActive(false);
        transform.rotation = Quaternion.identity;
    }
    
}

보스는 피격 시 일시적으로 sprite가 변하는 것이 아니라, 피격 애니메이션(Boss_Hit)을 재생시켜줄 것이기 때문에 Animator 변수를 생성한다.

일반 적 비행기는 Animator가 없기 때문에 Awake()에서 초기화할 때 enemyName이 "B"일 때에만 초기화하는 조건을 달아준다.

보스는 죽었을 때 아이템을 드롭하지 않도록 한다. (삼항 연산자 활용)

//Enemy
private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.gameObject.tag == "BorderBullet" && enemyName != "B")
    {
        transform.rotation = Quaternion.identity;
        gameObject.SetActive(false);
    }
        
    else if (collision.gameObject.tag == "PlayerBullet")
    {
        Bullet bullet = collision.gameObject.GetComponent<Bullet>();
        OnHit(bullet.damage);
        //collision.gameObject.SetActive(false);
        collision.gameObject.SetActive(false);
    }
        
}

그리고 적 비행기는 이동하다가 총알 경계에 부딪히면 비활성화되도록 했는데, 보스 비행기는 워낙 크기 때문에 총알 경계는 무시하도록 한다.

 

아직 보스는 총알을 발사하는 Fire()과 딜레이 시간을 부여하는 Reload()를 사용하지 않고 죽었을 때 아이템을 드롭하지 않기 때문에 MaxShotDelay, BulletObjA, BulletObjB, ItemCoin, ItemPower, ItemBoom, ObjectManager 값을 설정하지 않아도 되고, 플레이어를 향해 발사하는 총알도 아직은 없기 때문에 Player를 참조시키지 않아도 된다.

 

이제 이렇게 보스 적 비행기의 기본 설정이 끝났으면 프리펩으로 저장해주고, Scene에서는 삭제해준다.

그리고 보스 프리펩 또한 오브젝트 풀링에 등록한다. (EnemyB[] - enemyBPrefab)

*프리펩으로 저장하면 반드시 position은 (0, 0, 0)으로 초기화해준다.

//ObjectManger
using System.CodeDom.Compiler;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ObjectManager : MonoBehaviour
{
    public GameObject enemyBPrefab;
    public GameObject enemyLPrefab;
    public GameObject enemyMPrefab;
    public GameObject enemySPrefab;
    public GameObject itemCoinPrefab;
    public GameObject itemPowerPrefab;
    public GameObject itemBoomPrefab;
    public GameObject bulletPlayerAPrefab;
    public GameObject bulletPlayerBPrefab;
    public GameObject bulletEnemyAPrefab;
    public GameObject bulletEnemyBPrefab;
    public GameObject bulletBossCPrefab;
    public GameObject bulletBossDPrefab;
    public GameObject bulletFollowerPrefab;

    GameObject[] enemyB;
    GameObject[] enemyL;
    GameObject[] enemyM;
    GameObject[] enemyS;

    GameObject[] itemCoin;
    GameObject[] itemPower;
    GameObject[] itemBoom;

    GameObject[] bulletPlayerA;
    GameObject[] bulletPlayerB;
    GameObject[] bulletEnemyA;
    GameObject[] bulletEnemyB;
    GameObject[] bulletBossC;
    GameObject[] bulletBossD;
    GameObject[] bulletFollower;

    GameObject[] targetPool;

    private void Awake()
    {
        enemyB = new GameObject[1];
        enemyL = new GameObject[10];
        enemyM = new GameObject[10];
        enemyS = new GameObject[20];

        itemCoin = new GameObject[20];
        itemPower = new GameObject[10];
        itemBoom = new GameObject[10];

        bulletPlayerA = new GameObject[100];
        bulletPlayerB = new GameObject[100];
        bulletEnemyA = new GameObject[100];
        bulletEnemyB = new GameObject[100];
        bulletBossC = new GameObject[100];
        bulletBossD = new GameObject[100];
        bulletFollower = new GameObject[100];

        Generate();
    }

    void Generate()
    {
        //Enemy
        for (int index = 0; index < enemyB.Length; index++)
        {
            enemyB[index] = Instantiate(enemyBPrefab);
            enemyB[index].SetActive(false);
        }

        for (int index = 0; index < enemyL.Length; index++)
        {
            enemyL[index] = Instantiate(enemyLPrefab);
            enemyL[index].SetActive(false);
        }

        for (int index = 0; index < enemyM.Length; index++)
        {
            enemyM[index] = Instantiate(enemyMPrefab);
            enemyM[index].SetActive(false);
        }

        for (int index = 0; index < enemyS.Length; index++)
        {
            enemyS[index] = Instantiate(enemySPrefab);
            enemyS[index].SetActive(false);
        }

        //Item
        for (int index = 0; index < itemCoin.Length; index++)
        {
            itemCoin[index] = Instantiate(itemCoinPrefab);
            itemCoin[index].SetActive(false);
        }

        for (int index = 0; index < itemPower.Length; index++)
        {
            itemPower[index] = Instantiate(itemPowerPrefab);
            itemPower[index].SetActive(false);
        }

        for (int index = 0; index < itemBoom.Length; index++)
        {
            itemBoom[index] = Instantiate(itemBoomPrefab);
            itemBoom[index].SetActive(false);
        }

        //Bullet
        for (int index = 0; index < bulletPlayerA.Length; index++)
        {
            bulletPlayerA[index] = Instantiate(bulletPlayerAPrefab);
            bulletPlayerA[index].SetActive(false);
        }

        for (int index = 0; index < bulletPlayerB.Length; index++)
        {
            bulletPlayerB[index] = Instantiate(bulletPlayerBPrefab);
            bulletPlayerB[index].SetActive(false);
        }

        for (int index = 0; index < bulletEnemyA.Length; index++)
        {
            bulletEnemyA[index] = Instantiate(bulletEnemyAPrefab);
            bulletEnemyA[index].SetActive(false);
        }

        for (int index = 0; index < bulletEnemyB.Length; index++)
        {
            bulletEnemyB[index] = Instantiate(bulletEnemyBPrefab);
            bulletEnemyB[index].SetActive(false);
        }

        for (int index = 0; index < bulletBossC.Length; index++)
        {
            bulletBossC[index] = Instantiate(bulletBossCPrefab);
            bulletBossC[index].SetActive(false);
        }

        for (int index = 0; index < bulletBossD.Length; index++)
        {
            bulletBossD[index] = Instantiate(bulletBossDPrefab);
            bulletBossD[index].SetActive(false);
        }

        for (int index = 0; index < bulletFollower.Length; index++)
        {
            bulletFollower[index] = Instantiate(bulletFollowerPrefab);
            bulletFollower[index].SetActive(false);
        }
    }

    public GameObject MakeObj(string type)
    {
        
        switch (type)
        {
            case "EnemyB":
                targetPool = enemyB;
                break;
            case "EnemyL":
                targetPool = enemyL;
                    break;
            case "EnemyM":
                targetPool = enemyM;
                break;
            case "EnemyS":
                targetPool = enemyS;
                break;

            case "ItemCoin":
                targetPool = itemCoin;
                break;
            case "ItemPower":
                targetPool = itemPower;
                break;
            case "ItemBoom":
                targetPool = itemBoom;
                break;

            case "BulletPlayerA":
                targetPool = bulletPlayerA;
                break;
            case "BulletPlayerB":
                targetPool = bulletPlayerB;
                break;
            case "BulletEnemyA":
                targetPool = bulletEnemyA;
                break;
            case "BulletEnemyB":
                targetPool = bulletEnemyB;
                break;
            case "BulletEnemyBossC":
                targetPool = bulletBossC;
                break;
            case "BulletEnemyBossD":
                targetPool = bulletBossD;
                break;
            case "BulletFollower":
                targetPool = bulletFollower;
                break;
        }

        for (int index = 0; index < targetPool.Length; index++)
        {
            if (!targetPool[index].activeSelf)
            {
                targetPool[index].SetActive(true);
                return targetPool[index];
            }
        }

        return null;
    }

    public GameObject[] GetPool(string type)
    {
        switch (type)
        {
            case "EnemyB":
                targetPool = enemyB;
                break;
            case "EnemyL":
                targetPool = enemyL;
                break;
            case "EnemyM":
                targetPool = enemyM;
                break;
            case "EnemyS":
                targetPool = enemyS;
                break;

            case "ItemCoin":
                targetPool = itemCoin;
                break;
            case "ItemPower":
                targetPool = itemPower;
                break;
            case "ItemBoom":
                targetPool = itemBoom;
                break;

            case "BulletPlayerA":
                targetPool = bulletPlayerA;
                break;
            case "BulletPlayerB":
                targetPool = bulletPlayerB;
                break;
            case "BulletEnemyA":
                targetPool = bulletEnemyA;
                break;
            case "BulletEnemyB":
                targetPool = bulletEnemyB;
                break;
            case "BulletEnemyBossC":
                targetPool = bulletBossC;
                break;
            case "BulletEnemyBossD":
                targetPool = bulletBossD;
                break;
            case "BulletFollwer":
                targetPool = bulletFollower;
                break;
        }

        return targetPool;
    }
}

기존의 적 비행기 등장 텍스트는 잘라내기해서 어디에 따로 백업해두고, 보스만 등장하게끔 패턴을 작성해본다.

*보스는 크기가 커서 화면 중앙 위치에 나오도록 설정한다.

//GameManager
private void Awake()
{
    spawnList = new List<Spawn>();
    playerLogic = player.GetComponent<Player>();
    enemyObjs = new string[] { "EnemyL", "EnemyM", "EnemyS", "EnemyB"};
    ReadSpawnFile();
}

void SpawnEnemy()
{
    int enemyIndex = 0;
    switch(spawnList[spawnIndex].type)
    {
        case "L":
            enemyIndex = 0;
            break;
        case "M":
            enemyIndex = 1;
            break;
         case "S":
            enemyIndex = 2;
            break;
        case "B":
            enemyIndex = 3;
            break;
    }
    ...
}

오브젝트 풀링에 적 보스와 보스 총알을 추가하면서 동시에, 오브젝트 풀링을 통해 다른 스크립트에서 생성하는 로직들도 추가, 수정해준다.

실행해보고, 보스가 정해놓은 시간(Update()는 일반적으로 1초에 60번 호출되니까 4를 약 4초라고 생각) 뒤에 설정한 위치에서 등장하는지 확인한다.

 

보스는 지나가면 안된다. 적당하게 나와서 정지하고 보스전에 돌입하기위해 멈춰야한다.


#4. 패턴 흐름

//Enemy
private void OnEnable()
{
    switch (enemyName)
    {
        case "B":
            health = 3000;
            Invoke("Stop", 2.0f);
            break;
        case "L":
            health = 40;
            break;
        case "M":
            health = 10;
            break;
        case "S":
            health = 5;
            break;
    }
}

private void Stop()
{
    if (!gameObject.activeSelf)
        return;

    Rigidbody2D rigid = GetComponent<Rigidbody2D>();
    rigid.velocity = Vector2.zero;
}

Enemy 스크립트의 OnEnable()에서 보스가 활성화될 때마다 체력을 최대로 채워준다. 참고

그리고 보스전에 돌입하면서 보스가 멈출 수 있도록 Stop() 함수를 만든다.

 

그런데 이렇게 하면 바로 정지를 할 것이다. 그래서 충분히 이동한 후 정지하게끔 만들기 위해 2초 뒤 정지하도록 한다.

 

주의해야할 점

오브젝트 풀링을 통해 생성하고난 다음 바로 비활성화 시킨다. 그리고 소환할 때 다시 활성화시킨다. 그렇게 되면 OnEnable()은 2번 실행되니까 Stop()이 2번 실행될 것이다. 결국 에러가 발생할 수 있다.

→따라서 Stop 함수가 2번 사용되지 않도록 조건을 추가한다. : 활성화 여부 판단

 

멈춘 다음에는 보스전 패턴을 실행하기위해 생각하도록 한다. Think() 함수 추가

: 패턴 흐름에 필요한 변수 생성

public int patternIndex;
public int curPatternCount;
public int[] maxPatternCount;

보스가 한가지 패턴을 몇번 반복하다가 그 다음 패턴으로 넘어가도록 한다. (보스전의 페이즈) 여러 개의 패턴을 사용할 것이니까 배열로 생성한다. 현재 패턴이 패턴 개수를 넘기면 0으로 돌아오는 로직을 생성한다.

//Enemy
private void Stop()
{
    if (!gameObject.activeSelf)
        return;

    Rigidbody2D rigid = GetComponent<Rigidbody2D>();
    rigid.velocity = Vector2.zero;

    Invoke("Think", 2);
}

void Think()
{
    patternIndex = patternIndex == 3 ? 0 : ++patternIndex;

    switch (patternIndex)
    {
        case 0:
            FireFoward();
            break;
        case 1:
            FireShot();
            break;
        case 2:
            FireArc();
            break;
        case 3:
            FireAround();
            break;
    }
}

void FireFoward()
{
    Debug.Log("앞으로 4발 발사");
}

void FireShot()
{
    Debug.Log("플레이어 방향으로 샷건");
}

void FireArc()
{
    Debug.Log("부채 모양으로 발사");
}

void FireAround()
{
    Debug.Log("원 형태로 전체 공격");
}

그리고 각 패턴별로 함수를 생성한다. 보스가 패턴대로 공격을 한 뒤에 생각을 해야한다. 단순히 이렇게 할 것이 아니라 각자 나름대로 패턴을 몇번 사용할 것인지 변수 curPatternCount와 maxPatternCount 배열로 만들었다. 

//Enemy
void Think()
{
    patternIndex = patternIndex == 3 ? 0 : patternIndex + 1;
    curPatternCount = 0;

    switch (patternIndex)
    {
        case 0:
            FireFoward();
            break;
        case 1:
            FireShot();
            break;
        case 2:
            FireArc();
            break;
        case 3:
            FireAround();
            break;
    }
}

void FireFoward()
{
    Debug.Log("앞으로 4발 발사");
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireFoward", 2);
    else
        Invoke("Think", 2);
}

void FireShot()
{
    Debug.Log("플레이어 방향으로 샷건");
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireShot", 2);
    else
        Invoke("Think", 2);
}

void FireArc()
{
    Debug.Log("부채 모양으로 발사");
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireArc", 2);
    else
        Invoke("Think", 2);
}

void FireAround()
{
    Debug.Log("원 형태로 전체 공격");
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireAround", 2);
    else
        Invoke("Think", 2);
}

 패턴을 하나 끝내면 curPatternCount는 1 증가하고, 만약 증가한 curPatternCount가 현재 위치한 maxPatternCount[patternIndex]보다 아직 적다면, 현재 패턴을 그대로 다시 실행하도록 한다. 만약 같다면 Think()를 다시 실행해서 다음 패턴으로 넘어간다. 

: 각 패턴 별 횟수를 실행하고 다음 패턴으로 넘어가도록 구현

*다음 patternIndex가 갱신될 때마다 curPatternCount를 초기화해주지 않으면 계속 값이 커질 것이다.

: 패턴 변경 시, 패턴 실행 횟수 변수 초기화

void FireFoward()
{
    //Fire 4 Bullets Forward
    GameObject bulletL1 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletL2 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletR1 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletR2 = objectManager.MakeObj("BulletEnemyBossC");
    bulletL1.transform.position = transform.position + Vector3.down * 1.0f + Vector3.left * 0.5f;
    bulletL2.transform.position = transform.position + Vector3.down * 1.0f + Vector3.left * 0.75f;
    bulletR1.transform.position = transform.position + Vector3.down * 1.0f + Vector3.right * 0.5f;
    bulletR2.transform.position = transform.position + Vector3.down * 1.0f + Vector3.right * 0.75f;
    Rigidbody2D rigidL1 = bulletL1.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidL2 = bulletL2.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidR1 = bulletR1.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidR2 = bulletR2.GetComponent<Rigidbody2D>();

    Vector3 dirVecL1 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.left * 0.5f;
    Vector3 dirVecL2 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.left * 0.75f;
    Vector3 dirVecR1 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.right * 0.5f;
    Vector3 dirVecR2 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.right * 0.75f;
    rigidL1.AddForce(dirVecL1.normalized * 3.5f, ForceMode2D.Impulse);
    rigidL2.AddForce(dirVecL2.normalized * 3.5f, ForceMode2D.Impulse);
    rigidR1.AddForce(dirVecR1.normalized * 3.5f, ForceMode2D.Impulse);
    rigidR2.AddForce(dirVecR2.normalized * 3.5f, ForceMode2D.Impulse);
    
    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireFoward", 1.5f);
    else
        Invoke("Think", 2.5f);
}

void FireShot()
{
    //Random 5 Bullets
    for (int index = 0; index < 6; index++)
    {
        GameObject bullet = objectManager.MakeObj("BulletEnemyD");
        bullet.transform.position = transform.position;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec = player.transform.position - transform.position;
        Vector2 ranVec = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(0f, 2f));
        dirVec += ranVec;
        rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);
    }

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireShot", 1.25f);
    else
        Invoke("Think", 2.5f);
}

void FireArc()
{
    //Fire Arc Continue Fan Attack
    GameObject bullet = objectManager.MakeObj("BulletEnemyC");
    bullet.transform.position = transform.position;
    bullet.transform.rotation = Quaternion.identity;

    Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
    Vector2 dirVec = new Vector2(Mathf.Sin(curPatternCount), -1);
    rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireArc", 0.5f);
    else
        Invoke("Think", 2.5f);
}

void FireAround()
{
    Debug.Log("원 형태로 전체 공격");

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireAround", 0.75f);
    else
        Invoke("Think", 2.5f);
}

*패턴 반복 속도, 패턴 전환 속도 조절하여 난이도 맞춰줄 수 있다.

*총알 발사 속도도 조절할 수 있다.

이렇게 보스전 패턴 흐름 로직이 구현됐으면, 에디터에서 값을 설정해준다.

*PatternIndex는 -1로 값을 설정해놔야 처음에 보스전에 돌입했을 때 패턴 0부터 시작한다.


#5. 패턴 구현

FireForward()

첫번째 패턴은 총알 4발을 앞으로 발사하는 공격이다.

void FireFoward()
{
    //Fire 4 Bullet Forward
    GameObject bulletL1 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletL2 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletR1 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletR2 = objectManager.MakeObj("BulletEnemyBossC");
    bulletL1.transform.position = transform.position + Vector3.down * 0.75f + Vector3.left * 0.5f;
    bulletL2.transform.position = transform.position + Vector3.down * 0.75f + Vector3.left * 0.75f;
    bulletR1.transform.position = transform.position + Vector3.down * 0.75f + Vector3.right * 0.5f;
    bulletR2.transform.position = transform.position + Vector3.down * 0.75f + Vector3.right * 0.75f;
    Rigidbody2D rigidL1 = bulletL1.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidL2 = bulletL2.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidR1 = bulletR1.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidR2 = bulletR2.GetComponent<Rigidbody2D>();

    Vector3 dirVecL1 = player.transform.position - transform.position + Vector3.down * 0.75f + Vector3.left * 0.5f;
    Vector3 dirVecL2 = player.transform.position - transform.position + Vector3.down * 0.75f + Vector3.left * 0.75f;
    Vector3 dirVecR1 = player.transform.position - transform.position + Vector3.down * 0.75f + Vector3.right * 0.5f;
    Vector3 dirVecR2 = player.transform.position - transform.position + Vector3.down * 0.75f + Vector3.right * 0.75f;
    rigidL1.AddForce(dirVecL1.normalized * 3.5f, ForceMode2D.Impulse);
    rigidL2.AddForce(dirVecL2.normalized * 3.5f, ForceMode2D.Impulse);
    rigidR1.AddForce(dirVecR1.normalized * 3.5f, ForceMode2D.Impulse);
    rigidR2.AddForce(dirVecR2.normalized * 3.5f, ForceMode2D.Impulse);
    
    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireFoward", 2);
    else
        Invoke("Think", 2);
}

보스 적 비행기의 이미지를 반영하여 앞으로 총알 4발을 발사한다. 적 비행기가 총알을 발사하는 Fire()의 로직을 일부 갖고와서 재활용한다. 보스 적 비행기 이미지에서 총구가 어디에 있는지 위치를 고려하면서 생성위치와 발사방향 로직을 수정해준다.

 

일반적으로 총알이 적 비행기 프리펩보다 위쪽에 위치하게 되면 어색하기 때문에 적 비행기와 보스 적 비행기는 총알 프리펩의 Order in Layer보다 높은 숫자로 값을 바꿔준다.

 

FireShot()

두번째 패턴은 샷건 형태의 방사형 공격이다.

void FireShot()
{
    //Random 5 Bullets
    for (int index = 0; index < 5; index++)
    {
        GameObject bullet = objectManager.MakeObj("BulletEnemyD");
        bullet.transform.position = transform.position;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec = player.transform.position - transform.position;
        Vector2 ranVec = new Vector2(Random.Range(-0.5f, 0.5f), Random.Range(0f, 2f));
        dirVec += ranVec;
        rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);
    }

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireShot", 2);
    else
        Invoke("Think", 2);
}

작은 적 비행기의 총알 쏘는 로직을 재활용한다. 총알을 5개 쏘도록 할 것인데, 이대로면 총알이 모두 같은 위치에서 생성되어 같은 방향으로 발사된다. 총알은 EnemyBossD 프리펩을 사용한다.

위치가 겹치지 않게 랜덤 벡터(x는 -0.5에서 0.5 사이, y는 0에서 2 사이)를 더하여 구현한다.  그러면 생성위치는 transform.position으로 같지만, 랜덤하게 총알이 퍼지면서 발사된다. 퍼지는 범위를 더 넓게 하려면 랜덤 값의 범위를 크게 잡거나, 발사하는 총알의 개수를 늘린다.

*Vector2로 바꿔도 괜찮다.

Random.Range(값1, 값2) : 값1~(값2-1) 사이 랜덤한 수를 구하는 함수 * 인수로 int형, float형이 들어갈 수 있다.

 

FireArc()

세번째 패턴은 부채 형태의 연속 공격이다.

두번째 패턴과 유사하기 때문에 FireShot()의 로직을 재활용한다.  여기서는 회전하는 총알을 사용한다. 우선 회전값은 0으로 초기화해둔다.

transform.rotation = Quaternion.identity : 회전 값을 초기화 참고

//Fire Arc Continue Fan Attack
for (int index = 0; index < 6; index++)
{
    GameObject bullet = objectManager.MakeObj("BulletEnemyC");
    bullet.transform.position = transform.position;
    bullet.transform.rotation = Quaternion.identity;

    Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
    Vector2 dirVec = new Vector2(Mathf.Sin(curPatternCount), -1);
    rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);
}

발사하는 방향이 필요한데, 부채꼴 모양으로 쏘기 위해 새로운 방향벡터를 지정한다. y축은 -1만큼, x축은 삼각함수 중 하나인 sin을 사용하도록 한다.

Mathf.Sin() : 삼각함수 Sin

 

우선 삼각함수를 사용하는 방법을 모르니까 '1씩 증가하는 curPatternCount'를 인수로 사용해보도록 한다.
→ 실행해보면 총알을 부채꼴 모양으로 난사하듯이 발사하는 것 같다.

Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
Vector2 dirVec = new Vector2(Mathf.Sin((float)curPatternCount / maxPatternCount[patternIndex]), -1);
rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);

patternIndex가 2인 경우, FireArc() 패턴이 실행되고, 이때 maxPatternCount[patternIndex]가 100으로 설정해놨으니까
(float)curPatternCount / maxPatternCount[patternIndex]
'1씩 증가하는 curPatternCount를 100으로 나눈 몫을 float형으로 강제 형 변환한 값'을 인수로 집어넣어본다.

이 값은 0.0에서부터 계속 증가해서 1.0에 근사한 0.99까지 될 것이다. (curPatternCount는 결국 100이 될 것인데 가산되기 전 값인 99가 curPatternCount에 들어갈 것이니까) : 일반적으로 어림잡아 0~1까지 된다고 생각하면 된다.

→ 실행해보면 직선형태로 정아래방향( ↓ )에서 오른쪽방향(→)으로 살짝 움직이는 것을 볼 수 있다.

 

이것을 부드럽게 발사하기 위해 값을 하나 더 추가한다. 

Mathf.PI : 원주율 상수 ( π  = 3.14...)

Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
Vector2 dirVec = new Vector2(Mathf.Sin(Mathf.PI * curPatternCount / maxPatternCount[patternIndex]), -1);
rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);

float로 형 변환 하는 것 말고, Mathf.PI를 곱해보도록 한다.

→ 실행해보면 단순히 (float)으로 형 변환해준 것에서 다시 아래방향(↓)으로 되돌아온다.

Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
Vector2 dirVec = new Vector2(Mathf.Sin(Mathf.PI * 2 * curPatternCount / maxPatternCount[patternIndex]), -1);
rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);

왜 그렇냐면, PI(파이)는 원 둘레에서 반만 사용한 것이다. [ 1π = 180˚, 2π = 360˚] 따라서 원의 둘레는 PI에 2를 곱해줘야한다.

→ 실행해보면 정아래방향(↓)에서 시작해서 오른쪽방향(→)으로 갔다가 다시 왼쪽방향(→)으로 되돌아왔다가 정아래방향(↓)으로 되돌아온다. (부채꼴 모양으로 총알이 발사된다.)

: 3.14를 0~1까지 최대한 잘게 나눈 것을 곱해주면 0부터 (PI 값인) 3.14까지의 값이 잘게 적분되고, 여기에 원 둘레 360도까지를 구하려면 최대 값이 2PI가 되어야하므로 2를 곱해준 것.

더보기

*참고
radian은 '각도를 PI로 나타낸 것의 단위' 따라서 1PI radian = 180도, (1PI / 180) radian = 1도, 2PI = 360도가 된다. 그리고 PI를 수치적으로 근사하면 3.14로 나타낼 수 있다. [원 한바퀴 각도 360도 = 2PI ≒ 2 * 3.14]

*원의 둘레값을 많이 줄수록 빠르게 파형을 그리게 된다. (2PI * (0~1))보다 (10PI * (0~1))이 더 빠르게 많이 왔다갔다 총알이 발사된다.
Why? MaxPatternCount는 100으로 동일하니 (CurPatternCount가 채워지기까지 걸리는) 시간은 동일하고, 같은 시간 내에 (발사하는 총알 개수에 상관없이 총알 개수 관련 로직은 작성 안했으니까) 발사하는 반경이 360도인 것과 3600도인 것이 더 많이 빠르게 회전해야할 것이다.

그런데 플레이어가 거의 한 자리에만 있으면 총알을 피할 수 있다. 그래서 보통 이러한 부채꼴류의 공격은 총알 발사 횟수를 홀수로 두면 플레이어의 이동을 강제적으로 유발한다. 

 

그러면 Sin말고, Cos은 어떻게 될까?

Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 2 * curPatternCount / maxPatternCount[patternIndex]), -1);
rigid.AddForce(dirVec1.normalized * 4f, ForceMode2D.Impulse);

Cos()은 Sin()보다 90도 앞으로 간 파형을 갖고있다. : 시작 각도만 다를 뿐 발사 모양은 파형으로 비슷하다.

더보기

삼각함수

Sin 양수 All 양수
Tan 양수 Cos 양수

반지름이 1인 원의 좌표는 (cosθ, sinθ)

  sinθ conθ tanθ [= sinθ/cosθ]
0 1 0
30˚ (= π/6) 1/2 sqrt(3)/2 1/sqrt(3) (= 3/sqrt(3))
45˚ (= π/4) sqrt(2)/2 sqrt(2)/2 1
60˚ (= π/3) sqrt(3)/2 1/2 sqrt(3)
90˚ (= π/2) 1 0 -

π = 180˚
2π = 360˚
  

제 4사분면에서 삼각함수를 생각하면

sin0˚ = 0
sin30˚ = sin(π/6)  = 1/2
sin90˚ = sin(π/2) = 1
sin150˚ = sin(π - 30˚) = sin(5π/6) = sin30˚ = 1/2

sin180˚ = sin(π) = sin0˚ = 0

sin270˚ = sin(π + 90˚) = sin(3π/2) = -sin9 = -1

 

cos0˚ = 1
cos30˚ = cos(π/6)  = sqrt(3)/2
cos90˚ = cos(π/2) = 0
cos150˚ = cos(π - 30˚) = cos(5π/6) = -cos30˚ = -1/2

cos180˚ = cos(π) = -cos0˚ = -1

cos270˚ = cos(π + 90˚) = cos(3π/2) = cos9 = 0

따라서 위 삼각함수 실행결과를 보면,

(float) 형 변환 ([0~1], -1) 결과 : (0, -1) → (1, -1) 이동

(float) 형 변환 ([0~1], 1) 결과 : (0, 1) → (1, 1) 이동

y축 -1 sin
Sin(3.14(π) * [0~1], -1) 결과 : (0, -1) 시작해서 (1, -1) 갔다가 (0, -1) 되돌아옴 [sin(π/2) = 1, sin(π) = 0]

Sin(3.14(π) * 2 * [0~1], -1) 결과 : (0, -1) 시작해서 (1, -1) 갔다가 (0, -1) 되돌아와서 (-1, -1) 갔다가 (0, -1)로 되돌아옴 [sin(π/2) = 1, sin(π) = 0, sin(3π/2) = -1]

y축 1 sin

Sin(3.14(π) * [0~1], 1) 결과 : (0, 1) 시작해서 (1, 1) 갔다가 (0, 1) 되돌아옴 [sin(π/2) = 1, sin(π) = 0]

Sin(3.14(π) * 2 * [0~1], 1) 결과 : (0, 1) 시작해서 (1, 1) 갔다가 (0, 1) 되돌아와서 (-1, 1) 갔다가 (0, 1)로 되돌아옴 [sin(π/2) = 1, sin(π) = 0, sin(3π/2) = -1]

 

y축 -1 cos
Cos(3.14(π) * [0~1], -1) 결과 : (1, -1) 시작해서 (0, -1) 갔다가 (-1, -1) 되돌아옴 [cos(π/2) = 0, cos(π) = -1]

Cos(3.14(π) * 2 * [0~1], -1) 결과 : (1, -1) 시작해서 (0, -1) 갔다가 (-1, -1) 이동해서 (0, -1) 되돌아와서 (1, -1)로 다시되돌아옴 [cos(π/2) = 0, cos(π) = -1, cos(3π/2) = 0]

y축 1 cos
Cos(3.14(π) * [0~1], 1) 결과 : (1, 1) 시작해서 (0, 1) 갔다가 (-1, 1) 되돌아옴 [cos(π/2) = 0, cos(π) = -1]

Cos(3.14(π) * 2 * [0~1], 1) 결과 : (1, 1) 시작해서 (0, 1) 갔다가 (-1, 1) 이동해서 (0, 1) 되돌아와서 (1, 1)로 다시되돌아옴 [cos(π/2) = 0, cos(π) = -1, cos(3π/2) = 0]

Sin은 한가운데에서 시작하지만, Cos는 그렇지 않아서 좀 더 깔끔하다. 결국은 Sin을 사용해도 Cos을 사용해도 좋다. 이것이 3번째 패턴이다. 

 

FireAround()

네번째 패턴은 원 형태의 전체 공격이다.

세번째 패턴인 FireArc()의 로직을 재활용한다. 총알 50개를 발사할 것이다. 이번에는 좀 더 두꺼운 Enemy Bullet D 총알 프리펩을 사용한다.

Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 2 * index / roundNumA), Mathf.Sin(Mathf.PI * 2 * index / roundNumA));
rigid.AddForce(dirVec1.normalized * 4f, ForceMode2D.Impulse);

원 형태로 발사하려면 방향벡터를 잘 조절해줘야한다. 살짝만 조정해주면 되는데, 현재 index를 for문의 전체값(전체 총알개수 roundNumA)으로 나눠준다. = 원 한바퀴를 총알 개수로 잘라서 각도가 들어간다.
: 생성되는 총알의 순번을 활용하여 방향을 결정한다.

*x축도, y축도 똑같이 각도를 잘라서 기입해주면 된다. [x축에 대하여 360도(2π)를 총알 개수만큼 각도를 나누고, y축에 대하여 360도(2π)를 총알 개수만큼 각도를 나눈다.]

하지만 이렇게하면 x값과 y값이 똑같기 때문에 일직선 상으로만 총알이 발사될 것이다. 

→이럴 때는 x축은 Sin, y축은 Cos으로 설정해줘야한다. (혹은 반대로)

//Fire Around
int roundNumA = 50;
for(int index = 0; index < roundNumA; index++)
{
    GameObject bullet = objectManager.MakeObj("BulletEnemyD");
    bullet.transform.position = transform.position;
    bullet.transform.rotation = Quaternion.identity;

    Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
    Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 2 * index / roundNumA), Mathf.Sin(Mathf.PI * 2 * index / roundNumA));
    rigid.AddForce(dirVec1.normalized * 4f, ForceMode2D.Impulse);

    Vector3 rotVec = Vector3.forward * 360 * index / roundNumA + Vector3.forward * 90;
    bullet.transform.Rotate(rotVec);
}

보통 이렇게 총알이 원형으로 퍼져서 발사될 때는 총알 방향이 발사되는 방향을 바라보면서 회전하면 좋을 것이다.

2d 그래픽의 회전 방향벡터는 Vector3.forward 또는 Vector3.back의 z축 선상에 있다.

Vector3 rotVec = Vector3.forward * 360 * index / roundNumA + Vector3.forward * 90;

Vector3.forward * 360 * index / roundNumA : z축 기준으로 원 한바퀴를 총알 개수만큼의 각으로 나누고

→원의 접선방향으로 총알이 바라보게된다.

Vector3.forward * 90 : 지금 상태에서 z축 기준으로 90도만큼 회전값을 더해준다. (회전시킨다)

→원의 접선방향에서 총알이 원 중심에서부터 밖으로 퍼져나가는 방향을 바라보게된다.

* 거꾸로 원의 중심을 바라본다면 forward 대신 back을 사용하면 된다.

 

보정값(Vector3.forward * 90)이 반드시 더해져야한다.

 

이렇게 기본적인 패턴 로직을 다 작성했으면 총알 속도나 패턴 반복/전환 속도를 조절해서 밸런스를 조절해준다.

 

그런데 FireAround()의 경우 플레이어가 처음 총알을 피했다면 그 자리에 가만히 있어도 총알을 계속 피할 수 있을 것이다.  (같은 위치에서 계속 총알이 생성되기 때문에) 따라서 패턴을 살짝 꼬아보도록 한다.

void FireAround()
{
    //Fire Around
    int roundNumA = 40;
    int roundNumB = 50;
    int roundNum = curPatternCount % 2 == 0 ? roundNumA : roundNumB;
    for(int index = 0; index < roundNum; index++)
    {
        GameObject bullet = objectManager.MakeObj("BulletEnemyD");
        bullet.transform.position = transform.position;
        bullet.transform.rotation = Quaternion.identity;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 2 * index / roundNum), Mathf.Sin(Mathf.PI * 2 * index / roundNum));
        rigid.AddForce(dirVec1.normalized * 2f, ForceMode2D.Impulse);

        Vector3 rotVec = Vector3.forward * 360 * index / roundNum + Vector3.forward * 90;
        bullet.transform.Rotate(rotVec);
    }
    

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireAround", 1.5f);
    else
        Invoke("Think", 2.5f);
}

→패턴을 반복할 때마다 총알 개수를 다르게 한다. curPatternCount를 활용해서 짝수인지 여부에 따라 총알개수를 다르게 한다. 

: 패턴 횟수에 따라 생성되는 총알 갯수 조절로 난이도 상승


문제상황 - 피드백

1. 보스가 플레이어 총알에 맞아 피격됐을 때, 공격이 멈춘 이후에도 계속 피격당하고 공격패턴은 진행되지 않는 문제

(curPatternCount는 가산되지만 계속 피격모션만 재생되면서 데미지를 받음. 보스 총알이 생성되질 않음)

//Enemy
public void OnHit(int damage)
{
    if (health <= 0)
        return;

    health -= damage;

    if (enemyName == "B")
    {
        anime.SetTrigger("OnHit");
    }

    else
    {
        spriteRenderer.sprite = sprites[1];
        Invoke("ReturnSprite", 0.1f);
    }
    

    if (health <= 0)
    {
        Player playerLogic = player.GetComponent<Player>();
        playerLogic.score += enemyScore;

        //Random Ratio Item Drop
        int ran = enemyName == "B" ? 0 : UnityEngine.Random.Range(0, 10);
        if(ran < 5) //Nothing 0.5
        {
            Debug.Log("Not Item");
        }
        else if (ran < 8) //Coin 0.3
        {
            GameObject itemCoin = objectManager.MakeObj("ItemCoin");
            itemCoin.transform.position = transform.position;
        }

        else if (ran < 9) //Power 0.1
        {
            GameObject itemPower = objectManager.MakeObj("ItemPower");
            itemPower.transform.position = transform.position;
        }

        else if (ran < 10) //Boom 0.1
        {
            GameObject itemBoom = objectManager.MakeObj("ItemBoom");
            itemBoom.transform.position = transform.position;
        }

        CancelInvoke();
        transform.rotation = Quaternion.identity;
        gameObject.SetActive(false);
    }
    
}

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.gameObject.tag == "BorderBullet" && enemyName != "B")
    {
        transform.rotation = Quaternion.identity;
        gameObject.SetActive(false);
    }
        
    else if (collision.gameObject.tag == "PlayerBullet")
    {

        Bullet bullet = collision.gameObject.GetComponent<Bullet>();
        OnHit(bullet.damage);
        //collision.gameObject.SetActive(false);
        collision.gameObject.SetActive(false);
    }
        
}

기존 OnHit()가 작동하는 로직의 흐름을 살펴보면

1) 플레이어 총알에 맞아서 OnTriggerEnter2D에 의해 플레이어 총알이 데미지를 OnHit()에 넘겨주면서 실행되고 플레이어 총알은 비활성화된다.

2) OnHit()이 실행되면서 '보스 체력이 남아있을 때' health가 damage만큼 깎이고, 보스의 경우 enemyName == "B"이므로  OnHit 트리거를 발동시키면서 Boss_Hit 애니메이션이 작동한다.

private void OnEnable()
{
    switch (enemyName)
    {
        case "B":
            health = 3000;
            Invoke("Stop", 2.0f);
            break;
        case "L":
            health = 40;
            break;
        case "M":
            health = 10;
            break;
        case "S":
            health = 5;
            break;
    }
}

private void Stop()
{
    if (!gameObject.activeSelf)
        return;

    Rigidbody2D rigid = GetComponent<Rigidbody2D>();
    rigid.velocity = Vector2.zero;

    Invoke("Think", 2);
}

void Think()
{
    patternIndex = patternIndex == 3 ? 0 : patternIndex + 1;
    curPatternCount = 0;

    switch (patternIndex)
    {
        case 0:
            FireFoward();
            break;
        case 1:
            FireShot();
            break;
        case 2:
            FireArc();
            break;
        case 3:
            FireAround();
            break;
    }
}

void FireFoward()
{
    if (health <= 0)
        return;

    //Fire 4 Bullets Forward
    GameObject bulletL1 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletL2 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletR1 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletR2 = objectManager.MakeObj("BulletEnemyBossC");
    bulletL1.transform.position = transform.position + Vector3.down * 1.0f + Vector3.left * 0.5f;
    bulletL2.transform.position = transform.position + Vector3.down * 1.0f + Vector3.left * 0.75f;
    bulletR1.transform.position = transform.position + Vector3.down * 1.0f + Vector3.right * 0.5f;
    bulletR2.transform.position = transform.position + Vector3.down * 1.0f + Vector3.right * 0.75f;
    bulletL1.transform.rotation = Quaternion.identity;
    bulletL2.transform.rotation = Quaternion.identity;
    bulletR1.transform.rotation = Quaternion.identity;
    bulletR2.transform.rotation = Quaternion.identity;
    Rigidbody2D rigidL1 = bulletL1.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidL2 = bulletL2.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidR1 = bulletR1.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidR2 = bulletR2.GetComponent<Rigidbody2D>();

    Vector3 dirVecL1 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.left * 0.5f;
    Vector3 dirVecL2 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.left * 0.75f;
    Vector3 dirVecR1 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.right * 0.5f;
    Vector3 dirVecR2 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.right * 0.75f;
    rigidL1.AddForce(dirVecL1.normalized * 3.5f, ForceMode2D.Impulse);
    rigidL2.AddForce(dirVecL2.normalized * 3.5f, ForceMode2D.Impulse);
    rigidR1.AddForce(dirVecR1.normalized * 3.5f, ForceMode2D.Impulse);
    rigidR2.AddForce(dirVecR2.normalized * 3.5f, ForceMode2D.Impulse);
    
    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireFoward", 1.5f);
    else
        Invoke("Think", 2.5f);
}

void FireShot()
{
    if (health <= 0)
        return;

    //Random 5 Bullets
    for (int index = 0; index < 6; index++)
    {
        GameObject bullet = objectManager.MakeObj("BulletEnemyD");
        bullet.transform.position = transform.position;
        //bullet.transform.rotation = Quaternion.identity;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec = player.transform.position - transform.position;
        Vector2 ranVec = new Vector2(UnityEngine.Random.Range(-0.5f, 0.5f), UnityEngine.Random.Range(0f, 2f));
        dirVec += ranVec;
        rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);
    }

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireShot", 1.25f);
    else
        Invoke("Think", 2.5f);
}

void FireArc()
{
    if (health <= 0)
        return;

    //Fire Arc Continue Fan Attack
    GameObject bullet = objectManager.MakeObj("BulletEnemyC");
    bullet.transform.position = transform.position;
    bullet.transform.rotation = Quaternion.identity;

    Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
    Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 4 * curPatternCount / maxPatternCount[patternIndex]), -1);
    rigid.AddForce(dirVec1.normalized * 4f, ForceMode2D.Impulse);

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireArc", 0.6f);
    else
        Invoke("Think", 2.5f);
}

void FireAround()
{
    if (health <= 0)
        return;

    //Fire Around
    int roundNumA = 40;
    int roundNumB = 50;
    int roundNum = curPatternCount % 2 == 0 ? roundNumA : roundNumB;
    for(int index = 0; index < roundNum; index++)
    {
        GameObject bullet = objectManager.MakeObj("BulletEnemyD");
        bullet.transform.position = transform.position;
        bullet.transform.rotation = Quaternion.identity;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 2 * index / roundNum), Mathf.Sin(Mathf.PI * 2 * index / roundNum));
        rigid.AddForce(dirVec1.normalized * 2f, ForceMode2D.Impulse);

        Vector3 rotVec = Vector3.forward * 360 * index / roundNum + Vector3.forward * 90;
        bullet.transform.Rotate(rotVec);
    }
    

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
        Invoke("FireAround", 1.5f);
    else
        Invoke("Think", 2.5f);
}

보스 공격 패턴이 실행되는 로직을 흐름대로 살펴보면

1) 보스가 활성화되면서 OnEnable()이 실행된다. emeyName이 "B"인 경우 health는 3000이고 Stop() 함수가 2초 뒤 실행된다.

2) Stop() 함수가 실행되면 보스 프리펩 오브젝트가 비활성화 상태인지 판단하고, 비활성화 상태라면 return시킨다.

만약 활성화상태라면 보스를 정지시킨다. 그리고 Think() 함수를 2초 뒤 실행시킨다.

3) Think() 함수가 실행되면 현재 patternIndex가 패턴의 개수(= maxPatternCount.Length)에 도달했는지 판단하고 참이라면 다시 0으로 돌리고, 거짓이라면 patternIndex를 1 가산시킨다.

patternIndex가 0이라면 첫번째 패턴 FireFoward()를 실행하고
patternIndex가 1이라면 두번째 패턴 FireShot()를 실행하고

patternIndex가 2라면 세번째 패턴 FireArc()를 실행하고

patternIndex가 3이라면 네번째 패턴 FireAround()를 실행한다.

4-1) FireFoward() 함수가 실행되면 보스가 죽었다면(체력(health)이 0 이하) return시키고,

보스 체력이 0 이상이라면 4개의 총알을  특정 위치에 생성해서 회전값을 0으로 초기화시키고 플레이어를 향해 3.5의 힘으로 발사시킨다.
그리고 curPatternCount를 1 증가시키는데, '값이 업데이트된 curPatternCount'가 현재 maxPattern[patternIndex](=maxPattern[0])보다 적다면 다시 1.5초 뒤에 FireFoward() 함수를 반복하고

적지 않다면 2.5초 뒤 다음 패턴을 실행하기 위해 Think() 함수를 실행한다.

*이하 4-2, 4-3, 4-4 모두 비슷함.

 

결과적으로 보스 공격 패턴은 계속 진행되면서, 보스 피격도 정상적으로 같이 병행되어야한다. 보스 피격이 끝나면 더이상 피격모션과 데미지를 입는 건 끝나야하고, 진행되던 공격 패턴은 계속 정상적으로 진행되어야함.

 

플레이어 총알에 보스가 피격당했을 때 : PlayerBullet 태그를 가진 플레이어 총알이 보스에 닿을때마다 OnHit()가 호출되고, 플레이어 총알은 비활성화된다.

OnHit() 함수가 실행되면 보스 health가 0 이하가 아니니까 총알 데미지만큼 보스 health가 깎이고, 피격 모션이 재생된다.

피격당하는 시점의 PatternIndex가 2, curPatternCount가 10으로, 세번째 패턴인 FireArc()가 실행되고 있었다면 curPatternCount는 11로 증가한뒤 아직 maxPatternCount.Length인 99에 도달하지 않았으니까 다시 0.6초 뒤 FireArc()는 반복 실행되는데 

//Fire Arc Continue Fan Attack
GameObject bullet = objectManager.MakeObj("BulletEnemyC");
bullet.transform.position = transform.position;
bullet.transform.rotation = Quaternion.identity;

Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 4 * curPatternCount / maxPatternCount[patternIndex]), -1);
rigid.AddForce(dirVec1.normalized * 4f, ForceMode2D.Impulse);

이 부분을 무시하고 보스가 피격되면서 데미지를 입고, health가 닳는 이유가 뭘까 이 부분은 건너뛰면서 curPatternCount는 1 증가되는게 무슨 이유일까

도저히 Invoke()만으로 구현된 이 코드가 위 문제가 발생하는 원인을 찾지 못하겠어서 이제는 Invoke() 대신 코루틴을 사용해야할 시점이 온 것 같다. 현재 내부적으로 몇 초 뒤 어떤 함수를 호출하는 방식으로는 현재 어떤 함수가 내부적으로 Invoke()로 대기중인지 판단해서 제어하기가 어렵다. 참고

더보기
//Enemy
private void OnEnable()
{
    switch (enemyName)
    {
        case "B":
            health = 3000;
            //Invoke("Stop", 2.0f);
            StartCoroutine(StopAfterDelay(2.0f));
            break;
        case "L":
            health = 40;
            break;
        case "M":
            health = 10;
            break;
        case "S":
            health = 5;
            break;
    }
}

IEnumerator StopAfterDelay(float delay)
{
    yield return new WaitForSeconds(delay);

    if (gameObject.activeSelf)
    {
        Rigidbody2D rigid = GetComponent<Rigidbody2D>();
        rigid.velocity = Vector2.zero;

        StartCoroutine(ThinkAfterDelay(2.0f));
    }
}

IEnumerator ThinkAfterDelay(float delay)
{
    yield return new WaitForSeconds(delay);

    patternIndex = patternIndex == 3 ? 0 : patternIndex + 1;
    curPatternCount = 0;

    switch (patternIndex)
    {
        case 0:
            FireFoward();
            break;
        case 1:
            FireShot();
            break;
        case 2:
            FireArc();
            break;
        case 3:
            FireAround();
            break;
    }
}

void FireFoward()
{
    if (health <= 0)
        return;

    //Fire 4 Bullets Forward
    GameObject bulletL1 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletL2 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletR1 = objectManager.MakeObj("BulletEnemyBossC");
    GameObject bulletR2 = objectManager.MakeObj("BulletEnemyBossC");
    bulletL1.transform.position = transform.position + Vector3.down * 1.0f + Vector3.left * 0.5f;
    bulletL2.transform.position = transform.position + Vector3.down * 1.0f + Vector3.left * 0.75f;
    bulletR1.transform.position = transform.position + Vector3.down * 1.0f + Vector3.right * 0.5f;
    bulletR2.transform.position = transform.position + Vector3.down * 1.0f + Vector3.right * 0.75f;
    bulletL1.transform.rotation = Quaternion.identity;
    bulletL2.transform.rotation = Quaternion.identity;
    bulletR1.transform.rotation = Quaternion.identity;
    bulletR2.transform.rotation = Quaternion.identity;
    Rigidbody2D rigidL1 = bulletL1.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidL2 = bulletL2.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidR1 = bulletR1.GetComponent<Rigidbody2D>();
    Rigidbody2D rigidR2 = bulletR2.GetComponent<Rigidbody2D>();

    Vector3 dirVecL1 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.left * 0.5f;
    Vector3 dirVecL2 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.left * 0.75f;
    Vector3 dirVecR1 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.right * 0.5f;
    Vector3 dirVecR2 = player.transform.position - transform.position + Vector3.down * 1.0f + Vector3.right * 0.75f;
    rigidL1.AddForce(dirVecL1.normalized * 3.5f, ForceMode2D.Impulse);
    rigidL2.AddForce(dirVecL2.normalized * 3.5f, ForceMode2D.Impulse);
    rigidR1.AddForce(dirVecR1.normalized * 3.5f, ForceMode2D.Impulse);
    rigidR2.AddForce(dirVecR2.normalized * 3.5f, ForceMode2D.Impulse);
    
    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
    {
        //Invoke("FireFoward", 1.5f);
        StartCoroutine(FireFowardAfterDelay(1.5f));
    }
    else
    {
        //Invoke("Think", 2.5f);
        StartCoroutine(ThinkAfterDelay(2.5f));
    }
        
}

IEnumerator FireFowardAfterDelay(float delay)
{
    yield return new WaitForSeconds(delay);
    FireFoward();
}

void FireShot()
{
    if (health <= 0)
        return;

    //Random 5 Bullets
    for (int index = 0; index < 6; index++)
    {
        GameObject bullet = objectManager.MakeObj("BulletEnemyD");
        bullet.transform.position = transform.position;
        //bullet.transform.rotation = Quaternion.identity;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec = player.transform.position - transform.position;
        Vector2 ranVec = new Vector2(UnityEngine.Random.Range(-0.5f, 0.5f), UnityEngine.Random.Range(0f, 2f));
        dirVec += ranVec;
        rigid.AddForce(dirVec.normalized * 4f, ForceMode2D.Impulse);
    }

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
    {
        //Invoke("FireShot", 1.25f);
        StartCoroutine(FireShotAfterDelay(1.25f));
    }
    else
    {
        //Invoke("Think", 2.5f);
        StartCoroutine(ThinkAfterDelay(2.5f));
    }
}

IEnumerator FireShotAfterDelay(float delay)
{
    yield return new WaitForSeconds(delay);
    FireShot();
}

void FireArc()
{
    if (health <= 0)
        return;

    //Fire Arc Continue Fan Attack
    GameObject bullet = objectManager.MakeObj("BulletEnemyC");
    bullet.transform.position = transform.position;
    bullet.transform.rotation = Quaternion.identity;

    Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
    Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 4 * curPatternCount / maxPatternCount[patternIndex]), -1);
    rigid.AddForce(dirVec1.normalized * 4f, ForceMode2D.Impulse);

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
    {
        //Invoke("FireArc", 0.6f);
        StartCoroutine(FireArcAfterDelay(0.6f));
    }
    else
    {
        //Invoke("Think", 2.5f);
        StartCoroutine(ThinkAfterDelay(2.5f));
    }
}

IEnumerator FireArcAfterDelay(float delay)
{
    yield return new WaitForSeconds(delay);
    FireArc();
}

void FireAround()
{
    if (health <= 0)
        return;

    //Fire Around
    int roundNumA = 40;
    int roundNumB = 50;
    int roundNum = curPatternCount % 2 == 0 ? roundNumA : roundNumB;
    for(int index = 0; index < roundNum; index++)
    {
        GameObject bullet = objectManager.MakeObj("BulletEnemyD");
        bullet.transform.position = transform.position;
        bullet.transform.rotation = Quaternion.identity;

        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        Vector2 dirVec1 = new Vector2(Mathf.Cos(Mathf.PI * 2 * index / roundNum), Mathf.Sin(Mathf.PI * 2 * index / roundNum));
        rigid.AddForce(dirVec1.normalized * 2f, ForceMode2D.Impulse);

        Vector3 rotVec = Vector3.forward * 360 * index / roundNum + Vector3.forward * 90;
        bullet.transform.Rotate(rotVec);
    }
    

    //Pattern Counting
    curPatternCount++;

    if (curPatternCount < maxPatternCount[patternIndex])
    {
        //Invoke("FireAround", 1.5f);
        StartCoroutine(FireAroundAfterDelay(1.5f));
    }
    else
    {
        //Invoke("Think", 2.5f);
        StartCoroutine(ThinkAfterDelay(2.5f));
    }
}

IEnumerator FireAroundAfterDelay(float delay)
{
    yield return new WaitForSeconds(delay);
    FireAround();
}

그래서 Invoke()는 전부 코루틴를 활용하여 동일한 작업을 수행할 수 있도록 교체했다. 참고

실행해보면 보스 패턴은 의도대로 잘 작동되지만, 문제는 그대로 남아있었다.

//Enemy
public void OnHit(int damage)
{
    if (isHit)
        return;

    if (health <= 0)
        return;

    health -= damage;

    if (enemyName == "B")
    {
        anime.SetTrigger("OnHit");
    }

    else
    {
        spriteRenderer.sprite = sprites[1];
        StartCoroutine(ReturnSpriteAfterDelay(0.1f));
    }
    

    if (health <= 0)
    {
        Player playerLogic = player.GetComponent<Player>();
        playerLogic.score += enemyScore;

        //Random Ratio Item Drop
        int ran = enemyName == "B" ? 0 : UnityEngine.Random.Range(0, 10);
        if(ran < 5) //Nothing 0.5
        {
            Debug.Log("Not Item");
        }
        else if (ran < 8) //Coin 0.3
        {
            GameObject itemCoin = objectManager.MakeObj("ItemCoin");
            itemCoin.transform.position = transform.position;
        }

        else if (ran < 9) //Power 0.1
        {
            GameObject itemPower = objectManager.MakeObj("ItemPower");
            itemPower.transform.position = transform.position;
        }

        else if (ran < 10) //Boom 0.1
        {
            GameObject itemBoom = objectManager.MakeObj("ItemBoom");
            itemBoom.transform.position = transform.position;
        }

        CancelInvoke();
        transform.rotation = Quaternion.identity;
        gameObject.SetActive(false);
    }
    
}

private void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.gameObject.tag == "BorderBullet" && enemyName != "B")
    {
        transform.rotation = Quaternion.identity;
        gameObject.SetActive(false);
    }
        
    else if (collision.gameObject.tag == "PlayerBullet")
    {
        isHit = true;
        Bullet bullet = collision.gameObject.GetComponent<Bullet>();
        OnHit(bullet.damage);
        //collision.gameObject.SetActive(false);
        collision.gameObject.SetActive(false);
    }
        
}

private void OnTriggerExit2D(Collider2D collision)
{
    if (collision.gameObject.tag == "PlayerBullet")
        isHit = false;
}

그래서 플래그 변수 isHit을 추가하여 OnHit()을 실행하지 않도록 했는데, 이렇게 하니까 아예 총알을 맞아도 OnHit()에 진입을 못하는 문제가 발생했다.

 

문제를 해결했다.

정말 쉽지 않았던 문제였는데 당연하게도 결국 내 실수였다. (컴퓨터는 거짓말을 하지 않음 반드시 99% 내 실수일거라고 생각해야함) 오브젝트 풀링을 관리하는 ObjectManager 스크립트에서는 MakeObj()로 전달하는 인수값으로 일반 적 비행기의 총알을 "BulletEnemeA"와 "BulletEnemyB"로 불러온다. 그리고 보스의 총알은 "BulletEnemyBossC"와 "BulletEnemyBossD"로 불러오는데, Enemy 스크립트에서 MakeObj()를 호출할 때에 "BulletEnemyBossD" 대신 "BulletEnemyD"로 호출해주는 치명적이고 기초적인 실수를 저질렀다.

난 계속 Invoke() 혹은 코루틴으로 대기하는 부분이나 혹은 적들의 피격을 관리하는 OnHit()이 문제가 되지 않을까 싶어서 집중적으로 살펴보았고, 총알 생성하는 로직과 위의 2가지에 의해 내부적으로 뭔가 접근이 꼬이고 있는걸까(?) 혹은 프레임 진행 시 더 우선순위가 발생하고있나 싶었는데 전혀 아니였다.

 

그동안 계속 패턴 공격함수나 OnTriggerEnter()나 OnHit() 내에 "Hello", "Bullet!", "Hit"과 같은 문자열만 콘솔에 출력해볼 생각만 했지, OnTriggerEnter2D()에서 collision.gameObject나 collision.gameObject.tag(혹은 name 또는 position) 등의 실질적인 정보를 콘솔에 출력해보는 시도는 해본적 없었다. 그리고 위 시도를 안해봤더라도 플레이어 총알을 비활성화되는 후에도 문제가 발생하는 이런 상황에서 마지막에 collision.enable = false와 같이 극단적으로 전부 다 비활성화시켜보는 짓도 해봤다면 혼자서 괜한 "Bullet!"같은 문자열 따위(?)나 출력해봤을 때보다 더 빠르게 문제의 원인은 진단할 수 있었을 것이다.

 

미련하게 왜 계속 이렇게 고생을 하고 있었나 싶다. 사실 이러한 Debug.Log()로 출력해보는 1차적인 시도도 정말 좋지만 디버그로 한줄씩 코드를 테스트해봤다면 금방 문제를 찾아낼 수 있었을 텐데.

아무튼 이번 문제로 그동안 지레 겁먹고 못해봤던 코루틴도 능숙하게 활용하게 되었다. (생각보다 어렵지 않아서 허무했다) 그리고 문자열 인수 전달의 위험성이나 이때 default같은 (모든 case에 해당하지 않을 경우) 예외처리의 필요성도 확실히 알게됐다. 이렇게 한번 고생 제대로 하고나니 다음부턴 좀 더 빠르게 문제의 원인을 찾을 수 있을 것 같다.

Debug.Log()로 실질적인 '오브젝트 정보'를 출력해보자.
모든 1차 시도로 해결하지 못했을 때 (뭐든간에) 극단적으로 비활성화시켜보는 시도도 문제의 원인을 밝히는데 도움이 된다. 
(한줄) 디버그 모드 적극적으로 활용하자.
문자열(string타입) 인수 전달은 실수하지 않도록 반드시 주의를 기울여서 정확히 작성하자
모든 경우에 해당하지 않을 경우 default같은 예외처리를 해주자 (할거없으면 콘솔출력이라도 할수있게)
오브젝트 풀링같은 경우 문자열 인수전달보다 숫자 인수 전달이 정확도가 높다.

 

2. 보스가 죽어도 계속 공격하는 문제

//Enemy
void FireFoward()
{
    if (health <= 0)
        return;
    ...
    
void FireShot()
{
    if (health <= 0)
        return;
    ...

void FireArc()
{
    if (health <= 0)
        return;
    ...

void FireAround()
{
    if (health <= 0)
        return;
    ...
    
 public void OnHit(int damage)
 {
     if (health <= 0)
         return;

     health -= damage;

     if (enemyName == "B")
     {
         anime.SetTrigger("OnHit");
     }

     else
     {
         spriteRenderer.sprite = sprites[1];
         Invoke("ReturnSprite", 0.1f);
     }
     
    if (health <= 0)
    {
        ...
        else if (ran < 10) //Boom 0.1
        {
            GameObject itemBoom = objectManager.MakeObj("ItemBoom");
            itemBoom.transform.position = transform.position;
        }

        CancelInvoke();
        transform.rotation = Quaternion.identity;
        gameObject.SetActive(false);
    }
       
}

 

보스의 모든 공격 패턴 함수 첫 부분에 health가 0 이하면 return시키는 조건을 추가해서 죽었다면 공격패턴을 실행하지 않도록 한다. 그리고 확실하게 OnHit()에서 적이 비활성화되는 부분에 CancelInvoke() StopAllCoroutines()를 추가한다

Comments