14강카메라2D
따라가기, 스무딩, 리미트, 데드존.
이 강의 목차 (9)
Camera2D로 캐릭터를 따라가게 만든다.- 스무딩·리미트·데드존의 차이를 안다.
- 카메라가 게임 느낌을 얼마나 바꾸는지 체감한다.
카메라는 "무엇을 보여줄지"를 정하는 연출 장치다#
지금까지 화면은 고정되어 있었습니다. 캐릭터가 화면 밖으로 나가면 안 보였죠.
Camera2D 하나를 붙이면 화면이 캐릭터를 따라옵니다.
그런데 그냥 따라가기만 하면 멀미가 납니다. 카메라는 생각보다 섬세한 장치입니다.
카메라는 배우가 아니라 촬영감독입니다. 초보 감독은 배우를 정확히 화면 중앙에 놓고 딱 붙어 따라다닙니다. 보는 사람은 어지럽습니다.
좋은 감독은
- 배우가 조금 움직여도 바로 안 따라갑니다 (데드존)
- 따라갈 때도 부드럽게 밀립니다 (스무딩)
- 배우가 가는 방향을 미리 조금 보여줍니다 (룩어헤드)
- 세트장 밖은 안 보여줍니다 (리미트)
가장 단순한 형태#
Player 아래에 Camera2D를 자식으로 붙이기만 하면 끝입니다.
Player (CharacterBody2D)
├─ Sprite2D
├─ CollisionShape2D
└─ Camera2D부모를 따라가는 5강의 규칙 덕분에 코드 한 줄 없이 카메라가 캐릭터를 따라옵니다.
이 방식은 카메라가 캐릭터에 완전히 고정됩니다. 그래서 나중에 이런 걸 못 합니다.
- 컷신에서 다른 곳을 비추기
- 두 캐릭터의 중간을 비추기
- 화면 흔들림을 캐릭터와 분리하기
권장 — 카메라를 씬 루트 아래 독립 노드로 두고 코드로 따라가게 만드세요.
스무딩 — 부드럽게 따라가기#
Camera2D 인스펙터에서:
| 항목 | 의미 |
|---|---|
Position Smoothing → Enabled |
스무딩 켜기 |
Position Smoothing → Speed |
따라붙는 속도. 작을수록 느긋함 |
Rotation Smoothing |
회전도 부드럽게 |
| Speed | 느낌 |
|---|---|
| 0 (끔) | 캐릭터에 딱 붙음. 픽셀 단위로 정확하지만 딱딱하고 멀미가 남 |
| 2 | 아주 느긋함. 액션에는 답답, 탐험 게임에는 좋음 |
| 5 | 대부분의 게임에 무난한 기본값 |
| 15 | 거의 붙어 있지만 급정지 때만 살짝 밀림 |
직접 5분만 바꿔가며 움직여 보세요. 숫자 하나로 게임 장르가 바뀌는 느낌이 납니다.
리미트 — 화면 밖을 안 보여주기#
스테이지 가장자리에 가면 카메라가 더 나가지 않게 막습니다.
| 항목 | 값 |
|---|---|
Limit → Left / Top / Right / Bottom |
픽셀 좌표 |
Limit → Smoothed |
경계에 닿을 때 부드럽게 |
# 코드로 스테이지 크기에 맞춰 설정하기
func setup_limits(rect: Rect2) -> void:
limit_left = int(rect.position.x)
limit_top = int(rect.position.y)
limit_right = int(rect.end.x)
limit_bottom = int(rect.end.y)플레이어가 맵 구석에 서면 화면의 절반이 아무것도 없는 배경이 됩니다. "게임이 미완성처럼 보이는" 가장 흔한 원인입니다. 스테이지마다 반드시 설정하세요.
데드존 — 조금 움직여도 안 따라가기#
캐릭터가 화면 중앙의 작은 사각형 안에 있는 동안은 카메라가 가만히 있고, 그 밖으로 나가면 그때부터 따라가는 방식입니다.
플랫포머에서 특히 중요합니다. 점프할 때마다 화면이 위아래로 출렁이면 멀미가 납니다. 데드존을 세로로 넉넉히 잡으면 점프해도 화면이 안 움직입니다.
Godot의 Camera2D에는 데드존이 내장돼 있지 않아 직접 만듭니다.
extends Camera2D
@export var target_path: NodePath
@export var dead_zone := Vector2(60, 90)
@export var follow_speed := 6.0
@export var look_ahead := 40.0
var _target: Node2D
var _desired: Vector2
func _ready() -> void:
_target = get_node_or_null(target_path)
if _target:
_desired = _target.global_position
global_position = _desired
func _process(delta: float) -> void:
if _target == null:
return
var t := _target.global_position
# ── 데드존: 상자를 벗어난 만큼만 목표를 민다 ──
var d := t - _desired
if absf(d.x) > dead_zone.x:
_desired.x += d.x - signf(d.x) * dead_zone.x
if absf(d.y) > dead_zone.y:
_desired.y += d.y - signf(d.y) * dead_zone.y
# ── 룩어헤드: 가는 방향을 조금 미리 보여준다 ──
var vel: Vector2 = _target.get("velocity")
if vel != null and vel.length() > 10.0:
_desired += vel.normalized() * look_ahead * delta * 4.0
# ── 스무딩: 프레임에 안전한 형태(4강) ──
var t_amount := 1.0 - exp(-follow_speed * delta)
global_position = global_position.lerp(_desired, t_amount)카메라는 게임 규칙이 아니라 보기입니다(8강 기준). 그리고 렌더 프레임마다 갱신해야 부드럽습니다.
_physics_process에 넣으면 144Hz 모니터에서 카메라가 60번만 갱신되어 떨려 보입니다.
줌#
zoom = Vector2(2, 2) # 2배 확대 (더 좁게 보임)
zoom = Vector2(0.5, 0.5) # 축소 (더 넓게 보임)Godot 4에서 zoom 값이 클수록 확대됩니다(가까이 봄).
Godot 3에서는 반대였습니다. 옛날 자료를 볼 때 주의하세요.
부드러운 줌은 Tween으로 만듭니다.
func zoom_to(value: float, time := 0.4) -> void:
var tw := create_tween()
tw.set_ease(Tween.EASE_OUT)
tw.set_trans(Tween.TRANS_CUBIC)
tw.tween_property(self, "zoom",
Vector2(value, value), time)화면 흔들림 (Screen Shake)#
17강에서 본격적으로 다루지만, 카메라 기능이므로 여기서 기본형을 만듭니다.
var _shake := 0.0
var _shake_decay := 5.0
func shake(amount: float) -> void:
_shake = maxf(_shake, amount)
func _process(delta: float) -> void:
# ... 위의 따라가기 코드 ...
if _shake > 0.0:
_shake = maxf(_shake - _shake_decay * delta, 0.0)
offset = Vector2(
randf_range(-1, 1), randf_range(-1, 1)
) * _shake
else:
offset = Vector2.ZEROglobal_position을 흔들면 따라가기 계산과 싸웁니다.
offset은 카메라 위치와 별개로 화면만 밀어주는 값이라 안전합니다.
"기본 위치는 로직이, 흔들림은 offset이" — 역할을 나누는 게 핵심입니다.
여러 대상을 한 화면에 담기#
협동 게임이나 보스전에서 자주 필요합니다.
func _fit_targets(targets: Array[Node2D]) -> void:
if targets.is_empty():
return
var r := Rect2(targets[0].global_position, Vector2.ZERO)
for t in targets:
r = r.expand(t.global_position)
# 중심으로 이동
_desired = r.get_center()
# 다 담기도록 줌 조절
var view := get_viewport_rect().size
var pad := 240.0
var need := (r.size + Vector2(pad, pad)) / view
var z := 1.0 / maxf(maxf(need.x, need.y), 0.001)
zoom = zoom.lerp(Vector2(z, z), 0.06)카메라가 여러 개일 때
씬에 Camera2D가 여러 개 있으면 enabled가 켜진 것 중 마지막이 쓰입니다.
명시적으로 바꾸려면 make_current() 를 부릅니다.
$CutsceneCamera.make_current()
await ...
$PlayerCamera.make_current()컷신, 미니맵, 보스 등장 연출에서 카메라를 갈아끼우는 방식이 자주 쓰입니다.
실습 — 카메라 성격 3종 비교#
같은 스테이지를 세 세팅으로 돌아보세요.
| 액션 | 탐험 | 퍼즐 | |
|---|---|---|---|
| follow_speed | 12 | 3 | 999 (즉시) |
| dead_zone | (20, 40) | (100, 140) | (0, 0) |
| look_ahead | 60 | 20 | 0 |
| zoom | 1.2 | 0.8 | 1.0 |
- 세 세팅으로 각각 1분씩 플레이해 보세요. 같은 게임이 다른 게임처럼 느껴집니까?
dead_zone.y를 0으로 만들고 점프해 보세요. 화면이 출렁여서 어지럽습니까?- 리미트를 설정하지 않고 맵 구석으로 가보세요. 무엇이 보입니까?
look_ahead를 200으로 극단적으로 올려보세요. 어떤 문제가 생깁니까? (방향을 바꿀 때마다 화면이 홱 뒤집힙니다 — 그래서 룩어헤드도 부드럽게 보간해야 합니다)
- 개발자 — 카메라는 씬 루트 아래 독립 노드 + 코드로 따라가기로 만드세요. 나중에 컷신·보스전·화면 흔들림을 붙일 때 구조를 안 갈아엎습니다.
- 기획자 — "화면이 어지럽다"는 피드백은 대개 데드존 부재 + 스무딩 부족입니다.
카메라 스펙에
따라가기 속도 / 데드존 크기 / 룩어헤드 / 기본 줌네 항목을 넣어주세요. - 디자이너 — 카메라 줌 값은 아트 리소스 해상도를 결정합니다. 줌 1.5로 확대해서 보여줄 거라면 그림도 1.5배 해상도로 그려야 뭉개지지 않습니다. 이건 15강에서 이어집니다.
"카메라가 아무 효과가 없다"
→ Enabled 체크 확인. 또는 다른 Camera2D가 현재 카메라를 차지하고 있습니다.
"UI가 카메라를 따라 움직인다"
→ UI를 CanvasLayer에 넣으세요 (6강).
"카메라가 미세하게 떤다"
→ ① _physics_process에서 갱신 중 (→ _process로), ② 픽셀아트에서 소수점 위치 문제
(→ 아래 참고).
"픽셀아트가 카메라 이동 중에 흔들린다"
→ 카메라 위치가 소수점이라 픽셀이 반쯤 걸칩니다.
프로젝트 설정 → Rendering → 2D → Snap 2D Transforms to Pixel 을 켜거나,
카메라 위치를 round() 하세요. 15강에서 이어집니다.
- 카메라를 독립 노드로 두고 코드로 따라가게 만들었다
- 스무딩 값을 바꿔가며 느낌을 비교했다
- 리미트를 걸어 스테이지 밖이 안 보이게 했다
- 데드존이 왜 필요한지 안다
-
offset으로 화면 흔들림을 만들었다
다음 강의 — 카메라가 정해졌으니 화면 크기를 정할 차례입니다. 폴드의 커버 화면과 메인 화면처럼 비율이 완전히 다른 기기에 어떻게 대응할까요?