Ace Combat Zero: 유니티로 구현하기 #1 : Input

Lunetis·2021년 2월 28일
1

Ace Combat Zero

목록 보기
2/27

가장 기본이 되는 입력(Input)을 구성해보겠습니다.

에이스 컴뱃 제로가 플레이스테이션 2 게임이었던 만큼, 일단은 키보드/마우스 입력 대신 컨트롤러 입력에 대해서만 구현하려고 합니다.

앞으로 쭉 고생할 노예 듀얼쇼크 4 되시겠습니다.

(왼쪽 조이스틱 컨트롤러가 좀 불안정합니다만...)

새로운 입력 시스템

그런데 유니티에 손을 놓고 있는 동안 뭔가 새로운 입력 시스템이 추가되었더라고요.


(예?)

이전까지는 Input.GetAxis(...), Input.GetButtonDown(...)을 주구장창 써왔는데, 뭔가 새로운 시도를 할 때가 온 것 같네요.

패키지 매니저에 Input System이 있네요. 2021년 1월 21일에 1.0.2 버전으로 올라와 있었습니다.

그리고 이 패키지를 설치하면 Window - Analysis에 Input Debugger라는 창이 추가되고요,

듀얼쇼크 4를 꽂았더니 듀얼쇼크 장치를 인식합니다.

뭔가 여러가지 정보가 뜨는데 나중에 입력장치 연결할 때 쓰면 될 것 같습니다.




이제 새로운 입력 시스템을 도대체 어떻게 써먹는지 알아봐야겠네요.
입력을 받아서 상호작용할 대상에 Add Component - Player Input이라는 걸 추가했습니다.

여기서 Actions가 비어있는데 뭔가 Create Actions를 눌러야 할 것 같아서 눌러줬습니다.

Input Actions를 생성했을 때, 기본적으로 Move, Look, Fire 3가지 액션에 대한 기본값들이 추가되어 있습니다.

Move는 날리도록 하겠습니다. 이 게임은 일반적인 상하좌우 이동을 사용하지 않기 때문이죠.
(사실 내부 항목은 다 날렸습니다.)

출처: https://en.wikipedia.org/wiki/Aircraft_principal_axes

비행기의 3축 운동이라는 개념이 있습니다.
Roll, Pitch, Yaw 3가지 기동 방식을 이용해서 비행기를 조종하죠.
이 3가지를 컨트롤하도록 인풋 액션을 구성하려고 합니다.

에이스 컴뱃 시리즈에서는 두 가지 조작 방식을 지원하고 있으며, 7에서는 STANDARD, EXPERT 라는 이름으로 사용하고 있습니다.
비교적 현실적인 기동을 위해서(?) EXPERT 스타일로 만들도록 하겠습니다. 그동안 이 방식으로 게임을 해왔기도 하고요.

Roll은 왼쪽 컨트롤러 스틱의 X축,
Pitch는 왼쪽 컨트롤러 스틱의 Y축,
Yaw는 L1, R1 버튼으로 조종합니다.

Roll 액션을 만들고, 아날로그 컨트롤러를 사용하기 때문에 Control Type은 Axis (축)으로 설정했습니다.

바인딩은 왼쪽 스틱 - X로 두겠습니다.

비슷한 방식으로 Pitch도 설정했습니다.

Yaw는 아날로그 스틱이 아닌 버튼으로 조작합니다.
무기를 발사하는 것과는 다르게 방향성이 존재하므로 1D Axis Composite로 설정해 보겠습니다.

왼쪽을 음수, 오른쪽을 양수에 대응하게끔 짜면 되겠죠.

그리고 가속과 감속은 각각 R2, L2 트리거로 두었습니다.


버튼 입력

콜백 추가하기

우선 아날로그 컨트롤러 대신 비교적 단순한 버튼 입력을 테스트해보려 합니다.

Fire 액션에 해당하는 버튼 (듀얼쇼크 기준 O 버튼)을 누르면 그 액션에 할당된 함수가 실행됩니다.

// using UnityEngine.InputSystem; 을 추가해야 합니다.
public class AircraftController : MonoBehaviour
{
    public void Fire(InputAction.CallbackContext context)
    {
        Debug.Log("Fire");
    }
}

일단 Fire 액션에 실행될 함수를 간단하게 구현했습니다.
실제로 미사일이 날아가는 것 대신 콘솔에 로그를 찍어보겠습니다.

이전에 비행기에 추가했던 Player Input에서 Event를 추가해줍니다.

Behavior를 Invoke Unity Events로 설정하고, Events 드롭다운 메뉴를 클릭하면 Input Actions에 추가되었던 설정들이 나열됩니다.

Fire (CallbackContext)에 위에 만들었던 Fire() 함수를 추가했습니다.
실제로는 파라미터에 CallbackContext가 넘어가지만 그냥 파라미터가 없는 함수를 넣어보았습니다.

눌렀을 때 2개, 떼었을 때 1개씩 로그가 찍힙니다.
뭔가 여러 개의 이벤트에 다 Fire()가 반응하는 것 같습니다.

단순히 눌렀을 때만 실행되었으면 좋겠는데요...



InputActionPhase

context.action.phase 에는 InputActionPhase라는 데이터가 있습니다.

  • Started: 실행 시작 시 호출
  • Performed: 실행 확정 (완전히 실행) 시 호출
  • Canceled: 실행 종료 시 호출
  • Disabled: 액션이 활성화되지 않음
  • Waiting: 액션이 활성화되어있고 입력을 기다리는 상태

https://docs.unity3d.com/Packages/com.unity.inputsystem@1.0/manual/Interactions.html

위 5가지 InputActionPhase는 Interaction의 타입에 따라 달라집니다.

일반적인 버튼 입력 상황 (Default)에서는 Started와 Performed가 같은 타이밍에 실행됩니다. (실행 순서는 Started가 먼저입니다.)
그리고 Canceled는 버튼을 뗐을 때 실행되죠.


입력 시스템의 Interaction 중 Hold에 대한 예시를 들어보면,

Hold를 감지하기 위한 pressPoint가 있고, 이 pressPoint를 넘어가는 순간 started가 실행됩니다.
그리고 그 상태에서 duration만큼의 시간이 흐르면 그때가 되어서야 performed가 실행됩니다.

스마트폰 홈 화면에서 앱을 누르고 홀드하는 것과 비슷한 경우입니다.
그냥 터치했을 때는 앱이 실행되고, 오래 터치하고 있으면 앱 위치를 이동하는 모드로 변경되는 것처럼요.




다시 본론으로 돌아가서,

제가 실행하려는 Fire() 함수는 Started, Performed, Canceled 상황에 모두 실행되는 것 같습니다.
이 중 Started와 Performed는 버튼을 누를 때 같이 발생하므로, Started를 제외하고
Performed일 때만 실행되게 제한을 걸어보겠습니다.

public void Fire(InputAction.CallbackContext context)
{
    if(context.action.phase == InputActionPhase.Performed)
    {
        Debug.Log("Fire");
    }
}

Fire 함수를 조금 바꿔봤습니다.
매개변수로 Input.CallbackContext context를 받아오게 하고,
context.action.phasePerformed일 때만 로그를 찍게 하는 것이죠.

로그가 한 번만 찍혀나오는 것을 확인할 수 있었습니다.


다른 방법으로 콜백 추가하기

위에서는 Event에다가 이렇게 콜백으로 실행될 함수를 손수 넣어줬었죠.

에디터 상에서 추가하지 않고, 코드 상에서 콜백을 추가하는 방법도 존재합니다.

Fire에 추가했던 콜백 이벤트를 지워준 후, 코드를 아래와 같이 작성합니다.

private void OnEnable()
{
    InputActionMap playerActionMap = GetComponent<PlayerInput>().actions.FindActionMap("Player");
    InputAction fireAction = playerActionMap.FindAction("Fire");

    fireAction.performed += Fire;
}

public void Fire(InputAction.CallbackContext context)
{
    Debug.Log("Fire");
}

조작할 오브젝트에서 PlayerInput 데이터를 가져오고, 거기서 ActionMap을 가져옵니다.
그리고 그 맵에서 "Fire"에 해당하는 InputAction을 가져옵니다.

"Fire" InputAction이 Performed 상태에서만 Fire(...)함수를 실행하고 싶으므로,
fireAction.performed에 delegate를 추가해줍니다. 그냥 += 연산자로 더해주죠.


이 방식의 실행 결과도 위와 같습니다.

이렇게 구현하면 Inspector View - Player Input 내부의 Event에 아무것도 등록하지 않아도 됩니다.

실행하려는 코드의 시작부터 InputActionPhase를 체크하는 과정이 줄어들게 되지만,
Event 항목에서 아무것도 보이지 않으니, 처음 보는 사람은 "아니 이거 어디서 실행하는거임???" 하고 의문을 품을 수도 있겠네요.

물론 오브젝트에 추가된 스크립트를 열어보면 다들 이해할 겁니다.


트리거 (아날로그) 입력

버튼 입력은 어느정도 알았으니, 이제 트리거 입력을 얻어보겠습니다.
버튼 입력과 비슷하지만, 0 - 1 사이의 값을 가질 수 있습니다.

Action Type은 Value, Control Type은 Analog로 설정했습니다.
(위에 있는 Fire는 Action Type이 Button이었습니다.)

public void Accelerate(InputAction.CallbackContext context)
{
    float value = context.ReadValue<float>();
    Debug.Log("Input : " + value);
}

0 - 1 사이의 float 값이 들어올테니 ReadValue<float>()로 값을 얻어온 다음, 로그를 찍는 함수를 만들었습니다.

그리고 Events에 콜백 함수로 등록해주었습니다.



잘 입력되는 것 같죠.
여기서 특이한 점을 눈치채셨나요?




테스트할 때 저는 오른쪽 트리거를 최대한 눌렀다가 떼는 것을 2번, 중간까지 눌렀다가 떼는 것을 1번 했습니다.

Input: 1일 때 저는 오른쪽 트리거를 최대로 누르고 있었습니다.
그 때의 상황을 보면, Input : 1인 상태에서 로그가 가만히 멈춰있습니다.

그리고 누르고 있지 않을때도 로그에 0이 계속 출력되고 있지 않습니다. 한 번 출력되고 끝이죠.

즉, 지금 만든 Accelerate() 함수는 값에 변화가 있을 때만 작동된다는 것을 알 수 있습니다.

하지만 개발자가 원하는 건 대부분 트리거를 누르고 있는 동안 해당 동작을 계속 실행하는 방식이지,
이렇게 값이 바뀔 때만 실행하는 방식을 원하지는 않을 것입니다.


계속 실행시키기 위해서는 입력값을 받아서 멤버 변수에 저장하고,
update() 같은 곳에서 그 값을 이용해서 실행을 시켜야 하겠네요.

float accelerateValue;

public void Accelerate(InputAction.CallbackContext context)
{
    accelerateValue = context.ReadValue<float>();
}

void Update()
{
    Debug.Log(accelerateValue);
}

가속 관련 코드를 이렇게 바꿨습니다.
accelerateValue라는 멤버 변수에 값을 저장하고,
로그는 Update()에서 출력하도록 변경했습니다.

이제 0과 1이 계속 찍히는군요.


조이스틱 입력

이번에는 X, Y 축이 존재하는 조이스틱입니다.

비행기 주변을 돌아볼 때는 오른쪽 조이스틱을 사용합니다.
X, Y 값이 있는 Vector2로 Control Type을 설정했습니다.

Vector2 rightStickValue;

public void Look(InputAction.CallbackContext context)
{
    rightStickValue = context.ReadValue<Vector2>();
}

void Update()
{
    Debug.Log(rightStickValue);
}

트리거 입력과 비슷한 방식으로 코드를 추가하고,

Events에 함수를 등록했습니다.

조이스틱을 움직일 때마다 값이 바뀌는 것을 확인할 수 있습니다.


입력 테스트 (최종)

이제 게임에 필요한 모든 입력을 구현하고 테스트할 시간입니다.

지금의 Input Actions는 Gamepad 값과 PS4 컨트롤러 전용 값이 뒤섞인 혼종이군요.
나중에 Gamepad로 통일되도록 수정해야겠습니다.

모든 입력 콜백 함수를 구현하고,

이벤트에 콜백 함수를 등록했습니다.
근데 일일이 등록하는 게 은근히 귀찮네요.

아무튼,

Unity Input System을 활용해서 게임에 필요한 모든 입력값을 얻어낼 수 있었습니다.


여담

앞서 콜백을 추가하는 방법 두 가지를 설명했었습니다.

1. 기능을 구현한 다음, Inspector View의 Events에 등록하기

  • OnEnable() 같은 곳에 함수를 등록하는 과정을 거치지 않아도 된다는 장점이 있습니다.
  • Player Input 컴포넌트를 가지지 않는 오브젝트의 콜백도 등록할 수 있습니다.
  • 하지만 코딩으로 끝내면 되는 방법이 있는데도 불구하고 "굳이 Inspector View에서 일일이 설정해줘야 하나?" 라는 생각이 들기도 합니다.
  • 기능을 만들어놓고 "어 왜 실행 안 돼?" 하고 보면 Events에 등록을 안 해준 게 다반사였습니다.
  • started, performed, canceled 등 여러 InputActionPhase 중 한 가지만 필요한 경우에도 ifswitch 문이 필요하다는 번거로움도 존재합니다.

2. 기능을 구현한 다음, 코드 내에서 붙이기

  • Player Input 컴포넌트를 가지고 있는 객체를 컨트롤하는 경우, Inspector View를 조작하지 않고도 기능을 빠르게 추가할 수 있습니다.
  • 번거롭게 Events에 등록할 필요가 없죠.
  • 구현할 때부터 started, performed, canceled 등의 InputActionPhase에 따로따로 함수를 등록할 수 있습니다.
  • 프로젝트를 처음 보는 사람은 코드를 열어보기 전까지는 어디에 영향을 미치는 지 모를 것입니다.
    (하지만 github에서 코드만 보는 사람이라면 바로 알 수 있죠. 그나저나 누가 코드를 안 보죠??)
  • 기능이 많다면 초기화 코드가 많이 길어질 수도 있겠군요.

두 방법을 모두 사용할 수도 있죠.

Player Input 컴포넌트가 있는 객체는 코드로 함수를 붙이고,
컴포넌트를 가지고 있지 않은 객체까지 컨트롤할 때는 Events에 객체와 함수를 등록하는 방법으로 구현할 수도 있습니다.




다음은 이렇게 얻어온 입력값으로 비행기의 움직임을 구현할 차례입니다.



이 프로젝트의 작업 결과물은 Github에 업로드되고 있습니다.
https://github.com/lunetis/OperationZERO

0개의 댓글