언리얼 엔진의 UObject가 다른 에셋을 참조하고 있다면, 그 오브젝트를 로드할 때 그 에셋들이 전부 로드가 된다.
예를 들어, 어떤 아이템 박스 액터가 있다고 가정해보자. 이 아이템 박스는 플레이어가 상호작용하면, 3가지 아이템 중 한 가지의 아이템을 플레이어에게 지급한다. 이때 아이템 박스는 나오는 아이템을 시각적으로 보여주기 위해, 각 아이템에 대한 StaticMesh 레퍼런스를 가지고 있다. 이제 이 아이템 박스가 레벨 상에 로딩될 때면, 이미 3가지 아이템의 StaticMesh에 대한 로딩이 완료된 상태이다.
지금은 아이템 3가지를 예시로 들었지만, 아이템이 계속 추가되어 100가지가 된다면 로딩을 감당할 수 없을 것이다. 이런 상황에서 필요한 것이 에셋을 필요할 때 로딩해주는 에셋 지연 로딩이다.
TSoftObjectPtr 클래스는 지연 로딩을 가능하게 해주는, 포인터를 담고 있는 템플릿 클래스다. 명칭을 보면 언뜻 TObjectPtr과 유사해보인다. 실제로 TObjectPtr처럼 템플릿 클래스이기도 하고, 역할도 비슷하다.
UPROPERTY(EditAnywhere)
TSoftObjectPtr<UMyAsset> MyAssetPtr;
위와 같이 헤더파일에서 선언할 수 있다. 그럼 정확히 어떤 점에서 다를까? 그것은 포인터 객체를 참조하는 방식에 있다.
TSoftObjectPtr은 참조하는 객체에 대해 소프트 레퍼런싱을 한다는 것이 특징이다.
기존의 하드 레퍼런싱은 위에 서술한 것처럼 오브젝트가 로드가 되면 그에 참조된 에셋들도 전부 로드가 되는 방식이다. 반면, 소프트 레퍼런싱은 로드하는 타이밍을 개발자가 직접 제어할 수 있는 방식이다. 처음부터 로드가 되어있지 않다는 점을 유의해야 한다.
따라서 에셋이 로드가 되었다면, TSoftObjectPtr 객체를 통해 바로 접근을 할 수 있다. 물론 로드가 되어있지 않은 상태라면, 로드를 진행하고 나중에 접근해야한다.
TSoftObjectPtr를 통해 에셋 지연 로딩을 시도해보자. 우선 다른 명령들처럼 주 스레드(게임 스레드)에서 순차적으로 로딩되는 동기 지연 로딩부터 진행한다.
다음과 같은 헤더 파일이 있다고 가정해보자.
TSoftObjectPtr<UStaticMesh> Mesh; // 지연 로딩할 Static Mesh
UStaticMeshComponent* MeshComponent; // 지연 로딩한 에셋을 넣을 Static Mesh 컴포넌트
우리는 TSoftObjectPtr 형태의 Mesh를 동기적으로 지연 로딩해, MeshComponent의 StaticMesh로 설정해줄 것이다.
UStaticMesh* LoadedMesh = Mesh.LoadSynchronous(); // 동기 지연 로딩
if (LoadedMesh)
{
// 로드된 메시를 컴포넌트에 설정
MeshComponent->SetStaticMesh(LoadedMesh);
}
우선 Mesh 객체에서 LoadSynchronous
멤버 함수를 통해, 동기 지연 로딩을 수행한다. 이후 로드된 메시가 유효한지 검사하고, 컴포넌트의 StaticMesh로 할당한다.
이 방법은 우리가 이해하기 쉽고, 예측하기 쉬운 흐름으로 흘러가기 때문에 이용하기 편하다. 하지만 동기적으로 로딩을 자주 수행하다보면, 같은 스레드에서 실행되는 핵심적인 조작들이 끊길 가능성이 존재한다. 따라서 지연 로딩을 많이, 다양한 양을 수행해야 할 때는 비동기적으로 로딩하는 경우도 잦다.
비동기 지연 로딩은 동기 지연 로딩과 같지만, 로딩 작업을 주 스레드가 아닌 백그라운드 스레드에서 수행한다는 특징이 있다. 따라서 주 스레드의 명령 순서를 해치지 않는다.
먼저 헤더 파일부터 보자.
TSoftObjectPtr<UStaticMesh> Mesh;
UStaticMeshComponent* MeshComponent;
FStreamableManager StreamableManager;
다 똑같지만, FStreamableManager 클래스 객체인 StremableManager가 추가되었다. 이는 비동기 로딩에서 꼭 필요한 오브젝트다. FStreamableManager 오브젝트는 에셋을 스트리밍, 즉 로딩하고 그것을 메모리에 저장하는 역할을 수행한다. 설명에서 볼 수 있듯이, 이는 비동기 로딩뿐만 아니라 동기 로딩도 수행할 수 있는 오브젝트임을 기억하자.
이제 비동기 로딩하는 구현 코드를 살펴보자.
//비동기 지연 로딩
StreamableManager.RequestAsyncLoad(Mesh.ToSoftObjectPath(), FStreamableDelegate::CreateLambda([this]()
{
if (Mesh.IsValid())
{
MeshComponent->SetStaticMesh(Mesh.Get());
}
}));
StreamableManager의 멤버 함수인 RequestAsyncLoad
를 통해 비동기 지연 로딩을 수행한다. 첫번째 인자에는 FSoftObjectPath 객체가 들어가야 하는데, 이는 TSoftObjectPtr의 '레퍼런스 경로' 버젼이라고 생각하면 편하다. TSoftObjectPtr의 멤버 함수로 ToSoftObjectPath
가 존재하니 이를 활용해 비동기 로딩을 진행한다.
두번째 인자에는 비동기 로딩이 끝나고 나서 수행할 행동을 지정해주면 된다. 이는 람다 함수가 될 수도 있고, 델리게이트, 멤버 함수 등등 다양한 형태로 인자가 들어갈 수 있다. 단, 백그라운드 스레드에서 수행되는 비동기 로딩과 다르게 두번째 인자의 행동은 주 스레드에서 진행된다는 것에 유의하자.
위에도 서술했지만, FStreamableManager는 엔진에서 에셋 로딩을 관리하는 클래스이다. 이는 보통 GameInstance, 혹은 별도의 싱글톤 객체에서 한번 선언되고, 필요할 때 그곳에서 꺼내 쓰는 경우가 대부분이다. 그렇게 하는 이유에는 여러가지가 있지만, 중요한 두가지 이유가 존재한다.
결국 FStreamableManager 오브젝트를 사용할 시엔 하나의 객체로만 운영하는 것이 효율적이고 안정적인 방법이라고 볼 수 있다.