본문 바로가기

[내배캠] 본 캠프 개발 학습/매일매일 쓰는 TIL

10월 24일 목요일 본 캠프 개발 일지 | 유니티 숙련 강의 (2)

📌

 3D 숙련 주차 강의 듣는 나: 이론 설명에 열심히 끄덕끄덕하면서 쉬운데? 하다가 스크립트 코드 짜는 거 보면서 멘탈 붕괴됨. 선생님. 진도가 너무 빨라요,, (߹-߹)

학습 내용 

 

1. InputSystem(Behavior Invoke)
2. move 구현
3. Jump 구현 


1. InputSystem(Behavior Invoke)

 2D 게임에서 InputSystem 파트를 맡아서 이제 마스터 했다고 생각했는데 갑자기 튀어나온 Invoke. send Message는 액션을 함수로 전달해줘서 직접 그 함수를 스크립트에 작성해 써야했지만 Invoke는 Onclick 컴포넌트처럼 액션에 쓸 함수를 인스펙터에서 직접 할당해주는 형식이다. 

 그냥  send Message 쓰면 안 되나 싶어서 검색을 해봤는데. 

  • 성능이 중요하거나 컴파일 타임에 에러를 잡고 싶다면 Behavior.Invoke를 사용하는 것이 더 나은 선택입니다.
  • 유연성과 다형성이 중요하거나 특정 상황에서만 메서드를 호출해야 한다면 SendMessage를 사용할 수 있지만, 일반적으로는 성능과 유지보수 측면에서 Behavior.Invoke가 더 좋습니다.

InputSystem에서 두 방법 모두 상황에 따라 유용할 수 있지만, 안정성과 성능이 중요한 대부분의 경우 Behavior.Invoke가 더 나은 선택입니다.

 

....... 우울하다. 이럴 거면 처음부터 Invoke로 쓸 걸.

 

 어떻게 사용할까?

 방법은 비슷하다. 각 액션에 할당할 함수를 모은 스크립트(플레이어 컨트롤러)를 만들고 나중에 플레이어 오브젝트에 붙여서 사용! 여기서 중요한 점 하나.  InputValue를 매개변수로 받았던 SendMessage와 달리 Invoke는  InputAction.CallbackContext를 매개변수로 받아 콜백 시점을 정할 수 있다. 즉, 입력이 발생한 시점에 따라 메서드를 분리할 수 있다. 예를 들어, 버튼을 눌렀을 때, 떼었을 때, 또는 유지하고 있을 때 등 다양한 입력 상태를 구분한다는 것! 

 

2. move 구현

public class PlayerController : MonoBehaviour
{
    [Header("Movement")]
    public float moveSpeed; // 속도
    private Vector2 curMovementInput; //
public void OnMoveInput(InputAction.CallbackContext context)
    {
        if(context.phase == InputActionPhase.Performed) // 입력이 수행되면 
        {
            curMovementInput = context.ReadValue<Vector2>(); // 이동 백터 값(x,y)을 읽음
        }
        else if(context.phase == InputActionPhase.Canceled) // 입력이 취소되면
        {
            curMovementInput = Vector2.zero; // (0,0)
        }
    }
    
    private void Move()
    {
        Vector3 dir = transform.forward * curMovementInput.y + transform.right * curMovementInput.x;
// 이동 방향 결정
		dir *= moveSpeed; // 속도 조절 
        dir.y = rigidbody.velocity.y; // y축 현재값 유지 (강제로 0으로 만들지 않음)

        rigidbody.velocity = dir; // 이동 구현 
    }

 

 정리하면 OnMoveInput 에서 이동 입력을 감지하면 이동 방향을 curMovementInput에 넣고, Move에서  curMovementInput에 따라 캐릭터가 물리 엔진을 사용해 실제로 이동하게 된다. 

    private void FixedUpdate()
    {
        Move();
    }

 

매 프레임마다 호출되기 때문에 Update 메서드에 넣어주면 끝! 

2. jump 구현

 

 이전에 jump 구현을 해봤기 때문에 쉬울 거라고 생각했는데 move보다 더 큰 시련을 맞이했던 부분.

public float jumptForce; // 점프 높이 조절
public LayerMask groundLayerMask;

 

 public void OnJumpInput(InputAction.CallbackContext context)
    {
        if(context.phase == InputActionPhase.Started && IsGrounded()) // 입력이 들어온 시점 && IsGround가 true
        {
            rigidbody.AddForce(Vector2.up * jumptForce, ForceMode.Impulse);
        }
    }

 

여기까진 쉬웠다. 물리 엔진으로 순간적인 힘을 줘서 점프포스 만큼 띄운다는 간단한 코드니까.

bool IsGrounded()
    {
        Ray[] rays = new Ray[4]
        {
            new Ray(transform.position + (transform.forward * 0.2f) + (transform.up * 0.01f), Vector3.down),
            new Ray(transform.position + (-transform.forward * 0.2f) + (transform.up * 0.01f), Vector3.down),
            new Ray(transform.position + (transform.right * 0.2f) + (transform.up * 0.01f), Vector3.down),
            new Ray(transform.position + (-transform.right * 0.2f) +(transform.up * 0.01f), Vector3.down)
        };

        for(int i = 0; i < rays.Length; i++)
        {
            if (Physics.Raycast(rays[i], 0.1f, groundLayerMask))
            {
                return true;
            }
        }

        return false;
    }

 

 여기서 무한 점프를 막기 위한 코드가 추가되면서 난이도가 높아졌다. 나는 땅에 닿았는지 체크하기 위해 colliderenter 메서드를 썼기 때문에 ray는 바로 이해하기가 어려웠다. 하나씩 분석해보자. 

    {
        Ray[] rays = new Ray[4]
        {
            new Ray(transform.position + (transform.forward * 0.2f) + (transform.up * 0.01f), Vector3.down), // 앞
            new Ray(transform.position + (-transform.forward * 0.2f) + (transform.up * 0.01f), Vector3.down), // 뒤
            new Ray(transform.position + (transform.right * 0.2f) + (transform.up * 0.01f), Vector3.down), // 우
            new Ray(transform.position + (-transform.right * 0.2f) +(transform.up * 0.01f), Vector3.down) // 좌
        };

 
 상하좌우 네 방향에서 아래 쪽으로 ray(가상의 선)을 발사한다. 현재 위치에서 약간 위쪽에서 쏘는 이유는 캐릭터의 발 바로 아래에서 시작하지 않도록 하기 위함이다. 

for (int i = 0; i < rays.Length; i++)
{
    if (Physics.Raycast(rays[i], 0.1f, groundLayerMask))
    {
        return true;
    }
}

 

  지면 충돌 여부를 검사한다. 0.1f 길이의 ray를 발사해서 그 거리 안에 지면이 있다면 isground는 true가 된다. 아니라면 false를 반환한다. 

✅ 그런데 이 코드가 왜 플레이어 오브젝트의 collider 크기에 영향을 받을까? 

 이거 튜터님께 여쭤보려고 했는데 9시가 지나버렸다. 
 내일 여쭤보고 추가할 예정. 


📝 

 TIL 좋은 점. 분명 이해가 안 돼서 정리한 거였는데 정리하다보니 이해됨. 짱!