에셋 매니저
고친 사람 github-actions[bot]
에셋 매니저는 게임이 도는 동안 필요한 데이터 파일을 메모리에 올리고 내리는 일을 맡습니다. 게임 코드는 이름만 대고 그림이나 소리를 달라고 합니다. 파일을 찾아 읽고 다 쓰면 치우는 일은 에셋 매니저가 대신합니다. 같은 이름이 웹 개발과 금융에서는 다른 것을 가리킵니다.
쉽고 빠른 이해
에셋 매니저는 게임이 쓰는 그림·소리·모델 파일을 대신 챙겨 줍니다. 적 캐릭터 코드가 적 그림을 달라고 하면, 에셋 매니저가 디스크에서 그 파일을 읽어 메모리에 올린 뒤 건넵니다.
이게 없으면 코드 곳곳이 제각기 파일을 엽니다. 같은 그림이 메모리에 여러 벌 올라갑니다. 다 쓴 파일을 누가 치울지도 정해지지 않아 메모리가 차오릅니다.
- 이름으로 요청을 받습니다. 이미 올라와 있으면 그것을 건넵니다
- 없으면 디스크에서 읽어 올립니다. 디스크 읽기는 느립니다. 그래서 게임을 멈추지 않게 뒤에서 읽습니다
- 쓰는 곳이 몇 군데인지 세다가 0이 되면 메모리에서 내립니다
대가도 있습니다. 게임 코드는 파일을 직접 열지 못하고 에셋 매니저를 거쳐야 합니다. 다 쓴 에셋은 돌려준다고 알려야 합니다. 이 약속을 빠뜨리면 아무도 안 쓰는 데이터가 메모리에 남습니다.
에셋이 적은 작은 게임은 이 부품 없이도 됩니다. 시작할 때 에셋을 다 올려도 메모리가 남기 때문입니다.
상세
사무실 회의실의 불을 떠올려 봅니다. 첫 사람이 들어올 때 불을 켭니다. 사람이 더 들어와도 불을 또 켜지는 않습니다. 마지막 사람이 나갈 때 불을 끕니다.
에셋 매니저는 게임 데이터에 이 일을 해 줍니다. 이 데이터를 에셋이라고 부릅니다. 에셋은 게임이 쓰는 데이터 파일입니다. 그림, 3D 모델, 소리, 지도 데이터가 다 에셋입니다.
처음 찾는 에셋은 디스크에서 읽어 메모리에 올립니다. 이미 올라온 에셋은 다시 읽지 않고 같은 것을 건넵니다. 아무도 안 쓰게 되면 메모리에서 내립니다.
에셋 매니저는 보통 게임 엔진 안에 들어 있습니다. 게임 엔진은 화면 그리기·소리·입력 처리처럼 어느 게임이나 쓰는 기능을 모아 둔 소프트웨어입니다. 엔진에 따라 에셋 매니저를 리소스 매니저나 리소스 캐시라고도 부릅니다.
이 절은 에셋 매니저가 없을 때 무엇이 곤란한지부터 봅니다. 그다음 에셋 매니저가 맡는 일을 하나씩 따라갑니다.
에셋 매니저가 없을 때
에셋 매니저 없이 코드마다 파일 경로를 적어 직접 연다고 해 봅니다. 적 캐릭터 코드가 적 그림을 엽니다. 미니맵 코드도 같은 그림을 엽니다. 메모리에 같은 그림이 두 벌 생깁니다.
치우는 때도 문제가 됩니다. 한쪽이 다 썼다고 그림을 지우면, 다른 쪽은 지워진 메모리를 읽다가 멈춥니다. 반대로 아무도 안 지우면 다 쓴 그림이 계속 남습니다. 이렇게 안 쓰는 데이터가 메모리를 차지한 채 남는 일이 메모리 누수입니다.
게임 기기의 메모리는 크기가 정해져 있습니다. 큰 게임의 에셋을 한꺼번에 다 올릴 수는 없습니다. 그래서 무엇을 올리고 무엇을 내릴지를 누군가 한 곳에서 정해야 합니다. 에셋이 적어 다 올려도 메모리가 남는 작은 게임이라면 이 고민이 없습니다.
에셋 매니저가 이 일을 한 곳에 모읍니다. 게임 코드는 이름으로 요청합니다. 다 쓰면 다 썼다고 알리기만 합니다.
이름으로 찾고 한 벌만 올리기
에셋 매니저는 안에 표를 하나 둡니다. 에셋 이름을 넣으면 메모리에 올라온 데이터를 찾아 주는 표입니다. 흔히 해시테이블로 만듭니다. 해시테이블은 표가 커져도 평균적으로 한 번에 값을 찾아 주는 자료구조입니다.
이름으로는 파일 경로를 쓸 수 있습니다. 다만 파일을 옮기면 경로가 바뀝니다. 그래서 에셋마다 바뀌지 않는 고유 번호를 따로 붙이기도 합니다. 겹치지 않게 만든 번호인 UUID(Universally Unique Identifier, 범용 고유 식별자)를 흔히 씁니다.
요청이 오면 표부터 봅니다. 있으면 그 데이터를 건넵니다. 없으면 디스크에서 읽어 올리고 표에 적습니다. 그래서 같은 이름을 열 번 요청해도 디스크는 한 번만 읽습니다.
참조 수로 내릴 때 정하기
언제 내릴지는 그 에셋을 쓰는 곳의 수로 정합니다. 에셋마다 지금 쓰는 곳이 몇 군데인지 세어 둡니다. 이 수를 참조 수라고 부릅니다. 참조 수로 언제 치울지 정하는 방식이 참조 카운팅입니다.
요청을 받을 때마다 참조 수를 하나 올립니다. 돌려받을 때마다 하나 내립니다. 0이 되면 아무도 안 쓰는 것이므로 메모리에서 내릴 수 있습니다.
아래 코드는 같은 에셋을 두 번 받고 두 번 돌려주는 모습입니다. am 이 에셋 매니저입니다. 주석은 그 줄을 실행한 뒤의 상태입니다.
주석의 「읽음」은 디스크에서 새로 읽었다는 뜻입니다. 「꺼냄」은 표에 있던 것을 꺼냈다는 뜻입니다.
var a = am.load("hero"); // 읽음 · 참조 1
var b = am.load("hero"); // 꺼냄 · 참조 2
boolean same = (a == b); // true
am.release(a); // 참조 1
am.release(b); // 참조 0
둘째 요청은 디스크를 읽지 않습니다. 첫째와 같은 데이터를 받으므로 same 이 참입니다. 마지막 release 에서 참조 수가 0이 되면 에셋 매니저가 이 에셋을 내립니다.
함께 딸려 오는 에셋
에셋은 다른 에셋을 씁니다. 3D 모델은 겉면이 어떻게 보일지 정한 설정을 씁니다. 이 설정이 머티리얼입니다.
머티리얼은 다시 텍스처 여러 장을 씁니다. 텍스처는 모델 겉면에 입히는 그림입니다. 모델 하나를 띄우려면 이 셋이 다 메모리에 있어야 합니다.
그래서 모델을 요청하면 에셋 매니저는 딸린 에셋도 함께 올립니다. 딸린 에셋의 참조 수도 하나씩 올립니다. 모델을 내릴 때는 딸린 에셋의 참조 수를 하나씩 내립니다.
아래 그림은 기사와 병사 두 모델이 갑옷 머티리얼 하나를 함께 쓰는 모습입니다. 칸마다 지금의 참조 수를 적었습니다.
flowchart TD
K["기사 모델 · 참조 1"] -->|쓴다| M["갑옷 머티리얼 · 참조 2"]
S["병사 모델 · 참조 1"] -->|쓴다| M
M -->|쓴다| T1["쇠 텍스처 · 참조 1"]
M -->|쓴다| T2["흠집 텍스처 · 참조 1"]
기사 모델을 내리면 머티리얼의 참조 수가 1로 줄어듭니다. 병사가 아직 쓰고 있으므로 머티리얼과 텍스처 둘은 메모리에 남습니다. 병사까지 내리면 머티리얼이 0이 됩니다. 텍스처 둘도 함께 내려갑니다.
핸들로 건네기
에셋을 건네는 가장 쉬운 방법은 데이터가 놓인 메모리 주소를 그대로 주는 것입니다. 메모리 주소를 담은 값을 포인터라고 부릅니다. C 나 C++ 처럼 메모리를 직접 다루는 언어에서 씁니다.
포인터에는 위험이 있습니다. 포인터를 쥔 코드는 에셋이 내려간 뒤에도 그 주소를 읽을 수 있습니다. 그 메모리에는 이미 다른 데이터가 들어와 있을 수 있습니다. 이렇게 이미 치운 메모리를 가리키는 포인터가 댕글링 포인터입니다.
그래서 에셋 매니저는 주소 대신 번호를 건네기도 합니다. 이 번호가 핸들입니다. 핸들은 에셋 매니저의 표에서 그 에셋을 찾는 번호입니다.
핸들을 쓰면 데이터를 꺼낼 때마다 에셋 매니저가 표를 확인합니다. 내려간 에셋이면 데이터 대신 「없음」을 돌려줄 수 있습니다. 코드는 엉뚱한 메모리를 읽는 대신 없다는 것을 알게 됩니다.
핸들은 개발 중에도 쓸모가 있습니다. 그림을 고쳐 저장하면 에셋 매니저가 파일을 다시 읽어 표의 데이터만 바꿉니다. 핸들을 쥔 코드는 다음에 꺼낼 때 새 그림을 받습니다. 게임을 끄지 않고 고친 에셋을 바로 보는 이 기능이 핫 리로드입니다.
게임을 멈추지 않고 읽기
게임은 1초에 화면을 수십 번 새로 그립니다. 화면을 한 번 그린 결과가 프레임입니다. 1초에 60번 그린다면 한 프레임은 60분의 1초 안에 다 그려야 합니다.
디스크에서 파일을 읽는 일은 메모리를 읽는 것보다 훨씬 오래 걸립니다. 화면을 그리는 흐름이 큰 파일을 다 읽을 때까지 기다리면 이 60분의 1초를 넘깁니다. 그러면 다음 프레임을 제때 못 그려 화면이 멈칫합니다.
그래서 에셋 매니저는 읽기를 따로 도는 실행 흐름에 맡깁니다. 한 프로그램 안에서 따로 도는 실행 흐름이 스레드입니다. 화면을 그리는 스레드는 읽기를 기다리지 않고 다음 프레임으로 넘어갑니다.
요청한 코드에는 핸들을 먼저 돌려줍니다. 다 읽으면 그 에셋에 준비됐다고 표시합니다. 이렇게 다 읽기를 기다리지 않고 먼저 돌아오는 방식이 비동기 로딩입니다.
그러면 에셋 하나는 몇 가지 상태를 거칩니다. 막 요청을 받은 「요청됨」, 다른 스레드가 읽고 있는 「읽는 중」, 다 읽어 쓸 수 있는 「준비됨」입니다. 참조 수가 0이 되어 메모리에서 빠지면 「내려감」입니다. 아래 그림은 이 흐름입니다.
stateDiagram-v2
state "요청됨" as req
state "읽는 중" as loading
state "준비됨" as ready
state "내려감" as unloaded
[*] --> req
req --> loading: 다른 스레드가 읽기 시작
loading --> ready: 다 읽음
ready --> unloaded: 참조 수 0 (늦출 수도 있음)
unloaded --> [*]
게임 코드는 「준비됨」이 되기 전에는 데이터를 꺼내 쓰지 못합니다. 그동안 로딩 화면을 띄우거나, 흐린 임시 그림을 대신 보여 줍니다.
메모리 예산과 내리는 때
참조 수가 0이 돼도 바로 내리지 않는 에셋 매니저도 있습니다. 곧 다시 쓸 에셋을 내리면 디스크를 또 읽어야 하기 때문입니다. 이런 에셋 매니저는 에셋에 쓸 메모리 양을 미리 정해 둡니다. 이 양을 메모리 예산이라고 부릅니다.
예산이 남아 있으면 참조 수가 0인 에셋도 메모리에 둡니다. 예산을 넘으면 그런 에셋 가운데 가장 오래 안 쓴 것부터 내립니다. 이 규칙이 LRU(Least Recently Used, 가장 오래 안 쓰인 것)입니다.
자주 쓰는 데이터를 가까운 곳에 두었다가 다시 쓰는 일을 캐싱이라고 합니다. 메모리 예산을 두는 에셋 매니저는 에셋을 캐싱하는 셈입니다.
에셋을 하나씩이 아니라 묶음으로 올리고 내리기도 합니다. 지역 하나에서 쓰는 에셋을 한 묶음으로 둡니다. 게임에서는 이런 지역 하나를 레벨이라고 합니다. 난이도 단계가 아니라 지도의 한 구역이라는 뜻입니다.
플레이어가 레벨에 들어갈 때 그 묶음을 올립니다. 나갈 때는 한꺼번에 내립니다. 넓은 세계를 끊김 없이 돌아다니는 게임은 이 일을 쉬지 않고 합니다. 플레이어 둘레의 레벨만 올려 두는 이 방식이 레벨 스트리밍입니다.
텍스처도 거리에 따라 다룹니다. 카메라에서 먼 물체는 화면에 작게 보입니다. 그래서 해상도가 낮은 텍스처로도 충분합니다. 물체가 가까워지면 높은 해상도 텍스처로 바꿔 올리는 이 방식이 텍스처 스트리밍입니다.
에셋 파이프라인과 나누는 일
에셋 파이프라인은 게임을 내놓기 전에 원본 파일을 게임이 바로 읽는 형식으로 바꿔 두는 과정입니다. PNG(Portable Network Graphics) 로 저장한 그림 원본을 그래픽 카드가 바로 읽는 압축 텍스처 형식으로 바꾸는 일이 한 예입니다.
에셋 매니저는 그렇게 바뀐 파일을 게임이 도는 동안 다룹니다. 파이프라인이 출시 전에 할 일을 다 해 두므로, 에셋 매니저는 읽어서 올리고 내리는 일에만 집중합니다.
파이프라인은 흔히 에셋 수천 개를 큰 파일 몇 개에 이어 붙입니다. 작은 파일을 하나씩 따로 여는 것보다 큰 파일 몇 개를 열어 읽는 편이 빠르기 때문입니다. 그러면서 어떤 에셋이 어느 파일 몇 번째 바이트에 있는지 적은 목차를 함께 만듭니다.
에셋 매니저는 이 목차를 보고 요청받은 에셋을 찾습니다. 목차에서 에셋을 찾는 열쇠가 에셋 이름입니다. 그래서 파이프라인과 에셋 매니저는 같은 에셋 이름을 맞춰 써야 합니다.
같은 이름을 쓰는 다른 분야
「에셋 매니저」는 게임 밖에서도 쓰는 이름입니다. 에셋은 영어로 자산이라는 뜻입니다. 분야마다 가리키는 자산이 다릅니다.
| 분야 | 「에셋 매니저」가 가리키는 것 |
|---|---|
| 게임 개발 | 실행 중에 에셋을 올리고 내리는 엔진 부품. 이 문서의 뜻 |
| 웹 프레임워크 | 스타일 파일과 스크립트 파일을 모아 내보내는 도구. 파일 이름에 내용이 바뀔 때마다 달라지는 표시를 붙여 브라우저가 낡은 파일을 쓰지 않게 한다 |
| 정보 기술 운영 | 회사가 가진 컴퓨터와 소프트웨어 사용권을 목록으로 관리하는 도구 |
| 금융 | 고객의 돈을 맡아 투자하는 회사나 사람. 우리말로 자산운용사 |
대화에서 「에셋 매니저」가 나오면 옆에 붙은 말로 뜻을 가릅니다. 텍스처나 로딩 이야기면 게임 쪽입니다. 스크립트나 배포 이야기면 웹 쪽입니다.
관련 항목
에셋 매니저를 품은 게임 엔진의 구성 요소
게임 개발 · 게임 엔진 · 게임 루프 · 물리 엔진 · 그래픽스 파이프라인 · 프레임
에셋 매니저가 올리고 내리는 에셋 종류
에셋 · 텍스처 · 머티리얼 · 메시 · 셰이더 · 애니메이션 클립
에셋 매니저보다 앞서 에셋을 만드는 빌드 단계
에셋 파이프라인 · 쿠킹 · 패키징 · 청킹 · 텍스처 압축 · 매니페스트
에셋 매니저가 메모리를 관리하는 기법
참조 카운팅 · 핸들 · 댕글링 포인터 · 포인터 · 해시테이블 · UUID · LRU · 캐싱 · 메모리 누수 · 가비지 컬렉션
에셋 매니저가 쓰는 로딩 방식
비동기 로딩 · 지연 로딩 · 레벨 스트리밍 · 텍스처 스트리밍 · 스레드 · 로딩 화면 · 핫 리로드
에셋 매니저가 지켜야 하는 시간과 메모리 한도
프레임 예산 · 프레임 시간 · 프레임 드롭 · 메모리 · 디스크
에셋 매니저와 이름이 겹치는 다른 분야의 개념
다른 이름: asset manager · 애셋 매니저 · 에셋 관리자