리소스 시스템은 파일 시스템을 많이 사용한다. 게임엔진에서는 운영체제의 파일 시스템 API를 그대로 사용하지 않고 이를 감싼 랩핑 API를 엔진에 구현하여 사용하는 경우가 많다. 

이렇게 구현하는 이유

1. 멀티 플랫폼 지원을 위해

ㄴ 엔진의 파일 시스템 API를 사용하게 되면 엔진의 나머지 부분에서는 플랫폼의 차이를 신경쓰지 않아도 된다.

2. 운영체제의 파일 시스템 API가 게임 엔진에서 필요로 하는 모든 기능을 지원하지 못하는 경우가 있기 때문에

ㄴ 파일 스트리밍을 지원하는 엔진은 많지만 운영체제는 많지 않다.

 

6.1 파일 시스템

파일 이름과 경로를 다루는 기능

파일들을 열고 닫는 기능과 읽고 쓰는 기능

디렉토리의 내용을 검색하는 기능

스트리밍을 위한 비동기 파일 I/O 요청을 처리하는 기능

 

6.1.1 파일 이름과 경로

6.1.1.1 운영체제에 따른 차이

윈도우

ㄴ 다중 볼륨 지원 D: C: (볼륨을 나타내는 첫 번째 방법)

ㄴ \\ [원격 컴퓨터 이름] (볼륨을 나타내는 두 번째 방법)

ㄴ 파일 이름에 콜론을 사용할 수 없다.

ㄴ 경로 구분 문자로 \ 를 사용

유닉스

ㄴ 볼륨마다 개별적인 디렉터리 구조 를 가질 수 없다. 하나의 통합된 구조이다.

ㄴ 경로 구분 문자로 / 를 사용

mac os

ㄴ 콜론을 경로 구분 문자로 사용한다.

 

6.1.1.3 검색 경로

검색 경로는 경로들의 리스트를 담고 있는 문자열로 콜론이나 세미콜론 등의 특수한 문자로 구분된다.

리소스 파일의 검색 경로를 담은 텍스트 파일을 사용하여 런타임에 자원의 경로를 얻는 방법도 있지만 일반적인 경우 엔진은 자원의 경로를 미리 알고 있다.

 

6.1.1.4 경로 API

ㄴ 경로를 처리할 때 파일 이름 및 확장자를 분리하는 일

ㄴ 절대 경로와 상대 경로를 전환하는 일

윈도우 API shl-wapi.dll에 구현되어 있다.

 

6.1.2 기본 파일 I/O

파일 열기, 읽기, 쓰기에 두 가지 종류의 API가 있다.

1. 버퍼를 사용하는 방식

ㄴ API가 알아서 입력 및 출력 데이터 버퍼를 관리

2. 버퍼를 사용하지 않는 방식

ㄴ 프로그래머가 데이터 버퍼를 할당하고 관리

 

표준 C 라이브러리 함수

ㄴ fopen, fread, fclose, fwrite (버퍼를 사용하는 방식)

 

운영체제가 제공하는 함수

open, read, close, write (버퍼를 프로그래머가 관리하는 방식, 시스템 콜)

 

6.1.2.1 운영체제의 함수를 감싸는 경우와 그렇지 않은 경우

게임 엔진의 I/O API를 구현할 때 두 가지 방식으로 구현 할 수 있다.

 

1. 운영체제의 I/O API를 감싸 엔진의 I/O API를 구현하는 방식

2. 표준 C 라이브러리의 I/O 함수를 사용하는 방식

 

운영체제의 I/O API를 감싸 엔진의 I/O API를 구현하는 방식의 장점

ㄴ 서로 다른 플랫폼에서 똑같은 동작이 보이게 보장할 수 있다.

ㄴ 엔진에서 실제 필요한 기능들만 갖게 API를 단순화할 수 있다.

ㄴ 확장 기능을 지원할 수 있다. (하드 디스크, 네트워크, 휴대형 미디어에 담긴 파일의 처리)

 

6.1.2.2 동기적 파일 I/O

ㄴ 표준 C 라이브러리의 파일 I/O 라이브러리는 동기적으로 동작한다. (fread() 구문에서 block이 걸린다.)

 

6.1.3 비동기적 파일 I/O

ㄴ 스트리밍을 지원하기 위해서는 비동기 파일 I/O 라이브러리를 활용해야 한다.

ㄴ 스트리밍은 오디오, 텍스처 데이터 등 모든 데이터를 대상으로 사용 가능하다.

ㄴ 비동기적 I/O 라이브러리는 메인 프로그램에서 요청한 후 I/O 동작이 완료되기를 기다리는 기능도 지원하는 경우가 일반적이다. (asyncWait 함수)

ㄴ 비동기 연산 시간이 얼마나 걸릴지 예상 시간을 알 수 있는 기능을 제공하기도 한다.

 

6.1.3.1 우선순위 

비동기 I/O 연산에는 보통 우선순위가 있다. 예를 들어 오디오를 하드디스크에서 스트리밍하는 작업은 보통 게임 레벨의 개체들을 불러오는 일보다는 우선순위가 높아야 한다.

 

I/O 시스템은 우선순위가 높은 요청이 제한 시간 안에 완료될 수 있게 우선순위가 낮은 요청을 잠시 정지시킬 수 있어야 한다.

 

6.1.3.2 비동기 파일 I/O의 동작 방법

I/O 요청을 별도의 스레드에서 처리하는 방식으로 동작한다.

1. 메인 스레드에서 비동기 I/O 함수를 호출하면 큐에 요청을 넣는다.

2. I/O 스레드에서 큐에서 요청을 뽑아 동기적 I/O 함수를 사용하여 처리한다.

3-1. 작업이 완료되면 메인 스레드가 지정한 콜백 함수를 호출해 요청이 완료되었음을 알린다.

3-2. I/O 요청을 할 때 콜백 함수를 지정하지 않는 대신 세마포어를 이용하면 메인 스레드에서 I/O 작업이 완료될 때까지 기다린다.

 

6.2 리소스 매니저

리소스의 종류

ㄴ 메시, 머터리얼, 텍스처, 셰이더 프로그램, 애니메이션, 오디오 클립, 레벨 레이아웃, 충돌 기본 단위, 물리 매개변수 등

 

리소스 매니저가 해야 할 일 2가지

1. 리소스를 만들어 내는 오프라인 툴 체인을 관리

2. 런타임에 자원을 올리고 내리는 일을 관리

 

6.2.1 오프라인 리소스 관리와 툴 체인

6.2.1.1 자원의 리비전 컨트롤

자원 파일들을 상용 버전 관리 프로그램을 사용하거나 이런 시스템을 감싼 툴을 제작해 사용하기도 한다.

 

발생하는 문제

ㄴ 아트 파일들은 용량이 커서 중앙 저장소에서 로컬로 파일을 복사하는 방식은 효율이 떨어진다.

ㄴ 심볼릭 링크를 활용해 데이터를 복사하지 않고 저장소의 자원을 로컬 머신에서 볼 수 있는 툴을 만들면 해결 가능하다.

 

6.2.1.2 리소스 데이터베이스

자원의 메타 데이터(자원을 어떤 방식으로 가공할 것인지)를 저장하는 데이터베이스가 필요하다.

메타 데이터는 리소스 자원 안에 포함되어 있는 경우도 있다.

 

리소스 데이터베이스가 제공해야 할 기능

ㄴ 여러 종류의 리소스를 처리할 수 있는 기능

ㄴ 새 리소스를 만들 수 있는 기능

ㄴ 리소스를 지울 수 있는 기능

ㄴ 기존의 리소스를 살펴보고 수정할 수 있는 기능

ㄴ 리소스의 원본 파일을 디스크의 다른 장소로 옮길 수 있는 기능

ㄴ 리소스들끼리 교차 참조할 수 있는 기능, 교차 참조에 대한 참조 무결성을 유지하는 기능

ㄴ 리비전 히스토리를 관리하는 기능 (작업자, 작업내용에 대한 로그)

 

6.2.1.4 자원 다듬기 파이프라인 (ACP : asset conditioning pipeline)

 

1. 내보내기 도구 (Exporter)

ㄴ 보통 DCC(digital cotent creation) 프로그램이 기능을 지원한다.

 

2. 리소스 컴파일러 (컴파일 과정 없이 export 단계 이후 바로 사용할 수 있는 자원도 존재한다.)

ㄴ 데이터를 게임에 적합한 형태로 바꾸거나 계산이 필요하다면 데이터를 이용해 계산을 하는 일들을 진행한다.

 

3. 리소스 링커 (export와 컴파일 단계만 거친 후 바로 게임에서 사용할 수 있는 자원들은 필요 없는 단계)

ㄴ C++ 프로그램을 컴파일하는 과정에서 여러 obj 파일들을 하나의 실행 파일로 링크하는 것과 비슷하게 3D 모델과 같은 복합 리소스를 빌드하는 경우, 메시 파일, 머터리얼 파일, 뼈대 파일, 애니메이션 파일들을 하나의 리소스 파일로 묶어야 하기 때문에 필요한 단계이다.

 

리소스 의존 관계의 빌드 규칙

소스 파일에도 상호 의존성이 있듯 게임 자원에도 의존성이 있다.

메시가 참조하는 머터리얼이 여러 개 있을 수 있고, 머터리얼들은 여러 텍스처를 참조할 수 있다.

파이프라인에서 자원을 처리하는 순서는 상호 의존성에 영향을 받는다.

자원들을 올바른 순서로 빌드할 빌드 툴이 있어야 한다.

 

6.2.2 런타임 리소스 매니저

6.2.2.1 런타임 리소스 매니저의 역할

ㄴ 각 고유한 리소스는 메모리에 하나만 존재하도록 한다.

ㄴ 각 리소스의 수명을 관리한다.

ㄴ 필요한 리소스를 불러오고 더 이상 필요 없는 리소스는 내린다.

ㄴ 복합 리소스의 로딩을 처리한다. 3D 모델 같은 거

ㄴ 참조 무결성을 유지한다. (복합 리소스를 불러올 때 상호 참조 관계로 연결된 리소스들을 모두 불러올 수 있어야 한다.)

ㄴ 불러온 리소스들의 메모리 사용량을 관리하고 메모리의 적절한 곳에 저장한다.

 

6.2.2.2 리소스 파일과 디렉터리 구조

ㄴ pc 엔진은 보통 리소스 파일들을 디스크에 두고 디렉터리 트리로 관리한다.

 

여러 개의 리소스 파일들을 하나의 ZIP 파일이나 복합 파일로 압축하여 관리하는 엔진도 있다.

ㄴ 로딩 시간이 줄어들 수 있다. 하드 디스크의 연속적인 공간에 모든 데이터가 저장되기 때문에 탐색 시간(헤더 옮기는 시간)이 줄어들고, 파일을 여는 데 드는 시간도 여러 번에서 한 번으로 줄기 때문에

 

ZIP 형식을 사용할 경우 얻는 이점

1. ZIP 파일을 읽고 쓰는데 사용하는 무료 라이브러리가 존재한다. (zlib, zziplib)

2. ZIP 파일 안의 가상 파일들은 일반 파일 시스템과 마찬가지로 상대 경로로 접근할 수 있다.

3. ZIP 파일은 압축이 가능하기 때문에 디스크에서 리소스가 차지하는 용량을 줄일 수 있다.

4. ZIP 파일을 모듈로써 사용할 수 있다. (현지화해야 할 자원들을 지역 별로 묶어 지역별로 다른 ZIP 파일을 둘 수 있다.)

 

언리얼의 경우 패키지 (.pak) 라고 불리는 자체 제작한 복합 파일을 사용한다. 

 

6.2.2.3 리소스 파일 형식

자체 제작한 리소스 파일 형식을 사용하는 이유

ㄴ 표준화된 형식으로 엔진에서 쓰이는 모든 정보들을 표현하지 못하는 경우가 있기 때문에

 

가능한 한 많은 오프라인 처리를 통해 런타임에 처리할 작업을 줄이는 방법의 예

ㄴ 특정한 형태로 리소스 데이터를 메모리에 배열해야 하는 경우 오프라인 툴에서 데이터를 배열하게 하고

메모리에 올리기 위해 이진 데이터 형식을 사용할 수도 있다. (런타임에 리소스 데이터를 로드한 후 배열하데 사용되는 시간을 줄일 수 있다.)

 

6.2.2.4 리소스 GUID(Globally unique identifier)

GUID로 흔히 쓰이는 것은 리소스의 파일 시스템 경로(문자열)이다.

 

언리얼 엔진같은 경우는 패키지라는 큰 파일 안에 여러 리소스들을 저장하기 때문에 패키지 파일의 경로가 각 리소스를 고유하게 나타내지는 못하기 때문에 GUID를 파일 시스템 경로로 사용하지 못한다.(대신 해시 코드를 사용한다)

이를 해결하기 위해 패키지 안의 각 리소스는 파일 시스템 경로와 유사한 고유한 이름을 갖는다. 언리얼 엔진에서의 리소스 GUID는 패키지 파일의 이름 + 패키지 안의 리소스에 대한 경로를 합쳐 만들어진다.

 

6.2.2.5 리소스 레지스트리

리소스 매니저는 레지스트리를 이용하여 하나의 리소스만 메모리에 존재하도록 보장한다.

키-값 자료구조를 갖는 map, hashmap을 이용해서 GUID를 키로 사용하고 값은 리소스의 메모리 주소에 대한 포인터를 사용한다.

 

게임에서 리소스를 요청하는 경우 리소스 매니저는 리소스 레지스트리를 검색하여 리소스가 존재하면 리소스의 포인터를 반환하고 없다면 메모리에 리소스를 올리거나 그냥 실패 코드를 리턴하게 할 수 있다.

 

리소스 로드 작업은 시간이 많이 걸리는 작업(디스크에서 파일을 읽어 큰 용량의 리소스를 올려야 함) 이기 때문에 두 가지 방법으로 보통 처리한다.

 

1. 게임 플레이 중에는 아예 리소스를 로드하지 않는다.

ㄴ 게임 플레이가 시작되기 전에 미리 로드하는 방법

 

2. 리소스 로드를 비동기적으로 수행한다.

ㄴ 게임 플레이 도중 다른 레벨의 리소스를 미리 불러온다면 로딩 화면을 보지 않아도 된다.

 

6.2.2.6 리소스 수명

리소스의 수명은 메모리에 올라가는 순간부터 메모리에서 내려오는 순간까지를 뜻한다.

 

게임을 처음 시작할 때 불러 와서 게임이 진행되는 동안 항상 메모리에 상주해야 하는 리소스들의 수명은 사실상 무한대이기 때문에 글로벌 리소스 또는 글로벌 에셋 리소스라고 부른다.

 

글로벌 에셋 리소스의 예시로는 플레이어 캐릭터의 메시, 머터리얼, 텍스처, 애니메이션, 헤드업 디스플레이에 쓰이는 폰트, 텍스처 등이 있고 이들은 게임 내내 노출되는 리소스들이다.

 

게임의 특정 레벨의 수명을 따라가는 리소스들도 있다. 

 

게임속의 1회성 영상 (시네마틱)에 쓰이는 짧은 애니메이션과 오디오 클립은 영상이 시작하기 전에 미리 로드되었다가 영상 재생이 끝난 뒤 바로 메모리에서 내려가도록 만든다.

 

배경 음악이나 환경 사운드 효과, 풀 스크린 동영상 같은 리소스들은 실시간 스트리밍된다. 리소스를 구성하는 일부 바이트만 메모리에 존재하게 되는데 하드웨어가 한번에 읽어오는 4KiB와 같은 단위로 정하게 되며, 메모리에는 현재 재생 중인 청크와 다음 재생을 위해 메모리로 로드 중인 다음 청크, 두 개의 청크만 존재하게 된다.

 

리소스를 레벨의 수명을 따라가도록 하면 여러 레벨이 걸쳐 공유되는 리소스들은 계속 메모리에 올라갔다 내려갔다 반복하게 될 수 있는데, 리소스에 참조 카운터를 사용하게 되면 이런 문제를 막을 수 있다.


6.2.2.7 리소스와 관련된 메모리 관리

비디오 메모리에 있어야 하는 리소스들 (GPU가 빠르게 접근할 수 있도록)

ㄴ 텍스처, 정점 버퍼, 인덱스 버퍼, 셰이더 코드

 

글로벌 리소스와 실행 중 동적으로 오르는 리소스를 메모리 상의 서로 다른 장소에 두어 관리할 수도 있다.

 

리소스 매니저의 설계와 메모리 할당 하부 시스템의 설계는 밀접한 관계가 있다. 

메모리 할당자를 최대할 활용할 수 있게 리소스 매니저를 설계하는 경우도 있고 반대로 리소스 매니저가 가장 잘 활용할 수 있는 메모리 할당자를 설계하기도 한다.

리소스가 메모리에 올라갔다 내려갔다 하는 과정에서 메모리 단편화가 발생할 수 있는데 메모리 할당 시스템을 스택, 힙 기반으로 만들면 단편화 문제를 해결할 수 있다.

 

https://lemonyun.tistory.com/90

 

5. 엔진 지원 시스템

5.1 서브시스템 시작과 종료 게임 엔진은 서로 통신하는 수많은 하부 시스템이 모여 이루어진 복잡한 소프트웨어이다. 게임 엔진을 시작할 때 각 하부 시스템들을 정해진 순서에 따라 설정하고

lemonyun.tistory.com

힙 기반 리소스 할당

ㄴ 메모리 단편화를 무시하고 범용 힙 할당자 (new malloc)을 사용하는 방법, 가상 메모리 할당을 지원하는 운영체제 (PC) 에서는 메모리 단편화 문제가 덜하기 때문에 사용할 수 있다.

 

스택 기반 리소스 할당

ㄴ 게임 플레이 스타일이 특정한 경우에 사용 가능하다.

1. 게임이 선형 진행 방식이고 레벨 중심일 경우 (레벨1 - 로딩 - 레벨2 - 로딩 - 레벨3 이런 형식인 경우)

2. 레벨이 전부 메모리 안에 들어가는 경우 

 

풀 기반 리소스 할당

ㄴ 리소스 데이터를 똑같은 단위 크기의 청크로 불러오는 방법

ㄴ 모든 리소스 데이터가 똑같은 크기의 청크로 나누어질 수 있게 배열되어야 한다.

ㄴ 반드시 연속적으로 배열되어야 하는 데이터(배열, 구조체)들의 크기는 청크의 크기보다 작아야 한다. 

ㄴ 리소스 데이터를 설계할 때 청크로 나뉠 수 있도록 미리 고려하여 설계해야 한다. 배열과 같은 연속된 자료 구조 대신 연결 리스트를 사용하는 방법도 고려할 수 있다.

ㄴ 리소스 파일의 크기가 청크 크기의 배수가 되지 않는다면 파일의 마지막 청크에는 낭비되는 공간이 생길 수 있다는 단점이 있다.

ㄴ 각 청크의 크기는 I/O 버퍼의 배수가 되게 선택하여 청크를 메모리에 불러올 때 최적의 성능을 내도록 할 수 있다.

 

리소스 청크 할당자

ㄴ 청크에 쓰이는 메모리가 낭비되는 것을 줄이기 위한 방법으로 쓰이지 않는 공간이 있는 청크들에 대한 리스트와 각 청크에 있는 블록의 위치와 크기를 관리한다.

ㄴ 청크가 속한 레벨의 수명(이미 할당한 리소스의 수명)과 일치하는 메모리 요청(특정 레벨에 종속적인 데이터) 에만 리소스 청크 할당자를 사용하여 원하지 않을 때 리소스가 메모리에서 해제되는 현상을 방지한다.

 

6.2.2.8 복합 리소스와 참조 무결성

상호 의존하는 리소스들을 묶어 복합 리소스라고 부르기도 한다. (3D 모델 : 삼각형 메시, 뼈대, 애니메이션들의 리소스로 이루어짐)

대부분의 참조 관계는 의존 관계이다.

게임의 리소스 데이터베이스는 서로 의존하는 데이터 객체들로 이루어진 방향 그래프로 나타낼 수 있다.

 

데이터 객체끼리의 상호 참조는 내부 참조(같은 파일 안에 있는 두 객체 간의 참조)일 수도 있고 외부 참조(다른 파일에 있는 객체에 대한 참조)일 수도 있다.

 

6.2.2.9 리소스 간 상호 참조 처리

두 객체 간 상호 참조를 구현할 때는 포인터나 참조를 사용하지만 디스크 파일에 저장할 때는 포인터로 객체 간 의존성을 나타낼 수는 없다. (객체의 메모리 주소는 실행할 때마다 바뀔 수 있기 때문에)

 

1. GUID를 사용하는 방법

ㄴ 상호 참조 관계에 있을 수 있는 모든 리소스에 GUID를 부여한다.

ㄴ 런타임 리소스 매니저가 룩업 테이블을 전역적으로 관리해야 한다. (GUID와 객체에 대한 포인터를 키-값으로 갖는 테이블)

 

2. 포인터 교정 테이블 (여러 리소스 객체를 이진 파일로 저장한 뒤 메모리에 올려 사용하는 방법)

ㄴ 포인터를 파일 오프셋으로 변환하는 방법을 사용할 수 있다.

ㄴ 복합 리소스 내의 객체들을 이진 파일로 저장할 때, 파일에 각 객체에 있는 모든 포인터(다른 객체에 대한 참조)를 오프셋으로 대체하여 저장한다.

ㄴ 나중에 파일을 메모리에 불러올 때는 오프셋 값을 다시 포인터로 바꿔야 한다.

ㄴ 이진 파일에 포인터 교정 테이블(Fix-Up Table)을 둔다. 모든 객체내의 참조들의 위치를 기록한다. (파일에서는 오프셋으로 변경되어 있으므로 파일을 메모리에 올리려면 이 테이블에 있는 모든 위치의 오프셋을 포인터로 변경해줘야 한다.)

 

3. C++ 객체를 이진 파일로 저장하는 경우

ㄴ 객체의 생성자를 따로 불러줘야 함.

1. 그냥 생성자가 없는 C의 구조체나 가상함수가 없고 생성자에서 아무것도 하지 않는 데이터 타입(plain old data: PODS)만 이진 파일에 저장하는 방법

2. PODS가 아닌 객체들의 오프셋과 해당 객체들의 클래스 인스턴스 종류를 테이블에 같이 기록하고 placement new 문법(미리 할당된 메모리에 생성자를 호출하는 방법)을 사용하여 생성자를 호출하는 방법 

 

외부 참조 처리 (다른 리소스 파일안에 들어있는 객체를 참조하는 경우)

ㄴ 객체의 오프셋이나 GUID만으로는 표현할 수 없다. 객체가 담긴 리소스 파일에 대한 경로도 필요하다.

 

 

6.2.2.10 로드 후 초기화 과정, 정리 과정

리소스를 메모리기 올린 직후에 초기화 단계를 거치고 리소스의 메모리를 해제하기 전에는 정리 단계를 거친다.

리소스 타입마다 로드 후 초기화 및 정리 과정에서 할 일이 각기 다르다.

ㄴ C++ 에서는 다형성을 이용하여 클래스별로 다른 로드 후 초기화 및 정리 단계를 구현할 수도 있고 그냥 Init(), Destroy() 함수를 사용하여 구현할 수도 있다.

 

반드시 필요한 로드 후 초기화 과정 예시

ㄴ PC의 경우 3D 메시를 나타내는 정점과 인덱스 버퍼는 메모리에서 비디오 메모리로 옮겨져야 한다.

ㄴ 낡은 형식으로 된 메시 데이터를 최신 형식으로 변경하여 메모리의 최종 위치로 복사해야 하는 경우

 

보통 초기화 과정에서 새로운 데이터를 만들어 내기 때문에 리소스에 새로운 데이터를 저장하기 위한 여분의 메모리가 필요할 수 있다.

 

+ Recent posts