3강한 프레임에 무슨 일이 벌어지는가
게임 루프와 FPS의 정체.
이 강의 목차 (5)
- 게임 루프가 무엇이고 한 프레임 안에서 무슨 일이 순서대로 일어나는지 말할 수 있다.
- FPS가 실제로 무엇을 세는 숫자인지 안다.
- 화면에 FPS를 띄우고, 일부러 프레임을 떨어뜨려 본다.
게임은 무한 반복문이다#
모든 게임 프로그램의 뼈대는 이 다섯 줄입니다. 엔진이 무엇이든, 장르가 무엇이든 같습니다.
게임 시작
반복 {
1. 입력을 읽는다
2. 세상을 조금 갱신한다
3. 화면에 그린다
}
게임 종료이 반복 한 바퀴를 프레임(frame) 이라고 부릅니다. 1초에 이 반복을 60번 돌면 60 FPS(Frames Per Second)입니다.
공책 귀퉁이에 조금씩 다른 그림을 그려 넘기는 플립북을 떠올리세요.
- 한 장 = 한 프레임
- 1초에 몇 장 넘기는가 = FPS
- 다음 장을 그 자리에서 그리는 것 = 게임의 갱신 단계
플립북은 그림이 이미 그려져 있지만, 게임은 넘기기 직전에 다음 장을 즉석에서 그립니다. 그 그리는 시간이 너무 오래 걸리면 넘기는 속도가 느려집니다. 그게 프레임 드랍입니다.
한 프레임을 해부하면#
Godot이 한 프레임에서 실제로 하는 일을 순서대로 늘어놓으면 대략 이렇습니다.
| 순서 | 하는 일 | 내가 개입하는 곳 |
|---|---|---|
| 1 | 운영체제 이벤트 수집 (키·마우스·창 크기) | _input(), _unhandled_input() |
| 2 | 물리 단계 — 필요한 만큼 반복 | _physics_process() |
| 3 | 타이머·애니메이션 진행 | — |
| 4 | 게임 로직 단계 — 프레임당 1회 | _process() |
| 5 | 화면 그리기 (드로우 콜 전송) | _draw() |
| 6 | 화면 표시(present) 후 다음 프레임 대기 | — |
지금 눈여겨볼 것은 2번과 4번이 따로 있다는 사실 하나입니다. 왜 나뉘어 있는지는 8강에서 제대로 다룹니다. 여기서는 이렇게만 기억하세요.
_process— 그림을 그리는 리듬. 컴퓨터 성능에 따라 빨라지고 느려진다._physics_process— 계산을 하는 리듬. 기본값 초당 60회로 고정되어 있다.
게임에는 시계가 두 개 돌고 있습니다. 이걸 하나로 착각하는 데서 대부분의 이상 현상이 나옵니다.
FPS는 "부드러움"이 아니라 "여유"의 지표다#
FPS 숫자를 흔히 부드러움으로 읽지만, 개발자에게 FPS는 한 프레임에 쓸 수 있는 시간 예산입니다.
| 목표 FPS | 한 프레임에 허용되는 시간 |
|---|---|
| 30 FPS | 33.3 밀리초 |
| 60 FPS | 16.6 밀리초 |
| 120 FPS | 8.3 밀리초 |
| 144 FPS | 6.9 밀리초 |
60 FPS를 유지한다는 건 입력 읽기 + 물리 + 로직 + 그리기를 전부 16.6ms 안에 끝낸다는 뜻입니다. 16.6ms를 넘기는 순간 그 프레임은 늦고, 화면이 튑니다.
게임이 60 FPS로 보인다고 해서 여유가 있는 게 아닙니다. 대부분의 게임은 화면 주사율에 맞춰 일부러 기다립니다(V-Sync). 즉 8ms 만에 끝냈어도 남은 8ms는 놀면서 60 FPS를 표시합니다.
그래서 실제로 봐야 하는 숫자는 FPS가 아니라 한 프레임에 걸린 시간(frame time) 입니다. "60 FPS인데 가끔 끊긴다"는 프레임 타임이 들쭉날쭉하다는 뜻입니다.
왜 평균 FPS보다 1% low가 중요한가
평균 60 FPS라도, 100프레임 중 1번이 200ms 걸리면 사람은 그 한 번을 정확히 알아챕니다. 사람 눈은 평균이 아니라 불규칙함에 민감합니다.
그래서 성능 지표는 보통 이렇게 봅니다.
- 평균 FPS — 전체적인 여유
- 1% low / 0.1% low — 가장 나쁜 1%, 0.1% 프레임의 성능. 실제 체감을 좌우
기획 회의에서 "60 나오는데 왜 렉 걸린다고 하죠?"가 나오면 답은 대개 여기 있습니다.
실습 — FPS를 화면에 띄우기#
1) 라벨 만들기#
main.tscn을 엽니다.- 루트에 CanvasLayer 를 추가합니다. (UI는 카메라와 무관하게 화면에 고정되어야 하므로. 자세한 건 18강)
CanvasLayer아래에 Label 을 추가하고 이름을FpsLabel로 바꿉니다.- 인스펙터에서
Position을(12, 8)정도로 옮겨 좌상단에 둡니다.
2) 스크립트#
FpsLabel에 스크립트를 붙이고 아래를 넣습니다.
extends Label
var _acc := 0.0 # 갱신 간격 누적
var _frames := 0 # 그동안 그린 프레임 수
func _process(delta: float) -> void:
_acc += delta
_frames += 1
if _acc < 0.25:
return
# 0.25초마다 한 번만 글자를 바꾼다.
var fps := _frames / _acc
var ms := (_acc / _frames) * 1000.0
text = "%.0f FPS | %.2f ms" % [fps, ms]
_acc = 0.0
_frames = 0Engine.get_frames_per_second() 를 매 프레임 그대로 찍으면 숫자가 미친 듯이 흔들려
읽을 수가 없습니다. 게다가 글자를 바꾸는 것 자체가 비용입니다.
성능을 보려고 만든 표시기가 성능을 깎으면 곤란하죠. 그래서 0.25초에 한 번만 갱신합니다.
F5로 실행하면 좌상단에 60 FPS | 16.67 ms 같은 글자가 뜹니다.
숫자가 보이고, 창을 움직이거나 크기를 바꿔도 계속 갱신되면 성공입니다.
실습 — 일부러 프레임을 떨어뜨리기#
이제 프레임이 떨어진다는 게 어떤 느낌인지 몸으로 확인합니다.
main.tscn의 루트 노드에 스크립트를 붙이고 아래를 넣으세요.
extends Node2D
# 0이면 정상. 값을 올릴수록 매 프레임 쓸데없는
# 계산을 더 많이 해서 프레임이 떨어진다.
@export var load_level := 0
func _process(_delta: float) -> void:
var junk := 0.0
for i in load_level * 100000:
junk += sqrt(float(i))@export 를 붙였으므로 인스펙터에 Load Level 칸이 생깁니다. 코드를 고치지 않고
숫자만 바꿔가며 실험할 수 있습니다.
@export var 이름 := 기본값 이라고 쓰면 그 변수가 에디터 인스펙터에 노출됩니다.
개발자가 아닌 사람도 값을 만질 수 있게 됩니다.
이 강좌에서 앞으로 계속 쓰는 문법입니다. "밸런스 수치는 코드에 박지 않고 밖으로 뺀다"의 가장 기본형입니다. (19강에서 본격적으로)
Load Level을 0 → 1 → 3 → 8 로 올리며 FPS 숫자를 기록하세요.- 2강에서 만든 움직이는 캐릭터를 같이 띄워두고, 프레임이 떨어질 때 캐릭터가 느려지는지 아니면 뚝뚝 끊기며 같은 속도로 가는지 보세요.
프로젝트 설정 → Display → Window → V-Sync Mode를Disabled로 바꾸고 다시 재보세요. FPS 숫자가 어떻게 변합니까?
3번 실험의 결과가 알려주는 것
V-Sync를 끄면 FPS가 300, 900처럼 치솟습니다. "더 좋아진 것"처럼 보이지만 아닙니다. 모니터가 초당 60번밖에 못 보여주므로 나머지는 버려집니다. 게다가 화면 위아래가 서로 다른 프레임으로 그려져 가로로 찢기는 테어링(tearing) 이 생깁니다.
V-Sync를 끄는 건 성능 측정용이지 배포용 설정이 아닙니다. 다만 웹 빌드처럼 환경을 제어할 수 없는 곳에서는 이 값이 무시되기도 합니다.
- 개발자 —
_process안에서 도는 반복문은 프레임 수만큼 곱해집니다. "1000번 도는데 별거 아니지"가 초당 6만 번이 됩니다. - 기획자 — "화면에 적을 200마리 띄워주세요"는 프레임 예산 16.6ms를 쓰는 요청입니다. 숫자를 요청할 때 "몇 마리까지 60 FPS가 유지되나요?"를 함께 물으면 대화가 빨라집니다.
- 디자이너 — 이펙트 하나하나가 프레임 예산을 씁니다. 특히 화면을 가득 덮는 반투명 이펙트는 픽셀 수만큼 비용이 붙습니다. 6강에서 이유를 다룹니다.
"FPS 라벨이 안 보인다"
→ CanvasLayer 밖에 두면 카메라가 움직일 때 같이 밀려나 화면 밖으로 나갑니다.
반드시 CanvasLayer 자식으로 두세요.
"%.0f 부분에서 오류가 난다"
→ % 뒤 배열의 개수와 서식 지정자 개수가 맞아야 합니다.
"%.0f %.2f" % [a, b] 처럼 대괄호 배열로 넘깁니다.
"FPS가 항상 60에서 딱 멈춰 있다" → 정상입니다. V-Sync가 켜져 있어서입니다. 여유가 얼마나 남았는지는 ms 값을 보세요.
- 한 프레임 = 입력 → 갱신 → 그리기 한 바퀴다
- 60 FPS의 예산이 16.6ms라는 걸 안다
- 화면에 FPS와 ms가 표시된다
-
Load Level을 올려 프레임이 떨어지는 걸 봤다
다음 강의 — 프레임이 떨어졌을 때 캐릭터가 느려지지 않게 만드는 방법.
delta의 정체를 봅니다. 이 강좌에서 가장 자주 쓰게 될 개념입니다.