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

[멋쟁이사자처럼부트캠프] 유니티 게임 개발 5기(25일차) - Monster 게임 마무리 & 게임 수학(1)

by 독기품은토끼 2025. 6. 19.
✅ 오늘의 학습 목표
1. Monster 게임 아이템 획득 구현
2. 게임 수학
- 삼각함수 실습
- 벡터와 스칼라 알아보기

 

1. Monster 게임

어제 몬스터가 죽으면서 아이템이 뿌리는 것까지 구현했다.

오늘은 이 뿌린 아이템을 획득하는 걸 이번주 공부의 핵심인 인터페이스를 활용해서 구현해 볼 것이다.

 

1. 아이템 획득 구현

1.1. Item 인터페이스 & 아이템 스크립트

🥕 예행 작업
1. Scripts 생성 (CoinMonster, Potion)
using UnityEngine;

public interface IItem
{
    void Get();
}
using UnityEngine;

public class CoinMonster : MonoBehaviour, IItem
{
    public enum CoinType { Gold, Green, Blue }
    public CoinType coinType;

    void OnMouseDown()
    {
        Get();
    }

    public void Get()
    {
        Debug.Log($"{this.name}을 획득했습니다.");
        gameObject.SetActive(false);
    }
}
using UnityEngine;

public class Potion : MonoBehaviour, IItem
{
    public enum PotionType { Gold, Hp, Mp }
    public PotionType potionType;
    
    void OnMouseDown()
    {
        Get();
    }
    
    public void Get()
    {
        Debug.Log($"{this.name}을 획득했습니다.");
        gameObject.SetActive(false);
    }
}

 

우선 프리팹으로 만들어둔 아이템의 종류가 많으니 IITem이라는 인터페이스를 만들어주고,

각 아이템 스크립트를 만들어 아이템 획득 로직을 구현하였다.

 

 

1.2. 인벤토리 구현

아이템에 마우스를 클릭하면 이벤트가 발동되는 부분까지는 구현하였으니

이제 이 아이템이 인벤토리에 쌓이도록 구현해보겠다. (인벤토리 UI를 따로 구현하진 않고 리스트로 처리해 줄 것이다)

using System.Collections.Generic;
using UnityEngine;

public class Inventory : MonoBehaviour
{
    //public List<IItem> items = new List<GameObject>(); // 인터페이스라 인스펙터에 안 보임
    public List<GameObject> items = new List<GameObject>();

    public void AddItem(IItem item)
    {
        items.Add(item.Obj);
    }
}

 

여기서 List<IItem>이 아니라 List<GameObject>로 한 이유는 Unity 인스펙터에서 인터페이스 타입은 보이지 않기 때문이다.

그래서 아이템 오브젝트를 직접 리스트에 넣어줄 수 있게 GameObject 타입으로 선언해 주었다.

그리고 아이템 쪽에서는 자기 자신(gameObject)를 넘겨줄 수 있도록 obj 프로퍼티를 만들어주었다.

 

public interface IItem
{
    GameObject Obj { get; set; }
    void Get();
}

 

 

IITem 인터페이스에는 Get()만 있었는데, 위에서 말했듯이 인벤토리에 아이템을 추가할 때 GameObject가 필요해서 Obj 프로퍼티를 추가하였다.

이렇게 해주면 item.Obj처럼 오브젝트를 꺼내서 사용할 수 있다.

 

1.3. 아이템 스크립트 수정

기존에는 Get()에서 아이템을 비활성화 처리만 해주었는데,

이제는 인벤토리를 찾아서 거기에 등록까지 하는 것을 구현해 줄 것이다.

// Coin, Potion 스크립트에 추가해준다.
private Inventory inventory;

void Start()
{
    inventory = FindFirstObjectByType<Inventory>();

    Obj = gameObject;
}

public GameObject Obj { get; set; }

public void Get()
{
    Debug.Log($"{this.name}을 획득했습니다.");

    inventory.AddItem(this);

    gameObject.SetActive(false);
}

 

  • IItem 인터페이스에는 모든 아이템이 Obj라는 GameObject 프로퍼티를 갖도록 약속해 놓았기 때문에 이를 상속받는 Coin, Potion 스크립트에서는 반드시 obj를 구현해야 한다.
  • 그래서 public GameObject Obj { get; set; }를 클래스에 직접 구현해 주었다. (값이 저장될 공간이자, 나중에 꺼내 쓰는 Getter/Setter 역할을 한다.)
  • 그리고 Start() 메서드 안에서 obj = gameObject; 코드를 넣어 이 프로퍼티에 자기 자신의 오브젝트를 등록해 준다. (이렇게 구현해 주어야 인벤토리 스크립트에서 item.obj 로 접근이 가능하고 해당 오브젝트를 담을 수 있게 된다)

 

 

2. 플레이어 구현

지금까지는 아이템을 마우스로 클릭해야만 획득할 수 있었는데 이제 캐릭터를 만들어주어 캐릭터가 아이템에 닿으면 획득되도록 구현해 주겠다.

🥕 예행 작업
1. Script 생성 (PlayerController)

 

2.1. 플레이어 애니메이터 적용

🤷‍♀️ 애니메이터 부분은 중요한 부분만 정리하였습니다. 혹시라도 궁금하신 게 있다면 댓글 남겨주세욤

 

기존에 만든 정글 캐릭터를 다시 사용해주려고 하는데

이때에는 애니메이터 부분을 배우지 않았나? 암튼 달리기나 점프 애니메이션을 스크립트에서 구현했었다.

이제 애니메이터를 능숙하게 다루게 되었으니 자식 오브젝트들은 다 지워버리고~ 애니메이터를 구현해 주겠다.

 

 

Bool 타입의 Run 파리미터를 생성해 주고 트랜지션을 이미지와 동일하게 적용해 주었다.

 

public class PlayerController : MonoBehaviour
{
    private Animator animator;

    [SerializeField] private float moveSpeed = 3f;
    private float h, v;

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

    void Update()
    {
        Move();
    }

    void Move()
    {
        h = Input.GetAxis("Horizontal");
        v = Input.GetAxis("Vertical");

        if (h == 0 && v ==0) // Idle 상태
        {
            animator.SetBool("[Bool] Run", false);
        }
        else // Run 상태
        {
            // 플레이어 방향키에 맞게 왼,오 쳐다보게 설정
            int scaleX = h > 0 ? 1 : -1;
            transform.localScale = new Vector3(scaleX, 1, 1);

            animator.SetBool("[Bool] Run", true);

            var dir = new Vector3(h, v, 0).normalized;
            transform.position += dir * moveSpeed * Time.deltaTime;
        }
    }
}

 

 

2.2. 충돌 감지로 아이템 획득 구현

void OnCollisionEnter2D(Collision2D other)
{
    if (other.gameObject.GetComponent<IItem>() != null)
    {
        IItem item = other.gameObject.GetComponent<IItem>();
        item.Get();
    }
}

 

여기서 갑자기 헷갈렸던 게

GetComponent<Animator> 같이 컴포넌트를 갖고 오는 건 이해가 됐는데

이 오브젝트에는 IItem 스크립트 컴포넌트가 붙어있지도 않은데.. 아무리 상속받았어도 이게 가능한가? 하고 찾아와 봤다.

 

🚨 Unity가 하는 일

IItem item = other.gameObject.GetComponent<IItem>();
  1. 해당 GameObject에 붙은 모든 스크립트 컴포넌트를 검사한다
  2. 그중에서 IITem 인터페이스를 구현한 컴포넌트를 찾는다
  3. Coin이나 Potion 같은 컴포넌트가 붙어있고 IItem을 구현했다면
  4. 그걸 찾아서 IItem 타입으로 형변환해서 반환한다..
  5. "이 게임 오브젝트에 붙은 컴포넌트 중에서 IItem을 구현한 컴포넌트가 있으면 찾아서 반환해라" 라는 뜻

 

 

 

2.3. 공격 기능 구현

마우스 클릭 말고 이제 플레이어 캐릭터가 직접 공격해서 데미지를 넣도록 구현해 주겠다.

 

공격도 아이템 획득과 동일하게 충돌 감지로 처리해 줄 거기 때문에

플레이어 앞쪽에 투명한 오브젝트를 하나 만들어준다.

 

 

그런 다음 레이어를 위 이미지와 동일하게 설정해 주었다.

 

public class PlayerController : MonoBehaviour
{
    [SerializeField] private GameObject hitBox;

    private bool isAttack = false;

    void Attack()
    {
        if (Input.GetKeyDown(KeyCode.Space) && !isAttack)
        {
            StartCoroutine(AttackRoutine());
        }
    }

    // 공격 유지
    IEnumerator AttackRoutine()
    {
        isAttack = true;
        hitBox.SetActive(true);

        yield return new WaitForSeconds(0.25f);
        hitBox.SetActive(false);
        isAttack = false;
    }

    // 몬스터 공격
    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.GetComponent<Monster>() != null)
        {
            Monster monster = other.GetComponent<Monster>();
            StartCoroutine(monster.Hit(1)); // Monster 스크립트에 Hit 코루틴 public으로 선언
        }
    }
}

 

hitBox가 몬스터와 충돌되면 Hit 코루틴을 실행하도록 구현해 주었다.

그리고 코루틴이 중복 실행되는 것을 방지해 주기 위해 bool 타입인 isAttack 기능까지 추가해 주었다.

 

 

 

2. 게임 수학

🥕 예행 작업
1. Scene 생성 (Math)

1. 삼각함수

1.1. 사인 법칙

 

직삼각형의 경우 삼각함수나 피타고라스 공식을 활용해서 식을 계산할 수 있지만

두 변이 같지 않는 삼각형의 경우 Sin 법칙을 사용한다.

사인 법칙은 삼각형 한 변과 두 각이 주어졌을 때, 나머지 변의 길이를 구하는 공식이다.

a / sin(A) = b / sin(B)
풀어쓰면 b = (a * sin(B)) / sin(A)
using UnityEngine;

public class SinLaw : MonoBehaviour
{
    public float aAngle = 30f;
    public float bAngle = 90f;
    public float aSide = 1f;

    void Start()
    {
        float aRad = aAngle * Mathf.Deg2Rad;
        float bRad = bAngle * Mathf.Deg2Rad;
        
        float bSide = (aSide * Mathf.Sin(bRad)) / Mathf.Sin(aRad);
        
        Debug.Log(bSide);
    }
}

 

유니티에서 삼각함수는 라디안 단위이므로 Deg2Rad로 바꿔주어야 한다.

Mathf.Sin()으로 각도의 사인값을 구해서 bSide를 계산한다.

 

이 수식으로 게임 속에서 포탄이 날아가는 거리, 카메라 시야각에서 대상까지의 거리를 계산하는 데 사용된다.

 

1.2. 코사인 법칙

각 하나와 변 두 개가 있을 때 남은 변을 구하는 공식

using UnityEngine;

public class CosLaw : MonoBehaviour
{
    public float cAngle = 60f;
    public float aSide = 1f;
    public float bSide = 1f;

    void Start()
    {
        float cRad = cAngle * Mathf.Deg2Rad;
        float cSide = Mathf.Sqrt(Mathf.Pow(aSide, 2) + Mathf.Pow(bSide, 2) - 2 * aSide * bSide * Mathf.Cos(cRad));

        Debug.Log(cSide); // 1 출력
    }
}

 

 

 

1.3. Sin과 Cos로 원형 회전 만들기

using UnityEngine;

public class MathCircle : MonoBehaviour
{
    private float theta;

    void Update()
    {
        theta += Time.deltaTime;

        float x = Mathf.Cos(theta);
        float y = Mathf.Sin(theta);

        Vector2 pos = new Vector2(x, y);

        transform.position = pos;
    }
}

 

theta는 시간에 따라 계속 증가해서 오브젝트가 원을 따라오도록 구현해 주었다.

캐릭터 주위에 오로라 같은 기능 구현할 때 사용해 주면 좋을 거 같다.

 

 

2. 라이트 밝기 조절

using UnityEngine;

public class MathLight : MonoBehaviour
{
    private Light light;
    private float theta;
    [SerializeField] private float power;
    [SerializeField] private float speed;

    void Start()
    {
        light = GetComponent<Light>();
    }

    void Update()
    {
        theta += Time.deltaTime * speed;

        light.intensity = Mathf.Sin(theta) * power; // 삼각함수 그래프

        // light.intensity = Mathf.PerlinNoise(theta, 0) * power;
    }
}

 

결과를 확인해 보면

Sin 함수는 계속 올라갔다 내려갔다 하므로 밝기도 자연스럽게 깜빡이고,

PerlinNoise는 약간 불안정한 조명/유령 효과에 어울리도록 깜빡인다.

 

▶ PerlinNoise (퍼린 노이즈)

자연스러운 랜덤값을 만들고 싶을 때 사용하는 함수

Random.Range() 처럼 완전 불규칙한 랜덤이 아니라 부드럽게 이어지는 랜덤 값을 반환한다.

 

3. 터렛 실습

이번엔 Sin함수를 활용해서 터렛을 만들어보려고 한다.

 

3.1. 좌우 회전

우선 적을 찾기 전까지는 터렛이 부드럽게 좌우로 왔다 갔다 하는 동작을 구현해 보겠다.

using UnityEngine;

public class Turret : MonoBehaviour
{
    public Transform turretHead;

    private float theta;
    public float rotSpeed = 1f;
    public float rotRange = 60f;

    void Update()
    {
        theta += Time.deltaTime * rotSpeed;

        float rotY = Mathf.Sin(theta) * rotRange;

        turretHead.localRotation = Quaternion.Euler(0, rotY, 0);
    }
}

 

 

3.2. LooAt을 활용해서 플레이어 추적

터렛의 기본 동작은 좌우로 움직이는 것이지만,

이제 플레이어가 트리거에 들어오면 터렛이 자동으로 플레이어를 바라보게 만들어볼 것이다.

private bool isTarget;
public Transform target;

void Update()
{
    if (!isTarget)
        TurretIdle();
    else
        LookTarget();
}

void TurretIdle()
{
    theta += Time.deltaTime * rotSpeed;

    float rotY = Mathf.Sin(theta) * rotRange;

    turretHead.localRotation = Quaternion.Euler(0, rotY, 0);
}

void LookTarget()
{
    turretHead.LookAt(target);
}

void OnTriggerEnter(Collider other)
{
    if (other.CompareTag("Player"))
    {
        target = other.transform;
        isTarget = true;
    }
}

 

 

4. 벡터(Vector)와 스칼라(Scalar)

 

  • 벡터 : 크기와 방향을 가지고 있는 값 → 속도
  • 스칼라 : 크기만 가지고 있는 값 → 속력
using UnityEngine;

public class MathVector : MonoBehaviour
{
    public Vector3 vecA = new Vector3(3, 0, 0);
    public Vector3 vecB = new Vector3(0, 4, 0);

    void Start()
    {
        float size = Vector3.Magnitude(vecA + vecB);
        Debug.Log($"Magnitude : {size}"); // 5 출력

        float distance = Vector3.Distance(vecA, vecB);
        Debug.Log($"Distance : {distance}"); // 5 출력

        // 루트 생략
        float size2 = Vector3.SqrMagnitude(vecA + vecB);
        Debug.Log($"SqrMagnitude : {size2}"); // 25 출력
    }
}

 

  • Magnitude : 벡터의 길이
  • Distance : 두 벡터 사이의 거리
  • SqrMagnitude : 제곱값 (루트가 없어서 연산 처리가 빠름)

 

이렇게 오늘은 게임 수학에 대해서도 간략하게 배워보았는데

각 수식마다 실무 어느 부분에 적용되는지는 아래와 같이 정리해 보았다.

수학 게임에서의 활용
Sin, Cos 회전, 원운동, 패턴
사인/코사인 법칙 거리 계산, 위치 예측
PerlinNoise 자연스러운 효과 (조명, 움직임 등)
Vector 거리 체크, 방향 계산, 이동 등

 

 


 

 

오늘은 오후에 조퇴를 하는 바람에 게임 수학 부분부터는 추측해서 정리한 거라 실제 수업 내용과 다를 수 있다..

하필 늘 수업 내용 정리하시던 분들도 이번 수업은 기가 막히게 다들 조퇴를 하셨는지 ㅋㅋㅋ 아무것도 찾아볼 수 없었다..!! 내가 못 찾은 걸 수도ㅠ 뭐 어떡하겠어.. 매일 아침에 깔짝 복습해 주시니까 그때 대충 뭐 했는지 후다닥 스캔하고 따라 해야지..

그래서 이번 포스팅은 공개가 좀 늦었습니돰!! 절대 주말에 공부 안 한 거 아님!! ^-^