본문 바로가기
게임개발뜯어보기

오픈월드는 왜 로딩이 없나 - 청크 스트리밍과 히스테리시스

by 조생 2026. 9. 6.

오픈월드는 왜 로딩이 없나 — 청크 스트리밍, 프레임 예산, 히스테리시스

오픈월드 게임을 하다 보면 좁은 통로나 엘리베이터, 열리는 데 유난히 오래 걸리는 문을 만납니다. 그 잠깐의 정체는 로딩입니다. 오픈월드에 로딩이 없는 게 아니라, 세계를 청크로 쪼개 내 주위 것만 걷는 동안 올리고, 나머지는 메모리에 없는 것입니다. 이번 편은 그 걷는 동안을 직접 짜서 잰 편입니다. 청크를 통째로 한 프레임에 올리면 1,091개에 199ms 가 걸리고, 프레임마다 2ms 씩 나눠 올리면 최악 9.7ms 로 한 프레임 안에 듭니다. 경계에서 흔들리면 225번 켜졌다 꺼지던 청크가, 끄는 선을 0.6칸 멀리 두면 39번(첫 로드뿐)으로 잠잠해집니다.

 

35초 영상판 — 이 글의 실사 컷과 도식이 전부 이 영상에서 나왔습니다

그림 1 — 청크를 통째로 올린 순간. 한 프레임에 오브젝트 1,091개가 들어오고 그 프레임이 199ms 다. 언리얼 5.8 위의 리그 실측이고, 엔진의 월드 파티션·레벨 스트리밍은 쓰지 않았다

왜 이런 게 생겼나

옛날 게임은 레벨을 통째로 올렸습니다. 레벨 하나가 메모리에 들어가고, 다음 레벨로 가려면 화면을 검게 하고 다음 것을 읽었습니다. 로딩 화면은 그 사이의 정직한 대기 시간이었습니다. 그런데 세계가 한 화면에 안 들어갈 만큼 커지면 통째로가 불가능해집니다. 메모리에 다 안 들어가고, 들어간다 해도 읽는 데 몇십 초가 걸립니다. 그래서 세계를 조각으로 나누고, 플레이어가 있는 곳 주위의 조각만 메모리에 두는 방식이 생겼습니다. 이게 스트리밍입니다.

엔진들은 이 일을 제품으로 만들어 뒀습니다. 언리얼의 월드 파티션은 문서에서 "자동 데이터 관리와 거리 기반 레벨 스트리밍 시스템"이라고 소개되고, 월드를 스트리밍 가능한 격자 셀로 나눠 스트리밍 소스에서 로딩 범위 안의 셀을 올립니다(문서의 예시 값이 768m 입니다). 그 전 세대의 레벨 스트리밍 개요는 "플레이 중에 맵 파일을 메모리에 올리고 내리며 가시성을 켜고 끈다"고 적혀 있고, 스트리밍 볼륨은 시점이 볼륨 안에 있느냐로 올리고 내릴 것을 정합니다. Unity 의 SceneManager.LoadSceneAsync 는 씬을 백그라운드에서 비동기로 올린다는 API 이고, additive 모드는 기존 씬을 둔 채 새 씬을 얹습니다. 셋 다 같은 구조입니다. 세계를 조각으로, 조각을 거리로, 올리는 일을 프레임 뒤로.

문제는 걷는 동안이 공짜가 아니라는 것입니다. 올리는 데 시간이 걸리고, 그 시간이 한 프레임에 몰리면 화면이 턱 걸립니다. 경계 위에 서면 조각이 켜졌다 꺼졌다 합니다. 나간 것을 부수고 들어온 것을 새로 만들면 할당이 쌓입니다. 그리고 반경으로는 안 되는 큰 구역이 있습니다. 이 편은 그것들을 직접 짠 리그로 하나씩 켜고 끄며 잰 것입니다.

첫째, 세계를 청크로 나눈다

그림 2 — 팩 데모 블록으로 깐 세계. 원본 블록과 북·남 이웃 블록, 차고끼리 마주 보게 180° 돌린 복제본, 지평선용 먼 복제본 여덟. 오브젝트 15,828개, 10m 청크 840개. 오른쪽은 가운데 블록과 걷는 경로(빨강), 반경 1·2 창

 

무대는 언리얼 마켓의 거리 팩 데모 맵입니다. 그 맵의 오브젝트(메시·위치·회전·크기)를 카탈로그로 뽑아 세계를 깔았습니다. 블록을 통째로 복사해 북·남에 이웃 블록을 붙이고, 뒤쪽 차고끼리 마주 보게 180° 돌린 복제본을 하나 더 놓고, 지평선에는 먼 복제본 여덟 개를 두었습니다. 청크는 10m 입니다. 오브젝트 15,828개, 청크 840개, 청크당 평균 19개, 가장 무거운 청크는 177개입니다. 레벨에는 아무것도 없고, 카탈로그를 읽는 리그가 관찰자 주위 반경 안 청크의 오브젝트만 컴포넌트로 만들어 세웁니다. 반경 밖으로 나간 청크는 컴포넌트를 부숩니다. 그래서 메모리에 있는 것은 항상 반경 안뿐입니다. 반경 2(50m 창)면 청크 최대 33개, 오브젝트 최대 1,160개가 살아 있고, 나머지 1만 4천여 개는 카탈로그의 글자로만 존재합니다.

걷는 길은 손으로 그린 선을 엔진의 실제 충돌(캡슐 스윕)로 검증해 잡았습니다. 오브젝트 상자로 근사한 점유 지도로 A* 를 돌려 봤지만 게이트·아치·트럭 옆에서 유령 벽을 만들어 못 믿었습니다. 거리를 따라 남쪽으로 내려와 마당 모서리를 돌고, 블록 북쪽 건물 사이 틈으로 빠져나가는 85m 길입니다.

 

청크 스트리밍

  • 세계를 같은 크기의 청크로 나누고, 청크마다 무엇이 어디에 있나를 카탈로그로 둔다.
  • 관찰자 주위 반경 안 청크만 세우고, 밖은 부순다. 메모리에는 반경 안만 있다.
  • 반경은 청크 칸 수라, 10m 청크에서 반경 2 는 50m 창이다. 창 밖이 곧 메모리에 없는 것이다.

둘째, 통째로 올리면 그 프레임이 걸린다

그림 3 — 같은 경로, 같은 시각에 로드 시작. 왼쪽은 청크를 통째로 한 프레임에(예산 0), 오른쪽은 프레임마다 2ms 예산 안에서 가까운 청크부터. 세로축은 그 프레임이 스폰에 쓴 시간이고, 왼쪽의 199ms 막대는 그래프 위로 잘렸다

관찰자가 빈 무대에 서 있다가 로드를 시작합니다. 청크를 통째로 올리는 판은 반경 안 오브젝트 1,091개를 한 프레임에 세우고, 그 프레임이 199.0ms 입니다. 60fps 의 한 프레임은 16.7ms 이니 12프레임어치가 한 번에 멈춘 것입니다. 이게 턱입니다. 같은 조건을 여러 번 돌리면 첫 로드가 249.8ms 에서 463.7ms 사이로 흔들리는데, 디스크 캐시에 따라 메시를 처음 읽는 비용이 달라지기 때문입니다.

고치는 방법은 단순합니다. 프레임마다 쓸 수 있는 시간을 정하고, 그 안에서 가까운 청크부터 오브젝트를 하나씩 세우다가 시간이 다 되면 다음 프레임으로 넘깁니다. 예산 2ms 판은 같은 1,278개를 약 2초에 걸쳐 올리고, 최악 프레임이 9.7ms 입니다. 한 프레임 안입니다. 통째로 판의 21분의 1 이고, 화면은 걷는 동안 거리가 채워지는 것으로 보입니다. 훅의 걷는 동안 올립니다가 이 그림입니다.

다만 정직하게 적어야 할 것이 있습니다. 예산 판에서 8ms 를 넘긴 프레임이 캐시가 식은 실행에서 23개, 데운 실행에서 3개 있습니다. 전부 로드가 시작된 첫 60여 프레임에 몰려 있고, 그 프레임들은 오브젝트를 몇 개밖에 안 세웠는데 9~17ms 가 걸렸습니다. 어떤 메시를 처음 세우는 프레임입니다. 예산은 몇 개를 세우나는 나누지만, 메시 하나를 처음 읽는 비용은 나누지 못합니다. 그 비용은 다른 이름으로 풉니다. 곧 쓸 메시를 미리 읽어 두는 프리로드입니다. 영상에서는 미리 읽어 둔다고만 말했고, 구현은 이 편에서 다루지 않았습니다.

 

프레임 예산

  • 프레임마다 쓸 시간을 정하고, 가까운 청크부터 그 안에서만 세운다. 남은 건 다음 프레임.
  • 통째로 199ms 대 예산 2ms 판 최악 9.7ms. 세우는 총량은 같고 언제만 다르다.
  • 예산은 개수를 나누지 메시 하나의 첫 로드는 못 나눈다. 그건 프리로드의 몫.

셋째, 경계에 서면 펄럭인다

그림 4 — 청크 경계 위에서 좌우로 1.5m 흔들리는 8초. 빨강은 켜는 선과 끄는 선이 같은 판, 초록은 끄는 선을 0.6칸 멀리 둔 판. 선은 켜짐/꺼짐 누적 횟수, 막대는 그 프레임에 부순 오브젝트

반경은 켜는 선입니다. 청크 중심이 반경 안에 들어오면 세우고, 나가면 부숩니다. 그런데 관찰자가 그 선 위에 서서 조금씩 움직이면 어떻게 될까요. 리그에 경계 위에서 1.5Hz 로 좌우 1.5m 씩 흔들리는 모드를 넣고 8초를 쟀습니다. 켜는 선과 끄는 선이 같은 판은 225번 켜졌다 꺼졌습니다. 그 사이에 오브젝트 4,953개를 부수고 4,685개를 다시 세웠습니다. 같은 청크를요. 화면에서는 건물 한 덩어리가 깜빡입니다.

고치는 방법 역시 단순합니다. 끄는 선을 켜는 선보다 조금 멀리 둡니다. 반경 2 에서 켜고 2.6 에서 끄면, 경계 위의 작은 움직임은 어느 선도 넘지 못합니다. 같은 8초에 39번 켜졌고 전부 처음 들어올 때의 첫 로드입니다. 부순 것은 0 입니다. 이 켜는 선과 끄는 선 사이의 틈이 히스테리시스입니다. 온도 조절기가 20도에서 켜고 22도에서 끄는 것과 같은 이유입니다.

 

히스테리시스

  • 켜는 반경과 끄는 반경을 다르게 둔다. 끄는 쪽을 조금 멀리.
  • 같은 흔들림에서 225회 → 39회, 부순 것 4,953 → 0.
  • 틈이 넓을수록 잠잠하지만 메모리에 더 오래 남는다. 0.6칸은 이 판의 값이다.

넷째, 풀은 할당을 줄이지 히치를 줄이진 않는다

영상의 넷째 컷입니다. 나간 오브젝트를 부수지 않고 숨겨 뒀다가, 같은 메시가 다시 들어오면 그걸 꺼내 씁니다. 그림 4의 펄럭이는 판에 풀을 켜 봤습니다. 새로 만든 컴포넌트가 6,670개에서 904개로 줍니다. 7.4배입니다. 배치 횟수는 6,670으로 같고, 그중 5,766번이 재사용입니다.

그런데 첫 프레임의 히치는 그대로입니다. 풀을 꺼도 204ms, 켜도 203ms 입니다. 그 프레임에 세우는 606개는 풀에 아직 아무것도 없어서 전부 새로 만들어야 하고, 무엇보다 통째로 세우는 것이 문제이지 새로 만드는 것이 문제가 아니기 때문입니다. 풀은 할당을 줄입니다. 히치는 예산이 줄입니다. 둘은 다른 비용이고, 하나가 다른 하나를 대신하지 않습니다.

다섯째, 반경으로 안 될 때 — 좁은 틈에서 통째로 바꾼다

그림 6 — 직접 저작한 무대. 큰 구역 A, 두 벽 덩어리 사이 폭 2m 의 일직선 틈, 큰 구역 B. 틈에 들어서는 순간 A 를 내리고 B 를 예산 0.3ms 로 올린다. 오른쪽은 걷는 20초 동안 프레임마다 올린 오브젝트 수, 노란 상자가 틈 안에 있던 구간

이제 처음의 질문으로 돌아갑니다. 오픈월드의 좁은 통로와 엘리베이터는 왜 거기 있을까요. 앞의 넷은 내 주위 반경으로 세계를 조금씩 바꾸는 방법입니다. 그런데 두 구역이 메모리에 동시에 안 들어갈 만큼 크면 반경으로는 안 됩니다. 구역 A 를 다 내리고 나서야 구역 B 를 올릴 수 있는데, 그 사이에 화면이 비면 안 됩니다. 그래서 두 구역 사이에 앞뒤가 안 보이고 시간이 걸리는 자리를 일부러 둡니다. 벽 사이 틈, 엘리베이터, 열리는 데 오래 걸리는 문. 그 안에서 A 가 내려가고 B 가 올라옵니다.

리그에 구역 모드를 넣고 무대를 직접 저작했습니다. 팩 블록을 A 로, 같은 블록을 옮겨 B 로, 그 사이에 팩 건물 덩어리로 폭 2m 의 일직선 틈을 쌓았습니다. 걷는 이가 틈에 들어서면 A 의 청크가 전부 원하지 않음이 되어 내려가고, B 의 청크가 예산 안에서 올라옵니다. 틈을 빠져나올 때 B 는 다 올라와 있습니다. 한 가지 정직하게 적자면, 풀을 켜 두면 A 에서 내려온 컴포넌트가 같은 메시라 B 에 그대로 재사용되어 B 가 한 프레임에 올라와 버립니다. 그래서 이 컷은 풀을 끄고 예산을 0.3ms 로 좁혀, 올라오는 데 걸리는 시간이 보이게 찍었습니다. 이것이 좁은 통로가 하는 일입니다. 로딩 화면이 사라진 게 아니라, 통로가 로딩 화면입니다. 걷고 있을 뿐, 하는 일은 같습니다.

 

구역 교체

  • 반경 스트리밍은 조금씩, 구역 교체는 통째로. 후자는 두 구역이 메모리에 같이 못 들어갈 때 쓴다.
  • 교체는 앞뒤가 안 보이는 자리에서. 틈·엘리베이터·문이 그 자리이고, 그 길이가 곧 로딩 시간이다.
  • 이 무대의 틈에서 A 를 내리고 B 를 올렸다(수치는 그림 6).

정리

오픈월드에 로딩이 없는 게 아닙니다. 세계를 청크로 나눠 내 주위만 세우고(스트리밍), 세우는 일을 프레임 예산 안에서 나눠 하고(199ms → 9.7ms), 켜는 선과 끄는 선을 떼어 경계에서 펄럭이지 않게 하고(225회 → 39회), 나간 것을 풀에 넣어 할당을 줄입니다(6,670 → 904). 그리고 반경으로 안 되는 큰 구역은 좁은 틈에서 통째로 바꿉니다. 통로와 엘리베이터가 그 시간을 법니다. 로딩은 사라진 게 아니라 걷고 있습니다.

확인 못 한 것

  • 첫 로드는 실행마다 흔들립니다(같은 조건이 249.8ms 에서 463.7ms 까지). 디스크 캐시와 메시 첫 읽기의 비용이라, 이 글의 199ms 는 가장 최근 실행의 값이고 이 무대의 값이지 특정 게임의 값이 아닙니다.
  • 예산 판의 9~17ms 프레임(메시 첫 로드)은 프리로드로 풀어야 하는데, 이 편은 거기까지 가지 않았습니다.
  • 리그는 컴포넌트 생성·파괴만 셉니다. 실제 엔진 스트리밍은 디스크 I/O, 텍스처 스트리밍, 물리·내비 데이터까지 올리고, 그 비용은 여기 없습니다.
  • 이 세계는 한 블록의 복제본들이라 메시 종류가 한 벌뿐입니다. 새 구역마다 새 메시가 나오는 실제 세계에서는 첫 로드가 훨씬 자주 일어납니다.

참고 자료

이 글의 청크 카탈로그·반경 로드·프레임 예산·히스테리시스·오브젝트 풀 코드와 실측은 직접 짠 것입니다(언리얼 5.8 C++ 리그, 헤드리스 촬영). 엔진이 같은 구조를 제품으로 만들어 뒀다는 확인은 위 세 문서에서 왔고, 히스테리시스·예산·풀 자체는 인용 없이 이 리그의 실측으로만 말했습니다. 수치는 전부 이 판의 무대와 리그에서 나온 것이며 특정 게임의 값이 아닙니다.