10강가속·감속·중력 — 조작감 만들기
"무겁다/가볍다"를 수치로 번역한다.
- "무겁다 / 가볍다 / 미끄럽다"를 수치로 번역한다.
- 가속·감속을 프레임에 안전하게 구현한다.
- 코요테 타임과 점프 버퍼로 "억울함"을 없앤다.
조작감은 취향이 아니라 설계다#
9강의 코드는 이랬습니다.
velocity = dir * speed키를 누르면 즉시 최고 속도, 떼면 즉시 정지입니다. 반응은 가장 빠르지만, 캐릭터가 종이 인형처럼 느껴집니다.
여기에 세 개의 손잡이를 달면 거의 모든 조작감을 만들 수 있습니다.
| 손잡이 | 의미 | 크면 |
|---|---|---|
max_speed |
최고 속도 (px/s) | 빠르다 |
accel |
가속도 (px/s²) | 즉각 반응한다 |
decel |
감속도 (px/s²) | 칼같이 선다 |
| 성격 | accel | decel | 예 |
|---|---|---|---|
| 칼같은 조작 | 매우 큼 | 매우 큼 | 슈팅, 퍼즐, 격투 |
| 묵직함 | 작음 | 큼 | 갑옷 입은 전사 |
| 미끄러움 | 큼 | 작음 | 얼음판, 우주선 |
| 굼뜸 | 작음 | 작음 | 탱크, 대형 몬스터 |
"무겁다"는 대개 accel이 작다는 뜻이고, "미끄럽다"는 decel이 작다는 뜻입니다. 두 개는 다른 이야기입니다.
가속·감속 구현#
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()a.move_toward(b, step) 은 a에서 b 쪽으로 정확히 step 만큼 이동한 값을 줍니다.
b를 넘어가지 않고 딱 멈춥니다.
accel * delta 를 step으로 주면 "초당 accel 픽셀씩 빨라진다"가 되어
프레임이 어떻든 같은 곡선이 나옵니다. 4강에서 본 *= 0.9 의 함정이 없습니다.
move_toward vs lerp — 어떤 곡선인가
velocity = velocity.move_toward(target, accel * delta) # 직선
velocity = velocity.lerp(target, 0.2) # 지수 곡선move_toward— 일정한 비율로 다가감. 정확히 도착하고, 도착 시점을 계산할 수 있음.lerp— 남은 거리의 일정 비율씩. 처음엔 빠르고 갈수록 느려짐. 영원히 도착 안 함(무한히 가까워질 뿐).
게임플레이 수치는 move_toward가 예측 가능해서 좋습니다.
카메라나 UI처럼 "부드럽게만 따라오면 되는" 것은 lerp가 자연스럽습니다.
단, lerp(target, 0.2) 는 프레임 의존적입니다. 프레임에 안전한 형태는 이렇습니다.
var t := 1.0 - exp(-speed_factor * delta)
velocity = velocity.lerp(target, t)decel 값을 바꿔가며 키를 뗐을 때 얼마나 미끄러지는지 보세요.
| decel | 320px/s에서 멈추기까지 |
|---|---|
| 6000 | 0.05초 (거의 즉시) |
| 2600 | 0.12초 |
| 800 | 0.4초 (미끄럼) |
| 200 | 1.6초 (빙판) |
멈추는 시간 = max_speed / decel 입니다. 기획서에 이 값을 쓰면 개발자와 바로 통합니다.
점프를 제대로 만들기#
횡스크롤에서 점프는 조작감의 90%입니다.
기획 수치에서 물리 수치 뽑아내기#
기획자가 말하는 건 대개 이겁니다.
"점프 높이는 캐릭터 키의 3배(=96픽셀), 올라가는 데 0.35초"
이걸 중력과 점프력으로 바꾸는 공식이 있습니다.
@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배로 주면 훨씬 경쾌합니다.
현실 물리와는 다르지만, 사람은 "떠 있는 시간이 짧고 착지가 빠른" 쪽을 기분 좋게 느낍니다. 마리오도, 셀레스트도 이렇게 되어 있습니다.
가변 점프 — 짧게 누르면 낮게#
if Input.is_action_just_released("jump") and velocity.y < 0.0:
# 상승 중에 버튼을 떼면 위로 가는 속도를 반으로 깎는다.
velocity.y *= 0.45이 두 줄로 "톡 누르면 낮은 점프, 꾹 누르면 높은 점프"가 됩니다.
억울함을 없애는 두 가지 장치#
이 둘은 플레이어가 존재를 눈치채지 못하지만, 없으면 반드시 불평이 나오는 기능입니다.
발판에서 떨어진 직후 아주 짧은 시간(0.1초쯤) 동안은 점프를 허용합니다.
이름은 만화 〈로드러너〉의 코요테가 절벽 밖으로 나가고도 잠시 공중에 떠 있다가 떨어지는 장면에서 왔습니다.
없으면 — "분명 발판 위에서 눌렀는데 점프가 안 됐어요" 있으면 — 아무도 아무 말도 안 함
착지 직전에 미리 누른 점프를 기억했다가, 착지하는 순간 발동시킵니다.
없으면 — 빠르게 연속 점프할 때 한 번씩 씹힘 있으면 — 리듬대로 계속 뛰어짐
@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()점프한 뒤 _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 |
- 세 세팅을 차례로 넣고 같은 코스를 통과해 보세요. 느낌이 얼마나 다릅니까?
coyote_time을 0으로 만들고 발판 끝에서 뛰어보세요. 몇 번이나 실패합니까?buffer_time을 0으로 만들고 연속 점프를 해보세요.- 내려올 때 중력(
jump_time_down)을 올라갈 때와 같게 만들어보세요. 답답해집니까? - 가장 마음에 드는 값을 찾아 기록해 두세요. 캡스톤에서 씁니다.
실무에서 조작감을 튜닝하는 순서
- 최고 속도 부터 정한다 (화면을 가로지르는 데 몇 초?)
- 가속 — 키를 눌러 최고 속도에 닿는 시간 (
max_speed / accel) - 감속 — 키를 떼고 멈추는 시간 (
max_speed / decel) - 점프 높이와 체공 시간
- 코요테/버퍼 같은 보정 장치
- 마지막에 연출(17강) — 착지 먼지, 화면 흔들림, 스쿼시
순서를 지키는 게 중요합니다. 연출부터 붙이면 "이펙트 때문에 좋아 보이는 건지 조작이 좋은 건지" 구분이 안 되어 튜닝이 불가능해집니다.
- 개발자 — 조작 관련 수치는 전부
@export로 빼세요. 튜닝은 코드 수정이 아니라 숫자 조정이어야 합니다. 재컴파일 없이 바꿀 수 있어야 100번 시도해 볼 수 있습니다. - 기획자 — 조작감 피드백은 형용사 대신 시간과 거리로 쓰세요. "좀 더 묵직하게" → "최고 속도까지 0.1초 → 0.25초로", "멈추는 데 0.05초 → 0.2초로". 이렇게 쓰면 한 번에 원하는 결과가 나옵니다.
- 디자이너 — 조작감은 그림으로도 만듭니다. 가속 중에 캐릭터를 살짝 앞으로 기울이고, 급정지에 스쿼시를 넣으면 같은 수치인데도 더 묵직하게 느껴집니다. 17강에서 다룹니다.
"공중에서 두 번 뛴다"
→ 점프 후 _coyote = 0.0 을 안 했습니다.
"착지하면 저절로 튄다"
→ 점프 후 _buffer = 0.0 을 안 했습니다.
"점프 높이가 프레임마다 다르다" → 점프 초기 속도에 delta를 곱했습니다. 순간 값에는 delta를 곱하지 않습니다(4강).
"경사면에서 미끄러져 내려간다"
→ CharacterBody2D의 Floor Stop On Slope 를 켜세요.
움직이는 발판이 있으면 Floor Snap Length 도 함께 조정합니다.
- max_speed / accel / decel 세 손잡이의 역할을 안다
-
move_toward가 프레임에 안전한 이유를 안다 - 점프 높이·시간에서 중력과 점프력을 계산할 수 있다
- 코요테 타임과 점프 버퍼를 넣었다
- 내 취향의 수치 세트를 기록했다
P2 정리 — 조작되는 캐릭터가 생겼습니다. 다음 강의 — P3로 갑니다. 이제 때리고 맞습니다. 충돌 레이어와 마스크부터.