6강그리기 순서와 렌더링
z_index, Y-sort, CanvasLayer, 드로우 콜.
이 강의 목차 (7)
- 2D에서 무엇이 위에 그려질지를 결정하는 규칙을 안다.
z_index, Y-sort,CanvasLayer를 구분해서 쓴다.- 드로우 콜이 무엇이고 왜 많으면 느려지는지 감각적으로 안다.
2D에는 깊이가 없다. 순서만 있다#
3D는 카메라로부터의 거리를 계산해서 가까운 것이 앞에 보입니다. 2D는 그런 게 없습니다. 그냥 나중에 그린 것이 위에 덮입니다.
화가가 캔버스에 배경을 칠하고, 그 위에 산을 칠하고, 그 위에 사람을 칠합니다. 나중에 칠한 물감이 앞의 물감을 덮습니다. 2D 렌더링은 이걸 그대로 합니다. 실제로 painter's algorithm(화가 알고리즘) 이라고 부릅니다.
그래서 2D에서 "앞뒤"는 전부 그리는 순서를 어떻게 정하느냐의 문제입니다.
규칙 1 — 씬 트리의 순서#
기본 규칙은 아주 단순합니다.
Main
├─ Background ← 가장 먼저 그림 (제일 뒤)
├─ Ground
├─ Enemy
└─ Player ← 가장 나중에 그림 (제일 앞)자식은 부모보다 나중에 그려집니다. 즉 자식이 부모 위에 올라갑니다.
씬 독에서 노드를 위아래로 끌어 순서를 바꾸면 그리기 순서가 바뀝니다. 대부분의 정렬 문제는 여기서 해결됩니다.
규칙 2 — z_index#
트리 순서를 바꾸기 곤란할 때가 있습니다. 그럴 때 z_index로 강제로 끌어올립니다.
z_index = 10 # 기본은 0. 클수록 앞에 온다. 음수도 가능- 범위는 -4096 ~ 4096
- 같은 부모 안에서만 비교됩니다(기본값 기준)
z_as_relative가 켜져 있으면 부모의 z에 더해집니다(기본 켜짐)
프로젝트 후반에 "얘가 자꾸 뒤로 가요" 할 때마다 z_index = 100, z_index = 999 를 붙이다 보면
아무도 순서를 예측할 수 없는 상태가 됩니다.
권장 — 프로젝트 시작할 때 레이어 상수를 정해두고 그것만 씁니다.
# res://z_layers.gd (오토로드로 등록)
const BG := -100
const GROUND := 0
const ACTOR := 10
const EFFECT := 50
const UI_WORLD := 90규칙 3 — Y-sort (탑다운 게임의 핵심)#
탑다운(위에서 내려다보는) 게임에서는 아래쪽에 있는 것이 앞에 보여야 자연스럽습니다. 캐릭터가 나무보다 아래에 서 있으면 나무 앞에, 위에 서 있으면 나무 뒤에 가려져야 합니다.
이걸 매 프레임 z_index로 계산하려면 골치 아픕니다. Godot에는 기능이 있습니다.
부모 노드의 Y Sort Enabled 를 켜면, 그 자식들을 global_position.y 순서대로
자동 정렬해서 그립니다. Y가 큰(= 화면에서 아래쪽) 것이 나중에 그려져 앞에 옵니다.
설정 방법
- 캐릭터·나무·상자를 모두 담고 있는 부모 노드(예:
World)를 선택 - 인스펙터에서
Ordering → Y Sort Enabled체크 - 끝
정렬 기준은 이미지의 아래 끝이 아니라 노드의 원점(피벗) 입니다. 5강에서 "캐릭터 원점은 발밑으로"라고 한 이유가 여기 있습니다.
원점이 이미지 중앙에 있으면, 캐릭터가 나무 밑동보다 아래 서 있어도 중앙 좌표는 아직 위쪽이라 나무 뒤로 숨어버립니다.
Y-sort가 안 먹는 대표적인 경우
- 자식이 또 다른 Node2D 안에 묶여 있을 때 — Y-sort는 직계 자식들만 정렬합니다. 중간 컨테이너 노드에도 Y Sort를 켜야 관통합니다.
- z_index를 같이 쓴 경우 —
z_index가 먼저 적용되고, 같은 z 안에서 Y-sort가 돕니다. 즉z_index가 다르면 Y-sort는 무의미합니다. - 긴 물체 — 담장이나 다리처럼 세로로 긴 물체는 한 점 기준 정렬로는 해결이 안 됩니다. 보통 잘라서 여러 조각으로 나눕니다.
규칙 4 — CanvasLayer (UI는 세계 밖에 있다)#
카메라가 움직이면 세계의 모든 것이 같이 밀려납니다. 그런데 체력바나 점수는 화면에 고정되어야 합니다.
CanvasLayer는 카메라의 영향을 받지 않는 별도의 레이어를 만듭니다.
Main
├─ World (Node2D) ← 카메라가 비추는 세계
│ ├─ Player
│ └─ Enemies
└─ HUD (CanvasLayer) ← 화면에 고정
├─ HealthBar
└─ ScoreLabelCanvasLayer도 layer 번호가 있어서 여러 개를 쌓을 수 있습니다.
| layer | 용도 |
|---|---|
| -1 | 시차 배경(패럴랙스) |
| 0 | 기본 (게임 세계) |
| 1 | HUD |
| 10 | 일시정지 메뉴 |
| 100 | 화면 전환 페이드, 로딩 |
Label을 CanvasLayer 안이 아니라 World 안에 두고 카메라를 움직여 보세요.
글자가 화면 밖으로 밀려나갑니다. 3강에서 FPS 라벨을 CanvasLayer에 넣은 이유입니다.
드로우 콜 — 왜 많으면 느려지는가#
여기서부터는 "왜 게임이 느려지는가"의 뿌리입니다. 비개발자도 감각은 잡아두면 좋습니다.
CPU가 그래픽 카드에게 "이거 그려라" 하고 명령을 한 번 보내는 것이 드로우 콜 1회입니다.
문제는 명령을 보내는 행위 자체에 고정 비용이 있다는 점입니다. 그림이 아무리 작아도, 명령을 보내는 데 드는 비용은 똑같이 듭니다.
- 물건 100개를 한 상자에 담아 한 번 보내기 → 배송비 1회
- 물건 100개를 각각 따로 100번 보내기 → 배송비 100회
물건의 총량은 같은데 비용은 100배입니다. 그래픽 카드는 한 번에 많이 받는 걸 좋아합니다.
같은 텍스처를 쓰는 스프라이트들은 하나로 묶여(batching) 한 번에 보내집니다. 그래서 실무에서 이런 최적화가 나옵니다.
- 텍스처 아틀라스 — 여러 그림을 큰 이미지 한 장에 모아 붙인다. → 같은 텍스처이므로 한 번에 그려진다.
- 머티리얼 통일 — 셰이더나 블렌드 모드가 다르면 배칭이 끊긴다.
- z_index 남발 자제 — 순서가 뒤섞이면 배칭이 끊긴다.
z_index나 CanvasLayer로 A-B-A-B 순서를 만들면
같은 텍스처라도 중간에 다른 게 끼어들어 묶을 수 없게 됩니다.
"정렬을 손보다가 갑자기 느려졌다"의 흔한 원인입니다.
2D에서 진짜 무거운 것은 따로 있다
드로우 콜보다 더 자주 문제가 되는 건 오버드로(overdraw) 입니다.
같은 픽셀을 여러 번 덧칠하는 것을 말합니다. 반투명 이펙트를 화면 가득 10장 겹치면, 화면의 모든 픽셀을 10번씩 칠하는 셈입니다. 해상도가 클수록 정직하게 비례해 느려집니다.
증상 구분법
- 오브젝트 수를 줄였더니 빨라졌다 → 드로우 콜 문제
- 창 크기를 줄였더니 빨라졌다 → 오버드로(픽셀) 문제
이 구분 하나로 최적화 방향이 갈립니다. 11강 이후 실제 게임을 만들면서 다시 만납니다.
실습 — 순서를 직접 뒤집어 보기#
1) 겹쳐 놓기#
Sprite2D 세 개를 조금씩 겹치게 배치하고 이름을 A, B, C로 둡니다.
인스펙터의 Modulate 색을 각각 빨강·초록·파랑으로 바꾸면 구분하기 좋습니다.
씬 독에서 C를 A 위로 끌어 올려보세요. 화면에서 앞뒤가 바뀝니다.
2) 코드로 순서 바꾸기#
extends Node2D
@onready var sprites: Array[Node] = [$A, $B, $C]
var _top := 0
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept"): # 스페이스바
# 누를 때마다 다음 스프라이트를 맨 앞으로.
for i in sprites.size():
sprites[i].z_index = 0
sprites[_top].z_index = 10
_top = (_top + 1) % sprites.size()3) Y-sort 체험#
Main(또는 스프라이트들의 부모)에서Y Sort Enabled를 켭니다.- 스프라이트들을 위아래로 흩어 놓습니다.
- 하나를 드래그해 위아래로 움직이면 자동으로 앞뒤가 바뀝니다.
# 캐릭터를 마우스로 끌어 Y-sort를 확인하는 코드
extends Sprite2D
var _drag := false
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
_drag = event.pressed
elif event is InputEventMouseMotion and _drag:
global_position = get_global_mouse_position()- Y-sort를 켠 상태에서
z_index를 하나만 5로 올려보세요. Y-sort가 무시됩니까? Sprite2D의Offset을 조정해 원점을 발밑으로 옮기고 다시 정렬해 보세요. 정렬 결과가 더 자연스러워집니까?CanvasLayer를 하나 추가하고 스프라이트 하나를 그 안으로 옮겨보세요. Y-sort가 그 스프라이트에는 적용됩니까? (다른 캔버스이므로 적용되지 않습니다)
- 개발자 — 정렬 문제는 ① 트리 순서 → ② Y-sort → ③ z_index 순으로 시도하세요.
z_index부터 손대면 나중에 정리가 불가능해집니다. - 기획자 — "캐릭터가 나무에 가려요"는 대개 피벗 규칙 문제입니다. 버그 리포트에 "캐릭터 발이 나무 밑동보다 아래인데 뒤에 그려짐"처럼 위치 관계를 써주면 원인 파악이 몇 배 빨라집니다.
- 디자이너 — 스프라이트를 한 장의 아틀라스로 모아주면 드로우 콜이 줄어 실제로 빨라집니다. 또 반투명이 겹치는 이펙트는 비쌉니다. 화면을 덮는 이펙트는 개수를 미리 상의하세요.
"Y Sort를 켰는데 아무 일도 안 일어난다" → 켜야 하는 건 정렬 대상이 아니라 그 부모 노드입니다. 캐릭터가 아니라 캐릭터들을 담은 노드에 켭니다.
"UI가 캐릭터 뒤로 사라진다"
→ UI를 CanvasLayer 안으로 옮기세요. z_index로 해결하려 하면 계속 재발합니다.
"배경이 캐릭터를 덮는다" → 씬 독에서 배경이 캐릭터보다 아래에 있습니다. 위로 올리세요.
- 2D는 나중에 그린 것이 위에 온다
- 트리 순서 → Y-sort → z_index 순으로 해결한다
- Y-sort는 피벗 위치를 기준으로 정렬한다
- UI는 CanvasLayer에 둔다
- 드로우 콜과 오버드로의 차이를 말할 수 있다
P1 정리 — 여기까지가 강좌의 뼈대입니다. 프레임, delta, 좌표, 그리기 순서. 앞으로 나오는 모든 것이 이 넷 위에 얹힙니다.
다음 강의 — P2로 들어갑니다. 이제 플레이어가 조작합니다. 입력부터 봅니다.