소소한 나의 하루들

2d 종스크롤 슈팅(10) - 따라다니는 보조무기 만들기 본문

개발/유니티

2d 종스크롤 슈팅(10) - 따라다니는 보조무기 만들기

소소한 나의 하루 2024. 2. 25. 19:08

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

 

📚 유니티 기초 강좌

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

www.youtube.com

여러 슈팅게임에서 볼수 있었던 플레이어를 따라오는 보조무기를 만들어보도록 한다.


#1. 준비하기

주어진 에셋의 sprite에 대해 설정을 한다. (Pixels per Unit은 24, Filter Mode는 Point (no filter), Compression은 None) 설정이 끝나면 그대로 Scene에 드래그해서 오브젝트를 생성한다. (Follower) 그리고 Order in Layer는 플레이어보다 잘 보이게 더 높은 값으로 설정한다. 

Follower가 쏴야할 총알도 프리펩으로 만든다. 기존의 Plyare Bullet A를 Ctel+D해서 이름을 수정한다. 이미지도 변경하고, 콜라이더도 수정해준다. (Follower Bullet A)


#2. 기본 작동 구현

Follower 스크립트를 생성한다. Follower의 기본적인 로직은 Player와 거의 흡사하다.

우선 Player 로직에서 무엇이 필요할지 보면, 총알을 쏘아야하니까 총알발사 관련 로직을 복사하여 붙여넣는다. Follower의 움직임은 자체적으로 움직이는 것이 아니고, 플레이어를 따라가는 것이다. 그래서 Move()의 움직임 로직은 비우고 Follower()로 변경한다. 그리고 보조 총알도 오브젝트 풀링을 등록한다. (ObjectManager에서 추가)

 

그러면 이제 Follower 스크립트도 ObjectManager 스크립트를 가져와서 MakeObj()를 사용해야한다. 에디터에서 적절하게 참조를 할당해준다.

MaxShotDelay에 초기 값을 지정해주고 실행해보면 ObjectManager에 할당해준대로 FollowrBullet이 설정한대로 앞으로 잘 발사되는 것을 확인할 수 있다.

 

앞으로 해야할 것은 플레이어가 움직이는대로 보조무기도 따라오는 것이다.


#3. 팔로우 로직

따라갈 위치를 미리 정해주기위해 벡터 변수와 목적지로 부모 오브젝트(플레이어) 위치 변수를 생성한다.

Queue<타입> 변수명;

그리고 새로운 자료구조 Queue를 생성한다. 

*리스트와 사용방법이 비슷하다.


Queue

https://developer-talk.tistory.com/189

 

[C#]컬렉션, 큐(Queue) 자료구조

.NET 프레임워크가 제공하는 컬렉션 클래스에서 큐(Queue)를 소개합니다. Queue는 먼저 들어온 값이 먼저 나중에 나가는 FIFO(First In First Out)이라 부르는 자료 구조입니다. 대기열처럼 먼저 기다리는

developer-talk.tistory.com

FIFO(First Input First Out : 먼저 들어온 값이 먼저 나가는) 자료구조이다. 그래서 어떠한 작업을 순서대로 처리해야할 때 사용한다. *반대 개념은 스택(Stack)

Queue에 값을 추가하는 경우 Enqueue()를 사용하고, 값을 삭제하는 경우 Dequeue()를 사용한다. 


public Vector3 followPos;
public int followDelay;
public Transform parent;
public Queue<Vector3> parentPos;

void Watch()
{
    followPos = parent.position;
}

private void Follow()
{
    transform.position = followPos;
}

이렇게 변수 4개를 만든다. Follow() 함수에는 transform.position에 followPos만 대입해주면 되고, 따라갈 위치를 계속 갱신해주는 함수 Watch()를 생성한다. 그리고 에디터에서 parent 변수에 플레이어 오브젝트를 할당한다. 실행해보면 플레이어 위치를 따라가기는 하는데, 플레이어와 같은 위치에 딱 붙어서 따라간다.

void Watch()
{
    //Input Pos
    parentPos.Enqueue(parent.position);
    
    //Output Pos
    followPos = parentPos.Dequeue();
}

그래서 FollowDelay를 추가한 것이다. 여기서 Queue를 사용한다. 

Enqueue() : 큐에 데이터를 저장하는 함수

부모 오브젝트의 위치값을 parentPos 변수에 저장한다.

다시 나갈 때는 Dequeue() 함수를 사용한다. 우리는 Queue 변수 parentPos를 선언하면서 타입을 Vector3로 했기 때문에, Vector3 타입의 데이터가 반환된다.

Dequeue() : 큐의 첫 데이터를 빼면서 반환하는 함수

*Queue의 길이도 리스트와 마찬가지로 .Count로 크기를 갖고온다.

void Watch()
{
    //Input Pos
    parentPos.Enqueue(parent.position);
    
    //Output Pos
    if (parentPos.Count > followDelay)
    {
        followPos = parentPos.Dequeue();
    }
    
}

그런데 이렇게 하면 위에서와 마찬가지로 똑같다. 그래서 딜레이 시간을 넣어준다.

if (parentPos.Count > followDelay)

큐에 일정 데이터 갯수가 채워지면(=플레이어의 위치가 갱신되어서 일정 좌표벡터 데이터가 쌓이면) 그때부터 첫번째 데이터(위치)를 반환하도록 작성한다.

: 딜레이만큼 이전 프레임 위치를 보조 무기 위치에 적용한다.

 

보통 우리가 알고있는 슈팅게임은 보조 무기가 플레이어 비행기에 찰싹 달라붙지 않고, 플레이어가 멈추면 보조 무기도 멈추게 된다.

하지만 지금 이렇게 플레이어가 가만히 있을 때에도 그 위치가 계속 Update되기 때문에 보조무기가 따라오는 것이다.

따라서 플레이어가 가만히 있다면 (=플레이어의 현재 위치가 이미 parentPos Queue 변수에 들어있다면) 위치값을 넣어주지 않도록 조건을 설정한다. : 부모 위치가 가만히 있으면 저장하지 않도록 조건을 추가한다.

.Contains(데이터) : 데이터가 안에 들어있는지 확인하는 함수

void Watch()
{
    //Input Pos
    if (!parentPos.Contains(parent.position))
    parentPos.Enqueue(parent.position);
    
    //Output Pos
    if (parentPos.Count > followDelay)
    {
        followPos = parentPos.Dequeue();
    }
    else if (parentPos.Count < followDelay)
    {
        followPos = parent.position;
    }
    
}

실행하면 맨 처음 follower 보조무기의 위치는 (0, 0, 0)이다. 그래서 그냥 그 자리에 그대로 있을 것이다. (아직 parentPos에 채워진 데이터의 개수가 followDelay보다 커지지 않았으니까) 그래서 큐가 채워지기 전까지는 임시 방편으로 부모 위치를 적용한다.

 

보통 이런 보조무기는 처음에는 없는데, 플레이어의 파워모드가 올라가면 생기거나, 아이템을 획득하면 보조무기가 하나씩 생기는 방식이다.


#4. 파워 적용

//Player
public void OnTriggerEnter2D(Collider2D collision)
{
    if(collision.gameObject.tag == "Border")
    {
        switch(collision.gameObject.name)
        {
            case "Top":
                isTouchTop = true;
                break;
            case "Bottom":
                isTouchBottom = true;
                break;
            case "Left":
                isTouchLeft = true;
                break;
            case "Right":
                isTouchRight = true;
                break;
        }
    }

    else if(collision.gameObject.tag == "Enemy" || collision.gameObject.tag == "EnemyBullet")
    {
        if (isHit)
            return;

        isHit = true;
        life--;
        gameManager.UpdateLifeUI(life);

        if(life == 0) {
            gameManager.GameOver();
        }

        else
        {
            gameManager.RespawnPlayer();
        }

        gameObject.SetActive(false);
        collision.gameObject.SetActive(false);
        isHit = true;
    }

    else if (collision.gameObject.tag == "Item")
    {
        Item item = collision.gameObject.GetComponent<Item>();

        switch (item.type)
        {
            case "Coin":
                score += 1000;
                break;
            case "Power":
                if (power == maxPower)
                    score += 500;
                else
                {
                    power++;
                    AddFollower();
                }
                break;
                
            case "Boom":
                if (curBoom == maxBoom)
                    score += 500;
                else
                {
                    curBoom++;
                    gameManager.UpdateBoomUI(curBoom);
                }
                    
                break;
        }
        collision.gameObject.SetActive(false);
    }

}

void AddFollower()
{
    if (power == 4)
        followers[0].SetActive(true);
    else if (power == 5)
        followers[1].SetActive(true);
    else if (power == 6)
        followers[2].SetActive(true);
}

만약 이대로 실행하게 되면 3개의 보조무기가 같은 위치에 있게 될 것이다. 이렇게 되면 안되고, 비엔나소시지처럼 줄줄이 따라가야한다. 그렇게 하려면 Follower 1은 Player를, Follower2는 Follower2를, Follower3는 Follower 2를 따라다니도록 하면 된다. 

그리고 최대 파워가 올라간대로 에디터에서 수치는 6으로, 로직도 수정해준다. 로직을 수정하는 방법은 2가지가 있다.

witch (power)
        {
            case 1:
                //Power One
                GameObject bullet = objectManager.MakeObj("BulletPlayerA");
                bullet.transform.position = transform.position;
                Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
                rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            case 2:
                GameObject bulletR = objectManager.MakeObj("BulletPlayerA");
                GameObject bulletL = objectManager.MakeObj("BulletPlayerA");
                bulletR.transform.position = transform.position + Vector3.right * 0.1f;
                bulletL.transform.position = transform.position + Vector3.left * 0.1f;
                Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
                rigidR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;
            case 3:
            case 4:
            case 5:
            case 6:
                GameObject bulletRR = objectManager.MakeObj("BulletPlayerA");
                GameObject bulletCC = objectManager.MakeObj("BulletPlayerB");
                GameObject bulletLL = objectManager.MakeObj("BulletPlayerA");
                bulletRR.transform.position = transform.position + Vector3.right * 0.3f;
                bulletCC.transform.position = transform.position;
                bulletLL.transform.position = transform.position + Vector3.left * 0.3f;
                Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidCC = bulletCC.GetComponent<Rigidbody2D>();
                Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();
                rigidRR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidCC.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                rigidLL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
                break;

        }

1. case문을 더 작성해준다. 

switch (power)
{
    case 1:
        //Power One
        GameObject bullet = objectManager.MakeObj("BulletPlayerA");
        bullet.transform.position = transform.position;
        Rigidbody2D rigid = bullet.GetComponent<Rigidbody2D>();
        rigid.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
        break;
    case 2:
        GameObject bulletR = objectManager.MakeObj("BulletPlayerA");
        GameObject bulletL = objectManager.MakeObj("BulletPlayerA");
        bulletR.transform.position = transform.position + Vector3.right * 0.1f;
        bulletL.transform.position = transform.position + Vector3.left * 0.1f;
        Rigidbody2D rigidR = bulletR.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidL = bulletL.GetComponent<Rigidbody2D>();
        rigidR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
        rigidL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
        break;
    default:
        GameObject bulletRR = objectManager.MakeObj("BulletPlayerA");
        GameObject bulletCC = objectManager.MakeObj("BulletPlayerB");
        GameObject bulletLL = objectManager.MakeObj("BulletPlayerA");
        bulletRR.transform.position = transform.position + Vector3.right * 0.3f;
        bulletCC.transform.position = transform.position;
        bulletLL.transform.position = transform.position + Vector3.left * 0.3f;
        Rigidbody2D rigidRR = bulletRR.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidCC = bulletCC.GetComponent<Rigidbody2D>();
        Rigidbody2D rigidLL = bulletLL.GetComponent<Rigidbody2D>();
        rigidRR.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
        rigidCC.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
        rigidLL.AddForce(Vector2.up * 10, ForceMode2D.Impulse);
        break;

}

2. default로 처리한다. (case1도 아니고, case2도 아닌 나머지 상황에 대해서는 default로 처리한다)


문제상황 - 피드백

1. 필살기 사용 시 적과 적의 총알이 비활성화되지 않는 문제

필살기를 사용했을 때 적의 체력이 닳지 않는 것을 보아, Enemy 스크립트의 OnHit()에 접근하지 못했던 것 같아 Debug.Log()를 통해 문제가 되는 부분을 살펴보았다.

그 결과 필살기를 사용했을 때 필살기 폭탄 효과가 나타났다가 사라지는 부분까지는 실행이 되지만, Enemy 로직에 접근하지 못하는 것을 확인할 수 있었고, 여기에 활성화된 적 오브젝트에 대해서만 접근할 수 있도록 조건을 추가했다.

//Player
void Boom()
{
    if (!Input.GetButton("Fire2"))
        return;

    if (isBoomTime)
        return;

    if (curBoom == 0)
        return;

    curBoom--;
    isBoomTime = true;
    gameManager.UpdateBoomUI(curBoom);

    //Effect visible
    boomEffect.SetActive(true);
    Invoke("OffBoomEffect", 3.0f);
    Debug.Log("Hello!");

    //Remove Enemy
    //GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");
    GameObject[] enemiesL = objectManager.GetPool("EnemyL");
    GameObject[] enemiesM = objectManager.GetPool("EnemyM");
    GameObject[] enemiesS = objectManager.GetPool("EnemyS");

    for (int index = 0; index < enemiesL.Length; index++)
    {
        if (enemiesL[index].activeSelf)
        {
            Enemy enemyLogic = enemiesL[index].GetComponent<Enemy>();
            enemyLogic.OnHit(1000);
        }
       
    }

    for (int index = 0; index < enemiesM.Length; index++)
    {
        if (enemiesM[index].activeSelf)
        {
            Enemy enemyLogic = enemiesM[index].GetComponent<Enemy>();
            enemyLogic.OnHit(1000);
        }
    }

    for (int index = 0; index < enemiesS.Length; index++)
    {
        if (enemiesS[index].activeSelf)
        {
            Enemy enemyLogic = enemiesS[index].GetComponent<Enemy>();
            enemyLogic.OnHit(1000);
        }
    }

    //Remove Enemy Bullet
    //GameObject[] bullets = GameObject.FindGameObjectsWithTag("EnemyBullet");
    GameObject[] bulletsA = objectManager.GetPool("BulletEnemyA");
    GameObject[] bulletsB = objectManager.GetPool("BulletEnemyB");

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

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

}

if (enemiesM[index].activeSelf)

현재 활성화된 상태의 적에 대해서만 EnemyLogic에 접근해서 OnHit(1000)을 호출하고, 현재 모든 총알을 비활성화했어야 했다.

Comments