게임 개발/Unreal Engine 기본

[UE4] 언리얼 오브젝트의 기능

지노윈 2022. 2. 10. 14:51
반응형

클래스, 프로퍼티, 함수에 적합한 매크로로 마킹해 주면 UClass, UProperty, UFunction 으로 변합니다. 그러면 언리얼 엔진이 접근할 수 있어, 다수의 내부적인 처리 기능을 구현할 수 있습니다.


자동 프로퍼티 초기화

UObject는 생성자 호출 전 초기화시 자동으로 클래스의 UProperty/네이티브 멤버 모두 0으로 채워집니다.

이후 클래스 생성자에서 커스텀 값으로 초기화 가능합니다.

레퍼런스 자동 업데이트

AActor 또는 UActorComponent소멸되면, 리플렉션 시스템에 보이고 있는 그에 대한 모든 레퍼런스는 자동으로 null 이 됩니다. 댕글링 포인터 문제의 소지를 줄인다는 장점이 있지만, 다른 곳에서 포인터를 언제든지 소멸시켜 null이 될 수도 있습니다.

 

여기서 최고의 장점은 null 검사 안정성이 높다는 것인데, 일반적인 null 포인터의 경우와 null이 아닌 포인터가 삭제된 메모리를 가리키는 경우 둘 다 감지해 내기 때문입니다.

 

이 기능은 UPROPERTY 로 마킹되어 있거나 언리얼 엔진 컨테이너 클래스에 저장된 UActorComponent 또는 AActor 레퍼런스에만 적용된다는 점입니다.

 

Raw 포인터에 저장된 오브젝트 레퍼런스는 언리얼 엔진이 알지 못하기 때문에, 자동으로 null 되거나, 가비지 컬렉션이 방지되지 않습니다. 그렇다고 모든 UObject* 변수가 UProperty 가 되어야 한다는 뜻은 아닙니다. UProperty 가 아닌 오브젝트 포인터가 필요한 경우, TWeakObjectPtr 사용을 고려합니다.

 

이는 약 포인터로, 가비지 컬렉션을 방지하지는 않지만, 접근 전 질의를 통해 유효성 검사가 가능하며, 거기서 가리키는 오브젝트가 소멸된 경우 null 설정도 가능합니다.

 

참조된 UObject UProperty 가 자동으로 null 이 되는 또 한가지 경우는, 에디터에서 애셋을 'Force Delete' (강제 삭제)한 경우입니다. 그에 따라, 애셋인 UObject 에 대한 모든 코드 작업 시 이 포인터가 null 이 되도록 처리해야 합니다.

Serialization

UObject 가 Serialize 될 때, 모든 UProperty 값은 명시적으로 "transient" (휘발성) 마킹 또는 생성자 이후 기본 값에서 미변경 상태가 아닌 이상 자동으로 읽고 쓰기 가능합니다.

예를 들어 레벨에 AEnemy 인스턴스를 배치하고서, 그 Health 를 500 으로 설정하고 저장을 하면, UClass 정의 이외의 코드를 한 줄도 작성하지 않고도 다시 로드할 수 있습니다.

UProperty가 추가 또는 제거될 때, 기존 콘텐츠 로드는 매끄럽게 처리됩니다. 새 프로퍼티는 새 CDO 에서 기본값을 복사해 옵니다. 제거된 프로퍼티는 말없이 무시됩니다.

커스텀 작동 방식이 필요한 경우, UObject::Serialize 함수를 덮어쓰면 됩니다. 데이터 오류, 버전 번호 검사, 데이터 포맷 변경 시 자동 변환 또는 업데이트 수행 등에 유용하게 쓰일 수 있습니다.

프로퍼티 값 업데이트하기

UClass 의 클래스 디폴트 오브젝트(CDO) 가 변경되면, 엔진은 그 클래스의 모든 인스턴스 로드시 알아서 변경사항을 적용 시도합니다. 주어진 오브젝트 인스턴스에 대해, 업데이트된 변수 값이 이전 CDO 값과 일치한다면, 새로운 CDO 에 저장된 값으로 업데이트됩니다.

변수 값이 다른 경우, 그 변수가 의도적으로 설정되었다 가정하여 그 변경사항을 보존합니다. 예를 들어 AEnemy 오브젝트를 여럿 배치했고, AEnemy 생성자의 Health 기본값을 100 으로 설정한 레벨을 저장했다 칩시다. 여기서 또, Enemy_3 가 특별히 쎈 놈이라 그 Health 를 500 으로 설정했다 가정합시다. 여기서 마음이 바뀌어 Health 디폴트 값을 150 으로 올렸다고 가정합니다. 다음 번 레벨을 로드할 때, 언리얼은 CDO 를 변경했다고 알아채고, 이전 기본 Health 값(100)인 AEnemy 모든 인스턴스 Health 값을 150 으로 업데이트합니다. Enemy_3 의 Health 는 500 그대로 남아있는데, 이전 기본값이 아니기 때문입니다.

에디터 통합

UObject 와 UProperty는 에디터에 인식되며, 에디터는 별도의 코드를 작성할 필요 없이 이 값을 자동으로 노출시킬 수 있습니다. 이는 선택적으로 블루프린트 비주얼 스크립팅 시스템으로의 통합(integration)이 가능합니다. 변수와 함수의 노출 및 접근 여부를 제어할 수 있는 옵션이 많이 있습니다.

런타임 유형 정보 및 형변환

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();
    }
};

보시듯이, Speak 를 호출하면 MegaBoss 가 "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 는 널 포인터를 반환하므로 적절한 대응이 가능합니다. 위 코드에서, MegaBoss 에 대해서는 Incinerate 함수만 호출합니다.

가비지 컬렉션

언리얼에서는 더이상 참조되지 않거나 명시적으로 소멸 예약시킨 UObject 를 주기적으로 정리하는 가비지 컬렉션(garbage collection) 스키마를 사용합니다. 엔진에서는 레퍼런스 그래프를 만들어 어느 오브젝트가 아직 사용중이고 어느 것이 고아가 되었는지를 알아냅니다. 이 그래프 루트에는 "루트 세트"라 지정된 오브젝트 세트가 있습니다. 어떤 오브젝트도 루트 세트에 추가시킬 수 있습니다.

가비지 컬렉션이 발생하면, 엔진은 루트 세트부터 시작해서 알려진 UObject 레퍼런스 트리를 검색하여 참조된 오브젝트를 전부 추적할 수 있습니다. 참조되지 않은 오브젝트, 즉 트리 검색에서 찾지 못한 것들은 더이상 필요치 않은 오브젝트라 가정하고 제거합니다.

여기에 한 가지 숨어있는 실용적인 부분이 있는데, 전형적으로는 살려두고자 하는 오브젝트 UPROPERTY 레퍼런스를 유지하거나, 그에 대한 포인터를 TArray 또는 다른 언리얼 엔진 컨테이너 클래스에 저장해야 합니다.

종종 액터와 액터 컴포넌트는 예외인데, 액터는 보통 자신이 속한 레벨처럼 루트 세트로 다시 링크되는 오브젝트에, 그리고 액터 컴포넌트는 액터 자체에 레페런싱되기 때문입니다. 액터는 자신의 Destroy 함수를 호출하여 명시적으로 소멸 마킹할 수 있는데, 이는 진행중인 게임에서 액터를 제거하기 위한 표준적인 방식입니다. 컴포넌트는 DestroyComponent 함수로 명시적으로 소멸시킬 수 있으나, 보통은 소유 액터가 게임에서 제거될 때 소멸됩니다.

 

 

출처
https://docs.unrealengine.com/4.27/ko/ProgrammingAndScripting/ProgrammingWithCPP/UnrealArchitecture/Objects/Optimizations/