카테고리 없음

유니티 레벨 디자인을 위한 장치 - 3. 문(Door), 함정(Trap), 마스킹 활용

akama 2024. 10. 4. 23:00

게임 내에서 캐릭터가 어떤 방해도 없이 수월하게 움직인다면 게임이 너무 쉽고 단순해서 금방 지루해 질 수 있습니다. 그래서 게임은 점점 난이도를 높이는 장치를 조금씩 추가해 주어야 합니다. 이 예시 게임은 순간의 컨트롤 실수로 벽에 부딪히면 게임 오보가 되는 컨셉입니다. 따라서 난이도를 높이는 방법은 좁은 길목과 방향 전환이 많은 길목이 많으면 어려워 지며 이런 좁은 길에 문이 닫혀서 문에 충돌하는 장치를 추가하거나 함정이 땅에서 갑자기 튀어 나오는 부분이 있다면 좀 더 난이도를 높일 수 있습니다. 뿐만 아니라 월드 맵의 범위를 조금 넓히게 되면 배터리에 의한 게임 오버도 생각해야 하기 때문에 난이도가 더 올라가도록 조정할 수 있습니다. 그럼 문 오브젝트부터 순차적으로 만들어 보겠습니다.

 

1. 문 오브젝트 만들기 (마스킹의 활용)

일반적인 탑뷰 방식의 게임에서 문 캐릭터가 열고 공간과 공간을 이동할 때 통과하는 수단입니다. 이 게임에서는 문은 방해물로써 작용하도록 하기 때문에 열쇠로 여는 문이 아닌 자동문 개념으로 만들어 보겠습니다. 자동문의 컨셉은 일정 시간이 되면 문이 닫히고 다시 열리는 것을 반복하는 것입니다. 이유는 캐릭터가 문의 열리는 타이밍에 맞추어 통과하도록 유도하는 장치이기 때문입니다. 맵은 타일이고 오브젝트는 2D object를 활용해서 방해물을 만들겠습니다. 먼저 Hierarchy뷰에서 마우스 우클릭 2D Object - Sprite - Square 를 만들어서 문 srpite 리소스를 별도로 간단하게 만들어서 유니티 Resources 폴더에 넣어 주고 해당 스프라이트를 sprite renderer에 지정해 줍니다. 이전 타일맵 포스팅에서 TilemapFloor와 TilemapWall을 만들어서 바닥과 벽을 구성하였는데 각각은 order in layer가 0과 3이었습니다. 따라서 게임에서는 TilemapFloor보다 TilemapWall이 위에 있습니다. 탑뷰에서 반복적으로 상하 또는 좌우로 왔다갔다하는 문을 만드는데 문의 디자인은 아주 심플한 것이 좋으며 order in layer를 통해서 TilemapFloor 타일보다는 위에 가도록 합니다.

(좌) Door Sprite 오브젝트가 TilemapWall 위에 노출 (우) Door Sprite 오브젝트를 TilemapWall 아래 숨기기 필요

그림에서 보는 것과 같이 문이 벽에는 가려지는 것이 맞지만 좌측과 같이 그림이 그렇게 안 됩니다. 타일맵 위에 보여지면서 게임에서 문과 벽 구조물 간의 표현이 이상합니다. 이를 해결하기 위해서는 Sprite Mask를 사용해야 합니다. Hierarchy뷰에서 마우스 우클릭 2D Object - Sprite Mask를 선택하거나 유니티 상단 메뉴에서 GameObject - 2D Object - Sprite Mask를 통해서 생성 가능합니다.

inspector에서 Sprite Mask 컴포넌트

- Sprite: 마스크로 사용하는 스프라이트인데 다양한 모양의 스프라이트를 마스킹할 수 있습니다. 여기서는 네모난 마스킹이 필요해서 wall sprite로 했습니다.

- Alpha Cutoff: 알파 영역에 투명과 불투명이 혼합된 경우 이를 지정해주는 기준을 설정합니다.

- Custom Range: 클릭하면 마스킹할 Sorting Layer를 지정하고 order in Layer로 순서를 지정할 수 있습니다.

스프라이트 마스크를 만들고 해당 오브젝트의 크기를 문 오브젝트보다 커서 문을 마스킹할 수 있도록 scale을 조정해 줍니다. 다음은 문 오브젝트를 클릭해서 마스킹 타입을 정해 줍니다.

Sprite Renderer에 Mask Interaction

Sprite Renderer 컴포넌트를 보면 Mask Interaction이 있습니다.

- Visible Inside Mask: 마스크 영역에 있는 스프라이트만 보이도록 합니다.

- Visible Outside Mask: 마스크 영역에 있는 스프라이트만 숨깁니다.

Sprite Mask를 통해 마스킹한 영역(붉은색 박스)

벽에서 튀어 나오도록 해야 하기 때문에 벽에 숨기는 역할이 필요하므로 Visible Outside Mask를 지정해 주면 이제 마스크 영역에 들어선 문은 마치 벽 속으로 들어갔다가 튀어 나오면서 닫히는 것처럼 보이게 됩니다. 세팅이 끝나면 문을 움직이도록 스크립트를 만들어 줍니다. 스크립트 이름을 DoorHandler라고 해줍니다.

public class DoorHandler : MonoBehaviour
{
    public Transform doorStart;
    public Transform doorTarget;
    public float closeSpeed = 1.3f;
    public float openSpeed = 1.6f;
    public bool closing = false;

   void Start ()
   {
      StartingPosition ();
   }

   void StartingPosition ()
   {
      if (closing)
      {
          transform.position = doorStart.position;
      }
      if (!closing)
      {
          transform.position = doorTarget.position;
      }
   }

    void Update()
    {
        //Close
        if (closing)
        {    
            if (transform.position != doorTarget.position)
            {
                transform.position = Vector3.MoveTowards(transform.position, doorTarget.position, closeSpeed * Time.deltaTime);
            }
            if (transform.position == doorTarget.position)
            {
               StartCoroutine(DelayTimeAfterClose(2f));
            }
        }

        //Open
        if (!closing)
        {
            if (transform.position != doorStart.position)
            {
                transform.position = Vector3.MoveTowards(transform.position, doorStart.position, openSpeed * Time.deltaTime);
            }
            if (transform.position == doorStart.position)
            {
                StartCoroutine(DelayTimeAfterOpen(4.5f));
            }
        }
    }

    IEnumerator DelayTimeAfterClose(float t)
    {
        yield return new WaitForSeconds(t);
        closing = false;
    }

    IEnumerator DelayTimeAfterOpen(float t)
    {
        yield return new WaitForSeconds(t);
        closing = true;
    }

}

Empty 오브젝트를 만들고 이름을 Door라고 해줍니다. Door를 클릭한 상태에서 2D - Object - Square를 만들어서 Door sprite를 지정해 주고 이름을 DoorBody라고 변경합니다. Door를 클릭한 상태에서 Empty 오브젝트를 만들고 이름을 각각 StartPos와 EndPos로 붙여 줍니다. Door 오브젝트에는  DoorBody, StartPos, EndPos 세 개의 자식 오브젝트가 있습니다. StartPos와 EndPos는 스크립트에서 public으로 선언된 doorStart와 doorTarget 할당해 줍니다. boolean 변수인 closing의 체크 및 체크해제를 통해서 DoorBody위치가 다릅니다. closing이 true인 경우 DoorBody 문이 StartPos에 가있기 때문에 문이 열린 채로 시작하며 false인 경우는 반대로 닫힌 채로 시작합니다. 시작하면 각각 Vector3.MoveTowards를 통해서 이동했다가 코루틴을 통해서 closing을 true와 false로 변경하면서 열고 닫습니다. 이때 문의 열고 닫는 속도 및 닫힌 상태와 열린 상태의 시간도 설정할 수 있습니다.

가장 중요한 것은 DoorBody가 StartPos, EndPos에 position이 일치할 때까지 움직이기 때문에 반복적 플레이 테스트를 통해서 문의 크기에 맞는 가장 적절한 StartPos와 EndPos의 position을 찾아야 합니다. 위치를 일치시키는 방법 외에 콜라이더를 이용하여 특정 콜라이더에 닿으면 멈추고, 문을 열거나 닫게 만들 수도 있습니다. 구현 결과는 같더라도 구현 방법은 여러가지이니 이 부분은 상황에 맞게 스크립트를 짜는 것이 좋습니다.

 

2. 함정 오브젝트 만들기

함정은 문과 같이 벽에서 가시가 튀어나오게 할 수도 있으며 바닥에서 튀어 나오게 할 수도 있습니다. 이번에는 탑뷰에서 볼 때 바닥에서 튀어 나오는 함정을 만들어 보겠습니다. 준비물은 가시 함정의 애니메이션을 만들 수 있는 스프라이트 입니다. 가시가 숨어 있는 구멍이 있고, 그 구멍에서 가시가 튀어 나오는 스프라이트 애니메이션을 만들어야 합니다.

가시 함정(Spike Trap) 스프라이트 준비

Hierarchy뷰에서 2D Object - Sprite - Square 를 클릭해서 오브젝트를 만들고 이름을 Trap이라고 해줍니다. 클릭한 상태에서 Animation 창을 열고 Animator와 Clip을 만들어 줍니다. 2D 애니메이션 만드는 방법은 이전 포스팅을 참고하시거나 관련 서적 또는 글을 참고하시면 됩니다. Trap 애니메이션을 만든 후 가시 함정의 크기는 오브젝트의 크기를 조절하여 타일맵 한 칸에 잘 맞도록 조정해 줍니다. 위 그림을 보면 트랩의 애니메이션 중 어느 타이밍을 캐릭터가 가시에 의해 데미지를 받아서 아웃 판정을 받는지를 정해야 합니다. 가장 우측에 가시가 완전히 나온 상태에서 가시에 닿으면 게임 오버되는 것으로 디자인 하고 대신 두번째 스프라이트를 통해서 가시가 곧 튀어 나오기 직전이라는 신호를 주고 세 번째 스프라이트는 마지막 스프라이트가 되기까지 애니메이션으로 이어주는 스프라이트의 역할을 하도록 합니다. 좀 더 부드러운 애니메이션을 원한다면 중간중간에 스프라이트를 더 추가해서 넣으면 됩니다. 가시가 나오는 애니메이션과 들어가는 애니메이션 클립을 각각 만들어서 2개로 컨트롤을 해도 되고, 가시가 나오는 애니메이션과 들어가는 애니메이션을 한 개의 클립으로 만들어도 됩니다. 전자는 코루틴을 사용해서 가시가 나오고 들어가는 애니메이션을 구분하는 방법이고, 후자는 Animation창에서 가시가 나온 상태에서 들어가기까지 애니메이션의 시간과 스크립트 내에서 캐릭터에게 피해를 주는 오브젝트를 비활성시키는 시간을 일치시키는 방법입니다. 예시 게임에서는 전자를 이용해서 코루틴 스크립트로 함정이 작동되는 것을 만들어 보겠습니다. Trap 오브젝트를 클릭한 상태로 Empty 오브젝트를 만들고 해당 오브젝트에 BoxCollider2D 컴포넌트를 추가해 주고 IsTrigger를 체크해 주고 Tag를 Trap을 만들어 지정해 주고 이름을 TrapTrigger라고 설정해줍니다. PlayerMovement 스크립트를 열고 몇 가지를 추가해 줍니다.

public class PlayerMovement : MonoBehaviour
{

    void Start ()
    {
         gameManager = GameObject.Find("GameManager").GetComponent();
    }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Wall") || collision.CompareTag("Trap"))
        {
            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());
        }
    }
}

기존 스크립트에 Trap 관련 태그만 추가했습니다. 이로써 Trap 태그가 붙은 콜라이더 오브젝트에 충돌하면 벽(Wall)에 충돌해서 게임 오버되는 것과 같이 게임 오버 처리가 됩니다. 스크립트를 만들고 TrapSpike라고 해줍니다. 이 스크립트를 Trap오브젝트에 추가해 줍니다.

public class TrapSpike : MonoBehaviour
{
     public float untilTrapTime = 1.0f;
     public float TrapedTime = 1.5f;
     public float nothingPeriod = 4.5f;
     public GameObject trapTrigger;
     GameCotroller gameManager;
     Animator animator;
    
    void Start ()
    {
         gameManager = GameObject.Find("GameManager").GetComponent<GameController>();
         animator = GetComponent.GetComponent<Animator>();
         StartCoroutine(StartTrap ());
    }

    IEnumerator StartTrap ()
    {
        animator.SetBool("TrapSpike", true);
        yield return new WaitForSeconds(untilTrapTime);
        trapTrigger.SetActive(true);
        StartCoroutine(EndTrap ());
    }

    IEnumerator EndTrap ()
    {
        yield return new WaitForSeconds(TrapedTime);
        animator.SetBool("TrapSpike", false);
        trapTrigger.SetActive(false);
        yield return new WaitForSeconds(nothingPeriod);
        StartCoroutine(StartTrap ());
    }

}

2D 애니매이션을 통해 함정 오브젝트를 만들고 있습니다. 3D와 달리 뾰족한 가시가 아래에서 위로 튀어 나오는 것을 표현하기 까다롭습니다. 탑뷰의 평면적인 애니메이션으로 처리하기 위해서 가장 가시가 튀어 나온 타이밍에 맞추어 Trap 태그가 있는 오브젝트를 활성화시켜야 합니다. 가장 중요한 것은 애니메이션에서 최초 스프라이트에서 완전한 가시 스프라이트가 되기까지의 시간과 스크립트 내 untilTrapTime 타임이 같아야 합니다. 가시가 완전히 나오는 애니메이션 시점에서 Trap 태그 오브젝트가 발생해서 캐릭터에게 타격을 주어야 합니다. TrapedTime 시간을 통해서 튀어 나온 가시는 피해 함정으로 들어난 상태로 있게 되며 TrapedTime 시간이 종료됨과 동시에 가시가 들어가는 애니메이션을 플레이 하면서 Trap 태그 오브젝트를 비활성화 처리합니다. 애니메이션 상태는 가시 구멍만 보이는 첫번째 스프라이트 상태로 nothingPeriod 의 시간만큼 유지하게 됩니다. 이 시간이 끝나면 다시 StartTrap 코루틴을 실행하면서 반복하게 됩니다. 만든 방해물 오브젝트들은 Hierarchy뷰에서 드래그 해서 Project뷰에 있는 Prefab폴더(생성 후 이름지정)에 저장해서 프리팹으로 만들어 사용하면 좋습니다. 이전 프리팹 포스팅에서 설명했듯이 만약 수정할 부분이 생겼을 경우 같은 종류의 프리팹은 같이 수정사항을 적용받기 때문에 게임 개발을 할 때 다루기가 좋기 때문입니다. 다음 포스팅에서는 캐릭터의 이동을 방해하는 장치와 부활 포인트에 대해 포스팅하겠습니다.