UE5 BlueprintClass with '_C'

에크까망·2024년 9월 30일

들어가기 전에

  • 단순화를 위해 문체를 단정적으로 작성하였으나, 이는 어디까지나 개인적인 의견임을 양해 부탁드립니다.
  • 본문과 관련하여 오류 지적이나 의견이 있으시다면 댓글로 남겨주시면 감사하겠습니다.
  • 글 작성 시점은 2024/09 입니다.
  • 글 작성 기준은 UE5.4.4 입니다.
  • UClass ReflectionBuild Configuration 포스팅을 먼저 확인하셨다는 전제하에 작성되었습니다.

들어가며

이번 포스팅에서는 언리얼 엔진에서 Blueprint로 생성한 Class가 어떻게 다루어지는지 간략히 살펴보고, Class를 로딩할 때 왜 '_C' 를 붙여야 하는지에 대해 설명드리고자 합니다.

Class

Diagram

UBlueprint

언리얼 엔진 Editor에서 UObject 계열을 상속받아 블루프린트를 생성하면, 내부적으로는 'UBlueprint' 객체가 생성됩니다. 그렇다면 UBlueprint 내부에는 어떤 내용이 포함되어 있을까요?

우선 사용자가 구성한 'Drawing' 노드들과 각종 주석 등이 포함되어 있으며, 이와 함께 UBlueprint를 통해 생성된 'UClass' 역시 포함됩니다.

UBlueprint에서 UClass를 가져오기 위해서는 아래 함수를 사용할 수 있습니다.

UClass* UBlueprintEditorLibrary::GeneratedClass(UBlueprint* BlueprintObj)  
{  
    if (BlueprintObj)  
    {       
        return BlueprintObj->GeneratedClass->GetAuthoritativeClass();  
    }    

    return nullptr;  
}

C++에서 Blueprint Class에 추가된 변수나 함수에 접근하기 위해서는 Reflection 기능을 사용해야 합니다. 이에 대해서는 'UClass Reflection' 포스팅을 참고해주시기 바랍니다.

BlueprintClass 를 Loading 할때 왜 '_C' 를 붙여야 하지?

사실상 본 포스팅은 이 부분을 설명드리기 위해 작성되었습니다. 패키징 이후 C++에서 Class 로딩이 되지 않는 문제를 검색해 보면, 단순히 '_C를 붙여야 한다'는 식의 설명만 있는 경우가 많습니다.

명확한 설명이 있는 자료도 있겠지만, 여기서는 직접 그 이유를 이해해보는 방향으로 설명드리겠습니다. 실제로 그 이유는 위의 'Class' 섹션에 이미 나타나 있습니다.

아래 테스트는 UObject를 상속한 UMyTest를 생성하고, 이를 기반으로 Blueprint를 만든 상태에서 진행됩니다. 해당 Blueprint의 경로는 /Game/MyBlueprint/B_Test 입니다.

List In Package

아래 코드는 B_Test 패키지 내부의 오브젝트 목록을 출력합니다. DebugGame Editor와 쿠킹 이후 DebugGame 환경에서 각각 실행해보시기 바랍니다.

void PrintObjectsInPackage()  
{  
    UPackage* Package = LoadPackage(nullptr, TEXT("/Game/MyBlueprint/B_Test"), LOAD_None);  
    if (Package == nullptr)  
    {       
        UE_LOG(LogTemp, Warning, TEXT("유효하지 않은 패키지입니다."));  
        return;  
    }  
    // UPackage 안에 포함된 모든 UObject를 순회하며 출력  
    TArray<UObject*> ObjectsInPackage;  
    GetObjectsWithOuter(Package, ObjectsInPackage);  

    if (ObjectsInPackage.Num() == 0)  
    {       
        UE_LOG(LogTemp, Warning, TEXT("패키지에 오브젝트가 없습니다."));  
        return;  
    }  
    // 오브젝트 출력  
    for (UObject* Object : ObjectsInPackage)  
    {       
        if (Object)  
        {          
            UE_LOG(LogTemp, Log, TEXT("오브젝트 이름: %s, 클래스: %s"), *Object->GetName(), *Object->GetClass()->GetName());  
        }    
    }
}

DebugGame Editor

[2024.09.30-05.25.14:977][498]LogTemp: 오브젝트 이름: PackageMetaData, 클래스: MetaData
[2024.09.30-05.25.14:977][498]LogTemp: 오브젝트 이름: B_Test_C, 클래스: BlueprintGeneratedClass
[2024.09.30-05.25.14:978][498]LogTemp: 오브젝트 이름: B_Test, 클래스: Blueprint
[2024.09.30-05.25.14:978][498]LogTemp: 오브젝트 이름: Default__B_Test_C, 클래스: B_Test_C
[2024.09.30-05.25.14:978][498]LogTemp: 오브젝트 이름: SKEL_B_Test_C, 클래스: BlueprintGeneratedClass
[2024.09.30-05.25.14:978][498]LogTemp: 오브젝트 이름: Default__SKEL_B_Test_C, 클래스: SKEL_B_Test_C
...

다른 오브젝트는 제외하고, 아래 두 항목에 집중해주시기 바랍니다.

  • B_Test_C : BlueprintGeneratedClass
  • B_Test : Blueprint

B_Test는 Editor에서 생성한 Blueprint 자체이며, 우리가 실제로 사용하려는 것은 B_Test_C 입니다. 이 객체의 타입은 BlueprintGeneratedClass이며, Blueprint를 컴파일하여 생성된 UClass의 이름이 바로 B_Test_C입니다.
따라서 '_C'를 붙여야 하는 이유는 여기에 있습니다.

이후 Contents Cooking을 수행한 뒤, DebugGame 환경에서 다시 확인해보겠습니다.

DebugGame

[2024.09.30-05.32.11:988][  0]LogTemp: 오브젝트 이름: B_Test_C, 클래스: BlueprintGeneratedClass
[2024.09.30-05.32.11:988][  0]LogTemp: 오브젝트 이름: Default__B_Test_C, 클래스: B_Test_C

여기서는 B_Test가 존재하지 않습니다. 이는 Cooking 과정이 Runtime에 필요한 형태로 Asset을 가공하기 때문이며,
B_Test_C: BlueprintGeneratedClass를 생성하기 위한 원본 데이터인 B_Test: Blueprint는 더 이상 필요하지 않기 때문입니다.

Load Blueprint

다음으로, Blueprint를 직접 로딩하는 코드를 테스트해보겠습니다.

void TestBlueprint()  
{  
    UObject* BlueprintAsset = StaticLoadObject(UObject::StaticClass(), nullptr, TEXT("/Game/MyBlueprint/B_Test.B_Test"));  
    if (BlueprintAsset)  
    {       
        UBlueprint* LoadedBlueprint = Cast<UBlueprint>(BlueprintAsset);  
        if (LoadedBlueprint && LoadedBlueprint->GeneratedClass)  
        {          
            UMyTestObj* myTestObj = Cast<UMyTestObj>(LoadedBlueprint->GeneratedClass->GetDefaultObject());  
            if (myTestObj)  
            {             
                UE_LOG(LogTemp, Warning, TEXT("To UMyTestObj 캐스팅 성공"));  
            }          
            else  
            {  
                UE_LOG(LogTemp, Warning, TEXT("To UMyTestObj 캐스팅 실패"));  
            }       
        }       
        else  
        {  
            UE_LOG(LogTemp, Warning, TEXT("블루프린트 로드 실패 또는 클래스 없음"));  
        }    
    }    
    else  
    {  
        UE_LOG(LogTemp, Warning, TEXT("블루프린트 로드 실패"));  
    }    
}

DebugGame Editor

[2024.09.30-06.05.25:565][  0]LogTemp: Warning: To UMyTestObj 캐스팅 성공

예상대로 정상적으로 동작합니다.

DebugGame

[2024.09.30-06.09.38:694][  0]LogTemp: Warning: 블루프린트 로드 실패

패키지 내에 해당 오브젝트가 존재하지 않으므로 로딩에 실패합니다.

Load Blueprint Generated Class

이번에는 BlueprintGeneratedClass를 직접 로딩해보겠습니다.

void AEkUe54GameMode::TestBlueprintClass()
{
    UClass* BlueprintClass = Cast<UClass>(StaticLoadObject(UObject::StaticClass(), nullptr, TEXT("/Game/MyBlueprint/B_Test.B_Test_C")));

    if (BlueprintClass)
    {
        if (BlueprintClass->IsChildOf(UMyTestObj::StaticClass()))
        {
            UE_LOG(LogTemp, Warning, TEXT("Class UMyTestObj 확인"));
        }
        else
        {
            UE_LOG(LogTemp, Warning, TEXT("Class UMyTestObj 확인 실패"));
        }
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("로딩 실패"));
    }
}

DebugGame Editor

[2024.09.30-06.21.44:238][  0]LogTemp: Warning: Class UMyTestObj 확인

DebugGame

[2024.09.30-06.24.04:253][  0]LogTemp: Warning: Class UMyTestObj 확인

두 환경 모두 정상적으로 동작합니다.

여기서 문제

위 코드에서 아래와 같이 변경하면

UClass* BlueprintClass = Cast<UClass>(StaticLoadObject(UMyTestObj::StaticClass(), nullptr, TEXT("/Game/MyBlueprint/B_Test.B_Test_C")));
[2024.09.30-06.25.36:493][  0]LogTemp: Warning: 로딩 실패

로딩에 실패하게 됩니다. 그 이유를 직접 고민해보시고, 이해가 어려우시다면 UClass Reflection 포스팅의 Class Diagram을 다시 참고해보시기 바랍니다.

profile
Game Client Programmer

0개의 댓글