4강delta — 프레임에 흔들리지 않는 시간
왜 속도는 항상 "초당"으로 적어야 하는가.
이 강의 목차 (5)
delta가 무엇을 담고 있는 숫자인지 정확히 안다.- 왜
속도 * delta로 써야 하는지 실험으로 확인한다. - 델타를 곱하면 안 되는 경우도 구분한다.
문제 상황부터 본다#
캐릭터를 오른쪽으로 움직이는 가장 순진한 코드입니다.
func _process(_delta: float) -> void:
position.x += 5"한 프레임에 5픽셀씩 간다"는 뜻입니다. 잘 돌아가는 것처럼 보입니다. 그런데 이 코드에는 심각한 문제가 있습니다.
| 상황 | 1초에 이동하는 거리 |
|---|---|
| 60 FPS 나오는 PC | 5 × 60 = 300픽셀 |
| 30 FPS로 떨어진 노트북 | 5 × 30 = 150픽셀 |
| 144Hz 게이밍 모니터 | 5 × 144 = 720픽셀 |
같은 게임인데 기기마다 캐릭터 속도가 다릅니다. 좋은 컴퓨터에서는 캐릭터가 2배 빠르고, 프레임이 떨어지면 슬로우 모션이 걸립니다.
1990년대 PC 게임들이 이 방식으로 만들어졌습니다. 그래서 요즘 컴퓨터에서 옛날 게임을 돌리면 캐릭터가 순식간에 사라지거나 게임이 즉시 끝나버리는 일이 생깁니다. 당시 PC 케이스에 있던 터보(Turbo) 버튼은 이걸 맞추려고 CPU를 일부러 느리게 만드는 버튼이었습니다.
delta — "지난 프레임 이후 흐른 시간"#
Godot이 _process(delta) 로 넘겨주는 delta는 아주 단순한 값입니다.
직전 프레임이 끝난 뒤 지금까지 실제로 흐른 시간(초)
- 60 FPS면 delta ≈ 0.0166
- 30 FPS면 delta ≈ 0.0333
- 프레임이 잠깐 튀면 delta ≈ 0.1 처럼 커진다
여기에 초당 속도를 곱하면 "이번 프레임 동안 가야 할 거리" 가 나옵니다.
const SPEED := 300.0 # 초당 300픽셀
func _process(delta: float) -> void:
position.x += SPEED * delta| 상황 | delta | 이번 프레임 이동량 | 1초 합계 |
|---|---|---|---|
| 60 FPS | 0.0166 | 5픽셀 | 300픽셀 |
| 30 FPS | 0.0333 | 10픽셀 | 300픽셀 |
| 144 FPS | 0.0069 | 2.08픽셀 | 300픽셀 |
프레임이 어떻게 흔들려도 1초에 가는 거리는 같습니다. 프레임이 떨어지면 뚝뚝 끊겨 보이지만 느려지지는 않습니다.
position.x += 5 는 "한 번 밟을 때마다 5m 간다"입니다. 몇 번 밟느냐에 따라 결과가 달라집니다.
SPEED * delta 는 "시속 60km로 간다"입니다. 도로 사정이 어떻든 1시간이면 60km입니다.
게임의 모든 속도는 '초당'으로 말해야 합니다. '프레임당'은 말이 되지 않습니다.
실습 — 나란히 놓고 비교하기#
말로 백 번 듣는 것보다 한 번 보는 게 낫습니다.
1) 씬 구성#
main.tscn에 Sprite2D를 두 개 놓고 이름을 Bad, Good으로 바꿉니다.
Bad는 y=150, Good은 y=250 정도로 위아래로 떨어뜨려 놓으세요.
2) 스크립트#
main.tscn 루트에 붙입니다.
extends Node2D
const SPEED := 300.0
@onready var bad: Sprite2D = $Bad
@onready var good: Sprite2D = $Good
func _process(delta: float) -> void:
bad.position.x += 5.0 # 프레임당 5픽셀
good.position.x += SPEED * delta # 초당 300픽셀
# 화면 밖으로 나가면 왼쪽 끝으로 되돌린다.
if bad.position.x > 1000.0:
bad.position.x = 0.0
if good.position.x > 1000.0:
good.position.x = 0.0$Bad는get_node("Bad")의 줄임입니다. 자식 노드를 이름으로 찾아옵니다.@onready는 "이 변수는 노드가 씬에 다 붙은 다음에 채워라"라는 뜻입니다.
@onready 없이 var bad := $Bad 라고 쓰면, 아직 자식이 트리에 붙기 전이라 null이 들어와
Invalid access to property on a null instance 오류가 납니다. 매우 흔한 실수입니다.
3) 프레임 제한을 걸고 관찰#
3강에서 만든 Load Level 부하 코드를 같이 켜거나, 아래 한 줄로 FPS를 직접 제한합니다.
func _ready() -> void:
Engine.max_fps = 20 # 일부러 20 FPS로 묶는다Engine.max_fps를 60 → 30 → 20 → 10 으로 바꾸며 실행하세요.- 두 스프라이트 중 어느 쪽이 화면을 가로지르는 시간이 변합니까?
Bad는 프레임이 낮을수록 느려지고,Good은 끊겨 보이지만 시간은 같습니다.max_fps = 200으로 올려보세요. 이번엔Bad가 지나치게 빨라집니다.
Good이 항상 같은 시간에 도착하는 것을 눈으로 봤다면, 이 강의의 목적은 달성입니다.
delta를 곱하면 안 되는 것들#
여기서 초보가 반대 방향으로 실수합니다. 모든 곳에 delta를 곱하는 것입니다.
곱해야 하는 것 — 시간당 변화량
- 이동 속도:
position += velocity * delta - 회전 속도:
rotation += 2.0 * delta - 시간 감소:
cooldown -= delta - 체력 재생:
hp += 5.0 * delta
곱하면 안 되는 것 — 순간에 일어나는 일
- 점프 시작:
velocity.y = -400.0(delta 곱하면 점프 높이가 프레임마다 달라짐) - 순간 이동:
position = target - 데미지:
hp -= 10(한 대 맞은 건 한 대다) - 개수 세기:
combo += 1
조금 애매한 경우 — 감속(마찰)
아래 코드는 초보가 자주 쓰는 감속입니다.
velocity *= 0.9 # 매 프레임 10%씩 줄인다문제가 보이나요? 프레임 수만큼 곱해지므로 60 FPS와 30 FPS에서 감속 속도가 다릅니다. 60 FPS면 1초에 0.9의 60제곱(≈0.0018), 30 FPS면 0.9의 30제곱(≈0.042)이 됩니다. 20배 넘게 차이납니다.
프레임에 무관한 감속은 이렇게 씁니다.
# 초당 감속률 6.0. 프레임과 무관하게 같은 곡선.
velocity = velocity.lerp(Vector2.ZERO, 1.0 - exp(-6.0 * delta))또는 물리 프레임(8강)을 쓰면 delta가 항상 고정이라 *= 0.9 도 안전해집니다.
이것이 물리 계산을 _physics_process에 넣는 실용적인 이유 중 하나입니다.
delta가 커지는 순간을 조심하기#
노트북 뚜껑을 닫았다 열거나, 브라우저 탭을 다른 곳으로 옮겼다가 돌아오면 delta가 갑자기 2초처럼 커질 수 있습니다. 그러면 이런 일이 벌어집니다.
- 캐릭터가 벽을 뚫고 순간이동한다
- 총알이 적을 그냥 통과한다
Godot은 이걸 막으려고 한 프레임에 처리할 물리 스텝 수를 제한합니다
(프로젝트 설정 → Physics → Common → Max Physics Steps Per Frame).
직접 만드는 로직에서도 필요하면 이렇게 막습니다.
func _process(delta: float) -> void:
# 한 프레임에 0.1초 이상은 없던 일로 한다.
var dt := minf(delta, 0.1)
position.x += SPEED * dt- 개발자 —
_process(delta)의 delta와_physics_process(delta)의 delta는 다른 값입니다. 뒤쪽은 항상 고정값(기본 1/60)입니다. 8강에서 다룹니다. - 기획자 — 수치를 적을 때 반드시 단위를 "초당"으로 명시하세요. "이동속도 5"는 개발자가 프레임당인지 초당인지 되물어야 하는 숫자입니다. "이동속도 300px/s"는 물어볼 필요가 없습니다.
- 디자이너 — 애니메이션 재생 속도도 같은 원리입니다. "몇 프레임짜리 동작"보다 "몇 초짜리 동작" 으로 말해야 기기와 무관하게 같은 연출이 나옵니다.
"_delta 라고 밑줄이 붙어 있던데요?"
→ 안 쓰는 매개변수 이름 앞에 밑줄을 붙이는 관례입니다. 경고를 없애는 용도이고 동작은 같습니다.
"Invalid access to property or key 'position' on a base object of type 'Nil'"
→ $노드이름 이 못 찾았습니다. 이름 철자와 부모/자식 관계를 확인하세요.
@onready 를 빠뜨린 경우도 이 오류가 납니다.
"delta를 곱했는데도 속도가 이상하다"
→ _process와 _physics_process 양쪽에 같은 이동 코드를 넣어 두 번 움직이고 있을 확률이 높습니다.
- delta는 "지난 프레임 이후 흐른 초"다
- 속도는 항상 초당으로 정하고 delta를 곱한다
- 점프 초기 속도나 데미지에는 delta를 곱하지 않는다
-
*= 0.9식 감속이 프레임에 의존한다는 걸 안다
다음 강의 — 이제 "얼마나 움직이는가"는 해결됐습니다. 다음은 "어디로" 입니다. 좌표계, 원점, 피벗, 로컬과 글로벌을 다룹니다.