XR 플밍 - 9. UnityEngine2D 인터렉티브 프로그래밍 - 몬스터 움직임 구현, 타일맵 (5/22)

이형원·2025년 5월 22일
0

XR플밍

목록 보기
81/215

1. 몬스터 움직임 구현

1.1 Raycast를 이용한 구현 방식

방법은 몬스터를 기준으로 자신의 전방에서 아래로 Raycast를 쏘고, 바닥이 없으면 방향을 전환하는 방식이다.
또한 애니메이션은 Flip으로 처리한 방식이다.

using System.Collections;
using UnityEngine;

public class EnemyController : MonoBehaviour
{
    [SerializeField] private float moveSpeed;
    [SerializeField] private LayerMask groundLayer;

    private Rigidbody2D rigid;
    private Animator animator;
    private SpriteRenderer spriteRenderer;
    private Vector2 patrolVec;
    private bool isWaited;

    private readonly int IDLE_HASH = Animator.StringToHash("Idle");
    private readonly int PATROL_HASH = Animator.StringToHash("Patrol");

    void Start()
    {
        rigid = GetComponent<Rigidbody2D>();
        animator = GetComponent<Animator>();
        spriteRenderer = GetComponent<SpriteRenderer>();
        patrolVec = Vector2.left;
    }

    void Update()
    {
        Patrol();
    }

    private void Patrol()
    {
        Vector2 rayOrigin = transform.position + new Vector3(patrolVec.x, 0);
        Debug.DrawRay(rayOrigin, Vector2.down * 3f, Color.red);
        RaycastHit2D hit = Physics2D.Raycast(rayOrigin, Vector2.down, 3f, groundLayer);
        if (hit.collider == null)
        {
            //돌아가는 로직.
            StartCoroutine(CoTurnBack());
        }
    }

    private IEnumerator CoTurnBack()
    {
        spriteRenderer.flipX = !spriteRenderer.flipX;

        if (spriteRenderer.flipX)
        {
            patrolVec = Vector2.right;
        }
        else
        {
            patrolVec = Vector2.left;
        }

        animator.Play(IDLE_HASH);
        isWaited = true;
        rigid.velocity = Vector2.zero;
        yield return new WaitForSeconds(2f);
        isWaited = false;

        animator.Play(PATROL_HASH);
    }

    private void FixedUpdate()
    {
        if (isWaited == false)
            rigid.velocity = patrolVec * moveSpeed;
    }
}

1.2 내가 구현한 방식

나는 Raycast 방식으론 비용이 다소 들 거란 생각이 들어 다른 방법으로, Monster 아래에 Trigger를 달아 Trigger 인식이 되지 않으면 방향을 돌리는 방식으로 구현하였다.
또한 Trigger 영역을 늘리지 않기 위해, Monster의 Rotation 자체를 돌리는 방법으로 생각했다.

  • MonsterController
using UnityEngine;

public class MonsterController : MonoBehaviour
{
    private Animator animator;
    private Rigidbody2D rigid;
    private bool isWalking = false;
    private int moveDir = 1;

    [SerializeField] private int moveSpeed;
    [SerializeField] MonsterTrigger monsterTrigger;

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

    private void Update()
    {
        IsWalkable();
    }

    private void FixedUpdate()
    {
        MonsterMove();
    }

    private void MonsterMove()
    {
        animator.SetBool("IsWalk", isWalking);
        isWalking = true;
        rigid.velocity = Vector2.left * moveSpeed * moveDir;
    }
    private void IsWalkable()
    {
        if(!monsterTrigger.IsWalkable)
        {
            moveDir = -moveDir;
            gameObject.transform.rotation = moveDir > 0? Quaternion.Euler(0, 0, 0) : Quaternion.Euler(0, 180, 0);
        }
    }
}
  • MonsterTrigger
using UnityEngine;

public class MonsterTrigger : MonoBehaviour
{
    private bool isWalkable = true;
    public bool IsWalkable { get { return isWalkable; } }

    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Ground"))
        {
            isWalkable = true;
        }
    }
    
    private void OnTriggerExit2D(Collider2D collision)
    {
        if(collision.CompareTag("Ground"))
        {
            isWalkable = false;
        }
    }
    
}

Monster의 대각선 아래에 이와 같이 트리거 영역을 만들었다.

2. 타일맵

2.1 타일맵의 생성

맵과 레벨 디자인을 위해 타일맵을 생성해보자.
하이어라키 창에서 타일맵을 생성할 수 있다.

사각형 모양의 타일이나 육각형 모양의 타일, Isometric 등 다양한 타일맵 옵신열 선택할 수 있다.
이번에 사용할 에셋은 사각형 에셋이니 Rectangular로 생성해보자.

격자 무늬가 생기면서 Grid와 Tilemap이 생겼다. 여기서 Open Tile Palette로 타일맵을 생성해보자.

에셋스토어에서 받은 에셋이라면 기본적으로 팔레트 설정이 되어 있을 것이다. 외부 자료로 가져 온 경우라면 좌측 상단의 Create New Palette로 새 팔레트를 만들면 된다.

빈 팔레트에 이와 같이 넣을 스프라이트 이미지를 전부 선택한 다음에 옮겨주면 간단하게 팔레트를 만들 수 있다.

2.2 타일맵 기능

간단하게 버튼의 기능을 알아보고자 한다.

1. Grid 선택

맨 왼쪽 버튼을 누르고 Grid를 선택해보면, 인스펙터로 현재 칸에 어떤 정보가 들어 있는지 확인할 수 있다.

빈칸으로 보이지만 룰 타일이 들어 있는 모습이다.
이곳을 통해 그리드 칸에 무엇이 들어있는지 확인하고, 지우는 것도 여기서 할 수 있다.

2. Grid 데이터 옮기기

1번에서 사용한 Grid 선택으로 해당 Grid의 데이터를 옮겨줄 수 있다.

3. Active Brush

그림판처럼 그림을 그려줄 수 있다.

4. Filled box with Active Brush

영역을 설정해주고 그림을 그릴 수 있다.
룰 타일의 경우에는 한 칸만 설정하여 그릴 수 있다.

5. Marquee Select

그림판에서 색을 추출하는 것처럼 타일 정보를 골라내서 다시 그릴 수 있다.

6. Erase

지우개 기능이다.

  • Tip - 넓은 영역을 지우려면?

지우개를 써보면 넓은 영역을 지우는 게 안 되는데, 이걸 위해선 이와 같이 세팅해야 한다.

  • 빈 타일 영역을 설정

  • 이후 지우개가 빈 타일의 영역만큼 커진 것을 확인할 수 있음, 지운다.

2.3 타일맵의 충돌체 설정

타일맵의 충돌체를 만드는 방법은 아주 간단하다.

유니티에서는 이와 같이 Tilemap Collider 2D가 있으며 이를 사용하면 모든 타일에 자동적으로 충돌체가 생성된다.

  • 팁 - Composite Collider 2D의 활용

Effecter를 사용해야 하는 충돌체가 아니라면, Tile Collider를 설정한 다음 부모 오브젝트에 Composite Collider 2D를 활용하는 것도 좋은 방법이다.

이와 같이 처리하면 타일맵의 충돌체 각각의 계산하는 양보다, 통째로 한 덩어리의 충돌체로 계산하는 게 최적화 면에서 좋기 때문이다. 맵이 작을 때에는 큰 차이가 없지만 타일의 개수가 많아지면 거기에서부터 성능 최적화의 효과를 볼 수 있다.

2.4 Rule Tile

타일로 맵을 만들 수 있는 건 좋지만, 아무래도 하나하나 맵을 다 찍고 있으면 버겁다는 생각이 들 수 밖에 없다.
이때 Rule Tile을 설정하여 일종의 프리팹과 같은 형태로 만들 수도 있다.
프로젝트 창에서 Rule Tile을 만들어보자.

룰 타일은 사용할 타일의 종류를 리스트로 받고, 이후 타일 중심으로 8방향에 대한 조건을 입력하여 해당 타일이 어떤 형태를 유지하여야 하는지 세팅해주는, 규칙 타일을 만드는 것이다.
여기서 맵에 대한 룰 타일을 만들기 위해서 아래와 같이 타일 9개를 세팅하고, 조건을 걸었다.

초록색 화살표는 해당 타일 중심으로 해당 방향으로 타일이 존재해야 한다는 뜻이고, X방향은 타일이 없어야 한다는 뜻이다.
이 조건에 따라 생성되는 프리팹의 예시 사진도 볼 수 있다.

사각형으로 구성된 맵은 분명 맵 제작에서 자주 쓰일 것이다. 이제 이걸 사용하기 위해서 팔레트에서 세팅을 한다.

Default Sprite가 없어서 아무것도 보이지 않지만 이렇게 Rule Tile을 팔레트에 드래그 앤 드롭으로 하면 룰 타일을 생성할 수 있다.

해당 기능이 특히나 유용한 건 계속해서 룰을 지키는 구성 때문인지, 마음대로 만들었다가 지워도 타일이 알아서 룰에 맞게 교체된다.

꽤나 유용하게 사용할 수 있는 기능이라 예상되며, 필요 시에 룰 타일을 만들어서 맵 작업을 해야 할 것으로 보인다.

2.4 Tile Brush

타일 브러쉬의 종류는 이와 같이 있다. 기본형인 Default Brush 말고 다른 Brush의 기능을 알아보자.

1. GameObject Brush

게임오브젝트를 마치 타일 같이 가져와서 배치할 수 있다. 그리고 이 오브젝트는 실제로 그 오브젝트의 기능이 모두 작동된다.

버그인지는 몰라도 실수로 해당 게임 오브젝트를 지우개로 지워버리면 오브젝트 자체가 날아가버리니 주의하자.

2. Group Brush

box로 지정하여 타일맵을 만드는 것과는 미묘하게 다르다

3. Random Brush

타일을 등록하고 해당 타일중 랜덤하게 뽑을 수 있다.

4. Line Brush

타일을 일렬로 그을 때 편리하다.

2.5 2D Pixel Perfect

Virtual Camera에 Cinemachine Pixel Perfect를 달아보자.

타일이 많은 경우 카메라가 전환되면서 경계선이 깜빡거리는 듯한 현상을 볼 수 있는데 그걸 없애주는 기능이라고 생각하면 된다.

3. 카메라 전환과 애니메이션 전환에 관한 고민

지금까지의 강의에서 강사님 기준으로는 애니메이션을 Flip하여 전환하는 방식을 취했다. 하지만 나는 이에 반해 오브젝트 자체를 180도 회전시키는 방식으로 좌우 이동을 구현했다.

이렇게 구현한 데에는 아래와 같은 이유가 있었다.

  1. 애니메이션만 Flip시키니 캐릭터의 애니메이션과 실제 충돌체가 일치하지 않는 현상을 발견했다. (살짝 우측으로 치우침)
  2. 몬스터의 경우 충돌체 트리거로 바닥을 인식하여 좌우로 이동하는 메소드로 구성했는데, 충돌체 트리거를 양쪽에 다는 것보다 회전시키면서 하나로 탐지하는게 편할 것이다.

이와 같은 이유로 플레이어의 애니메이션을 오브젝트를 회전시키는 방식으로 변경했고, 카메라워킹 내용까지 포함해 아래와 같이 변경했다.

using Cinemachine;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    private Animator animator;
    private Rigidbody2D rigid;
    private bool isJumped = false;
    private bool isLanded;
    private float inputX;
    
    [SerializeField] CinemachineVirtualCamera virtualCamera;
    [SerializeField] private float moveSpeed;
    [SerializeField] private float jumpPow;

    private readonly int IDLE_HASH = Animator.StringToHash("Idle");
    private readonly int WALK_HASH = Animator.StringToHash("Walk");
    private readonly int JUMP_HASH = Animator.StringToHash("Jump");


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

    private void Update()
    {
        PlayerInput();
        if (Input.GetKeyDown(KeyCode.Space))
        {
            isJumped = true;
        }
    }

    private void FixedUpdate()
    {
        PlayerMove();
        if (isJumped && isLanded) PlayerJump();
    }

    private void PlayerInput()
    {
        inputX = Input.GetAxis("Horizontal");        
    }

    private void PlayerMove()
    {
        if(inputX == 0)
        {
            animator.Play(IDLE_HASH);
            return;
        }
        rigid.velocity = new Vector2(inputX * moveSpeed, rigid.velocity.y);
        animator.Play(WALK_HASH);
        //spriteRenderer.flipX = inputX < 0;
        //gameObject.transform.rotation = inputX > 0 ? Quaternion.Euler(0, 0, 0) : Quaternion.Euler(0, 180, 0);
        if(inputX > 0)
        {
            gameObject.transform.rotation = Quaternion.Euler(0, 0, 0);
        }
        else
        {
            gameObject.transform.rotation = Quaternion.Euler(0, 180, 0);
        }
    }

    private void PlayerJump()
    {
        rigid.AddForce(Vector2.up * jumpPow, ForceMode2D.Impulse);
        animator.Play(JUMP_HASH);
        isLanded = false;
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            isJumped = false;
            isLanded = true;
        }
    }
}

이는 최종 해결된 코드이긴 처음에 해당 방법대로 코드를 적용했을 때 아래와 같은 문제가 발생했었다.

또한 이전까지 고민하고 있던 카메라의 화면 이동 문제로, Outline으로 카메라 영역을 싸는 것에 대해 고민이 깊었다.
이 두 문제를 해결하기 위해서 꽤 머리를 싸맸는데, 결론적으로 고친 방법은 다음과 같다.

  1. 카메라는 Framing Transposer로 선택, 캐릭터 자체가 회전하다 보니 카메라 워킹에서는 차라리 Body 부분을 그냥 수정

세팅은 아래와 같이 했다.

  1. Bound 영역을 Trigger로 설정

바운드 영역을 Trigger로 설정하면 카메라도 그대로 빠져나오는 거 아닌가 싶기는 했지만, 의외로 빠져나가지 않았다.

  1. 결과

profile
게임 만들러 코딩 공부중

0개의 댓글