언리얼의 Cast<T>는 UObject 베이스 클래스들을 동적으로 안전하게 형변환 해주는 함수입니다.
캐스팅에 실패하게 될 경우 nullptr을 반환해주게 됩니다. 캐스팅을 할 경우 null 체크는 필수겠죠?
해당 포스팅은 언리얼 Cast<T>의 동작을 하나 하나 분석해 정리하는데 목적을 두었습니다.
다양한 타입을 캐스팅해야 하는 캐스팅의 특성상 가독성은 우주로 날아가는 템플릿 기반으로 되어있어
잘못 파악한 경우도 있을 수 있습니다. 틀린 부분이 혹여나 있다면 댓글로 잡아주시면 정말 감사하겠습니다.
Cast함수는 내부적으로 TCastImpl 구조체의 인라인 함수인 DoCast 함수를 호출하게끔 되어있습니다.
// Dynamically cast an object type-safely.
template <typename To, typename From>
FORCEINLINE To* Cast(From* Src)
{
return TCastImpl<From, To>::DoCast(Src);
}
DoCast함수는 CastType에 따라 각기 다르게 형변환을 처리하게 됩니다.
enum class ECastType
{
UObjectToUObject,
InterfaceToUObject,
UObjectToInterface,
InterfaceToInterface,
FromCastFlags
};
UObject, Interface, FromCastFlags 총 3가지로 나뉜다고 볼 수있습니다.
여기서 Interface와 UObject로 구분되어 있는 이유는 언리얼의 UInterface와 대표적으로 AActor는
UObject를 최상위 부모로 두지만 그 이후 내려가는 뼈대가 아예 다르다고 볼 수 있기 때문입니다.
위 CastType 중 가장 생소한 것은 FromCastFlags 부분일텐데요.
해당 캐스팅은 미리 설정된 비트 플래그를 기반으로 변환을 수행할 수 있는지 검증합니다.
비트 플래그 기반이기에 캐스팅 속도가 가장 빠릅니다.
ECastType은 TGetCastType 템플릿 함수를 이용해 얻어지며, 해당 과정을 통해
어떤 CastType으로 DoCast 함수를 수행할지 결정하게 됩니다.
template <typename From, typename To, bool bFromInterface = TIsIInterface<From>::Value, bool bToInterface = TIsIInterface<To>::Value, EClassCastFlags CastClass = TCastFlags<To>::Value>
struct TGetCastType
{
#if USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_STRUCTARRAY
static const ECastType Value = ECastType::UObjectToUObject;
#else
static const ECastType Value = ECastType::FromCastFlags;
#endif
};
template <typename From, typename To > struct TGetCastType<From, To, false, false, CASTCLASS_None> { static const ECastType Value = ECastType::UObjectToUObject; };
template <typename From, typename To > struct TGetCastType<From, To, false, true , CASTCLASS_None> { static const ECastType Value = ECastType::UObjectToInterface; };
template <typename From, typename To, EClassCastFlags CastClass> struct TGetCastType<From, To, true, false, CastClass > { static const ECastType Value = ECastType::InterfaceToUObject; };
template <typename From, typename To, EClassCastFlags CastClass> struct TGetCastType<From, To, true, true , CastClass > { static const ECastType Value = ECastType::InterfaceToInterface; };
이렇게 소스만 넣어두면 당근 분석이…어렵겠죠?…
그렇기에 인터페이스 클래스인지 판단하는 조건은 무엇인지?
비트플래그로 캐스팅하는 조건은 무엇인지? 도 정리해보겠습니다.
마지막엔 해당 캐스트 타입으로 각기 어떻게 함수가 동작하는지 여부를 설명하겠습니다.
TIsInterface
아래는 해당 UObject가 IInterface 기반인지 확인하는데 사용되는 컴파일 타임에 확인하는 메타 함수입니다.
해당 함수는 다음과 같은 조건으로 IInterface 인지 검증을 하게 됩니다.
<1> UObject는 IInterface가 아닙니다.
<2> UClassType typedef 멤버가 없는 유형은 IInterface가 아닙니다.
<3> UClassType::StaticClassFlags에 CLASS_Interface가 설정되지 않은 타입은 IInterface가 아닙니다.
/**
* Metafunction which detects whether or not a class is an IInterface. Rules:
*
* 1. A UObject is not an IInterface.
* 2. A type without a UClassType typedef member is not an IInterface.
* 3. A type whose UClassType::StaticClassFlags does not have CLASS_Interface set is not an IInterface.
*
* Otherwise, assume it's an IInterface.
*/
template <typename T, bool bIsAUObject_IMPL = TPointerIsConvertibleFromTo<T, const volatile UObject>::Value>
struct TIsIInterface
{
enum { Value = false };
};
template <typename T>
struct TIsIInterface<T, false>
{
template <typename U> static char (&Resolve(typename U::UClassType*))[(U::UClassType::StaticClassFlags & CLASS_Interface) ? 2 : 1];
template <typename U> static char (&Resolve(...))[1];
enum { Value = sizeof(Resolve<T>(0)) - 1 };
};
첫번째 함수는 TPointerIsConvertibleFromTo는 C++의 네이티브 서브 클래스 기반 변형을 사용하여
포인터가 UObject* 유형인지 판단하고 내부적으로 여러 오버로드 함수를 생성하고 매개 변수를 사용하여
주어진 데이터 유형을 결정합니다.
두번째 함수는 플래그 타입과 ClassType을 통해 검증하게 됩니다.
실제로 UInterface 클래스의 헤더를 보면 매크로를 통해 플래그가 설정되게 되며, UClassType typedef 멤버를
가지고 있습니다.
class COREUOBJECT_API UInterface : public UObject
{
DECLARE_CLASS_INTRINSIC(UInterface, UObject, CLASS_Interface | CLASS_Abstract, TEXT("/Script/CoreUObject"))
};
class COREUOBJECT_API IInterface
{
protected:
virtual ~IInterface() {}
public:
typedef UInterface UClassType;
};
EClassCastFlags
/**
* Flags used for quickly casting classes of certain types; all class cast flags are inherited
*/
enum EClassCastFlags : uint64
{
CASTCLASS_None = 0x0000000000000000,
CASTCLASS_UField = 0x0000000000000001,
CASTCLASS_FInt8Property = 0x0000000000000002,
CASTCLASS_UEnum = 0x0000000000000004,
CASTCLASS_UStruct = 0x0000000000000008,
CASTCLASS_UScriptStruct = 0x0000000000000010,
CASTCLASS_UClass = 0x0000000000000020,
CASTCLASS_FByteProperty = 0x0000000000000040,
CASTCLASS_FIntProperty = 0x0000000000000080,
CASTCLASS_FFloatProperty = 0x0000000000000100,
CASTCLASS_FUInt64Property = 0x0000000000000200,
CASTCLASS_FClassProperty = 0x0000000000000400,
CASTCLASS_FUInt32Property = 0x0000000000000800,
CASTCLASS_FInterfaceProperty = 0x0000000000001000,
CASTCLASS_FNameProperty = 0x0000000000002000,
CASTCLASS_FStrProperty = 0x0000000000004000,
CASTCLASS_FProperty = 0x0000000000008000,
CASTCLASS_FObjectProperty = 0x0000000000010000,
CASTCLASS_FBoolProperty = 0x0000000000020000,
CASTCLASS_FUInt16Property = 0x0000000000040000,
CASTCLASS_UFunction = 0x0000000000080000,
CASTCLASS_FStructProperty = 0x0000000000100000,
CASTCLASS_FArrayProperty = 0x0000000000200000,
CASTCLASS_FInt64Property = 0x0000000000400000,
CASTCLASS_FDelegateProperty = 0x0000000000800000,
CASTCLASS_FNumericProperty = 0x0000000001000000,
CASTCLASS_FMulticastDelegateProperty = 0x0000000002000000,
CASTCLASS_FObjectPropertyBase = 0x0000000004000000,
CASTCLASS_FWeakObjectProperty = 0x0000000008000000,
CASTCLASS_FLazyObjectProperty = 0x0000000010000000,
CASTCLASS_FSoftObjectProperty = 0x0000000020000000,
CASTCLASS_FTextProperty = 0x0000000040000000,
CASTCLASS_FInt16Property = 0x0000000080000000,
CASTCLASS_FDoubleProperty = 0x0000000100000000,
CASTCLASS_FSoftClassProperty = 0x0000000200000000,
CASTCLASS_UPackage = 0x0000000400000000,
CASTCLASS_ULevel = 0x0000000800000000,
CASTCLASS_AActor = 0x0000001000000000,
CASTCLASS_APlayerController = 0x0000002000000000,
CASTCLASS_APawn = 0x0000004000000000,
CASTCLASS_USceneComponent = 0x0000008000000000,
CASTCLASS_UPrimitiveComponent = 0x0000010000000000,
CASTCLASS_USkinnedMeshComponent = 0x0000020000000000,
CASTCLASS_USkeletalMeshComponent = 0x0000040000000000,
CASTCLASS_UBlueprint = 0x0000080000000000,
CASTCLASS_UDelegateFunction = 0x0000100000000000,
CASTCLASS_UStaticMeshComponent = 0x0000200000000000,
CASTCLASS_FMapProperty = 0x0000400000000000,
CASTCLASS_FSetProperty = 0x0000800000000000,
CASTCLASS_FEnumProperty = 0x0001000000000000,
CASTCLASS_USparseDelegateFunction = 0x0002000000000000,
CASTCLASS_FMulticastInlineDelegateProperty = 0x0004000000000000,
CASTCLASS_FMulticastSparseDelegateProperty = 0x0008000000000000,
CASTCLASS_FFieldPathProperty = 0x0010000000000000,
};
언리얼은 UClass, APawn등과 같이 다양한 내장 유형에 ClassCastFlags를 할당하고
해당 클래스의 ClassCastFlags 속성에 저장하며 이 Flag는 상속이 되게 됩니다.
TCastFlags 템플릿 함수는 To 유형에 따라 해당 Flag를 얻어 각 유형에 대해 특화된
TCastFlags 함수 버전을 만들고 To 유형에 특화된 적이 없는 경우 CAST_CLASS_None으로
반환하여 해당 유형에 ClassCastFlags가 없음을 나타내게 됩니다. 이를 통해 플래그 기반 캐스팅을 하게 되는데요.
바로 아래에 후술하도록 하겠습니다.
#define DECLARE_ALL_CAST_FLAGS \\
DECLARE_CAST_BY_FLAG(UField) \\
DECLARE_CAST_BY_FLAG(UEnum) \\
DECLARE_CAST_BY_FLAG(UStruct) \\
DECLARE_CAST_BY_FLAG(UScriptStruct) \\
DECLARE_CAST_BY_FLAG(UClass) \\
DECLARE_CAST_BY_FLAG(FProperty)
자 이제 각 DoCast 함수의 동작에 대해서 정리해보도록 하겠습니다.
FromCastFlags
// This is the cast flags implementation
FORCEINLINE static To* DoCast( UObject* Src )
{
return Src && Src->GetClass()->HasAnyCastFlag(TCastFlags<To>::Value) ? (To*)Src : nullptr;
}
먼저 플래그 기반인데요! 바로 위에 정리한 방식으로 From 클래스의 ClassCastFlags를
확인하여 변환하려고 하는 To 클래스가 해당 플래그가 있는지 여부를 확인하고,
C스타일 명시적 캐스팅을 진행하게 됩니다.
UObjectToUObject
FORCEINLINE static To* DoCast( UObject* Src )
{
return Src && Src->IsA<To>() ? (To*)Src : nullptr;
}
가장 많이 쓰이게 되는 오브젝트 간 캐스팅입니다.
대표적으로 여러분이 특정 판정을 통해 액터 포인터 타입으로 반환받았을 경우 이를 특정 캐릭터 클래스로 캐스팅해
처리해야할 일이 있다거나 할때 해당 함수가 동작하게 됩니다.
해당 함수는 내부적으로 IsA라는 함수를 다시 한번 호출하게 되는데요.
IsA 함수는 내부적으로 IsChildOf라는 함수를 사용하게 됩니다.
#if USTRUCT_FAST_ISCHILDOF_COMPARE_WITH_OUTERWALK || USTRUCT_FAST_ISCHILDOF_IMPL == USTRUCT_ISCHILDOF_OUTERWALK
bool UStruct::IsChildOf( const UStruct* SomeBase ) const
{
if (SomeBase == nullptr)
{
return false;
}
bool bOldResult = false;
for ( const UStruct* TempStruct=this; TempStruct; TempStruct=TempStruct->GetSuperStruct() )
{
if ( TempStruct == SomeBase )
{
bOldResult = true;
break;
}
}
}
해당 함수는 반복문을 돌면서 To의 UClass와 동일한 클래스가 확인될때까지
From의 부모 클래스를 GetSuperStruct()함수를 통해 받아와 계속 비교를 통해 같은 클래스를 얻게된다면,
성공처리로 판단해 최종적으로 C스타일 명시적 캐스팅을 진행하게 됩니다.
UObjectToInterface
언리얼은 UObject와 IInterface도 서로 변환할 수 있습니다.
저는 구현 간 AI나 플레이어 캐릭터 클래스에 각기 다른 IInterface 클래스를 다중 상속하는 형태로
구현을 많이 해왔는데요. 잘쓰면 정말 좋은 기능이니 도큐먼트 등을 참고해서
언리얼 Interface 클래스에 대해서 알아보시는 것도 추천드립니다.
FORCEINLINE static To* DoCast( UObject* Src )
{
return Src ? (To*)Src->GetInterfaceAddress(To::UClassType::StaticClass()) : nullptr;
}
To::UClassType::StaticClass( )는 Interface에 해당하는 UInterface의 UClass로,
UClass의 Interfaces 속성은 이 클래스에서 구현된 모든 Interface를 최종 Interface의
상위 클래스를 포함하지 않는 구조를 가지고 있습니다.
따라서 Interfaces를 통해 UObject와 Interface 간의 연결을 설정할 수 있습니다.
GetInterfaceAddress함수는 다음과 같이 구현되어 있습니다.
void* UObjectBaseUtility::GetInterfaceAddress( UClass* InterfaceClass )
{
void* Result = NULL;
if ( InterfaceClass != NULL && InterfaceClass->HasAnyClassFlags(CLASS_Interface) && InterfaceClass != UInterface::StaticClass() )
{
// Script interface
if ( !InterfaceClass->HasAnyClassFlags(CLASS_Native) )
{
if ( GetClass()->ImplementsInterface(InterfaceClass) )
{
// if it isn't a native interface, the address won't be different
Result = this;
}
}
// Native interface
else
{
for( UClass* CurrentClass=GetClass(); Result == NULL && CurrentClass != NULL; CurrentClass = CurrentClass->GetSuperClass() )
{
for (TArray<FImplementedInterface>::TIterator It(CurrentClass->Interfaces); It; ++It)
{
// See if this is the implementation we are looking for, and it was done natively, not in K2
FImplementedInterface& ImplInterface = *It;
if ( !ImplInterface.bImplementedByK2 && ImplInterface.Class->IsChildOf(InterfaceClass) )
{
Result = (uint8*)this + It->PointerOffset;
break;
}
}
}
}
}
return Result;
}
정말 복잡한 구조를 가지고 있는데요. 간단히 설명 드리자면 UObject의 모든 SuperClass를
순회하면서 Interface 정보를 모으고 각 Interface에 대해 Interface의 SuperClass를 순회하여
To Interface의 UClass와 동일한지 확인합니다.
모든 언리얼 제공 캐스팅 연산자 중에서 가장 무겁고 솔직히 사용할 일이 없을 것 같다고
저 개인적으로는 생각합니다.
또한 Interface는 이전에 설명드렸듯이 다중 상속을 통해 구현되며 실제로 언리얼 인터페이스의
포인터 주소가 Offset을 가져 UObject의 주소에 추가되어 있는 방식입니다.
InterfaceToUObject
template <typename From, typename To>
struct TCastImpl<From, To, ECastType::InterfaceToUObject>
{
FORCEINLINE static To* DoCast( From* Src )
{
To* Result = nullptr;
if (Src)
{
UObject* Obj = Src->_getUObject();
if (Obj->IsA<To>())
{
Result = (To*)Obj;
}
}
return Result;
}
}
Interface의 _getUObject() 함수를 통해 UObject를 얻은 후 UObjectToUObject 방식의
Cast를 진행하게 됩니다.
InterfaceToInterface
FORCEINLINE static To* DoCast( From* Src )
{
return Src ? (To*)Src->_getUObject()->GetInterfaceAddress(To::UClassType::StaticClass()) : nullptr;
}
마지막으로 Interface To Interface는 먼저 IInterface를 UObject로 변환한 다음
UObjectToInterface의 캐스팅 방식을 사용하게 됩니다.
언리얼은 이외에 언리얼 G.C가 Mark&Sweep 구조를 가지고 있지만 내부적으로는 참조
카운팅 방식으로 이 마킹 과정을 진행하기에 약 참조를 위해 제공되는 TWeakObjectPtr 및
TSubClassOf도 Cast를 지원합니다. 이런 캐스팅들이 최종적으로 오늘 정리한 Cast<T>를
사용한다는 것이 포인트입니다.
항상 노션을 통해서 정리하다가 노션에서 옮겨와 간만에 블로그 포스팅을 해보네요.
도움이 되는 포스팅이길 바랍니다...
'Programming > Unreal Engine' 카테고리의 다른 글
Ue4 Camera Shake (언리얼 카메라 쉐이크) (0) | 2019.10.21 |
---|---|
Ue4 Change Class Names (언리얼 클래스 명 수정) (0) | 2019.10.21 |