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

10강가속·감속·중력 — 조작감 만들기

"무겁다/가볍다"를 수치로 번역한다.

이 강의 목차 (5)
  1. 조작감은 취향이 아니라 설계다
  2. 가속·감속 구현
  3. 점프를 제대로 만들기
  4. 억울함을 없애는 두 가지 장치
  5. 실습 — 세 가지 캐릭터를 나란히
이번 강의의 목표
  • "무겁다 / 가볍다 / 미끄럽다"를 수치로 번역한다.
  • 가속·감속을 프레임에 안전하게 구현한다.
  • 코요테 타임점프 버퍼로 "억울함"을 없앤다.

조작감은 취향이 아니라 설계다#

9강의 코드는 이랬습니다.

gdscript
velocity = dir * speed

키를 누르면 즉시 최고 속도, 떼면 즉시 정지입니다. 반응은 가장 빠르지만, 캐릭터가 종이 인형처럼 느껴집니다.

여기에 세 개의 손잡이를 달면 거의 모든 조작감을 만들 수 있습니다.

손잡이 의미 크면
max_speed 최고 속도 (px/s) 빠르다
accel 가속도 (px/s²) 즉각 반응한다
decel 감속도 (px/s²) 칼같이 선다
조합이 만드는 캐릭터 성격
성격 accel decel
칼같은 조작 매우 큼 매우 큼 슈팅, 퍼즐, 격투
묵직함 작음 갑옷 입은 전사
미끄러움 작음 얼음판, 우주선
굼뜸 작음 작음 탱크, 대형 몬스터

"무겁다"는 대개 accel이 작다는 뜻이고, "미끄럽다"는 decel이 작다는 뜻입니다. 두 개는 다른 이야기입니다.

가속·감속 구현#

gdscript
extends CharacterBody2D

@export var max_speed := 320.0
@export var accel := 2000.0    # 초당 속도 증가량
@export var decel := 2600.0    # 초당 속도 감소량

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

	if dir != Vector2.ZERO:
		# 목표 속도를 향해 accel 만큼씩 다가간다.
		velocity = velocity.move_toward(
			dir * max_speed, accel * delta)
	else:
		# 입력이 없으면 0을 향해 decel 만큼씩.
		velocity = velocity.move_toward(
			Vector2.ZERO, decel * delta)

	move_and_slide()
move_toward — 프레임에 안전한 접근

a.move_toward(b, step)a에서 b 쪽으로 정확히 step 만큼 이동한 값을 줍니다. b를 넘어가지 않고 딱 멈춥니다.

accel * delta 를 step으로 주면 "초당 accel 픽셀씩 빨라진다"가 되어 프레임이 어떻든 같은 곡선이 나옵니다. 4강에서 본 *= 0.9 의 함정이 없습니다.

move_toward vs lerp — 어떤 곡선인가
gdscript
velocity = velocity.move_toward(target, accel * delta)  # 직선
velocity = velocity.lerp(target, 0.2)  # 지수 곡선
  • move_toward — 일정한 비율로 다가감. 정확히 도착하고, 도착 시점을 계산할 수 있음.
  • lerp — 남은 거리의 일정 비율씩. 처음엔 빠르고 갈수록 느려짐. 영원히 도착 안 함(무한히 가까워질 뿐).

게임플레이 수치는 move_toward가 예측 가능해서 좋습니다. 카메라나 UI처럼 "부드럽게만 따라오면 되는" 것은 lerp가 자연스럽습니다.

단, lerp(target, 0.2) 는 프레임 의존적입니다. 프레임에 안전한 형태는 이렇습니다.

gdscript
var t := 1.0 - exp(-speed_factor * delta)
velocity = velocity.lerp(target, t)
감속 3종 비교

decel 값을 바꿔가며 키를 뗐을 때 얼마나 미끄러지는지 보세요.

decel 320px/s에서 멈추기까지
6000 0.05초 (거의 즉시)
2600 0.12초
800 0.4초 (미끄럼)
200 1.6초 (빙판)

멈추는 시간 = max_speed / decel 입니다. 기획서에 이 값을 쓰면 개발자와 바로 통합니다.

점프를 제대로 만들기#

횡스크롤에서 점프는 조작감의 90%입니다.

기획 수치에서 물리 수치 뽑아내기#

기획자가 말하는 건 대개 이겁니다.

"점프 높이는 캐릭터 키의 3배(=96픽셀), 올라가는 데 0.35초"

이걸 중력과 점프력으로 바꾸는 공식이 있습니다.

gdscript
@export var jump_height := 96.0  # 최고점 높이(px)
@export var jump_time_up := 0.35  # 올라가는 시간(s)
@export var jump_time_down := 0.28  # 내려오는 시간(s)

var _grav_up: float
var _grav_down: float
var _jump_force: float

func _ready() -> void:
	# h = (g * t^2) / 2  를 풀면 g = 2h / t^2
	_grav_up = 2.0 * jump_height / pow(jump_time_up, 2)
	_grav_down = 2.0 * jump_height / pow(jump_time_down, 2)
	_jump_force = _grav_up * jump_time_up
올라갈 때와 내려올 때 중력을 다르게

거의 모든 2D 액션 게임이 이렇게 합니다. 내려올 때 중력을 1.5~2배로 주면 훨씬 경쾌합니다.

현실 물리와는 다르지만, 사람은 "떠 있는 시간이 짧고 착지가 빠른" 쪽을 기분 좋게 느낍니다. 마리오도, 셀레스트도 이렇게 되어 있습니다.

가변 점프 — 짧게 누르면 낮게#

gdscript
if Input.is_action_just_released("jump") and velocity.y < 0.0:
	# 상승 중에 버튼을 떼면 위로 가는 속도를 반으로 깎는다.
	velocity.y *= 0.45

이 두 줄로 "톡 누르면 낮은 점프, 꾹 누르면 높은 점프"가 됩니다.

억울함을 없애는 두 가지 장치#

이 둘은 플레이어가 존재를 눈치채지 못하지만, 없으면 반드시 불평이 나오는 기능입니다.

코요테 타임 (Coyote Time)

발판에서 떨어진 직후 아주 짧은 시간(0.1초쯤) 동안은 점프를 허용합니다.

이름은 만화 〈로드러너〉의 코요테가 절벽 밖으로 나가고도 잠시 공중에 떠 있다가 떨어지는 장면에서 왔습니다.

없으면 — "분명 발판 위에서 눌렀는데 점프가 안 됐어요" 있으면 — 아무도 아무 말도 안 함

점프 버퍼 (Jump Buffer)

착지 직전에 미리 누른 점프를 기억했다가, 착지하는 순간 발동시킵니다.

없으면 — 빠르게 연속 점프할 때 한 번씩 씹힘 있으면 — 리듬대로 계속 뛰어짐

gdscript
@export var coyote_time := 0.10
@export var buffer_time := 0.12

var _coyote := 0.0    # 남은 코요테 시간
var _buffer := 0.0    # 남은 버퍼 시간

func _physics_process(delta: float) -> void:
	# ── 타이머 감소 ──────────────
	_coyote = maxf(_coyote - delta, 0.0)
	_buffer = maxf(_buffer - delta, 0.0)

	if is_on_floor():
		_coyote = coyote_time  # 땅에 있으면 계속 충전
	if Input.is_action_just_pressed("jump"):
		_buffer = buffer_time  # 눌렀다는 사실을 기억

	# ── 중력 ──────────────────
	var g := _grav_up if velocity.y < 0.0 else _grav_down
	velocity.y += g * delta

	# ── 점프 성립 조건 ─────────────
	if _buffer > 0.0 and _coyote > 0.0:
		velocity.y = -_jump_force
		_buffer = 0.0
		_coyote = 0.0              # 중복 발동 방지

	if Input.is_action_just_released("jump") \
			and velocity.y < 0.0:
		velocity.y *= 0.45

	# ── 좌우 ──────────────────
	var h := Input.get_axis("move_left", "move_right")
	var target := h * max_speed
	var rate := accel if h != 0.0 else decel
	velocity.x = move_toward(velocity.x, target, rate * delta)

	move_and_slide()
코요테와 버퍼는 반드시 0으로 리셋한다

점프한 뒤 _coyote를 0으로 안 만들면 공중에서 한 번 더 뛸 수 있습니다. _buffer를 0으로 안 만들면 착지할 때마다 자동으로 튀어 오릅니다.

실습 — 세 가지 캐릭터를 나란히#

같은 코드에 값만 바꿔 세 캐릭터를 만들고 직접 비교합니다. @export로 빼두었으므로 인스펙터에서 숫자만 바꾸면 됩니다.

날렵함 표준 묵직함
max_speed 380 320 240
accel 4000 2000 700
decel 5000 2600 900
jump_height 80 96 120
jump_time_up 0.28 0.35 0.5
직접 바꿔보기
  1. 세 세팅을 차례로 넣고 같은 코스를 통과해 보세요. 느낌이 얼마나 다릅니까?
  2. coyote_time을 0으로 만들고 발판 끝에서 뛰어보세요. 몇 번이나 실패합니까?
  3. buffer_time을 0으로 만들고 연속 점프를 해보세요.
  4. 내려올 때 중력(jump_time_down)을 올라갈 때와 같게 만들어보세요. 답답해집니까?
  5. 가장 마음에 드는 값을 찾아 기록해 두세요. 캡스톤에서 씁니다.
실무에서 조작감을 튜닝하는 순서
  1. 최고 속도 부터 정한다 (화면을 가로지르는 데 몇 초?)
  2. 가속 — 키를 눌러 최고 속도에 닿는 시간 (max_speed / accel)
  3. 감속 — 키를 떼고 멈추는 시간 (max_speed / decel)
  4. 점프 높이와 체공 시간
  5. 코요테/버퍼 같은 보정 장치
  6. 마지막에 연출(17강) — 착지 먼지, 화면 흔들림, 스쿼시

순서를 지키는 게 중요합니다. 연출부터 붙이면 "이펙트 때문에 좋아 보이는 건지 조작이 좋은 건지" 구분이 안 되어 튜닝이 불가능해집니다.

  • 개발자 — 조작 관련 수치는 전부 @export로 빼세요. 튜닝은 코드 수정이 아니라 숫자 조정이어야 합니다. 재컴파일 없이 바꿀 수 있어야 100번 시도해 볼 수 있습니다.
  • 기획자 — 조작감 피드백은 형용사 대신 시간과 거리로 쓰세요. "좀 더 묵직하게" → "최고 속도까지 0.1초 → 0.25초로", "멈추는 데 0.05초 → 0.2초로". 이렇게 쓰면 한 번에 원하는 결과가 나옵니다.
  • 디자이너 — 조작감은 그림으로도 만듭니다. 가속 중에 캐릭터를 살짝 앞으로 기울이고, 급정지에 스쿼시를 넣으면 같은 수치인데도 더 묵직하게 느껴집니다. 17강에서 다룹니다.
자주 나는 오류

"공중에서 두 번 뛴다" → 점프 후 _coyote = 0.0 을 안 했습니다.

"착지하면 저절로 튄다" → 점프 후 _buffer = 0.0 을 안 했습니다.

"점프 높이가 프레임마다 다르다" → 점프 초기 속도에 delta를 곱했습니다. 순간 값에는 delta를 곱하지 않습니다(4강).

"경사면에서 미끄러져 내려간다"CharacterBody2DFloor Stop On Slope 를 켜세요. 움직이는 발판이 있으면 Floor Snap Length 도 함께 조정합니다.

여기까지 확인
  • max_speed / accel / decel 세 손잡이의 역할을 안다
  • move_toward가 프레임에 안전한 이유를 안다
  • 점프 높이·시간에서 중력과 점프력을 계산할 수 있다
  • 코요테 타임과 점프 버퍼를 넣었다
  • 내 취향의 수치 세트를 기록했다

P2 정리 — 조작되는 캐릭터가 생겼습니다. 다음 강의 — P3로 갑니다. 이제 때리고 맞습니다. 충돌 레이어와 마스크부터.