본문 바로가기

내일배움캠프/[P4-Solo.] PartyRoom

[TIL 24.05.10] Unity 개인프로젝트 입문(1)

강의를 들으면서 따라하기만 해서 따라는 했지만 결국 제대로 이해하지 못한 내용이나, 새롭게 알게 된 내용에 대해 정리했다. 

 

목   차

     


    Input System

    Input Actions 설정하기

    1. Input System 패키지를 다운받는다.
    2. Input Actions Asset을 생성한다. (Input 폴더)
    3. Action Maps 및 Actions 추가: 
      1. Action Map 예시: 캐릭터 컨트롤, 메뉴 탐색 등.
      2. Actions 예시: Move, Jump, Fire 등.
    4. Bindings 설정: 각 Action에 대해 어떤 키를 눌러야 상호작용할 것인지 등을 정하는 작업.
    5. Control Schemes: 다양한 입력장치를 위한 것. (키보드&마우스 / 게임패드 등)

    스크립트에서 Input Actions 사용하기

    다음은 내가 인터넷에서 Input Actions를 적용하는 법을 찾았을 때 나온, 참조를 통해 Input Actions를 받아오는 방법이다. 

    1. Input Actions를 스크립트에서 참조해야 한다.
      using UnityEngine.InputSystem; // Input System 네임스페이스 추가 필요.
      
      public class PlayerController : MonoBehaviour
      {
      	//YourInputActionsClassName은 내가 위의 과정을 따라 만든 InputAction의 이름.
          private YourInputActionsClassName inputActions; // Input Actions 인스턴스
          
          private void Awake()
          {
              // Input Actions 인스턴스 생성
              inputActions = new YourInputActionsClassName();
          }
      }
    2. 스크립트에서 참조한 Input Actions를 OnEnable과 OnDisable 함수 등을 통해 활성화 시에만 입력을 받도록 한다.
      private void OnEnable()
          {
              // Input Actions 활성화
              inputActions.Enable();
          }
      
      private void OnDisable()
          {
              // Input Actions 비활성화
              inputActions.Disable();
          }
    3. 입력 이벤트에 대한 콜백을 연결한다.
      //Awake 함수 안에 구현.
      inputActions.Player.Move.performed += OnMove;
      inputActions.Player.Move.canceled += OnMove;
    4. 실제 움직임을 적용할 메서드 OnMove 등을 만든다.
      private void OnMove(InputAction.CallbackContext context)
          {
              // 움직임 처리 로직
              Vector2 moveInput = context.ReadValue<Vector2>().normalized;
          }

    #CallbackContext

    위의 설명에서 inputActions.Player.Move 다음에 나오는 performed와 canceled 등을 칭한다. 

    또 4번의 OnMove 함수 인자에서도 볼 수 있다.

     

    이 객체는 사용자의 입력을 했을 때와 관련된 상세정보를 담고 있다.

    자료형은 InputAction.CallbackContext 이고, 이렇게 선언된 context 뒤에 .을 붙여 프로퍼티처럼 불러올 수 있다.

    • action: 이벤트를 발생시킨 액션의 정보를 전달한다.
      이때 정보 = Input Action을 만들 때 두 번째 줄에 있던 것들: 액션의 이름(Move), 바인딩, 활성화 상태 등.
    • readValue: 사용자의 입력 값에 접근하여 처리한다. 주로 이동에 많이 사용된다.
      위의 OnMove 함수에 그 예가 있다.
    • phase: 입력 액션의 상태를 확인하여 그에 맞는 처리를 한다.
      • phase의 종류: 
        Started: 입력이 시작될 때 발생한다. 
        Performed: 입력이 완전히 수행되었을 때 발생한다. (인식 성공)
        Canceled: 입력이 취소될 때 발생한다. (입력 중단 or 다른 조건에 의해 무효화됨.)
        Waiting: 액션 설정에서 지연(delay) 설정이 있을 때 입력 완료까지 대기 상태임을 나타낸다.
        Disabled: 액션이 비활성화 상태라는 것을 알린다.
      • 이것들은 위의 3번 예시처럼 사용하거나,
        context.phase == InputActionPhase.Performed 등과 같은 조건문으로 활용가능하다.
    • time: 입력 이벤트가 발생한 시간을 확인한다.
    • control: 이벤트를 발생시킨 특정 입력장치를 확인한다.
      이를 통해 어떤 키 또는 버튼이 눌렸는지 알 수 있다.

    Player Input 활용

    위의 내용이 우리 강의와 달라 조금 더 찾아보니, Player Input 컴포넌트의 유무가 차이를 만든다는 것을 발견했다. 그래서 여기서부터는 송지원 튜터님이 설명하신 그 방법으로 다시 설명한다. 또한, 이번 과제에도 이 방법을 우선 활용해려 한다. 이 설명들은 GPT를 참고하여 작성햇다.

    1. Player Input 컴포넌트를 Player 게임 오브젝트에 추가한 후, Input Actions(TopDownController2D)를 컴포넌트의 Actions 삽입한다.
    2. Input Action의 Action 두 개(현재로써는 Move와 Look)을 OnMove와 OnLook 메소드에 연결한다. 이때, InputValue를 자동으로 CallbackContext로 전환해줄 것이다.

    참고로, GPT는 OnMove와 OnLook의 인자를 InputValue 대신 InputAction.CallbackContext로 하라고 했다. 만약 그렇게 바꾼다면, 내 코드는 다음과 같아져야 할 것이다:

    using UnityEngine;
    using UnityEngine.InputSystem;
    
    public class PlayerInputController : TopDownController
    {
        private Camera camera;
        private PlayerControls controls; // Input Actions 클래스 //<1>
    
        private void Awake()
        {
            camera = Camera.main;
            
            // Input Actions 인스턴스 생성
            controls = new PlayerControls();
    
            // 입력 이벤트에 메소드 연결
            controls.Player.Move.performed += context => OnMove(context); //<2>
            controls.Player.Look.performed += context => OnLook(context);
        }
        
        public void OnMove(InputAction.CallbackContext context)
        {
            Vector2 moveInput = context.ReadValue<Vector2>().normalized;
            CallMoveEvent(moveInput); //TopDownController의 이벤트 발생.
        }
    
        public void OnLook(InputAction.CallbackContext context)
        {
            Vector2 newAim = context.ReadValue<Vector2>();
            Vector2 worldPos = camera.ScreenToWorldPoint(newAim);
            newAim = (worldPos - (Vector2)transform.position).normalized;
    
            if (newAim.magnitude >= .9f)
            {
                CallLookEvent(newAim);
            }
        }
    }

     <1>: PlayerControls는 Input System에 기본적으로 딸려오는 클래스이며 게임 내 입력을 관리하기 위한 모든 액션 맵과 액션을 포함한다. 이 클래스의 인스턴스를 통해 (객체 이름).(액션 맵 이름) 에 있는 Action에 대한 이벤트 리스너를 추가할 수 있다.

     

    <2>: 이 코드의 의미는 다음과 같다.

    • controls.Player.Move.performed 를 예로 들겠다.
      • controls: PlayerControls의 인스턴스. 모든 Input Action을 포함하고 있다.
      • Player: 내가 만든 특정 "액션 맵". 
      • Move: 내가 만든 액션 맵의 "액션" 중 하나.
      • performed: Input System에서 제공하는 CallbackContext 중 phase의 이벤트.
    • 이벤트 리스너 작동하는 법
      • performed는 바로 위의 설명과 같이 이벤트이다. 액션이 활성화 상태이기만 하면 Move 기준 W, A, S, D 중 하나를 누르는 순간 해당 이벤트가 트리거될 것이다.
      • += 연산자는 이벤트의 구독으로, performed 이벤트 이후 호출될 다음 함수가 기호 뒤에 들어가게 된다. 여러 함수가 호출되어야 한다면, 순서에 맞게 같은 방식으로 추가해주면 된다.
      • += 를 통해 performed와 연결된 리스너에는 InputAction.CallbackContext 타입의 데이터가 제공된다. 여기서 제공된 데이터는 += 뒤의 람다식의 parameter에 들어가게 된다.
      • 이 람다식을 통해 OnMove 메서드가 이벤트와 연결된다. 그리고 이벤트가 발생한다면 이 함수 역시 실행된다.

     참고로, 람다 식을 사용하지 않으면 그냥 OnMovePerformed; 가 += 뒤에 오고, 메서드의 인자는 InputAction.CallbackContext context로 똑같다. 

     

    ...공부하다 보니 내용이 너무 방대해졌는데, 알고보니 강의에서 InputValue를 쓴 건 그냥 그게 훨씬 쉽고 간단하기 때문이고, 내가 배운 이 내용들은 숙련 주차 이상부터 준비가 되어 있다고 한다. 뭐 예습했다 치고 넘어가자. 

    <3>: Action 역시 아직도! 아직도 이해가 잘 되지 않아 조금 더 자세히 공부해보았다. 

    1. Event 선언(TopDownController 클래스): Action은 델리게이트 타입 중 하나로, 반환값이 없다. (있는건 func)
      public event Action<Vector2> OnMoveEvent;
      • Vector2 타입의 매개변수 하나를 받는 메서드를 참조할 수 있다.
      • OnMoveEvent가 이벤트의 이름이다.
    2. Event 호출(TopDownController 클래스): OnMoveEvent와 연결된 이벤트들을 줄줄이 실행해준다. 만약 연결된 이벤트가 없다면, 아무런 동작도 하지 않는다.
      public void CallMoveEvent(Vector2 direction)
      {
          OnMoveEvent?.Invoke(direction);
      }
      이제 이 함수를 어딘선가 부른다면, OnMoveEvent와 연결된 함수들도 줄줄이 실행될 것이다. 
    3. Event와 연결된 메서드 실행(TopDownMovement 클래스): 위의 OnMoveEvent와 연결된 메서드들이 여기에 있다.
      private void Start()
      {
          // OnMoveEvent에 Move를 호출하라고 등록함
          movementController.OnMoveEvent += Move;
      }
      
      private void Move(Vector2 direction)
      {
          // 이동방향만 정해두고 실제로 움직이지는 않음.
          // 움직이는 것은 물리 업데이트에서 진행(rigidbody가 물리니까)
          movementDirection = direction;
      }

    즉, 여기까지의 메커니즘을 보면 다음과 같다. 

    1. WASD를 통해 키를 입력받는다.
    2. 이 키를 눌렀다는 이벤트가 PlayerInputController의 OnMove 함수로 전달되고, OnMove 함수는 입력받은 Vector2를 다시 CallMoveEvent로 전달한다.
    3. TopDownController 에서 CallMoveEvent 안의 OnMoveEvent 이벤트를 발생시킨다. 
    4. OnMoveEvent의 리스너들은 TopDownMovement에 있는데, 여기의 Move 메서드가 바로 그 리스너이다. 이 Move메서드는 클래스에 방향만 어느 쪽이라 지정해주는 역할을 한다.
    5. 이렇게 입력된 방향은 같은 클래스의 FixedUpdate에서 ApplyMovement를 통해 이 메서드로 전달이 되고, 다시 ApplyMovement 메서드는 direction에 속도 보정치 5를 곱해준 후(추후 변수로 대체 예정) 해당 클래스에 선언된 Rigidbody2D 객체에 대입되어 실제 플레이어의 움직임을 유발한다.
    6. 이 때, 키 입력이 없다면 InputValue를 받아올 때 자동으로 Vector2는 zero로 설정이 되고, 이에 따라 키 입력이 없을 때 캐릭터 역시 그 자리에 멈출 수 있게 된다.

    좋아! 아마도 이제 Input System을 통한 플레이어의 움직임은 완벽히(?) 이해했어! 라고 믿고 싶다.

    플레이어 애니메이션

    마우스 방향에 따라 플레이어 방향 바꾸기

    우선, 나는 Unity Asset Store에서 2D Character - Astronaut 라는 에셋을 활용해서 플레이어를 만들었다. 여기에는 귀여운 우주인 캐릭터가 각 모션마다 세 방향: 앞 뒤 옆의 이미지가 약 21프레임 정도로 준비가 되어 잇었다. 이걸 끌어서 Animation 클립까지는 만들었지만, 어떻게 해야 내 입력을 통해 Animation 종류를 바꾸는지에 대해 약간 헷갈리는 부분이 있어 찾아보았다.

    #방법1: Animation 클립 이름으로 불러오기.

    가장 간단한 방법 중 하나는 애니메이터 컨트롤러에 그냥 여러 클립을 추가해놓고 Animation.Play("PlayerFront")와 같은 식으로 불러오는 것이다. 또한, Play 메서드는 두 번째 인자로 layer, 세 번째 인자로 플레이 시간의 비례를 받아올 수 있다.

     

    #방법2: Parameter을 통해 전달하기

    애니메이터 컨트롤러에서 Parameters 탭을 클릭하고, 필요한 변수들을 만든다.

    Animator.SetBool (Bool 자리에는 맞는 자료형) 한 다음에 첫 번째 인자로는 파라미터의 이름, 두 번째에는 그 파라미터에 들어갈 값을 입력하면 된다.


     

    마무리

    으에엥.. 너무 피곤하다. 

    정말 한게 없는 줄 알았는데 곰곰히 생각해보니 벌써 필수 요구사항을 거의 다 끝낸 나 자신을 발견했다. 뭐지...?