저는 TSubClassOf라는 메서드를 이해하기 위해 많은 길을 돌아왔습니다. 

비록 저는 이해가 안되는 것이 많아 역순으로 알아봤지만

이 글을 보는 사람들은 정순으로 보시면 좋을 것 같습니다. (미래의 나 포함)

 

중요!!!) 공부목적으로 만든 블로그입니다. 틀린 내용이 있으면 알려주세요. 감사합니다 

 

가장먼저 클래스와 인스턴스에 대해 알아보겠습니다 .


1.  클래스 

    - 설계도 ->실체가 없으며  같은 종류의 객체들이 어떤 공통된 특성과 행동을 가질지 정의합니다.

2. 인스턴스

    - 인스턴스는 클래스라는 설계도를 바탕으로 메모리에 생성된 실제 객체 입니다.



그러면 왜 클래스와 인스턴스를 구별하는것인가??라는 생각이 들었습니다.
그리고 제가 내린 결론은 다음과 같았습니다! 

1. 설계와 구현을 분리시킵니다.
 - 이는 설계도를 바탕으로 여러개의 인스턴스를 만들게 되어, 코드의 재사용성과 유지보수성을 높일 수 있습니다.

2. 메모리 효율성 : 
 - 클래스는 단지 정의만 제공하므로 메모리 상에 공간을 차지 하지 않지만,
   인스턴스는 메모리에 할당되어 실제 데이터를 저장하게 됩니다.

3. 동작의 독립성 : 
  - 하나의 클래스로부터 만들어진 여러 인스턴스는 각자 독립적인 상태를 가지므로,
    서로 다른 데이터 값을 유지할 수 있습니다.

 

예를들어 인스턴스반환형인 AActorToSpawn* 은 메모리에 이미 생성된 객체를 직접 참조하고 사용할 때 사용합니다 .


또, 인스턴스는 실제로 런타임에 해당 객체가 있어야 정상 작동한다는 점과

메모리의 관점에서 좀 더 생각을 해보면서 , 
TsubClassOf와 StaticClass를 왜 사용하는지를 알게 되었습니다.

동적으로 런타임도중에 인스턴스를 생성하며, 안정성과 메모리측면에서 효율적이라는 생각이 들었습니다.
이걸 알게 되었으니!! 본격적으로 TsubClassOf와 StaticClass 그리고 UClass* 라는 것을 알아보러 가겠습니다 !

 

가장먼저  UClass란  ?


UClass* 는 클래스 자체를 가리킵니다.

다시말해 “클래스의 메타데이터(리플렉션 정보)”를 보관하는 객체라고 볼 수 있습니다.
객체를 생성할 때 클래스 정보를 기반으로 생성할 수 있고,
객체를 생성하지 않고도 클래스 정보를 참조하거나 사용할 수 있습니다.
그리고 모든 클래스를 가리킬 수 있으며, 블루프린트에 올리면 아래의 사진과 같이 모든 객체가 나오게 됩니다.


Uclass*를 만일 특정 클래스 계층에만 사용하려고 한다면 타입검사를 별도로 하여야하는데,
이는  잘못된 클래스에 사용한다면 런타임 에러가 발생할 수 있음을 의미합니다.

 // TODO : Uclass는 하드레퍼런스이고, 이걸 메모리에 올리는데 실질적인 활용은 런타임에 하기 때문에?

 // 이부분 정리가 필요해, 특히 나는 단순히 Uclass가 무슨 클래스를 가르키는지에 대한 정보를 런타임에서 수행하기 때문 // 이라고 생각했는데 하드레퍼런스라는걸 알게되고 좀 더 정리가 필요하겠어

여러 클래스를 다뤄야하는 경우에는 UClass* , 특정계층이라면 TSubClassOf

 

//.h
UClass* ActorToSpawnClass;

//.cpp
AActorToSpawn* tmp = GetWorld()->SpawnActor<AActorToSpawn>(ActorToSpawnClass,tmpLocation, tmpRotation);

으로 코드를 작성하면 블루프린트에서 다음과 같이 설정할 수 있게 됩니다.

리플렉션 등록후 소환할수있는 액터의 목록을 보면 다음과 같습니다 !

 

 

 

2. StaticClass란 ? 

 StaticClass()는 특정 클래스의 UClass 객체의 주소를 반환하는 "정적"메서드 입니다.
 - 모든 UClass()로 선언된 클래스는 자동으로 StaticClass() 메서드를 생성합니다.
 - 다시 말해 특정 클래스에 대한 UClass* 를 반환합니다.
 - 클래스 메타 데이터를 통해 런타임 도중 클래스 타입을 확인하거나 인스턴스를 생성합니다.
 - 런타임에서 특정 로직을 작성하는데 필요합니다.
 - Uclass * 의 반환형을 받기 위해 이용하는 함수입니다 !

//TODO : 이거가 지금 런타임에 수행되는거고 언리얼 엔진에서 공식 문서를 보면 STATIC으로 할당받은 클래스와

// SubclassOF로 정의한 함수끼리의 비교가 런타임 도중에 체크가 된단말이지 짐작은 가긴하는데 Uclass가 반환형이기 때문에 해당 Uclass에 담긴 클래스의 정보가 무엇인지는 런타임 도중에 체크가 된다는 말인것 같아 이부분 확실하게 집고 넘어가자 

사용방법은 다음과 같습니다 .

아, 시작하기에 앞서서 저는 ActorToSpawnClass2를 AActorToSpawn에 연결해줬습니다. 

그리고 사실 다음과 같이 리플렉션을 달아놓으면 이 로직을 작성할 필요가 없습니다. 

UPROPERTY(EditAnywhere, Category = "Spawn")
UClass* ActorToSpawnUclass2;

왜냐하면 UClass* AActorToSawnClass2 ; 에 리플렉션을 달아놓으면 블루프린트에서 전부 고를수 있기 때문입니다.

 

작성한 이유는 제 자신의 공부를 위해 적었습니다.

UClass* ActorToSpawnClass2 = AActorToSpawn::StaticClass();
AActorToSpawn* tmp = GetWorld()->SpawnActor<AActorToSpawn>(ActorToSpawnClass2,tmpLocation, tmpRotation);

여기에서 AActorToSpawn의 클래스 주소를 받아와서 UClass* 형태로 받아온것을 볼 수 있습니다.

이유는 해당 소환될 액터는 동적으로 생성되기 때문에  클래스의 메타데이터가 필요하기 때문입니다.

 

 

cpp파일 생성

 

잠시 부연설명을 하면 StaticClass에서 UClass주소를 받아왔고, 추가설정이 없기 때문에 

CPP파일의 원본 액터가 소환되는것을 볼 수 있습니다 .

 

그렇다면 블루프린트를 생성시키려면 어떻게 해야할까요?

UClass* ActorToSpawnClass2 = AActorToSpawn::StaticClass();

ActorToSpawnClass2 = LoadClass<AActorToSpawn>(nullptr, TEXT("/Game/Blueprints/Bp_ActorToSpawn.Bp_ActorToSpawn_C"));

AActorToSpawn* tmp = GetWorld()->SpawnActor<AActorToSpawn>(ActorToSpawnClass2,tmpLocation, tmpRotation);

 

 

그리고 Class2에 동적으로 블루프린트 경로를 적어줘, BP파일을 불러왔습니다

StaticClass()는 "정적 메서드"로 기본적으로 C++ 클래스의 UClass*를 반환합니다.

그리고, 동적으로 블루프린트 클래스를 로드할 경우 LoadClass 등을 이용합니다.

 

 

 

BP를 동적으로 로드

 

BP를 동적으로 로드해와서 블루프린트 액터를 소환에 성공하였습니다 .

 

StaticClass를 요약해서 범용적으로 사용될 수 있지만 특정 로직에 의해 "런타임" 에러가 발생할 수 있습니다.
또, 클래스가 동적 유연성이 떨어지게 될거라 생각합니다.

왜냐하면 특정 클래스의 UClass 객체를 반환하므로, 항상 동일한 클래스가 사용되기 때문입니다.

물론 리플렉션을 달아놓는다면 블루프린트상에서 클래스를 설정할 수는 있지만

동적인 단계 예를들어 라이프사이클 같은 곳에서 사용하기 위해서는

로직에 분기점이 필연적으로 많아지게 될것이라는게 제 생각입니다.

그리고 무엇보다 에러가 런타임도중에 생기는게 자꾸 걸립니다.

 

이 StaticClass는 언리얼엔진 리플렉션과도 연관이 많은 친구라고 알고 있습니다.
언리얼엔진은 UCLASS에 대해 객체를 생성하고, 이를 통해 리플렉션 시스템에서 
클래스의 메타 데이터를 사용할 수 있게합니다.

 

 

아 그리고 저는 위에서 Bp
Bp를 불러오기 위해 추가적인 작업을 하는 것과 분기점을 많이 작성해야하는 것이 

매우 불편해보였습니다. 그래서 사용하는 것이 TSubClassOf라고 생각합니다.


TSubclassOf란?

특정 클래스의 계층 구조만 허용하는 템플릿 래퍼입니다.

사용방법은 다음과 같습니다.

TSubClassOf<AActorToSpawn> Actor = 
    AActorToSpawn의 계층 구조 (본인포함 자식클래스만) :: StaticClass();



잠시 삼천포로 빠져서 , 해당 AActorToSpawn  StaticClass()를  헤더파일에서 사용하기 편할까요?
저는 아니라고 생각합니다. 의존성에 관련한 문제를 헤더파일을 통해 많이 겪어보면서
우리가 보통 스폰할 액터는 액터와는 다른 클래스에서 사용하게됩니다.
그렇다면 우리는 헤더파일에 전방선언을 해줘야 의존성이 줄어들게 됩니다.
하지만 헤더파일에 class AActorToSpawn을 선언을 해주고 
UClass* ActorToSpawnClass = AActorToSpawn::StaticClass();로직을 작성했다고 치면
에러가 발생할 것입니다.
헤더파일에서는 AActorToSpawn :: StaticClass()를 사용할 수 없습니다.

왜냐하면 헤더 파일에서 직접 사용하게 되면 불필요한 의존성을 주입해야만 합니다.

따라서 StaticClass대신에 전방선언은 포인터 및 참조만 사용할 수 있습니다.

그래서 헤더파일에 선언할때는 TSubClassOf<AActorToSpawn> Actor ; 만 선언해주고

StaticClass는 Cpp에서 구현해야한다고 생각합니다.



다시 본론으로 돌아가서 
저는 보통 언리얼 에디터상에서 ActorToSpawn.cpp의 파일을 
블루프린트로 래핑해 BP_ActorToSpawn을 만들어 값을 편하게 입력합니다.
그렇기 때문에 저에게 있어 소환하고 싶은 액터는 ActorToSpawn.cpp이 아닌 BP_ActorToSpawn일것입니다.
BP_ActorToSpawn은 ActorToSpawn.cpp를 상속받은 자식이기 때문에 ,
BP_ActorToSpawn를 동적으로 소환하기 위해서는 클래스 메타데이터가 필요합니다. 

왜냐하면 런타임에서 실행되는 에디터에게 컴파일단계에서 알려줘야하기 때문입니다



따라서 TSubClassOf를 사용하는 이유는 다음과 같다라고 생각이 들었습니다.


1. 상속받은 자식을 참고하기 편하다 (블루프린트 등등) 

 

2. TSubClassOf는 내부적으로 Ulass*를 저장하지만 실제 객체를 생성하거나 관리하는 역할은 하지 않습니다.

   -  내부적으로 UClass*를 보관하는 타입 안전 래퍼
   - 컴파일단계에서 클래스의 계층구조가 아니라면 컴파일 에러가 발생합니다. 
   - 런타임단계가 아닌 컴파일단계에서 오류를 확인할 수 있어 타입 안전성이 향상됩니다.

   

3. 동적으로 액터를 소환하고 싶을 때, 에디터한테 해당 클래스의 메타데이터를 알려줘야합니다.
   -> 그래서 TSubClassOf로 클래스의 메타데이터를 보내줍니다.

   

 

4.  TSubClassOf는 실제 객체를 생성하거나 관리하는 역할은 하지 않습니다.
예를 들어서  World->SpawnActor라는 메서드를 통해 액터를 소환하고 , TSubClass에서 클래스 타입을 참조할 수 있게 
메타데이터를 보내줍니다

 

5. 컴파일 타임과 런타임에 걸쳐 전달된 UClass*를 관리해 줍니다.

  - 컴파일 단계에서 타입 안전성을 보장하며, 런타임 도중 UClass*를 전달 받을 수 있습니다. 

  -  TSubclassOf 함수는 UClass* 의 클래스 메타 데이터를 다루는 함수

 

6. 또한, 계층의 자식들만 참고하겠다는 것을 코드를 통해 보여주므로, 가독성을 높여줍니다.

 

//TODO : 좀 알아봐야할것

1. 하드 레퍼런스

2. 런타임 도중 uclass*를 전달받아서 비교가 그때부터 가능하다는건지

그니깐 엔진이 UClass라는 클래스 메타데이터를 구체적으로 해석하는 시점이 RUNTIME이라는건가?

3.NewObject

    UObject 기반의 객체를 생성할 때 사용하는 함수로, 주로 컴포넌트나 UObject 파생 클래스 생성에 적합합니다 

  SpawnActor

   액터를 생성할 때는 일반적으로 UWorld::SpawnActor를 사용합니다. 액터는 월드에 배치되어야 하므로 NewObject보다     SpawnActor가 적합합니다.

 

4. 보통 액터를 생성할 때는 NewObject보다는 UWorld::SpawnActor를 사용하는 것이 일반적입니다. NewObject는 UObject 기반의 객체(예: 컴포넌트나 기타 UObject 파생 클래스)를 생성할 때 주로 사용됩니다.

5. 하드 vs. 소프트 레퍼런스: UClass*와 TSubclassOf는 기본적으로 하드 레퍼런스

    그렇다면 TSoftClassptr??

 

 

 

 

예시 )

//.h
UPROPERTY(EditAnywhere, Category = "Spawn")
TSubclassOf<AActorToSpawn> ActorToSpawnClass;
//.cpp
AActorToSpawn* tmp = GetWorld()->SpawnActor<AActorToSpawn>(ActorToSpawnClass,tmpLocation, tmpRotation);

로 코드를 작성하면 블루프린트에서 다음과 같은 사진을 확인 할 수 있게 됩니다 !



이제 제가 위에 올린 사진을 보면 이해가 잘 될것입니다 !
각자의 장단점을 잘 알아두고 적재적소에 사용하도록 연습해야겠습니다 !

지금까지의 생각으로는 

타입안전성을 중시한다면 TSubClassOf ,

부모자식관계가 아닌 즉 계층구조가 없는 클래스들끼리의 로직은 UClass*

계층구조가 있는 클래스들끼리의 로직은 TSubClassOf 을 이용하면 좋겠다 라는 생각이 드네요

 

마지막으로 클래스 메타데이터란 ? 

클래스 메타데이터를 가장 마지막에 배치한 이유는 이 글의 모든것을 공부하면서 소름이 돋았기 때문입니다.

메타데이터데이터 메타몽하면서 놀다가

메타 데이터의  활용을 느끼게 되었습니다.


클래스 메타데이터는 클래스 정의, 속성, 함수, 상속 관계와 같은 정보가 포함된 데이터입니다.

Unreal Engine에서의 클래스 메타데이터의 역할 : 
Unreal Engine은 클래스와 객체를 효율적으로 관리하기 위해 리플렉션 시스템을 사용합니다. 

리플렉션은 클래스 메타데이터를 통해 실행 시점에 클래스와 객체를 동적으로 다룰 수 있게 합니다.

다시말해, 메타데이터를 통해 객체를 동적으로 생성할 수 있고

상속계층을 확인 및 특정클래스인지의 여부를 알 수 있습니다. 

또, UPROPERTY,UFUNCTION처럼 클래스 내부의 멤버함수 및 멤버변수에 대한 정보를 
Category = "ABC" 로 표현해,  속성(멤버 변수)을 에디터에 노출 시킬 수 있습니다.

오늘 작성한 글에서 결국에 가장 중요한 것은 런타임 도중 동적으로 인스턴스를 생성한다는 말은 
메모리측면과 타입안전성 그리고 리플렉션 시스템을 통해 협업 및 디버깅 단계에서 개발 속도가 빨리지게 하는 방법들이였습니다.

언리얼 정말 대단한것 같습니다.. 공부하면 할수록 대단합니다;;

아니 진짜로 메타데이터라는 개념을 이번에 공부하면서 알게 되었는데 
이 메타데이터를 활용해서 동적으로 객체 생성을 할 수 있게 되고,
타입 안전성이 생기며, 여러사람이 협업 및 디버깅도 수월하게 된다는게 신기합니다

공부목적으로 만든 블로그입니다. 틀린 내용이 있다면 말씀해주시면 감사하겠습니다 !

 

 

'Unreal > Unreal 공부 내용' 카테고리의 다른 글

2025 / 02 / 14 TIL : Unreal Assert (2/3) 미완성  (0) 2025.02.14
GetOverlappingActors(TArray<AActor*> Array) , IsA  (0) 2025.02.09
Timer(SetTimer...)  (0) 2025.01.24
구면선형보간법(SLERP)  (0) 2025.01.22
선형 보간  (0) 2025.01.22

+ Recent posts