✅ 오늘의 학습 목표
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; 문으로 코루틴이 이미 실행 중이면 코루틴이 반복적으로 실행되지 않도록 막아주었다.


나머지 몬스터들의 애니메이션도 동일하게 만들어주면 된다.
1.2. Death (죽는) 애니메이션 추가

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 에서
레이어 충돌 행렬을 조절해 줄 수 있는데
여기서 충돌을 무시할 레이어 조합을 지정해서 불필요한 충돌을 방지할 수 있다.

이렇게 몬스터와 코인이 부딪혀도 충돌되지 않고, 코인도 코인끼리 겹쳐져서 쌓이는 것을 확인할 수 있다.
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달용 회고로 저런 미니 게임을 한번 만들어봐야겠다.
근데 매번 이런 다짐을 하지만 ㅠ 수업 끝나면 지쳐서 아무것도 못하겠음!!! ㅋㅋㅋㅋ 자격증 시험도 다가오는데 에바다! 우웩
효율적인 공부를 위해 한번 계획표를 짜봐야겠돰.. 총총총🥕
'Unity > 멋쟁이사자처럼 부트캠프' 카테고리의 다른 글
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(26일차) - 게임수학(2) 및 2D플랫포머 게임(1) (1) | 2025.06.23 |
|---|---|
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(25일차) - Monster 게임 마무리 & 게임 수학(1) (0) | 2025.06.19 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(23일차) - 캡슐화 및 인터페이스&상속 실습 (4) | 2025.06.17 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(22일차) - Interface(인터페이스)를 활용한 스크립트 작성 (0) | 2025.06.16 |
| [멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(21일차) - C# 기초(10) / 형 변환과 상속의 개념 (1) | 2025.06.13 |