티스토리 뷰

언리얼 엔진에서 사용되는 함수들 중에서 Outer를 인자로 요구하는 함수들이 있습니다: NewObject, CreatePackage, SavePackage... 대부분의 경우, 해당 함수를 호출하는 클래스(this)를 인자로 집어넣으면 상관 없다고 하지만, 이유도 모르고 무작정 this를 설정했다가는 나중에 생각지도 못한 난관에 봉착할 수 있습니다.

Outer가 무엇일까요? 저같은 경우, 이 괴상망측한 용어를 NewObject를 처음 사용하게 되었을 때 만났습니다. 처음에는 단어 의미도 몰라서 검색했던 기억이 납니다. Outer에 대한 공식 문서의 언급은 UObject 인스턴스 생성에서 처음볼 수 있습니다. 해당 문서에서는 Outer를 다음과 같이 설명하고 있습니다.

Outer : 생성중인 Object 에 대한 Outer 로 설정할 UObject 입니다.

 

Outer의 설명을 알기 위해서 Outer가 뭔지 알아야 하는 설명을 알려주고 있습니다... 넘어가도록 하겠습니다.

 

Outer

언리얼 AnswerHub에서의 Outer에 대한 설명을 인용하겠습니다 :

객체의 "Outer"는 객체를 "소유"하는 객체입니다. 예를 들어, 구성 요소는 액터 또는 상위 구성 요소가 소유하고 액터는 해당 레벨이 소유합니다. UObject에서 파생 된 클래스의 객체를 생성 할 때마다 생성된 객체를 Outer 제공합니다. (CreateDefaultSubobject는 암시적으로 현재 객체(=this)를 Outer Object로 제공합니다.)
때때로 객체는 해당 객체를 소유하지 않고 다른 객체에 대한 포인터를 갖습니다. 예를 들어 UNavMovementComponent에는 형제 UPathFollowingComponent에 대한 포인터가 있습니다. 이 경우, UPathFollowingComponent는 Outer로 UNavMovementComponent를 갖지 않습니다.

 

설명이 너무 깔끔하게 되어 있어서 제가 설명할 부분은 없는 것 같으므로 예시를 한 번 들어보겠습니다. 레벨에 위아래로 둥둥 떠다니는 액터를 만든다고 하겠습니다(Unreal 공식 문서의 C++ 프로그래밍 듀토리얼의 FloatingActor를 연상하시면 충분할 것 같습니다). 일단 둥둥 떠다니는 로직은 신경쓰지 않도록 하고, 이 액터를 만들기 위해서는 최소한 '스태틱 메시 컴포넌트' 정도는 갖춰야 할 것입니다. 둥둥 떠다니는 로직에 대한 재사용성을 위해서 별도의 Custom Movement 컴포넌트를 만들 수도 있겠습니다.

설명의 단순화를 위해 스태틱 메시 컴포넌트만을 가지도록 하는 액터를 간단히 소개하겠습니다:

/* header file */
class SomeModule_API AFloatingActor : public AActor
{
// Something about AActor...


private:    
    UFUNCTION(BlueprintReadWrite, VisibleAnywhere, Category=Custom, meta=(AllowPrivateAccess=true))
    class UStaticMeshComponent* SMComp;
};



/* source file */

// codes...

AFloatingActor::AFloatingActor()
{
    SMComp = CreateDefaultSubobjet<UStaticMeshComponent>(TEXT("SMComp"));
    RootComponent = SMComp;
}  

 

스태틱 메시를 정적으로 생성한 모습입니다. 여기서 사용되는 CreateDefaultSubobject는 내부적으로 Outer를 this로 설정합니다. 즉, FloatingActor는 자연스레 SMComp의 Outer Object가 됩니다. 액터가 액터 컴포넌트를 소유한다는 관점에서 바라봤을 때 합리적이라고 볼 수 있습니다.

UStaticMeshComponent가 AActor를 Outer로 가지게 된다는 것을 방금 알게 되었습니다. 여기서 중요한 것은 AActor 역시 Outer를 가지게 된다는 것입니다. 즉, SomeObject->GetOuter()->GetOuter()->... 이런 식으로 Outer Chain이 형성되어 있다는 얘기이기도 합니다. 이것을 확인하기 위해서는 호출 가능한 함수에서 다음과 같이 로그를 찍어보면 알 수 있습니다:

UObject* Outer = this;
while(Outer != nullptr)
{
    // 현재 객체의 이름을 로그에 남깁니다. 의사코드로 작성하겠습니다.
    UE_LOG(SomeLogClass, Log, TEXT("Current Object Class : %s"), *Outer->Class());
    Outer->GetOuter():
}

 

AActor 클래스에서 이것을 실행하게 되면 다음과 같은 결과를 얻을 수 있습니다 :

Current Object Class : FloatingActor_c
Current Object Class : Level
Current Object Class : World
Current Object Class : Package

 

즉, 레벨은 액터를 소유하고 있고, 월드는 레벨을, 패키지가 월드를 소유하고 있는 것을 마지막으로 Outer Chain이 끝을 맺게 됩니다. Outer Chain을 위로 타고 올라갔을 때 끝을 한 번에 알고 싶다면 다음과 같은 함수를 사용할 수 있습니다:

UPackage * GetOutermost() const

 

언리얼 엔진에서는 이 Outer를 통해서 형성된 OuterChain을 GetOuter()를 통해서 어떤 패키지에 소유되어 있는 지 알 수 있습니다. 또한 이 Outer는 GC에 유용한 정보를 제공합니다. 가비지 컬렉팅의 경우, 예를 들어, 액터가 Destroy된다면 이것을 Outer로 삼고 있는 모든 객체에 대해서 다음 가비지 컬렉팅에 수집되어 메모리가 해제됩니다.

 

레퍼런스


How can I understand the data member 'Outer' in the UObjectBase class
이득우의 게임블로그
Unreal Engine 4.9 Release Note

댓글