Carrot
본문 바로가기
Unity/멋쟁이사자처럼 부트캠프

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(24일차) - Monster 게임 업그레이드

by 독기품은토끼 2025. 6. 18.
✅ 오늘의 학습 목표
1. Monster 게임 제작하기

 

1. Monster 게임 제작하기

어제 몬스터들이 걸어 다니고, 클릭하면 몬스터가 죽는 기능을 구현했었다.

오늘은 이 실습을 좀 더 업그레이드 해보겠담

  • 몬스터가 공격받고 / 죽는 애니메이션 추가하기
  • 몬스터 리젠 기능 만들기
  • 몬스터가 죽을 때 아이템(Coin)을 드랍하도록 구현하기

 

1. 애니메이션 추가

1.1. Hit (공격받는) 애니메이션 추가

// Monster 스크립트, 애니메이션 적용을 위한 작업
private Animator animator;

void Start()
{
    animator = GetComponent<Animator>();
}

 

우선 몬스터의 동작에 따라 애니메이션의 동작이 달라져야 하니까

애니메이션 값을 불러올 수 있도록 Animator을 선언해 주고 Get 명령어로 값을 가져온다.

 

 

몬스터가 공격받는 애니메이션을 추가해 줄 것이다.

몬스터의 메인 애니메이터(Run이나 Walk, Fly)에서 추가할 애니메이션(Hit)을 넣어주면 된다.

 

void Hit(float damage)
{
    // 파라미터 호출
    animator.SetTrigger("[Trigger] Hit");

    hp -= damage;

    if (hp <= 0)
    {
        Debug.Log("몬스터 쥬금!");
        Destroy(gameObject);
    }
}

 

애니메이터에서 Trigger로 파라미터를 만들어준 후 해당 파라미터를 코드에 적용시켜 주면

몬스터를 클릭하면 몬스터가 공격받는 애니메이션이 작동된다.

 

// 공격할 때에는 움직이지 않도록 하기 위해 선언한 값
private bool isMove = true;

void Move()
{
    if (!isMove)
    return;
}

void OnMouseDown()
{
    //Hit(1);
    StartCoroutine(Hit(1));
}

IEnumerator Hit(float damage)
{       
    isMove = false;
    animator.SetTrigger("[Trigger] Hit");

    hp -= damage;

    if (hp <= 0)
    {
        Debug.Log("몬스터 쥬금!");
        Destroy(gameObject);
    }

    // 애니메이션 동작을 위해 코루틴을 사용해서 잠깐 오브젝트 멈춰주기
    yield return new WaitForSeconds(0.5f);
    isMove = true;
}

 

몬스터가 공격받을 때 몬스터가 잠깐 멈춰지도록 설정해 주었다.

 

🚨  그런데 여기서 코루틴 문제가 하나 발생한다!

마우스 클릭을 연속으로 하면 맞는 채로 움직이는 현상 나타나게 되었는데

코루틴이 반복적으로 실행되면서 첫 번째 클릭으로 생긴 코루틴이 두 번째 것보다 먼저 끝나버려서 발생하는 문제이다.

첫번째 코루틴에서 두번째 코루틴이 동작중일 때 isMove = true가 실행돼서 몬스터가 다시 움직이게 되고 그 사이에 애니메이션은 여전히 맞는 중이라 연출이 원하는 대로 나타나지 않는다는 뜻이다.

 

private bool isHit = false;

IEnumerator Hit(float damage)
{
    if (isHit)
        yield break;

    isHit = true;
    isMove = false;
    // 파라미터 호출
    animator.SetTrigger("[Trigger] Hit");

    hp -= damage;

    if (hp <= 0)
    {
        Debug.Log("몬스터 쥬금!");
        Destroy(gameObject);
    }

    // 애니메이션 동작을 위해 코루틴을 사용해서 잠깐 오브젝트 멈춰주기
    yield return new WaitForSeconds(0.5f);
    isHit = false;
    isMove = true;
}

 

isHit 이라는 코루틴 중복 실행 방지용 플래그용으로 변수를 하나 만들어주고

yield break; 문으로 코루틴이 이미 실행 중이면 코루틴이 반복적으로 실행되지 않도록 막아주었다.

 

🥕 Hit 애니메이션의 Loop를 꺼주세요! 이 영상은 안 껐습니다..

 

나머지 몬스터들의 애니메이션도 동일하게 만들어주면 된다.

 

 

1.2. Death (죽는) 애니메이션 추가

🥕 Loop 끄기

IEnumerator Hit(float damage)
{
    if (isHit)
        yield break;

    isHit = true;
    isMove = false;

    hp -= damage;

    if (hp <= 0)
    {
        animator.SetTrigger("[Trigger] Death");
        yield break;
    }

    animator.SetTrigger("[Trigger] Hit");

    // 애니메이션 동작을 위해 코루틴을 사용해서 잠깐 오브젝트 멈춰주기
    yield return new WaitForSeconds(0.65f);
    isHit = false;
    isMove = true;
}

 

hp가 0 이하로 떨어지면 죽는 애니메이션이 동작되도록 if문 안에 animator를 구현해주었다.

기존 코드에서는 Hit 애니메이션이 수행된 후 if문이 실행되어 고블린 기준으로 3번 클릭하면 맞는 동작을 3번 수행한 후 고블린이 쓰러지게 되었었는데,

animator.SetTrigger("[Trigger] Hit"); 실행 순서를 animator.SetTrigger("[Trigger] Death"); 뒤로 옮겨 주어 공격을 2번 맞고 3번째 클릭했을 때 바로 죽는 애니메이션이 동작되도록 수정해 주었다.

 

 

 

1.3. 프레임 조절

 

움직임을 좀 더 자연스럽게 처리해 주기 위해 애니메이션의 프레임 수를 조절해 주었다.

Samples가 화면에 나타나지 않을 때에는 오른쪽 상단에 [⋮ → Show Sample Rate]를 클릭해 준다.

 

 

2. 몬스터 생성 (젠)

이번에는 만들었던 몬스터 오브젝트들을 프리팹으로 만든 후

몬스터 4마리의 종류 중 1마리가 랜덤으로 나오고, 위치도 랜덤한 위치에 나오도록 구현해 주겠다.

🥕 예행 작업
1. Script 생성 (SpawnManager)
using System.Collections;
using UnityEngine;

public class SpawnManager : MonoBehaviour
{
    // 몬스터 종류가 이미 정해진 경우
    [SerializeField] private GameObject[] monsters;

    // n초마다 몬스터를 랜덤으로 생성하는 기능
    IEnumerator Start()
    {
        while (true)
        {
            yield return new WaitForSeconds(3f);

            // 몬스터 종류 랜덤으로 나타나도록
            var randomIndex = Random.Range(0, monsters.Length);

            // 몬스터가 랜덤 위치에 나타나도록
            var randomX = Random.Range(-8, 9);
            var randomY = Random.Range(-3, 5);
            var createPos = new Vector3(randomX, randomY, 0);

            Instantiate(monsters[randomIndex], createPos, Quaternion.identity); // 몬스터 생성 -> 원점에 생성
        }
    }
}

 

몬스터의 종류를 배열에 담아두고 Random 메서드를 써서 종류를 랜덤 하게 뽑아오게 하였고 위치도 Random.Range()를 써서 x, y값을 랜덤하게 지정해 주었다.

 

❗ Tip

Start() 안에서 바로 코루틴을 쓰면 따로 StartCoroutine()을 안 해도 자동으로 실행된다.

 

좀 기다리면 랜덤 위치에 랜덤 몹이 젠되는 것을 확인할 수 있다.

 

3. 아이템 (코인)

이제 몬스터가 죽으면 코인을 드랍하는 걸 구현하려고 한다.

드랍한 코인을 줍는 기능과 드랍될 때 중력을 활용해 줄 것이기 때문에 Rigidbody와 Collider를 함께 사용해 줄 것이다.

 

 

3.1. 기본 틀

 

우선 코인이 떨어졌을 때 게임 씬 너머로 떨어지는 걸 방지하기 위해 Ground 오브젝트를 만든 후 Box Collider을 적용해 주었고

상호작용을 위해 Coin 오브젝트에 Rigidbody 컴포넌트를 추가해 주었다. (콜라이더도 당연히 같이 있어야 함)

그리고 코인은 몬스터가 죽을 때마다 생성되어야 하기 때문에 Prefab으로 만들어주었다.

 

❗ Tip

인스펙터 창에서 프리팹의 설정을 변경했다면 원본 프리팹에 Override 해줄 수 있다.

 

public class SpawnManager : MonoBehaviour
{
    // 코인 구현
    [SerializeField] private GameObject coinPrefab;
    
    public void DropCoin(Vector3 dropPos)
    {
        Instantiate(coinPrefab, dropPos, Quaternion.identity);
    }
}
public abstract class Monster : MonoBehaviour
{
    // Coin을 위해 SpawnManager 불러오기
    public SpawnManager spawner;

    IEnumerator Hit(float damage)
    {
        if (hp <= 0)
        {
            animator.SetTrigger("[Trigger] Death");

            spawner.DropCoin(transform.position); // 코인 생성

            yield return new WaitForSeconds(3f);
            Destroy(gameObject);

            yield break;
        }
    }
}

 

몬스터가 죽을 때 코인을 떨어뜨리게 하려고 SpawnManager 스크립트의 DropCoin()을 호출해 줬다.

 

 

 

3.2. Spawner Null 값 참조

NullReferenceException: Object reference not set to an instance of an object

 

3.1. 에서 구현한 코드를 적용하면 위와 같은 에러가 나타난다.

몬스터 프리팹 안에 들어있는 spawner가 null이라서 생긴 문제인데,
프리팹은 씬에 배치된 게 아니라 런타임에 Instantiate로 생성되기 때문에 직접 씬에 있는 오브젝트(SpawnManager)를 참조할 수 없다.

 

public abstract class Monster : MonoBehaviour
{
    // Coin을 위해 SpawnManager 불러오기
    public SpawnManager spawner;
    
    void Start()
    {
        spawner = FindFirstObjectByType<SpawnManager>();

        sRenderer = GetComponent<SpriteRenderer>();
        animator = GetComponent<Animator>();

        Init();
    }
}


이럴 때에는 Find 메서드를 활용해서 SpawnManager을 참조할 수 있도록 수정해 주면 된다.

 

▶ GetComponent와 FindFirstObjectByType의 차이점

메서드 설명 사용 예시
GetComponent<T>() 자기 자신이나 자식 오브젝트에 붙어 있는 컴포넌트를 찾을 때 사용 GetComponent<Animator>()
FindFirstObjectByType<T>() 씬 전체에서 해당 타입의 오브젝트를 하나 찾아옴 FindFirstObjectByType<SpawnManager>()

 

 

3.3. Layer 처리

 

영상을 보면 코인이 몬스터 머리 위에 붙어 있거나 몬스터가 움직일 때 코인이 같이 따라 움직이는 등 코인과 몬스터의 충돌로 인한 문제가 발생한 것을 알 수 있다.

이 부분을 해결해 주기 위해 Layer을 설정해 주겠다.

 

 

인스펙터 창에서 레이어를 추가해 준 후 몬스터는 몬스터 레이어를, 코인은 코인 레이어를 적용해 주었다.

 

 

Edit → Project Settings → Physics 2D → Layer Collision Matrix 에서

레이어 충돌 행렬을 조절해 줄 수 있는데

여기서 충돌을 무시할 레이어 조합을 지정해서 불필요한 충돌을 방지할 수 있다.

 

근데 이때 내가 설정을 뭘 어떻게 했길래.. 코인 Rigidbody가 작동을 안하고 저렇게 똑 떨어지지? ㄷㄷㄷㄷ 모르겠음 ㄷㄷㄷㄷ

 

이렇게 몬스터와 코인이 부딪혀도 충돌되지 않고, 코인도 코인끼리 겹쳐져서 쌓이는 것을 확인할 수 있다.

 

3.4. 아이템 시각 효과

 

코인을 추가적으로 더 만들어주고 몬스터에게 코인이 가려지지 않도록 Order in Layer도 설정해 주었다.

 

public class SpawnManager : MonoBehaviour
{
    // 코인 구현 (1개용)
    // [SerializeField] private GameObject coinPrefab;
    
    // 코인 구현 (여러개)
    [SerializeField] private GameObject[] items;
}

 

그런 다음 여러 개의 코인을 처리해 줄 것이기 때문에 코인도 몬스터 오브젝트와 동일하게 배열 값으로 가져왔다.

 

public class SpawnManager : MonoBehaviour
{
	public void DropCoin(Vector3 dropPos)
    {
        var randomX = Random.Range(0, items.Length);

        // 아이템 생성
        GameObject item = Instantiate(items[randomX], dropPos, Quaternion.identity);

        // 아이템 뿌리기 - 방향 구현
        Rigidbody2D itemRb = item.GetComponent<Rigidbody2D>();
        itemRb.AddForceX(Random.Range(-2f, 2f), ForceMode2D.Impulse);
        itemRb.AddForceY(3f, ForceMode2D.Impulse);

        // 아이템 뿌리기 - 회전 구현
        float ranPower = Random.Range(-1.5f, 1.5f);
        itemRb.AddTorque(ranPower, ForceMode2D.Impulse);
    }
}

 

이제 몬스터가 죽으면서 코인을 뿌릴 때 랜덤한 방향으로 튕겨 나가게 만들어 시각효과를 나타내보았다.

 

AddForce로 좌우 방향은 -2f ~ 2f 사이 랜덤, 위쪽으로는 -1.5f ~ 1.5f 사이 랜덤으로 튕기게 설정해 주었고,

AddTorque도 같이 써서 코인이 회전하면서 떨어지도록 구현하였다.

 

 

코인이 위에서 바닥으로 떨어지다 보니 바닥에서 무한정 구르는 현상이 나타났는데,

이때에는 코인의 Rigidbody에서 Damping값을 수정해 주면 된다. (나는 둘 다 0.5로 설정해 주었다.)

 

 

좌우 위아래 랜덤한 위치에서 코인이 나타나고, 회전값도 랜덤으로 주었기 때문에 어떤 아이템은 빠르게 회전/느리게 회전하는 것을 확인할 수 있다.

 

 

 


 

 

 

오늘 수업은 복습에 가깝다! 이번 주에 배운 상속과 인터페이스를 활용해서 여태 배웠던 기능들을 조금 더 업그레이드해서 구현했다고 봐도 무방할 것 같다.

 

 

그리고 오늘 어쩌다 지금 하고 있는 부트캠프 커리큘럼을 봤는데 Cat 횡 스크롤 게임이 하나의 미니 프로젝트였다는 것을 깨닫게 되었다...

실은 이 이미지를 보고 혹해서 부캠을 신청했던 건데

아래 *이해를 돕기 위한 참고용 Unity 기반 게임 이미지입니다. 이걸 이제 봐버림!

저 이미지와 동일하게 게임을 만드는 건 줄 알았는데 아니었나보다...

 

흠.. 얼추 여태 배웠던 기능들 활용하면 저 이미지의 게임을 만들 수는 있을 것 같긴한뎀.. 흠흠흠

1달 미니 프로젝트가 끝났으니 1달용 회고로 저런 미니 게임을 한번 만들어봐야겠다.

근데 매번 이런 다짐을 하지만 ㅠ 수업 끝나면 지쳐서 아무것도 못하겠음!!! ㅋㅋㅋㅋ 자격증 시험도 다가오는데 에바다! 우웩

효율적인 공부를 위해 한번 계획표를 짜봐야겠돰.. 총총총🥕