1. 소개
Godot 4는 캐릭터 이동에 큰 변화를 도입했습니다. 기존의 KinematicBody2D와 KinematicBody3D 노드는 CharacterBody2D와 CharacterBody3D로 이름이 바뀌었고, move_and_slide() API는 완전히 재설계되었습니다. Godot 3에서 마이그레이션하든 새로 시작하든, 이 가이드는 알아야 할 모든 것을 다룹니다.
CharacterBody2D와 CharacterBody3D는 코드로 제어하는 캐릭터를 위해 설계된 물리 바디입니다. RigidBody와 달리 힘에 자동으로 반응하지 않으며, 이동을 전적으로 스크립트로 제어합니다. 그래서 플레이어 캐릭터, NPC, 그리고 정밀하고 결정론적인 이동이 필요한 모든 것에 이상적입니다.
2. Godot 3에서 달라진 점
Godot 3에서 Godot 4로 넘어가면서 캐릭터 이동과 관련해 가장 중요하게 바뀐 점은 다음과 같습니다:
-
KinematicBody2D가 이제CharacterBody2D가 되었습니다 -
KinematicBody3D가 이제CharacterBody3D가 되었습니다 -
velocity가 이제 내장 속성이 되었습니다 — 더 이상move_and_slide()에 전달하지 않습니다 -
move_and_slide()는 인자를 받지 않습니다 —velocity속성에서 직접 읽습니다 -
move_and_slide_with_snap()은 제거되었습니다 — 대신floor_snap_length속성을 사용하세요 -
is_on_floor(),is_on_wall(),is_on_ceiling()은 이전과 동일하게 동작합니다 -
get_gravity()가 Godot 4.4에 추가되어 프로젝트의 기본 중력을 Vector로 읽을 수 있게 되었습니다 — 매우 편리합니다
Godot 3에서는 move_and_slide()가 결과 속도를 반환했습니다. Godot 4에서는 bool(충돌이 발생했는지 여부)을 반환하며, 속도는 velocity 속성에서 직접 갱신됩니다.
3. 기본 2D 플랫포머 컨트롤러
이것은 표준적인 횡스크롤 플랫포머 컨트롤러입니다. 캐릭터는 좌우로 이동할 수 있고, 지면에 있을 때 점프할 수 있습니다. 이것은 새 CharacterBody2D 스크립트를 만들 때 Godot이 생성하는 템플릿입니다.
extends CharacterBody2D
const SPEED = 300.0
const JUMP_VELOCITY = -400.0
func _physics_process(delta: float) -> void:
# Apply gravity when not on floor
if not is_on_floor():
velocity += get_gravity() * delta
# Jump when on floor and jump pressed
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Horizontal movement
var direction := Input.get_axis("ui_left", "ui_right")
if direction:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
move_and_slide()
한 줄씩 살펴보기
-
extends CharacterBody2D— 스크립트가 CharacterBody2D를 상속하여velocity,move_and_slide(),is_on_floor()등에 접근할 수 있게 됩니다. -
SPEED = 300.0— 수평 이동 속도(초당 픽셀). -
JUMP_VELOCITY = -400.0— 2D에서는 Y축이 아래를 향하므로 음수입니다. 음수 값은 캐릭터를 위로 이동시킵니다. -
get_gravity() * delta— 매 프레임 프로젝트 중력을 적용합니다.get_gravity()는 아래를 향하는Vector2(예:Vector2(0, 980))를 반환합니다.delta를 곱하면 프레임 레이트에 독립적이 됩니다. -
Input.get_axis("ui_left", "ui_right")— 눌린 방향키에 따라 -1.0에서 1.0 사이의 float를 반환합니다. 아날로그 입력을 지원합니다. -
move_toward(velocity.x, 0, SPEED)— 입력이 없을 때 부드럽게 0으로 감속합니다. 세 번째 인자는 호출당 최대 변화량입니다. -
move_and_slide()— 현재velocity를 사용해 바디를 이동시키고, 충돌을 처리하고, 표면을 따라 미끄러지며,velocity를 자동으로 갱신합니다.
CharacterBody2D에는 형상(예: RectangleShape2D 또는 CapsuleShape2D)이 할당된 CollisionShape2D 자식 노드가 최소 하나 이상 필요합니다. 이것이 없으면 move_and_slide()가 어떤 충돌도 감지하지 못합니다.
4. 기본 2D 탑다운 컨트롤러
탑다운 게임(RPG, 트윈스틱 슈터 등)에서는 중력 없이 네 방향으로 이동합니다. 코드는 플랫포머보다 간단합니다.
extends CharacterBody2D
const SPEED = 200.0
func _physics_process(delta: float) -> void:
var input_direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
velocity = input_direction * SPEED
move_and_slide()
작동 방식
-
Input.get_vector()는 네 개의 입력 액션으로부터 정규화된Vector2를 반환합니다. 이는 대각선 이동을 올바르게 처리합니다 — 벡터의 길이는 항상 1.0 또는 0.0이므로 대각선 이동이 상하좌우 이동보다 빠르지 않습니다. -
탑다운 시점이므로 중력이 적용되지 않습니다.
velocity를 직접 설정하기 때문에(관성 없음) 입력이 없으면 캐릭터가 즉시 멈춥니다.
가속과 마찰 추가하기
가속과 감속으로 더 부드러운 조작감을 원한다면:
extends CharacterBody2D
const SPEED = 200.0
const ACCELERATION = 1200.0
const FRICTION = 1000.0
func _physics_process(delta: float) -> void:
var input_direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
if input_direction != Vector2.ZERO:
velocity = velocity.move_toward(input_direction * SPEED, ACCELERATION * delta)
else:
velocity = velocity.move_toward(Vector2.ZERO, FRICTION * delta)
move_and_slide()
5. 3D 3인칭 컨트롤러
3D 버전도 동일한 패턴을 따릅니다. 주요 차이점은 Vector3를 사용하고 transform.basis를 이용해 2D 입력을 3D 월드 공간 이동으로 변환한다는 점입니다.
extends CharacterBody3D
const SPEED = 5.0
const JUMP_VELOCITY = 4.5
func _physics_process(delta: float) -> void:
# Apply gravity
if not is_on_floor():
velocity += get_gravity() * delta
# Jump
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Get input and convert to 3D direction
var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
velocity.x = direction.x * SPEED
velocity.z = direction.z * SPEED
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
velocity.z = move_toward(velocity.z, 0, SPEED)
move_and_slide()
2D와의 주요 차이점
-
단위: 3D에서는 1 단위 = 1 미터입니다(관례). 따라서
SPEED = 5.0은 초당 5미터를 의미하며 — 2D 픽셀 값보다 훨씬 작은 숫자입니다. -
점프 속도가 양수: 3D에서는 Y축이 위를 향하므로(2D와 반대)
JUMP_VELOCITY = 4.5는 양수입니다. -
방향 변환:
transform.basis * Vector3(input_dir.x, 0, input_dir.y)는 2D 입력을 캐릭터가 바라보는 방향에 상대적인 3D 방향으로 변환합니다. 입력의 Y가 Z축(3D의 앞/뒤)에 매핑됩니다. -
X와 Z만 제어:
velocity.x와velocity.z를 개별적으로 설정하고,velocity.y는 중력과 점프를 위해 남겨둡니다.
get_gravity() 메서드는 Godot 4.4에서 추가되었습니다. 이전 버전에서는 2D의 경우 Vector2(0, ProjectSettings.get_setting("physics/2d/default_gravity")), 3D의 경우 Vector3(0, -ProjectSettings.get_setting("physics/3d/default_gravity"), 0)를 사용하세요.
6. 주요 속성 레퍼런스
CharacterBody2D와 CharacterBody3D는 모두 다음의 중요한 속성을 공유합니다. 인스펙터나 코드에서 조정하세요.
| 속성 | 타입 | 기본값 | 설명 |
|---|---|---|---|
velocity |
Vector2 / Vector3 | Vector2.ZERO |
캐릭터의 속도. move_and_slide()를 호출하기 전에 설정하세요. 호출 후에는 결과 속도로 갱신됩니다.
|
floor_snap_length |
float | 1.0 |
move_and_slide_with_snap()을 대체합니다. 캐릭터를 바닥에 스냅하는 거리. 0으로 설정하면 비활성화됩니다. 경사와 계단에 유용합니다.
|
up_direction |
Vector2 / Vector3 | Vector2.UP |
어느 방향이 "위"인지 정의합니다. 이에 따라 무엇이 바닥, 벽, 천장으로 간주되는지 결정됩니다. 기본값은 2D에서 (0, -1), 3D에서 (0, 1, 0)입니다.
|
floor_stop_on_slope |
bool | true |
true일 때, 정지 상태에서 캐릭터가 경사면을 미끄러져 내려가지 않습니다. 플랫포머에 필수적입니다. |
floor_max_angle |
float | 0.785 (45°) |
걸어 다닐 수 있는 최대 경사 각도(라디안). 이보다 가파른 표면은 벽으로 취급됩니다. 60도로 설정하려면 deg_to_rad(60)을 사용하세요.
|
max_slides |
int | 6 |
move_and_slide() 호출당 최대 충돌 반복 횟수. 값이 클수록 정확하지만 느립니다.
|
wall_min_slide_angle |
float | 0.262 (15°) |
벽 미끄러짐을 위한 최소 각도. 캐릭터가 거의 평행한 벽에 걸리는 것을 방지합니다. |
platform_on_leave |
PlatformOnLeave | ADD_VELOCITY |
움직이는 플랫폼을 떠날 때의 동작. ADD_VELOCITY는 운동량을 유지하고, ADD_UPWARD_VELOCITY는 위쪽 성분만 추가하며, DO_NOTHING은 플랫폼 속도를 무시합니다.
|
slide_on_ceiling |
bool | true |
true일 때, 천장에서 미끄러짐을 허용합니다. false일 때, 천장에 닿으면 수평 속도가 멈춥니다. |
7. move_and_slide() 이후의 충돌 감지
move_and_slide()를 호출한 후, 발생한 충돌을 검사할 수 있습니다. 이는 특정 콜라이더 타입에 반응하거나, 착지 시 사운드 이펙트를 재생하거나, 벽 점프 메커니즘을 구현할 때 유용합니다.
func _physics_process(delta: float) -> void:
# ... set velocity here ...
move_and_slide()
# Check all collisions from this frame
for i in get_slide_collision_count():
var collision := get_slide_collision(i)
var collider := collision.get_collider()
print("Collided with: ", collider.name)
print("Normal: ", collision.get_normal())
print("Position: ", collision.get_position())
# Practical example: bounce off enemies
for i in get_slide_collision_count():
var collision := get_slide_collision(i)
if collision.get_collider().is_in_group("enemies"):
velocity = collision.get_normal() * 300
break
유용한 KinematicCollision 메서드
get_collider()— 충돌한 객체.get_normal()— 충돌 표면의 법선. 표면에서 멀어지는 방향을 가리킵니다.get_position()— 충돌이 발생한 월드 공간상의 점.get_travel()— 충돌하기 전에 바디가 이동한 거리.get_remainder()— 적용되지 않은 남은 이동량.get_collider_velocity()— 콜라이더의 속도(움직이는 플랫폼에 유용).
8. 마이그레이션 치트 시트
Godot 3 코드를 Godot 4로 변환하기 위한 빠른 레퍼런스:
| Godot 3 | Godot 4 |
|---|---|
KinematicBody2D |
CharacterBody2D |
KinematicBody3D |
CharacterBody3D |
velocity = move_and_slide(velocity, Vector2.UP) |
velocity = ... |
move_and_slide_with_snap(vel, snap, up) |
floor_snap_length = 4.0 |
var vel = move_and_slide(...)velocity를 반환 |
move_and_slide()velocity 속성이 직접 갱신됨 |
var gravity = ProjectSettings.get("physics/2d/default_gravity") |
get_gravity() (4.4+) |
move_and_slide(..., up_direction, ...) |
up_direction = Vector2.UP (속성으로 설정) |
move_and_slide(..., stop_on_slope, ...) |
floor_stop_on_slope = true (속성으로 설정) |
Godot 3 프로젝트를 Godot 4에서 열면, 엔진이 프로젝트 변환을 제안합니다. 노드 이름을 바꾸고 스크립트 업데이트를 시도하지만, move_and_slide() 호출을 항상 올바르게 변환하지는 못합니다. 이동 코드를 직접 수정해야 할 가능성이 높습니다.
9. 흔한 실수
실수 1: move_and_slide()에 인자를 전달하기
# ERROR: move_and_slide() takes no arguments in Godot 4
velocity = move_and_slide(velocity, Vector2.UP)
# Set velocity, then call move_and_slide() with no arguments
velocity.x = direction * SPEED
move_and_slide()
실수 2: move_and_slide() 전에 velocity 설정을 잊어버리기
func _physics_process(delta: float) -> void:
move_and_slide() # velocity is Vector2.ZERO — nothing happens
실수 3: _physics_process() 대신 _process() 사용하기
move_and_slide()는 고정된 속도(기본 60Hz)로 실행되는 _physics_process() 내에서 호출해야 합니다. _process()를 사용하면 물리가 렌더링 프레임 레이트에 묶여 동작이 일관되지 않게 됩니다.
실수 4: CollisionShape를 설정하지 않기
CollisionShape 자식이 없는 CharacterBody2D/CharacterBody3D는 모든 것을 통과해 지나갑니다. 충돌 형상이 없으면 Godot이 에디터에 경고 아이콘을 표시합니다.
실수 5: 매 프레임 velocity.y를 중력으로 덮어쓰기
# This overwrites any jump velocity!
velocity.y = gravity * delta # should be +=, not =
# Gravity accumulates over time
velocity += get_gravity() * delta
실수 6: 바닥에 있을 때 중력을 적용하기
중력은 항상 if not is_on_floor()로 감싸세요. 이 검사가 없으면 바닥에 있는 동안에도 중력이 계속 누적됩니다. 캐릭터가 가장자리에서 걸어 나가면, 자연스럽게 떨어지는 대신 극단적인 속도로 급강하합니다.