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

7강입력 받기

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

이 강의의 Godot 예제 프로젝트
7강 예제 내려받기 godot-2d-lecture-07-input.zip · 4.0 KB

압축을 풀고 Godot 4.3 이상에서 가져오기(Import) → project.godot 선택 → F5. 씬 1개 · 스크립트 1개 · 안내는 폴더 안 README.md 에 있습니다.

이 강의 목차 (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()
어느 쪽을 쓸까

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

_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")
Input.get_vector(a,b,c,d)negative_x−x방향으로 센다positive_x+x방향으로 센다negative_y−y방향으로 센다positive_y+y방향으로 센다이름은 사람이 읽으라고 붙인 것일 뿐이다"move_left" 에 오른쪽 화살표를 등록하면 캐릭터는 왼쪽으로 간다엔진은 문자열을 읽지 않는다. 뜻을 정하는 건 인자의 자리다.
뜻을 정하는 것은 액션 이름이 아니라 인자의 자리다. 첫 번째는 빼고, 두 번째는 더한다.

이 한 줄이 반환하는 것:

  • 아무것도 안 누르면 (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() 를 쓰세요.

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

위 그림은 오른쪽(+X)이 정면입니다. 그래서 look_at() 결과와 그림 방향이 맞습니다.

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가 아니었을까요? 두 개의 시계를 제대로 봅니다.