언리얼 C++ 프로그래밍

[언리얼 C++] 언리얼 오브젝트 처리 (2)

언린이 2021. 7. 18. 11:21

언리얼 엔진의 오브젝트 처리 시스템에 대한 설명은 [언리얼 C++] 언리얼 오브젝트 처리 (1) (tistory.com) 글을 참고하시기 바랍니다.

이 글에서는 오브젝트 처리 시스템이 제공해주는 기능들에 대해 설명하도록 하겠습니다.

 

 

1. 에디터 통합

UObject와 UProperty는 에디터에 인식되며, 에디터는 별도의 코드를 작성할 필요 없이 해당 값들을 자동으로 노출시킬 수 있습니다. 이는 선택적으로 블루프린트 비주얼 스크립팅 시스템으로의 통합이 가능합니다.

 

그리고 변수와 함수의 노출 및 접근 여부를 제어할 수 있는 여러가지 옵션이 존재합니다.

프로퍼티 지정자 효과
BlueprintCallable 멀티캐스트 델리게이트 전용입니다. 프로퍼티를 노출시켜야 블루프린트 코드에서 호출할 수 있습니다.
BlueprintReadOnly 이 프로퍼티는 블루프린트에서 읽을 수는 있지만 변경은 안됩니다. BlueprintReadWrite 지정자와 호환되지 않습니다.
BlueprintReadWrite 이 프로퍼티는 블루프린트에서 읽거나 쓸 수 있습니다. BlueprintReadOnly 지정자와 호환되지 않습니다.
EditAnywhere 이 프로퍼티는 아키타입이나 인스턴스 양쪽의 프로퍼티 창에서 편집할 수 있습니다. 이 지정자는 어떤 "Visible" 지정자와도 호환되지 않습니다

프로퍼티 선언시, 위 표에 있는 프로퍼티 지정자를 붙여 프로퍼티가 엔진과 에디터의 다양한 부분에서 어떻게 작동하는지를 제어할 수 있습니다. 위 표에서는 제가 자주 사용하는 프로퍼티 지정자들만 정리한 것이고 더 많은 프로퍼티 지정자들이 존재합니다.

프로퍼티에 대한 더 많은 정보를 얻고 싶다면 프로퍼티 | 언리얼 엔진 문서 (unrealengine.com) 글을 참고하시기 바랍니다.

 

 

2. 런타임 타입 정보 및 형변환

UObject는 언리얼 엔진 리플렉션 시스템의 일부이므로, 항상 자신이 무슨 UClass인지 알고 있으며, 형변환을 실시간으로 할 수 있습니다.

네이티브 코드에서, 모든 UObject 클래스에는 그 부모 클래스로 설정된 커스텀 "Super" typedef가 있어, 덮어쓰기 행위에 대한 제어가 쉽게 가능합니다.

 

class AEnemy : public ACharacter
{
    virtual void Speak()
    {
        Say("Time to fight!");
    }
};

class AMegaBoss : public AEnemy
{
    virtual void Speak()
    {
        Say("Powering up! ");
        Super::Speak();
    }
};

위 예제에서 AMegaBoss 인스턴스의 Speak() 함수를 호출하면 "Powering up! Time to fight!" 라고 말하게 될 것입니다.

 

또한, 템플릿 Cast 함수를 사용해서 베이스 클래스에서의 오브젝트를 좀 더 파생된 클래스로 안전하게 형변환하거나, IsA를 사용해서 오브젝트가 특정 클래스의 것인지 질의할 수 있습니다.

class ALegendaryWeapon : public AWeapon
{
    void SlayMegaBoss()
    {
        TArray<AEnemy> EnemyList = GetEnemyListFromSomewhere();

        // The legendary weapon is only effective against the MegaBoss
        for (AEnemy Enemy : EnemyList)
        {
            AMegaBoss* MegaBoss = Cast<AMegaBoss>(Enemy);
            if (MegaBoss)
            {
                Incinerate(MegaBoss);
            }
        }
    }
};

위 예제 코드에서 Cast<>() 함수를 사용하여 AEnemy를 AMegaBoss로 형변환을 시도하였습니다. 문제의 오브젝트가 실제로 AMegaBoss가 아닌 경우, Cast<>() 함수는 널 포인터를 반환하므로 적절한 대응이 가능합니다.

 

 

3. 가비지 컬렉션

언리얼에서는 더이상 참조되지 않거나 명시적으로 소멸 예약시킨 UObject를 주기적으로 정리하는 가비지 컬렉션 스키마를 사용합니다. 엔진에서는 레퍼런스 그래프를 만들어 어느 오브젝트가 아직 사용중이고, 어느 것이 사용중이지 않은가를 알아냅니다. 이 그래프 루트에는 "루트 세트"라 지정된 오브젝트 세트가 있습니다. 어떤 오브젝트도 루트 세트에 추가시킬 수 있습니다. 가비지 컬렉션이 발생하면, 엔진은 루트 세트부터 시작해서 알려진 UObject 레퍼런스 트리를 검색하여 참조된 오브젝트를 전부 추적할 수 있습니다. 참조되지 않은 오브젝트, 즉 트리 검색에서 찾지 못한 것들은 더이상 필요치 않은 오브젝트라 가정하고 제거합니다.

 

여기에 한가지 숨어있는 실용적인 부분이 있는데, 전형적으로는 살려두고자 하는 오브젝트에 UPROPERTY 레퍼런스를 유지하거나, 그에 대한 포인터를 TArray 또는 다른 언리얼 엔진 컨테이너 클래스에 저장해야 합니다. 종종 액터와 그 컴포넌트는 예외인데, 액터는 보통 자신이 속한 레벨처럼 루트 세트로 다시 링크되는 오브젝트에 레퍼런싱되고 액터의 컴포넌트는 액터 자체에 레퍼런싱되기 때문입니다. 액터는 자신의 Destroy() 함수를 호출하여 명시적으로 소멸 마킹할 수 있는데, 이는 진행중인 게임에서 액터를 제거하기 위한 표준적인 방식입니다. 컴포넌트는 DestroyComponent() 함수로 명시적으로 소멸시킬 수 있으나, 보통은 소유 액터가 게임에서 제거될때 소멸됩니다.

 

언리얼 엔진의 가비지 컬렉션은 빠르고 효율적이며, 걸리는 부하를 최소화시키기 위한 기능이 다수 내장되어 있습니다. 가비지 컬렉션 방법 및 시점을 보다 정교하게 제어할 수 있는 기능도 프로젝트 세팅의 엔진, 가비지 컬렉션 아래에서 대부분 찾을 수 있습니다.

세팅 기능 설명
Create Garbage Collector UObject Clusters 프로젝트 세팅에서 켜거나 끌 수 있습니다 (기본으로 켜져 있습니다). 켜면 관련된 오브젝트들을 하나의 가비지 컬렉션 클러스터에 묶어, 오브젝트 각각이 아닌 클러스터 하나만 검사할 수 있도록 해놨습니다. 즉 전체 클러스터를 하나의 오브젝트로 취급할 수 있기에 도달가능성 파악이 빨라지지만, 클러스터의 개별 항목 전부를 같은 프레임에 언해시 및 삭제 준비를 하므로, 클러스터가 너무 클 경우 버벅임이 생길 수 있습니다. 일반적인 경우, 클러스터를 만들면 가비지 컬렉션 퍼포먼스가 향상되며 도달가능성 분석에 소요되는 시간이 단축됩니다.
Merge GC Clusters 클러스터 병합을 켜면 한 클러스터의 오브젝트가 다른 클러스터의 오브젝트를 참조할 때 클러스터를 합치도록 합니다. 참고로 병합을 유발시킨 레퍼런스를 지워도 새로 병합된 클러스터는 어떤 식으로든 분해되거나 나뉘지 않습니다. 이 기능의 정상 작동을 위해서는 Create Garbage Collector UObject Clusters 옵션도 켜져있어야 합니다. 그러면 가비지 컬렉터의 언해시 및 오브젝트 소멸 작업 빈도를 낮추지만, 더욱 많은 수의 오브젝트를 한꺼번에 언해시 및 소멸시킵니다. 추가로 클러스터 병합을 하지 않았다면 일어났을 가비지 컬렉션이 일어나지 않는 경우가 있을 수 있는데, 클러스터 내 오브젝트로의 레퍼런스가 있으면 전체 클러스터가 가비지 컬렉션 대상에서 제외되기 때문입니다.
Actor Clustering Enabled 프로젝트 세팅 에서 이 옵션을 켜고, bCanBeInCluster 변수를 true 로 설정하거나, 코드에서 CanBeInCluster 함수가 true 를 반환하도록 덮어써 주면 액터를 클러스터에 넣을 수 있습니다. 기본적으로, 스태틱 메시 액터와 리플렉션 캡처 컴포넌트를 제외한 액터 및 컴포넌트에는 이 기능이 꺼져있습니다. 이 기능은 한꺼번에 소멸시킬 것으로 예상되는 액터 그룹을 만드는 데 좋은데, 보통 레벨에 배치한 스태틱 메시가 속한 서브레벨을 언로드하지 않고서는 소멸되지 않도록 하는 것입니다.
Blueprint Clustering Enabled 블루프린트의 UBlueprintGeneratedClass 및 관련 데이터, 이를테면 공유 UPROPERTY 및 UFUNCTION 데이터같은 것은, 이 세팅을 켜서 클러스터로 묶을 수 있습니다. 여기서 한 가지 중요한 점은, 이렇게 생긴 클러스터는 블루프린트의 개별 인스턴스가 아닌, Blueprint Generated Class 자체를 참조한다는 점입니다.
Time Between Purging Pending Kill Objects 프로젝트 세팅에서 가비지 컬렉션 발동 빈도를 조절할 수 있습니다. 해당 하이 레벨 컨트롤은 특히나 버벅임 방지에 좋습니다. 컬렉션 발동 간격을 줄임으로써, 다음 도달가능성 분석 패스에 걸릴 도달불가 오브젝트 발생 가능성을 낮추고, 동시에 너무 많은 액터를 정리하느라 발생할 수 있는 버벅임도 피할 수 있습니다.

위 표는 프로젝트의 가비지 컬렉터 퍼포먼스 튜닝에 흔히 사용되는 세팅입니다.

 

 

4. 네트워크 리플리케이션

오브젝트 처리 시스템에는 네트워크 통신과 멀티플레이어 게임을 원활히 하기 위한 탄탄한 함수성 세트가 포함되어 있습니다. 이에 대한 내용은 Networking and Multiplayer | 언리얼 엔진 문서 (unrealengine.com) 글을 참고하시기 바랍니다.

 

그리고 UProperty에는 태그를 붙여 네트워크 플레이 도중 데이터의 리플리케이트 여부를 엔진에게 알릴 수 있습니다. 여기에 흔히 쓰이는 모델은, 서버에서 변수가 변경되면, 엔진에서 그 변화를 감지하여 모든 클라이언트에 신뢰성있게 전송하는 것입니다. 클라이언트에서는 리플리케이션을 통해 변수가 변할때, 옵션을 통해 콜백 함수를 받을 수 있습니다.

 

UFunction 역시도 태그를 붙여 원격 머신에서 실행시킬 수 있습니다. 예를 들어 "server" 함수는 클라이언트 머신에서 호출시 실제로는 서버 머신에서 해당 액터의 서버 버전이 실행되도록 합니다. 반면 "client" 함수는, 서버에서 호출될 수는 있지만 해당 액터의 소유중인 클라이언트 버전이 실행됩니다.