언리얼의 UClass개념
UClass는 언리얼c++에서 사용되는 자료형이다.
언리얼 c++로 프로그래밍을 하면 다음과 같은 매크로들을 이용해서
언리얼 클래스를 작성한다.
...
#include "ABasePlayableCharacter.generated.h"
UCLASS(Abstract)
class CREDERE_API ABasePlayableCharacter : public ACharacter
{
GENERATED_BODY()
public:
ABasePlayableCharacter();
...
private:
UPROPERTY()
TObjectPtr<class ANavigation> Navigation;
...
UFUNCTION()
void Foo();
,,,
};
위와 같은 형식으로 UCLASS, GENERATED_BODY매크로를 이용해서
클래스를 선언하면 언리얼 소스코드가 컴파일될때 언리얼 헤더 툴이 매크로를 분석해서 언리얼 클래스에 대한 메타데이터(UPROPERTY매크로 선언 변수, UFUNCTION매크로 선언 함수들의 이름,타입 그리고 클래스의 타입과 상속구조까지 전반적인 정보들)를 생성한다. 이 정보들은 언리얼 프로젝트의 Intermediate폴더에 저장된다.
위 사진과 같이 모든 UCLASS선언 클래스들은 컴파일 시 각각의 .generated.h, .gen.cpp 파일이 Intermediate폴더에 추가로 생성된다.
이렇게 생성된 UCLASS정보는 다음과 같은 코드로 정보를 얻을 수 있다.
UClass* classInfo = (UObject객체 포인터)->GetClass();//런타임 타입 조회
(UObject클래스)::StaticClass();//컴파일 타임 조회
위 코드를 응용해서 어떤 임의의 객체가 특정 클래스 타입의 자손인지 검사하는 코드도 작성가능하다.
if(parentActor->GetClass()
->IsChildOf(ABasePlayableCharacter::StaticClass())
{
//parentActor 객체가 ABasePlayableCharacter의 자손인 경우
}
UClass객체는 게임이 시작할 때 클래스마다 1개만 생성되고 게임이 종료될때 메모리가 해제된다. 그렇기 때문에, 특정 클래스의 인스턴스에서 GetClass()함수를 호출한다면 (특정클래스)::StaticClass()와 결과가 항상 같다.
BP에디터에서 자연스럽게 검색하는 함수들 모두 사실 UCLASS에 저장된 정보를 출력해주고 있는거다. 예를 들어서 GameplayAbility를 상속한 블루프린트에서는 다음과 같은 노드 호출이 가능하다.
위와 같은 일이 가능하려면 언리얼 에디터가 이미 컴파일된 ApplyGameplayEffectToTarget이라는 함수의 이름, 파라미터,리턴 타입을 미리 알고 있어야 가능하다. 그 정보는 언리얼 엔진에서 아래 코드와 같이 선언이 되어있다.
/** Apply a gameplay effect to a Target */
UFUNCTION(BlueprintCallable, Category = Ability, DisplayName = "ApplyGameplayEffectToTarget", meta=(ScriptName = "ApplyGameplayEffectToTarget"))
TArray<FActiveGameplayEffectHandle> BP_ApplyGameplayEffectToTarget(FGameplayAbilityTargetDataHandle TargetData, TSubclassOf<UGameplayEffect> GameplayEffectClass, int32 GameplayEffectLevel = 1, int32 Stacks = 1);
UFUNCTION이라는 매크로로 UCLASS의 멤버 함수임을 선언하고
BlueprintCallable이라는 키워드를 줘서 '블루프린트에서 호출 가능한 함수' 임을 UCLASS에 저장한다.
게임 프로그래밍에서는 Cast를 할 일이 빈번하다.
대표적으로 충돌처리가 있다.
void ATriggerDetector::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
ABasePlayableCharacter* playerChar = Cast<ABaseplayableCharacter>(OtherActor);
if(playerChar)
{
//플레이어 캐릭터에 대한 충돌처리
}
...
}
위 코드는 Capsule Component가 포함된 액터에서 충돌이벤트 처리를
하는 코드인데, 충돌 시 상대방의 정보는 AActor*
로 받아온다.
그 이유는 레벨 상에 존재하는 모든 충돌체들은 AActor의 자손이기 때문이다.
프로그래머는 위 함수를 구현하면서 특정 클래스로의 DownCasting을 진행해야지 특정 클래스에 맞는 충돌처리를 실행시켜 줄 수 있다(AActor의 virtual function이 아니라 ABasePlayableCharacter에 작성한 로직을 실행시키는 경우).
이 Cast
함수가 바로 UCLASS정보를 이용해서 구현되어 있다.
Cast
함수의 구현부를 보면 IsA
함수를 이용하는데,
FORCEINLINE bool IsA(const UClass* SomeBase) const
{
checkfSlow(SomeBase, TEXT("IsA(NULL) cannot yield meaningful results"));
if (const UClass* ThisClass = GetClass())
{
return ThisClass->IsChildOf(SomeBase);
}
return false;
}
IsA함수는 이와 같이 GetClass()함수로 UClass정보를 가져와서
자식 클래스의 인스턴스가 맞는지 검사하는 동작을 수행한다.
위와 같은 기능이 기존 c++에 있기는 하다. 바로 dynamic_cast이다.
c++코드를 RTTI(Run Time Type Information)옵션을 켜고 컴파일하면 사용할 수 있는데, 이 방식은 가상함수 정보가 저장되는 vTable에 현재 객체의 타입정보를 추가로 저장하는 방식으로 구현된다.
코드는 다음과 같다.
ABasePlayableCharacter* playerChar = dynamic_cast<ABaseplayableCharacter*>(OtherActor);
if(playerChar)
{
//플레이어 캐릭터에 대한 충돌처리
}
문법은 동일하지만, 언리얼 방식과 다른 점은 언리얼 방식은 별도의 UCLASS객체를 포인터를 객체마다 저장하는 방식이지만 c++방식은 객체의 vTable에 데이터를 추가 저장하는 방식이라는 것이다. 이에 따른 성능차이도 존재한다.
그 근거로 인터넷에서 다음과 같은 정보들을 찾아봤다.
https://forums.unrealengine.com/t/dynamic-casting-best-practice/126339/9
->언리얼 포럼글에서 언리얼은 별도의 리플렉션 시스템을 이용하기에 dynamic_cast대비 성능이점이 있다는 정보
https://koreanfoodie.me/1169
-> 언리얼 Release빌드에서 O(1)으로 IsChildOf연산을 할 수 있다는 정보
언리얼 엔진은 성능을 중요시하는 게임 엔진이지만 성능부하 가능성이 있는 Garbage Collecting을 사용한다. GC의 대상이 되기 위한 조건은 다음과 같다
- Garbage Collector가 모든 객체에 대한 포인터를 알고 있어야 함
- 각각의 객체가 참조하고 있는 객체 정보를 알아야 함
1번조건은 언리얼 객체를 생성할 때 GC에 등록하는 방법으로 만족시킬 수 있는데, 2번조건을 위해서는 UCLASS정보가 꼭 필요하다.
그 이유는 특정 Class가 참조하고 있는 다른 Class정보가 있어야 GC을 위한 그래프 자료구조를 구성할 수 있고 그 정보를 UCLASS에서 얻어오기 때문이다.