CDO에서 기본 서브 오브젝트를 생성하면 새로운 언리얼 오브젝트와 CDO 인스턴스 간에는 아래와 같은 관계를 가지게 됩니다.
생성자에서 서브 오브젝트를 생성할 수 도 있고,
WebConnection = CreateDefaultSubobject<UWebConnection>(TEXT("MyWebConnection"));
Init()함수에서 서브 오브젝트를 생성할 수 도 있다.
WebConnectionNew = NewObject<UWebConnection>(this);
전자 방식은 이는 기획상으로 변경될 여지가 거의 없는 오브젝트 묶음을 한번에 빠르게 생성할 때 유용합니다. 후자 방식은 느리지만 세팅에 따라 다양한 오브젝트를 생성할 수 있어서 유연하게 상황에 대처할 수 있다는 장점이 있습니다.
언리얼에서 게임 컨텐츠 구조는 이처럼 부모-자식 관계가 확장된 구조를 따릅니다.
언리얼에서 모든 게임 컨텐츠의 기초은 월드(World)에서부터 시작됩니다. 월드에는 최소 하나의 레벨이 지정되는데, 이를 퍼시스턴트 레벨(Persistent Level)이라고 합니다. 이 퍼시스턴트 레벨에는 추가로 월드의 초기 설정 값들을 지정한 월드세팅(World Settings) 언리얼 오브젝트가 있습니다. 기본 생성된 월드에는 레벨을 실시간으로 추가할 수 있는데, 월드에 무관하게 추가/삭제가 가능하도록 설계된 레벨을 스트리밍 레벨(Streaming Level)이라고 합니다. 이러한 레벨은 월드에서 독립적으로 행동하는 단위 오브젝트인 액터(Actor)의 묶음으로 구성되어 있습니다. 이 액터들은 다시 컴포넌트들로 구성되어 있지요. 지금까지 설명드린 월드의 계층 구성은 아래와 같이 정리할 수 있습니다.
이렇게 생성된 계층 구조는 설계에 따라 기본 서브 오브젝트 방식으로 관리할지, 런타임에서 실시간으로 생성해 관리할지 방식이 나뉘어지게 됩니다.
설명을 위해 월드 내에서 게임의 룰을 관장하는 액터인 게임모드를 가지고 예시를 들어보겠습니다. FPS 게임을 제작할 때 레벨 디자이너가 레벨을 공들여서 제작했습니다. 이렇게 제작된 레벨에 플레이어들이 접속해 플레이를 할 때에는 데스매치(DeathMatch)룰을 적용할지, 깃발 뺏기(Capture the Flag) 룰을 적용할지를 방장이 정하도록 유연하게 적용할 수 있게 만드는 것이 좋은 디자인일 겁니다.
이 상황에서 월드세팅 오브젝트가 퍼시스턴트레벨 오브젝트에 스태틱 방식인 기본 서브 오브젝트로 포함되어 버리면, 월드세팅마다 레벨이 하나씩 만들어져야 합니다. 이러면 중복된 맵으로 인해 용량이 낭비되겠지요. 그래서 월드에서 월드세팅과 게임 모드는 후자인 다이나믹 방식으로 로딩되게 설계되어 있습니다.
반면에 액터는 설계자가 지정한 컴포넌트의 조합에 따라 월드에서 스스로 동작하도록 설계되어 있습니다. 그래서 액터는 스태틱하게 컴포넌트와 함께 생성하고 다 같이 소멸하는 것이 효과적입니다. 그래서 액터와 컴포넌트는 전자인 스태틱하게 로딩하도록 설계되어 있습니다.
월드에 속한 액터를 빠르게 검색하는 방법은 두 가지입니다.
하나는 For Ranged Loop 구조에 맞게 추가된 FRangedWorld를 사용하는 방법이 있습니다.
이 방식은 모든 액터를 간편하게 검색할 수 있다는 장점이 있습니다. 액터 내 컴포넌트는 모두 기본 서브 오브젝트로 등록되어 있으니 쉽게 검색이 가능합니다.
UWorld* CurrentWorld = GetWorld();
for (const auto& Entry : FActorRange(CurrentWorld))
{
AB_LOG(Warning, TEXT("Actor : %s"), *Entry->GetName());
TArray<UObject*> Components;
Entry->GetDefaultSubobjects(Components);
for (const auto& CEntry : Components)
{
AB_LOG(Warning, TEXT(" -- Component : %s"), *CEntry->GetName());
}
}
다른 하나는 TActorIterator를 사용하는 방식입니다. 이 방식은 액터 중에서도 우리가 원하는 타입만 선별해서 목록을 빠르게 뽑아낼 수 있어서 대부분 많이 사용하는 방식입니다. 아래는 현재 월드에서 StaticMeshActor 타입만 뽑아낸 예시입니다.
for (TActorIterator<AStaticMeshActor> It(CurrentWorld); It; ++It)
{
AB_LOG(Warning, TEXT("StaticMesh Actor : %s"), *It->GetName());
}
액터를 포함해 현재 월드에 로딩된 모든 언리얼 오브젝트를 가져오기 위해서는 TObjectIterator를 사용하면 됩니다.
for (TObjectIterator<UWebConnection> It; It; ++It)
{
UWebConnection* Conn = *It;
AB_LOG(Warning, TEXT("WebConnection Object Host : %s"), *Conn->Host);
}