2D Godot 2D 게임 기초
P2 움직임과 물리

7강입력 받기

키를 하드코딩하지 않고 액션으로 묶는다.

이 강의 목차 (6)
  1. 키를 코드에 박으면 안 되는 이유
  2. 입력 맵 설정하기
  3. 읽는 방법 두 가지
  4. 4방향 입력을 한 줄로
  5. 실습 — 사방으로 움직이는 캐릭터
  6. 실습 — 마우스 방향 보기
이번 강의의 목표
  • 입력 맵(Input Map) 으로 키를 액션 이름에 묶는다.
  • 폴링(Input.is_action_pressed)과 이벤트(_input)의 차이를 안다.
  • 대각선 이동이 빨라지는 문제를 이해하고 고친다.

키를 코드에 박으면 안 되는 이유#

가장 순진한 입력 처리는 이렇습니다.

gdscript
if Input.is_key_pressed(KEY_D):
	position.x += 300 * delta

동작은 합니다. 그런데 이 코드는 다음을 전부 못 합니다.

  • 화살표 키로도 움직이게 하기
  • 게임패드 지원
  • 모바일 터치 지원
  • 사용자가 키 설정 바꾸기
  • 왼손잡이용 대체 키

그리고 이런 코드가 프로젝트 전체에 흩어져 있으면, 나중에 게임패드를 붙이려면 모든 파일을 다 뒤져야 합니다.

입력 맵 — 이름과 장치를 분리한다

"D키"라고 쓰지 말고 "move_right" 라는 이름으로 씁니다. 그 이름에 어떤 키·패드 버튼·터치가 연결될지는 프로젝트 설정에서 따로 정합니다.

text
"move_right"
   ← D키, →키, 패드 스틱 오른쪽, D-Pad 오른쪽

코드는 이름만 알면 되고, 장치가 늘어나도 코드는 한 줄도 안 바뀝니다.

입력 맵 설정하기#

프로젝트 → 프로젝트 설정 → 입력 맵(Input Map) 탭을 엽니다.

  1. 위쪽 칸에 move_left 를 입력하고 추가
  2. 목록에 생긴 move_left 오른쪽 + 를 눌러 키를 등록
    • 키보드 A
    • 키보드
    • 게임패드 왼쪽 스틱 왼쪽 (Joypad Axis)
  3. 같은 방식으로 move_right, move_up, move_down, jump, attack 을 만듭니다
이미 들어있는 ui_ 액션들

Godot에는 ui_left, ui_accept, ui_cancel 같은 액션이 기본 등록돼 있습니다. UI 조작 전용이라 생각하고, 게임플레이에는 직접 만든 액션을 쓰는 게 좋습니다. 나중에 "메뉴에서 스페이스를 눌렀는데 캐릭터가 점프한다" 같은 사고를 막습니다.

기본 액션을 보려면 입력 맵 탭 아래의 "내장 액션 표시" 를 켜세요.

읽는 방법 두 가지#

방법 1 — 폴링 (매 프레임 물어보기)#

"지금 이 키가 눌려 있나?"를 매 프레임 확인합니다. 이동처럼 계속 유지되는 입력에 씁니다.

gdscript
func _physics_process(delta: float) -> void:
	if Input.is_action_pressed("move_right"):
		position.x += 300.0 * delta
함수 의미
is_action_pressed("x") 지금 눌려 있는 중인가 (계속 true)
is_action_just_pressed("x") 이번 프레임에 막 눌렸는가 (1프레임만 true)
is_action_just_released("x") 이번 프레임에 막 떼었는가
get_action_strength("x") 얼마나 눌렸나 (0.0~1.0, 아날로그 스틱용)

방법 2 — 이벤트 (알림 받기)#

"키가 눌리는 사건이 발생했다"는 알림을 받습니다. 한 번만 반응하는 입력에 좋습니다.

gdscript
func _unhandled_input(event: InputEvent) -> void:
	if event.is_action_pressed("attack"):
		_shoot()
	elif event.is_action_pressed("ui_cancel"):
		_toggle_pause()
어느 쪽을 쓸까
  • 이동, 조준, 계속 누르는 것 → 폴링 (_physics_process 안에서)
  • 점프, 공격, 메뉴 열기, 대화 넘기기 → 이벤트 (_unhandled_input)

이유는 놓침 때문입니다. 프레임이 튀는 순간 아주 짧게 눌렀다 뗀 키는 폴링으로는 아예 못 볼 수 있습니다. 이벤트는 큐에 쌓이므로 놓치지 않습니다.

_input 과 _unhandled_input 의 차이

입력 이벤트는 아래 순서로 흘러갑니다. 중간에 누가 "내가 처리했다"고 표시하면 거기서 멈춥니다.

text
_input()            ← 무조건 가장 먼저. 모든 노드
  ↓
UI(Control) 노드들   ← 버튼 클릭, 텍스트 입력 등
  ↓
_shortcut_input()
  ↓
_unhandled_input()  ← UI가 안 먹은 입력만 여기 온다

게임플레이 입력은 _unhandled_input에 넣으세요. 그러면 채팅창에 글자를 치는 동안 캐릭터가 점프하는 사고가 안 생깁니다. UI가 이미 그 입력을 먹었기 때문입니다.

_input에 넣으면 UI보다 먼저 가로채므로, 정말 전역적인 것(스크린샷 키 등)에만 씁니다. 직접 처리를 끝냈음을 알리려면 get_viewport().set_input_as_handled() 를 부릅니다.

4방향 입력을 한 줄로#

상하좌우를 각각 if로 쓰면 길어집니다. Godot에 전용 함수가 있습니다.

gdscript
var dir := Input.get_vector(
	"move_left", "move_right", "move_up", "move_down")

이 한 줄이 반환하는 것:

  • 아무것도 안 누르면 (0, 0)
  • 오른쪽만 누르면 (1, 0)
  • 오른쪽+아래를 누르면 (0.707, 0.707)이미 정규화되어 있습니다
  • 아날로그 스틱이면 기울인 만큼 (0.4, 0.0) 같은 중간값
대각선이 1.41배 빨라지는 고전 버그

직접 만들면 이런 코드가 나옵니다.

gdscript
var dir := Vector2.ZERO
if Input.is_action_pressed("move_right"): dir.x += 1
if Input.is_action_pressed("move_down"):  dir.y += 1
# 오른쪽+아래 = (1, 1) → 길이가 1.414!

(1, 1) 벡터의 길이는 √2 ≈ 1.414입니다. 그래서 대각선으로 가면 40% 빨라집니다. 플레이어는 곧바로 이걸 발견하고 모든 이동을 지그재그로 하기 시작합니다.

해결dir = dir.normalized() 로 길이를 1로 맞추거나, 애초에 Input.get_vector() 를 쓰세요.

실습 — 사방으로 움직이는 캐릭터#

2강에서 만든 player.tscn의 스크립트를 아래로 교체합니다.

gdscript
extends Node2D

@export var speed := 300.0

func _physics_process(delta: float) -> void:
	var dir := Input.get_vector(
		"move_left", "move_right", "move_up", "move_down")
	position += dir * speed * delta

세 줄입니다. 이제 WASD·화살표·게임패드 전부로 움직입니다.

여기까지 확인

키보드로 사방 이동이 되고, 대각선이 더 빠르지 않으면 성공입니다. 게임패드가 있다면 꽂고 확인해 보세요. 코드를 한 줄도 안 고쳐도 됩니다.

실습 — 마우스 방향 보기#

조준을 붙여봅니다.

gdscript
func _physics_process(delta: float) -> void:
	var dir := Input.get_vector(
		"move_left", "move_right", "move_up", "move_down")
	position += dir * speed * delta

	# 마우스가 있는 쪽을 바라본다.
	look_at(get_global_mouse_position())

look_at()노드의 X축(오른쪽)이 대상을 향하도록 회전시킵니다. 그림이 위쪽을 보고 그려져 있다면 90도 어긋나 보입니다. 그때는 이렇게 보정합니다.

gdscript
rotation = (get_global_mouse_position()
		- global_position).angle() + PI / 2
터치 기기(폴드)에서의 입력

마우스가 없는 기기에서는 위 코드가 동작하지 않습니다. 두 가지 대응이 있습니다.

1) 마우스 에뮬레이션 켜기 프로젝트 설정 → Input Devices → Pointing → Emulate Mouse From Touch 터치를 마우스처럼 처리해 줍니다. 간단한 게임엔 이걸로 충분합니다.

2) 가상 조이스틱 만들기 TouchScreenButton 노드나 Control + 드래그로 직접 만듭니다. 26강 웹 배포 후 폰에서 플레이하려면 이게 필요합니다.

gdscript
# 터치 위치를 조이스틱 방향으로 바꾸는 최소 예
func _input(event: InputEvent) -> void:
	if event is InputEventScreenDrag:
		var v := event.position - _touch_origin
		_stick = v.limit_length(64.0) / 64.0
직접 바꿔보기
  1. 입력 맵에 move_right의 대체 키로 화살표 를 추가하고 확인하세요.
  2. speed를 인스펙터에서 100 / 300 / 900 으로 바꿔가며 조작감을 비교하세요.
  3. attack 액션을 만들고 _unhandled_input에서 print("탕!") 를 찍어보세요. 연타했을 때 누른 횟수만큼 정확히 찍히는지 확인하세요.
  4. 같은 것을 is_action_pressed 폴링으로 바꿔보세요. 한 번 눌렀는데 수십 번 찍힙니다. 이게 폴링과 이벤트의 차이입니다.
  • 개발자 — 입력 맵 이름은 동사로 통일하세요(jump, attack, interact). 나중에 리매핑 UI를 만들 때 이름을 그대로 화면에 보여주게 됩니다.
  • 기획자 — 조작 스펙을 쓸 때 "어떤 키"가 아니라 "어떤 액션" 으로 적으세요. "스페이스로 점프" 대신 "점프 — 기본 키 스페이스, 패드 A". 키는 바뀌지만 액션은 안 바뀝니다.
  • 디자이너 — 조작 안내 UI는 연결된 장치에 따라 아이콘이 바뀌어야 합니다. 키보드/플스/엑박 3종 아이콘 세트를 초기에 준비해두면 나중에 급하지 않습니다.
자주 나는 오류

"액션이 없다는 오류가 난다"The InputMap action "xxx" doesn't exist. 프로젝트 설정에서 오타를 확인하세요. 액션 이름은 대소문자를 구분합니다.

"키를 눌러도 반응이 없다" → ① 게임 창에 포커스가 없음, ② 액션에 키가 등록 안 됨, ③ UI 노드가 입력을 먹고 있음(_unhandled_input까지 안 옴).

"게임패드 스틱을 살짝 건드려도 캐릭터가 움직인다" → 스틱 데드존 문제입니다. 입력 맵의 각 조이패드 축 항목에서 Deadzone 값을 0.2~0.3 정도로 올리세요.

여기까지 확인
  • 입력 맵에 액션을 만들고 여러 장치를 묶었다
  • 폴링과 이벤트를 언제 쓰는지 구분한다
  • Input.get_vector()로 대각선 문제를 피했다
  • _unhandled_input을 쓰는 이유를 안다

다음 강의 — 방금 코드를 _physics_process에 넣었습니다. 왜 _process가 아니었을까요? 두 개의 시계를 제대로 봅니다.