카테고리 없음

유니티 Rigidbody2D, Collider2D, 이벤트 함수

akama 2024. 10. 1. 18:45

이번 포스팅에서는 유니티에서 이벤트를 처리하는 방법과 관련 함수에 대해 알아보겠습니다. 이벤트라는 것은 무엇인가가 발생하는 것으로 게임에서는 유저의 플레이에 의해서 발생하게 됩니다. 게임에서 이벤트는 장르에 따라 또는 게임 무대에 따라서 매우 다양합니다. 어느 지역에 들어서면 적들이 출연하거나 NPC가 나타나는 이벤트, NPC를 클릭하면 퀘스트 창이 나오는 이벤트, 비행기 슈팅에서 보스를 처치하면 미션이 클리어 되는 이벤트 또는 보스의 Phase 2단계로 넘어가는 이벤트 등 모든 이벤트는 플레이에서 비롯됩니다. 이 플레이를 통해서 게임 오브젝트는 어떻게 이벤트가 발생했다고 알 수 있을까요? 일반적으로 유저의 입력을 통해서 게임 오브젝트에 이벤트를 호출하게 됩니다. 사실 캐릭터의 움직이는 조작, 전투, 승리와 죽음 모두 이벤트이며 이런 것들은 유저의 컨트롤을 통해서 입력이 발생하고 이를 게임 오브젝트의 이벤트 함수가 처리되는 것입니다. 게임에서 기술 사용을 위한 버튼 클릭이나 방향 조작 외에는 캐릭터 오브젝트와 사물, 배경, 적, NPC 오브젝트와의 이벤트는 서로 상호작용을 통해서 발생합니다. 게임에서는 게임 오브젝트 간의 '충돌'이나 '접촉'을 통해 상호 간에 이벤트를 위한 입력이 진행되는 것입니다. 유니티에서도 오브젝트끼리 충돌하거나 접촉하는 것을 통해서 이벤트를 만듭니다. 간단한 예를 들어서 총을 쏘는 게임에서 적 몬스터 오브젝트와 플레이어가 발사하는 총알 오브젝트가 충돌 또는 접촉하면 적 몬스터의 체력을 깎는 이벤트를 실행하게 되고 체력이 0이 되면 적 몬스터의 죽는 애니메이션이 이루어지도록 하면서 게임의 이벤트가 처리됩니다.

 

1. Rigidbody2D

게임 오브젝트는 물리적 이벤트를 처리할 때 물리적 요인을 지닌 몸(body)을 필요로 하는데 유니티에서는 RigidBody2D라는 컴포넌트를 사용합니다. Rigidbody2D는 2D 물리 엔진을 사용할 때 필요한 컴포넌트이며 2D에도 오브젝트에 중력을 받도록 할 수 있게 합니다. Rigidbody2D는 강체라고 하는데 물리 영향을 받는 오브젝트의 몸으로 보시면 됩니다.

- Body Type: Dynamic, Kinematic, Static 세 가지 타입이 있습니다.

Dynamic은 오브젝트가 긴 설명 필요없이 Mass, Drag, Gravity Scale 등 오브젝트 내부적 물리 요인과 충돌, 충격에 의한 외부적 물리에 영향을 받습니다.

- Kinematic은 오브젝트가 물리적 요인에 영향을 받지 않는 타입입니다.

- Static은 고정의 움직이지 않는 오브젝트 타입입니다. Static 오브젝트끼리는 고정된 오브젝트로써 물리적 충돌이 없지만 Dynamic 오브젝트 타입과는 충돌이 발생합니다.

Rigidbody2D를 사용하면 대부분은 기본값으로 Dynamic으로 설정되어 있습니다. 물리의 중력 낙하, 충돌로 인한 힘의 상쇄, 포격 시 포물선을 그리는 포탄 등 게임에서 물리 현상을 활용하려면 해당 오브젝트 타입은 Dynamic이어야 합니다.  Kinematic을 사용하는 경우도 있습니다. 아래 게임이 대표적인 Kinematic 오브젝트 타입 예입니다.

Mr. Bullet Game

Mr. Bullet이라는 게임인데 총을 쏴서 벽이나 오브젝트에 충돌하는 총알이 튕기고 이 총알로 적을 처치하거나 목표물을 제거하는 게임입니다. 이 게임에서 Raggdol(플레이어 캐릭터 or 적)은 물리 영향을 받도록 Dynamic입니다. 그러나 발사된 총알은 물리의 영향을 받지 않는 Kinematic 타입입니다. 일반적으로 총알이 오브젝트에 몇 번 부딪히면 총알이 박히거나 또는 튕기더라도 그 힘이 크게 상쇄된 상태이며 중력에 의해 튕겨져 나가는 방향에 영향을 받게 되지만 이 게임은 튕겨지는 총알이 주요 컨셉이고 이를 살리기 위해서는 물리적 영향을 배제시켜야 합니다. 이 정도 예시면 Dynamic과 Kinematic 타입의 RigidBody2D 타입을 충분히 이해할 수 있습니다. 

- Material: 2D의 물리적 영향을 주는 말 그대로 재질(Material)을 줄 때 사용합니다. 이 부분은 나중에 다루겠습니다.

- Simulated: RigidBody2D와 Collider2D의 상호작용하도록 합니다. 체크되어 있어야만 합니다.

- Use Auto Mass: 오브젝트의 질량을 자동 감지할 때 사용합니다.

- Mass: 오브젝트의 질량입니다. 수치가 크면 중력을 크게 받고 무거운 물체가 됩니다. 이 값은 최하가 0.0001입니다.

- Linear Drag: 오브젝트의 움직이는 방향에 마찰을 주는 저항값입니다. 높으면 물리적 마찰 때문에 이동이 어렵습니다.

- Angular Drag: 오브젝트의 회전 방향에 마찰을 주는 저항값입니다.

- Gravity Scale: 중력값으로 0이 되면 RigidBody2D가 있더라도 낙하하지 않습니다.

- Collision Detection: 충돌 감지를 정의하는데 Discrete와 Continuous가 있습니다. Discrete는 충돌 포인트가 매번 업데이트 되면서 새 지점을 지정하지만 Continuous는 첫번째 충돌을 충돌 포인트로 지정합니다. 일반적으로 Discrete 사용합니다.

- Sleeping Mode: 프로세서의 절약을 위한 것인데 오브젝트가 휴식기 일때 수면모드에 들어서도록 하는 기능인데 일반적으로 Start Awake를 사용해서 오브젝트가 깨어 있도록 합니다.

- Interpolate: 물리적 충돌에 의해 오브젝트 움직임을 보간합니다. 충돌에 의한 움직임이 어색할 때 사용하는데 2D에서는 대부분 None(사용 안 함)을 합니다.

- Constrains: RigidBody2D 오브젝트의 움직임을 제약합니다. Freeze Position인데 체크한 축은 해당 오브젝트가 고정되어서 움직이지 않습니다. 즉 해당 축으로는 물리 영향을 받지 않습니다. Z값을 체크하면 Gravity Scale에 의한 낙하도 하지 않게 됩니다.

 

2. Collider2D

오브젝트의 강체라는 RigidBody2D가 있더라도 이를 강체에 전달하고 오브젝트 간의 물리적 상호작용 역할은 Collider2D가 하게 됩니다. 오브젝트의 겉표면 껍질같은 것인데 이 부분에 접촉이나 충돌 감지하게 되고 이벤트를 처리할 수 있게 되는 것입니다. 생긴 겉모양에 따라서 몇 가지 타입이 있는데 많이 사용하는 종류만 소개하겠습니다.

- Box Collider2D: 직사각형 충돌 영역

- Circle Collider2D: 원형 충돌 영역

- Polygon Collider2D: 다각형 충돌 영역

- Edge Collider2D: 선형 충돌 영역

- Composite Collider2D: 고유한 모양 없이 vertex를 사용한 다양한 형태의 충돌 영역

- Edit Collider: Collider를 편집해서 크기를 조정할 수 있습니다. 조정하면 아래 Size X, Y값이 변합니다.

- Material: 마찰이 튕기는 물리적 효과를 부여할 때 사용합니다.

- Is Trigger: Collider가 트리거로 작동하게 만들 때 사용합니다.

- Used By Effector: 물리적 효과를 주는 다양한 Effector2D를 사용할 때 체크해야 합니다. Effector2D는 나중에 플랫폼 2D 게임을 예시로 만들 때 소개하겠습니다.

- Used By Composite: 복합 콜라이더인 Composite Collider2D를 사용할 때 체크합니다. 

- Auto Tiling: Sprite Renderer에서 Draw Mode가 Tiled로 되어 있을 때 체크해야 합니다. 타일 스프라이트의 크기 변경 시 자동으로 Collider의 크기가 조정되게 해줍니다.

Offset: 오프셋 설정입니다. X, Y가 각각 0이면 오브젝트를 중심에 두고 위치가 일치하게 됩니다.

Size: Collider 크기 설정입니다.

- Edge Radius: Collider의 모서리 반지름 값을 조정해 주는 것이며 수치가 크면 모서리가 둥글게 되면서 Collider는 커집니다.

 

3. OnCollisionEnter2D 와 OnTriggerEnter2D

유니티에는 오브젝트 간에 충돌과 접촉에 관여하는 함수가 있습니다. 두 함수는 사용법이 유사하지만 성질이 다른 함수로 관련 성질을 알고 접근해야 합니다. 먼저 OnCollisionEnter2D는 유니티에서 제공하는 '물리'와 관련이 있습니다. 이 함수는 특정 태그를 지닌 오브젝트와 충돌이 시작되면 시작되는 시점에 이벤트를 한 번 호출하는 함수입니다.

void OnCollisionEnter2D(Collision2D collision)
{
    if (collision.gameObject.tag == "Wall")
    {
        Debug.Log("Game Over");
    }
}
void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.CompareTag("Wall"))
    {
        Debug.Log("Game Over");
    }
}

두 함수의 예시인데 비슷하지만 다른 점이 있습니다. OnCollisionEnter2D는 두 오브젝트의 Collider가 충돌했을 때 이벤트를 호출합니다. 따라서 충돌하는 오브젝트의 Collider에는 Is Trigger가 모두 체크해제되어 있어야 합니다. 따라서 충돌에 의한 것이므로 매개변수 타입도 Collision2D입니다. 반면 OnTriggerEnter2D 두 오브젝트의 Collider 중 한 개라도 Is Trigger가 체크 되어 있으면 이벤트를 호출합니다. OnTriggerEnter2D는 물리적 충돌이 아닌 Collider간 접촉이나 통과를 통해서 실행되는 것입니다. 그래서 매개변수도 Collider2D 타입을 받아서 관련 태그가 있는 오브젝트의 접촉을 확인합니다. 한 가지 알아둘 점은 Collider의 Is Trigger 때문에 OnCollisionEnter2D 와 OnTriggerEnter2D는 같이 사용할 수 없습니다. 게임 예시에서는 뱀 꼬리 물기와 같은 탑뷰 방식의 게임이기 때문에 벽에 충돌은 있더라도 물리적 요소가 필요 없기 때문에 이벤트 처리는 주로 OnTriggerEnter2D 함수로 합니다. 참고로 OnTriggerEnter2D외에도 OnTriggerStay2D, OnTriggerExit2D가 있습니다. OnTriggerStay2D는 특정 태그의 오브젝트 Collider에 계속 접촉해 있으면 매 프레임 이벤트를 호출하는 함수이며, OnTriggerExit2D는 특정 태그의 오브젝트 Collider에서 벗어나 비접촉이 발생하는 시점에 이벤트를 호출하는 함수입니다. 만약 2D 게임이 아닌 3D 게임을 만들 경우는 각 함수 뒤에 붙은 2D만 제거하면 3D에서 사용하는 OnCollisionEnter와 OnTriggerEnter가 됩니다.

 

4. 이벤트 함수 적용

이제 함수를 적용하기 위해 이전에 만들었던 PlayerMovement 스크립트를 더블클릭해서 열어 줍니다.

public class PlayerMovement : MonoBehaviour
{
    public Transform player;
    public float moveSpeed = 5f;
    float setSpeed;
    bool isMoveUp = false;
    bool isMoveDown = false;
    bool isMoveRight = false;
    bool isMoveLeft = false;
    bool isOver = false;

   GameController gameManager;
    Animator animator;
   ShakeCamera shakeCamera;
    
    void Start ()
    {
        player = GameObject.FindGameObjectWithTag("Player").transform;
        animator = GetComponent.GetComponent<Animator>();
        shakeCamera = GetComponent<ShakeCamera>();
        gameManager = GameObject.Find("GameManager").GetComponent<GameController>(); 
        setSpeed = moveSpeed;
    }

   void Update ()
   {
        Vector3 moveDirectionRight = new Vector3(1, 0, 0).normalized;
        Vector3 moveDirectionLeft = new Vector3(-1, 0, 0).normalized;
        Vector3 moveDirectionUp = new Vector3(0, 1, 0).normalized;
        Vector3 moveDirectionDown = new Vector3(0, -1, 0).normalized;
   }
    
    void FixedUpdate ()
    {
        if(!isOver)
       {
            if (isMoveUp)
            {
                player.transform.Translate(moveDirectionUp * moveSpeed * Time.deltaTime);
            }
            if (isMoveDown)
            {
                player.transform.Translate(moveDirectionDown * moveSpeed * Time.deltaTime);
            }
            if (isMoveRight)
            {
                player.transform.Translate(moveDirectionRight * moveSpeed * Time.deltaTime);
            }
            if (isMoveLeft)
            {
                player.transform.Translate(moveDirectionLeft * moveSpeed * Time.deltaTime);
            }
       }
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Wall"))
        {
            animator.SetBool("isCollision", true);
            SoundManager.Instance.PlaySFX("Collision");
            moveSpeed = 0f;
            isOver = true;

            this.gameObject.GetComponent<Rigidbody2D>().velocity = new Vector3(0,0,0) * 0f;
            if (isOver)
            {
                shakeCamera.Shaking(this.gameObject.transform.position);
            }
            gameManager.GameOver();
            StartCoroutine(LateDestroyed());
        }
    }

    IEnumerator LateDestroyed ()
    {
        yield return new WaitForSeconds(1.0f);
        gameObject.SetActive(false);
    }

    public void MoveUp ()
    {
        moveSpeed = setSpeed;
        isMoveUp = true;
        isMoveDown = false;
        isMoveLeft = false;
        isMoveRight = false;
    }

    public void MoveDown()
    {
        moveSpeed = setSpeed;
        isMoveUp = false;
        isMoveDown = true;
        isMoveLeft = false;
        isMoveRight = false;
    }

    public void MoveRight()
    {
        moveSpeed = setSpeed;
        isMoveUp = false;
        isMoveDown = false;
        isMoveLeft = false;
        isMoveRight = true;
    }
    public void MoveLeft()
    {
        moveSpeed = setSpeed;
        isMoveUp = false;
        isMoveDown = false;
        isMoveLeft = true;
        isMoveRight = false;
    }
}

PlayerMovement 스크립트에 굵은 줄은 OnTriggerEnter2D 함수를 추가하면서 같이 추가된 명령들이며 Wall이라는 태그의 오브젝트와 충돌 시 Collision 애니메이션을 발생 시키고, 사운드 매니저의 Instance를 통해서 사운드를 출력하며, 캐릭터의 이동속도를 0으로 변경하면서 게임오버에 의한 키조작 방지를 위해 bool 변수를 이용한 명령들이 들어가 있습니다. 충돌 효과를 주기 위해 캐릭터에 ShakeCamera 컴포넌트를 부여하고 이에 접근해서 충돌처리 지점을 전달하는 Shaking 함수를 호출합니다. 이벤트 처리 시 즉각적인 캐릭터 오브젝트의 비활성화로 인해 캐릭터의 '죽는 애니메이션'이 작동 못하는 것을 방지하고자 코루틴을 사용합니다. 사실 캐릭터를 폭파시켜서 없애는 것이 아니라면 굳이 오브젝트를 비활성화할 필요는 없습니다. 비활성화 또는 오브젝트의 파괴(Destroy)가 불필요할 경우에는 코루틴을 사용하지 않아도 됩니다.

public class GameController : MonoBehaviour
{
      public void GameOver()
    {
          LeanTween.scale(FailBoard, new Vector3(1, 1, 1), 0.3f).setEase(LeanTweenType.easeOutQuart);
          FailBoard.SetActive(true);
    }
}

마지막으로 게임 매니저에 접근해서 게임오버 함수를 호출하면서 해당 함수로 인해 게임오버 UI 팝업창이 뜨도록 만듭니다. 지금까지 스크립트에서 사용된 것들은 이전 시간에서 다룬 것들이 들어가 있습니다. 이 정도를 처리할 수 있게 되면 아주 간단한 게임을 만들 수 있습니다. 현재까지 예시 게임을 보면 UI와 배경이 있고, 캐릭터를 만들었으며, 레벨씬에서 게임에 들어 갈 수 있고, 스테이지이 이동이 되며 스테이지에서 목표 오브젝트를 회수해서 미션 성공 및 클리어를 하거나 또는 충돌에 의해 게임 오버가 되기도 하면서 게임은 간단하지만 게임의 기본적인 구조가 만들어졌습니다. 간단한 캐주얼 게임의 경우에는 이 단계가 되면 레벨을 구성하게 됩니다. 이전 시간에 다룬 타일맵을 통해서 간단한 구조 배경을 만들면서 스테이지를 구성하게 됩니다. 그러나 스테이지가 맵만 조금 다를 뿐 목표가 같고 긴장감이 없다면 재미가 없습니다. 레벨에 따른 변화를 조금씩 주면서 난이도에 의한 재미를 선사해야 합니다. 난이도는 수치 변경이나 새로운 장치에 의해 결정됩니다. 수치 변경에 의한 난이도 조절은 주로 몬스터는 제거해야 하는 게임들에서 사용합니다. 캐주얼이나 퍼즐 장르의 게임에서는 주로 새로운 장치에 의해서 난이도가 달라집니다. 예를 들어서 새로운 장치라면 방해물, 새로운 몬스터, 부스터, 함정, 새로운 목표물 추가 등이 있습니다. 관련 종류가 스테이지에서 많아질 수록 스테이지의 난이도가 어려워지게 됩니다. 이렇게 스테이지의 난이도를 위해서 각종 장치를 배치하는 것을 레벨 디자인이라고 합니다. 다음 포스팅부터는 레벨 디자인을 위해 필요한 장치들에 대해서 다루어 보겠습니다.