Unity의 Input System은 기존 Input.GetKey(), Input.GetAxis() 방식보다 한 단계 추상화된 입력 처리 시스템이다.
핵심은 딱 하나다.
“키보드의 W를 눌렀다”가 아니라 “이동이라는 입력이 발생했다”를 다루는 것.
이 관점만 잡으면 Input System 구조가 상당히 단순해진다.
전체 구조
Unity Input System은 크게 다음 관계로 이해하면 된다.
실제 입력 장치
↓
Keyboard / Mouse / Gamepad / Touch
↓
Input Action
↓
Input Action Map
↓
PlayerInput 또는 C# 코드
↓
게임 로직
예를 들어 플레이어 이동을 만든다고 하자.
W / A / S / D
↓
Move Action
↓
Player Action Map
↓
PlayerController
↓
이동
여기서 중요한 것은 W 자체를 게임 코드가 알 필요가 없다는 것이다.
Input System을 사용하는 이유
기존 방식에서는 보통 이렇게 작성한다.
if (Input.GetKey(KeyCode.W))
{
Move(Vector3.forward);
}
이 코드에는 문제가 있다.
게임 로직이 키보드에 직접 결합되어 있다.
게임 로직
↓
Keyboard
↓
W
게임패드로 바꾸려면?
if (Input.GetKey(KeyCode.W))
를 뜯어고쳐야 한다.
Input System에서는 이렇게 분리한다.
게임 로직
↑
Move
↑
Input Action
↑
WASD / Gamepad Stick / Joystick / ...
따라서 게임 로직 입장에서는
“W가 눌렸다”
가 아니라
“Move 입력값이 들어왔다”
만 알면 된다.
Input Action
가장 중요한 개념이다.
Input Action = 게임에서 발생하는 입력의 의미
예를 들면:
Move
Jump
Attack
Interact
Dash
Pause
Look
Fire
이런 것들이 Action이다.
예를 들어:
Move
라는 Action에
WASD
Left Stick
을 연결할 수 있다.
그러면 게임 코드는 장치가 무엇인지 신경 쓸 필요가 없다.
Input Action Asset
Unity에서 Input System을 사용하면 보통
PlayerInputActions.inputactions
같은 파일을 만든다.
이 파일이 입력 설정 데이터다.
대략 이런 구조가 된다.
PlayerInputActions
│
├── Player
│ ├── Move
│ ├── Look
│ ├── Jump
│ ├── Attack
│ └── Interact
│
└── UI
├── Navigate
├── Submit
└── Cancel
여기서 Player, UI 같은 것이 Action Map이다.
Action Map
Action을 묶어놓은 그룹이다.
예를 들어:
Player
├── Move
├── Look
├── Jump
└── Attack
그리고
UI
├── Navigate
├── Submit
└── Cancel
이렇게 만들 수 있다.
왜 굳이 나누냐?
게임 플레이 중에는
Player Action Map 활성화
UI Action Map 비활성화
메뉴를 열면
Player Action Map 비활성화
UI Action Map 활성화
같은 처리가 가능하기 때문이다.
즉 입력 컨텍스트를 관리하는 단위라고 보면 된다.
Binding
Action에 실제 입력 장치를 연결하는 것이 Binding이다.
예를 들어:
Move
라는 Action이 있다면:
Move
├── W
├── A
├── S
├── D
└── Gamepad Left Stick
이런 식으로 Binding을 설정할 수 있다.
좀 더 정확히는 WASD를 하나의 2D Vector Composite로 묶는다.
Move
└── 2D Vector
├── Up → W
├── Down → S
├── Left → A
└── Right → D
그러면 최종적으로 코드에서는:
Vector2 moveInput
하나만 받으면 된다.
Control
Control은 실제 입력 장치의 개별 입력 요소다.
예를 들어 키보드:
Keyboard
├── w
├── a
├── s
└── d
게임패드:
Gamepad
├── leftStick
├── rightStick
├── buttonSouth
└── buttonNorth
마우스:
Mouse
├── position
├── delta
├── leftButton
└── rightButton
즉 관계를 정리하면:
Device
↓
Control
↓
Binding
↓
Action
↓
Action Map
Action의 Input Value
Action은 입력값도 가진다.
예를 들어 Jump:
Jump
→ Button
→ bool
Move:
Move
→ Vector2
Look:
Look
→ Vector2
Trigger:
Fire
→ float
대표적으로:
| Action | Type |
|---|---|
| Jump | Button |
| Attack | Button |
| Move | Vector2 |
| Look | Vector2 |
| Trigger | Axis / Float |
가장 중요한 부분: Input Action의 생명주기
Input System에는 Action을 감지하는 대표적인 이벤트가 있다.
started
performed
canceled
예를 들어 버튼을 누른다면:
누르기 시작
↓
started
입력 발생
↓
performed
손을 뗌
↓
canceled
다만 모든 Action이 정확히 이런 식으로만 동작한다고 단순화하면 안 된다. Action의 타입과 Interaction 설정에 따라 발생 시점이 달라진다.
일단 기본적으로는 이렇게 이해하면 충분하다.
실제 코드
예를 들어 Move Action을 만든다고 하자.
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
[SerializeField]
private InputActionReference moveAction;
private void OnEnable()
{
moveAction.action.Enable();
}
private void OnDisable()
{
moveAction.action.Disable();
}
private void Update()
{
Vector2 input = moveAction.action.ReadValue<Vector2>();
Vector3 movement = new Vector3(
input.x,
0f,
input.y
);
transform.position += movement * Time.deltaTime;
}
}
여기서 핵심은:
Vector2 input = moveAction.action.ReadValue<Vector2>();
이다.
게임 코드는 이제
W인지
A인지
Gamepad인지
Joystick인지
를 전혀 모른다.
그냥
Vector2
를 받는다.
Callback 방식
Input System은 이벤트 방식으로도 받을 수 있다.
private void OnEnable()
{
moveAction.action.performed += OnMove;
moveAction.action.canceled += OnMove;
}
private void OnDisable()
{
moveAction.action.performed -= OnMove;
moveAction.action.canceled -= OnMove;
}
private void OnMove(InputAction.CallbackContext context)
{
Vector2 input = context.ReadValue<Vector2>();
Debug.Log(input);
}
이 방식은
입력 발생
↓
콜백 호출
↓
게임 코드 실행
이라는 구조다.
PlayerInput
그리고 Unity에는 PlayerInput이라는 컴포넌트가 있다.
이걸 사용하면 입력 이벤트를 좀 더 쉽게 연결할 수 있다.
예를 들어:
Player
└── PlayerInput
그리고 Input Actions를 연결한다.
PlayerInput은 내부적으로 Action Map과 Action을 관리하면서 게임 오브젝트의 스크립트에 입력을 전달할 수 있다.
대표적인 방식이:
Send Messages
Unity Events
C# Events
등이다.
하지만 실무에서는 PlayerInput을 무조건 써야 하는 것은 아니다.
InputActionReference
개인적으로 Unity 개발에서 이해해두면 좋은 것이 이것이다.
[SerializeField]
private InputActionReference moveAction;
Inspector에서 특정 Action을 직접 연결할 수 있다.
예:
PlayerInputActions
└── Player
└── Move
를
PlayerController
└── Move Action
에 연결한다.
그러면 코드에서:
moveAction.action.ReadValue<Vector2>();
로 읽을 수 있다.
이 방식은 코드와 Input Asset의 결합을 적절하게 유지하면서도 구조가 명확하다.
Input System을 이해하는 가장 좋은 예
FPS 게임을 생각해보자.
Input Actions:
Player
├── Move
├── Look
├── Jump
├── Fire
├── Reload
└── Interact
Binding:
Move
├── WASD
└── Gamepad Left Stick
Look
├── Mouse Delta
└── Gamepad Right Stick
Jump
├── Space
└── Gamepad South Button
Fire
├── Mouse Left Button
└── Gamepad Right Trigger
게임 코드는:
Move(Vector2 input)
Look(Vector2 input)
Jump()
Fire()
만 생각한다.
즉:
┌── Keyboard
│
├── Mouse
│
└── Gamepad
↓
Binding
↓
Action
↓
PlayerController
↓
Game Logic
이게 Input System의 핵심 구조다.
그리고 여기서 한 단계 더 중요한 것
Input System은 입력을 처리하는 시스템이지 게임플레이 로직 자체가 아니다.
좋은 구조는:
Input System
↓
Input Reader
↓
Player Controller
↓
Character / Gameplay
예를 들어:
public Vector2 MoveInput { get; private set; }
private void Update()
{
MoveInput = moveAction.action.ReadValue<Vector2>();
}
그리고 PlayerController가:
Vector2 input = inputReader.MoveInput;
을 사용한다.
그러면 나중에 AI가 플레이어를 조작하거나 리플레이 시스템을 만들 때도:
Keyboard
↓
Input Reader
↓
Player
뿐만 아니라
AI
↓
Player
또는
Replay Data
↓
Player
같은 구조로 확장하기 쉬워진다.
결국 Input System의 본질은 키보드 입력을 받는 API가 아니라 물리적인 입력 장치와 게임 로직 사이를 추상화하는 계층이다.
이걸 이해한 상태에서 Input Action / Action Map / Binding / Control / Interaction / Processor / PlayerInput을 순서대로 보면 Unity Input System이 거의 다 정리된다.