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

9강CharacterBody2D로 캐릭터 움직이기

물리 바디 4형제와 move_and_slide.

이 강의 목차 (6)
  1. 물리 바디 4형제
  2. CharacterBody2D의 구조
  3. move_and_slide() — 딱 한 줄이 해주는 일
  4. "미끄러진다"는 게 무슨 뜻인가
  5. 중력과 바닥 판정 (횡스크롤)
  6. 실습 — 벽이 있는 방 만들기
이번 강의의 목표
  • 물리 바디 4형제의 역할을 구분한다.
  • CharacterBody2D + move_and_slide() 로 벽에 막히는 캐릭터를 만든다.
  • 벽에 부딪혔을 때 미끄러지는 이유를 안다.

물리 바디 4형제#

Godot 2D의 물리 노드는 넷입니다. 성격이 완전히 다릅니다.

노드 움직이나 밀리나 누가 조종하나 대표 용도
StaticBody2D 아무도 벽, 바닥, 지형
RigidBody2D 물리 엔진 상자, 공, 파편
CharacterBody2D 내 코드 플레이어, 적
Area2D 내 코드 감지 영역, 함정, 아이템
핵심 구분 — 누가 위치를 정하는가
  • RigidBody2D — "던져 놓으면 알아서 굴러간다." 중력·마찰·반발을 물리 엔진이 계산합니다. 내가 position을 직접 대입하면 싸웁니다.
  • CharacterBody2D — "내가 매 프레임 어디로 갈지 정한다." 물리 엔진은 "거기 가려는데 벽이 있다" 만 알려줍니다. 중력도 내가 직접 더합니다.

플레이어는 왜 RigidBody가 아닐까요? 폭탄에 맞으면 날아가지만, 걸을 때는 물리 법칙을 무시하고 즉시 멈춰야 하기 때문입니다. "현실적인 물리"와 "기분 좋은 조작감"은 대개 반대입니다.

그럼 RigidBody2D는 언제 쓰나
  • 굴러다니는 통, 무너지는 상자 더미, 흩어지는 파편
  • 물리 퍼즐(앵그리버드 같은)
  • 밀 수 있는 짐

플레이어와 상호작용시킬 때는 CharacterBody2D 쪽에서 힘을 줍니다.

gdscript
for i in get_slide_collision_count():
	var c := get_slide_collision(i)
	var body := c.get_collider()
	if body is RigidBody2D:
		body.apply_central_impulse(-c.get_normal() * 120.0)

CharacterBody2D의 구조#

text
Player                (CharacterBody2D)
├─ Sprite             (Sprite2D)
└─ CollisionShape2D   ← 없으면 아무 일도 안 일어난다
콜라이더가 없으면 물리 바디가 아니다

CharacterBody2D를 만들어 놓고 CollisionShape2D를 안 붙이면 아무것도 안 부딪힙니다. 에디터에 노란 경고 삼각형이 뜹니다.

그리고 그림과 콜라이더는 아무 관계가 없습니다. 스프라이트를 2배로 키워도 콜라이더는 그대로입니다. 11강에서 자세히 다룹니다.

CollisionShape2D를 선택하고 인스펙터의 Shape 에서 도형을 고릅니다.

도형 특징
RectangleShape2D 사각형. 가장 흔하고 가장 빠름
CircleShape2D 원. 모서리에 안 걸려서 이동이 부드러움
CapsuleShape2D 알약. 캐릭터에 가장 많이 씀
ConvexPolygonShape2D 볼록 다각형
ConcavePolygonShape2D 오목 가능. 정적 지형에만
캐릭터에는 캡슐이 좋다

사각형 콜라이더는 계단이나 바닥 이음새의 미세한 턱에 모서리가 걸립니다. 캡슐은 아래가 둥글어서 자연스럽게 넘어갑니다. 탑다운이면 원, 횡스크롤이면 캡슐이 무난한 기본값입니다.

move_and_slide() — 딱 한 줄이 해주는 일#

gdscript
extends CharacterBody2D

@export var speed := 300.0

func _physics_process(_delta: float) -> void:
	var dir := Input.get_vector(
		"move_left", "move_right", "move_up", "move_down")
	velocity = dir * speed
	move_and_slide()
move_and_slide()가 하는 일
  1. velocity 를 보고 이번 물리 스텝에 갈 거리를 계산한다 (delta를 내부에서 곱해준다)
  2. 그 방향으로 움직여 본다
  3. 뭔가에 부딪히면 거기서 멈추지 않고, 벽을 따라 미끄러진다
  4. 실제로 이동한 결과를 velocity 에 다시 반영한다
  5. 부딪힌 목록을 기록한다 (get_slide_collision() 으로 조회)
move_and_slide()에는 delta를 곱하지 않는다
gdscript
velocity = dir * speed  # ○ 초당 속도를 그대로
move_and_slide()

velocity = dir * speed * delta  # ✕ 두 번 곱해져 엄청 느려진다
move_and_slide()

velocity초당 픽셀 단위입니다. delta는 함수가 알아서 곱합니다. Godot 3에서 4로 넘어오며 바뀐 부분이라 옛날 예제를 보면 헷갈립니다.

"미끄러진다"는 게 무슨 뜻인가#

벽에 45도로 부딪혔다고 해봅시다.

  • 멈추기(move_and_collide) — 벽에 닿는 순간 완전히 정지. 벽을 문지르며 걸을 수 없음
  • 미끄러지기(move_and_slide) — 벽에 수직인 성분만 제거하고, 벽을 따라가는 성분은 유지
text
      벽
      │
 →→→→→│      멈추기: 여기서 끝
   ↘  │
     ↓│      미끄러지기: 벽을 따라 아래로 계속
     ↓│

이 덕분에 벽에 비스듬히 붙어서도 부드럽게 이동합니다. 대부분의 게임은 이 동작을 원합니다. 그래서 이름이 slide입니다.

move_and_collide 를 쓰는 경우

"미끄러지면 안 되는" 것들에 씁니다.

  • 벽에 닿으면 터지는 총알
  • 벽에 닿으면 튕겨 나오는 공 (반사각을 직접 계산)
gdscript
var col := move_and_collide(velocity * delta)
if col:
	# 벽 법선을 기준으로 반사
	velocity = velocity.bounce(col.get_normal())

move_and_collidedelta를 직접 곱해야 합니다. (move_and_slide 와 반대)

중력과 바닥 판정 (횡스크롤)#

탑다운이면 여기까지로 충분합니다. 횡스크롤이면 중력이 필요합니다.

gdscript
extends CharacterBody2D

@export var speed := 300.0
@export var jump_force := 420.0
@export var gravity := 1200.0

func _physics_process(delta: float) -> void:
	# 1) 중력 — 공중에 있을 때만 누적
	if not is_on_floor():
		velocity.y += gravity * delta

	# 2) 점프 — 바닥에 있을 때만
	if Input.is_action_just_pressed("jump") and is_on_floor():
		velocity.y = -jump_force   # 위는 음수(5강)

	# 3) 좌우 이동
	var h := Input.get_axis("move_left", "move_right")
	velocity.x = h * speed

	move_and_slide()
is_on_floor()는 어떻게 판단하나

move_and_slide() 가 부딪힌 면들의 법선(수직 방향) 을 보고, 그 각도가 Floor Max Angle(기본 45도) 안쪽이면 "바닥"으로 칩니다.

CharacterBody2D 인스펙터의 Motion Mode 도 여기 관여합니다.

  • Grounded — 바닥/벽/천장을 구분한다. 횡스크롤·플랫포머용 (기본값)
  • Floating — 위아래 개념이 없다. 탑다운은 이걸로 바꾸세요

관련 함수들:

gdscript
is_on_floor()      # 바닥에 서 있나
is_on_wall()       # 벽에 붙어 있나
is_on_ceiling()    # 천장에 머리를 박았나
get_floor_normal() # 바닥의 기울기
탑다운인데 캐릭터가 벽에 붙어서 안 움직인다

Motion ModeGrounded로 되어 있으면 Godot이 위쪽을 "하늘"로 간주해 이상한 판정을 합니다. 탑다운은 반드시 Floating 으로.

실습 — 벽이 있는 방 만들기#

1) 벽 만들기#

  1. main.tscnStaticBody2D 추가, 이름 Wall
  2. 자식으로 CollisionShape2D 추가 → ShapeRectangleShape2D
  3. 뷰포트에서 주황색 핸들을 끌어 크기 조절
  4. 눈에 보이게 하려면 ColorRectSprite2D를 자식으로 하나 더 붙입니다

같은 방식으로 벽을 3~4개 만들어 방을 만듭니다. (Wall을 선택하고 Ctrl+D로 복제하면 빠릅니다)

2) 플레이어를 CharacterBody2D로 바꾸기#

기존 player.tscn 루트가 Node2D라면 우클릭 → 타입 변경(Change Type)CharacterBody2D 를 고릅니다. 그리고 CollisionShape2D를 자식으로 추가합니다.

3) 스크립트#

gdscript
extends CharacterBody2D

@export var speed := 300.0

func _physics_process(_delta: float) -> void:
	var dir := Input.get_vector(
		"move_left", "move_right", "move_up", "move_down")
	velocity = dir * speed
	move_and_slide()

	# 부딪힌 것들을 확인해 본다.
	for i in get_slide_collision_count():
		var c := get_slide_collision(i)
		print("부딪힘: ", c.get_collider().name,
			" 법선 ", c.get_normal())

Motion ModeFloating으로 바꾸는 것을 잊지 마세요.

여기까지 확인

캐릭터가 벽을 통과하지 않고, 벽에 비스듬히 밀면 벽을 따라 미끄러지면 성공입니다. 출력 패널에 부딪힌 벽 이름이 찍힙니다.

직접 바꿔보기
  1. 디버그 → 충돌 도형 표시(Visible Collision Shapes) 를 켜고 실행하세요. 파란 윤곽선이 실제 판정 영역입니다. 그림과 다른 게 보이나요?
  2. 콜라이더를 사각형 → 캡슐 → 원으로 바꿔가며 벽 모서리에 비볐을 때 느낌을 비교하세요.
  3. move_and_slide()move_and_collide(velocity * delta) 로 바꿔보세요. 벽에 닿으면 어떻게 됩니까?
  4. 벽을 StaticBody2D 대신 RigidBody2D로 만들고 밀어보세요.
  • 개발자CharacterBody2D는 물리 엔진이 밀어주지 않습니다. 넉백·바람·컨베이어 같은 외력은 전부 직접 velocity에 더해야 합니다.
  • 기획자 — "적이 플레이어를 밀어냈으면 좋겠어요"는 자동으로 되는 게 아니라 밀어내는 코드를 새로 만드는 일입니다. 밀림 세기·지속시간을 함께 정해주세요.
  • 디자이너 — 콜라이더는 그림보다 약간 작게 잡는 게 대부분 기분이 좋습니다. 특히 플레이어의 피격 판정은 보이는 것보다 작아야 "억울하게 맞았다"가 줄어듭니다. 반대로 플레이어의 공격 판정은 보이는 것보다 커야 시원합니다. (11강)
자주 나는 오류

"캐릭터가 벽을 그냥 통과한다" → ① CollisionShape2DShape이 비어 있음, ② 레이어/마스크가 안 맞음(11강), ③ position을 직접 대입하고 있음.

"캐릭터가 엄청 느리다"velocity에 delta를 곱했습니다. 빼세요.

"바닥에 서 있는데 is_on_floor()가 false다"Motion ModeFloating이면 바닥 개념이 없습니다. Grounded로 바꾸세요. 또는 중력을 안 더해서 바닥을 누르고 있지 않은 상태일 수 있습니다. move_and_slide() 다음에 호출해야 값이 갱신됩니다.

"공중에서 무한 점프가 된다"is_on_floor() 검사를 빠뜨렸습니다.

여기까지 확인
  • 물리 바디 4형제를 구분한다
  • move_and_slide()에 delta를 곱하지 않는다
  • 미끄러짐이 무엇인지 안다
  • 탑다운은 Motion Mode를 Floating으로 둔다
  • 충돌 도형 표시를 켜서 실제 판정을 봤다

다음 강의 — 이제 움직이긴 합니다. 그런데 기분이 좋지는 않습니다. 가속·감속·코요테 타임으로 조작감을 만듭니다.