강의 목표 :
- 언리얼 C++ 만의 컴포지션 기법을 사용해 복잡한 언리얼 오브젝트를 효과적으로 생성하는 방법 학습
- "컴포지션 기법"을 사용해 오브젝트의 포함 관계를 설계하는 방법 학습
- "확장 열거형 타입"의 선언과 활용 방법의 학습
강의 내용 :
- 컴포지션을 구현하는 독특한 패턴에 대해 학습
- 클래스 기본 객체를 생성하는 생성자 코드를 사용해 복잡한 언리얼 오브젝트를 생성
- 확장 열거형을 사용해 다양한 메타 정보를 넣고 활용할 수 있다.
- 컴포지션 기법은 게임의 복잡한 객체를 설계하고 생성할 때 유용하게 사용된다.
[ ✔️컴포지션 Composition ]
📍컴포지션? :
객체 지향의 설계는 크게 "상속"과 "컴포지션"으로 요약할 수 있다.
상속은 성질이 같은 부모-자식 클래스의 관계를 의미하는 Is-A 관계
컴포지션은 성질이 다른 두 객체에서 어떤 객체가 다른 객체를 소유하는 Has-A 관계
컴포지션을 활요하면 복합적인 기능을 가지는 거대한 클래스를 효과적으로 설계하는데 유용하게 사용할 수 있다.
📍모던 객체 설계 기법 :
SOLID : 좋은 객체지향 설계 패턴들의 모음
핵심은 상속을 단순화 하고, 단순한 기능을 가진 다수의 객체를 조합해 복잡한 객체를 구성(컴포지션 기법)하는데 있다.
실물을 구현하기 보다는 기획의도를 파악해서 상위개념을 기획하고 설계를 진행해야한다.
컴포지션으로 설계 햇다고 하여 모든 것이 해결되는건 아니다.
효과적인 설계를 위해서는 프로그래밍 언어가 제공하는 고급 기법을 활용해줘야한다. (고급 기법이란...?)
📍언리얼 엔진에서 컴포지션 구현법 :
언리얼 오브젝트에 다른 언리얼 오브젝트를 조합하는 경우... (UObject + UObject)
- 방법 1 : CDO에 미리 언리얼 오브젝트를 생성해 조합한다. (필수적 포함)
- 방법2 : CDO에 빈 포인터만 넣고 런타임에 언리얼 오브젝트를 생성해 조합한다. (선택적 포함)
( SubObject : 내가 소유한 언리얼 오브젝트 / Outer : 나를 소유한 언리얼 오브젝트 )
📍UObject::CreateDefaultSubObject<T> :
언리얼 엔진에서 UClass는 UObject 와 연관된 CDO 를 가진다.
CDO는 엔진이 초기화 될때 클래스 생성자를 통해 최초로 단 한번만 할당된다.
때문에 생성자 호출 이후 로직에 대한 정보를 포함할 수 없다.
이는 UObject 생성자는 어떠한 런타임 로직을 포함 할수 없다는 것을 의미하기도 한다.
CreateDefaultSubObject() 함수는 UObject의 생성자에서만 호출한다.
해당 함수가 호출되면 서브 오브젝트 클래스의 CDO 인스턴스를 생성한다.
(참고로 UObject::NewObject<T>() 함수는 엔진이 초기화 된 이후 오브젝트 생성을 위해 일반저긍로 사용된다. 때문에 UObject 생성자에서 호출되지 않도록 해야한다.)
첫번째 인자로 FName 타입으로 식별자를 넣어줘야한다.
FName은 일반적인 문자열(String)이 아니기에 이름작성시 "NAME_" 접두사를 앞에 붙여줌으로써 FName 타입임을 알려주는게 일반적이다.
UTeacher 클래스는 UPerson 클래스를 상속한다.
UTeacher 객체가 생성되기 전에 UPerson 클래스의 생성자가 먼저 호출되며 Card 멤버가 생성된다.
때문에 UTeacher 클래스의 생성자에선 CreateDefaultSubObject<UCard>() API를 호출해줄 필요없이, SetCardType() 함수를 호출하여 Card 객체의 설정값만 수정해주면 된다.
📍실습 : 컴포지션을 활용한 클래스 설계
향후 확장성을 위해 Card 클래스를 컴포지션으로 구현
필수적 포함 방식으로 구현 예정 (언리얼 오브젝트 생성자에 미리 언리얼 오브젝트를 포함)
이 카드는 소유자를 구별하는 용도로 사용된다. (학생 : 학생증, 교사 : 교사증, 외부인 : 출입증, etc...)
코드 :
Person Class :
// Person.h
UENUM()
enum class ECardType : uint8
{
Student = 1 UMETA(DisplayName = "For Student"),
Teacher = 2 UMETA(DisplayName = "For Teacher"),
Staff = 3 UMETA(DisplayName = "For Staff"),
Invalid
};
UCLASS()
class UNREALCOMPOSITION_API UPerson : public UObject
{
GENERATED_BODY()
public:
UPerson();
// FORCEINLINE : 100% inline 함수 보장하진 않음
FORCEINLINE const FString& GetName() const { return Name; }
FORCEINLINE void SetName(const FString& InName) { Name = InName; }
FORCEINLINE class UCard* GetCard() const { return Card; }
FORCEINLINE void SetCard(class UCard* InCard) { Card = InCard; }
protected:
UPROPERTY()
FString Name;
UPROPERTY()
TObjectPtr<class UCard> Card;
};
// Person.cpp
#include "Person.h"
#include "Card.h"
UPerson::UPerson()
{
Name = TEXT("홍길동");
Card = CreateDefaultSubobject<UCard>(TEXT("NAME_Card"));
}
멤버 변수(프로퍼티) 로 Card 클래스를 가지고 있다.
주소 값 이기에 TObjectPtr<T> 타입 사용
"필수적 포함" 방식으로 Card 를 소유할 예정이기에 생성자에서 Person 클래스 초기화시 미리 생성해준다.
GameInstance Header :
UE_LOG(LogTemp, Log, TEXT("======================"));
TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };
for (const auto Person : Persons)
{
const UCard* OwnCard = Person->GetCard();
check(OwnCard);
ECardType CardType = OwnCard->GetCardType();
// case1)
//UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 %d"), *Person->GetName(), CardType);
const UEnum* CardEnumType = FindObject<UEnum>(nullptr, TEXT("/Script/UnrealComposition.ECardType"));
if (CardEnumType)
{
FString CardMetaData = CardEnumType->GetDisplayNameTextByValue((int64)CardType).ToString();
// case2) META 데이터 가져오기
//UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 %s"), *Person->GetName(), *CardMetaData);
}
}
UE_LOG(LogTemp, Log, TEXT("======================"));
출력시 열거형 CardType의 기본 타입이 uint8 이기에 형식 지정자 %d 와 만나서 해당하는 정수값으로 출력이 된다.
하지만, 이렇게 되면 카드종류 구분은 되겠지만 해당 카드가 어떤 용도인지 명확하지 않다.

열거형 선언시 각 인자에 UMETA 매크로를 사용하여 문자열을 넣어줬다.
이를 가져와서 출력해주도록 한다.
const UEnum* CardEnumType = FindObject<UEnum>(nullptr, TEXT("/Script/UnrealComposition.ECardType"));
FString CardMetaData = CardEnumType->GetDisplayNameTextByValue((int64)CardType).ToString();
결과 :


📍포인터 타입 멤버 변수 :
C++ 방식에선 변수의 타입을 포인터(주소) 타입을 가지도록하여 #include 대신 전방선언을 사용하도록 함으로써 의존성을 낮춰줄 수 있었다.
하지만 해당 방식은 UE4까지는 정석이었으나 UE5 부터는 다른 방식이 표준안이 되었다.
선언부에 대해선 원시포인터(*) 대신에 TObjectPtr<T> 를 사용해줘야한다.
================================
개인 공부 기록용 포스팅입니다.
댓글, 질문 환영해요~!
Unreal Engine Ver : 5.1.1
Refer : Inflearn_이득우의 언리얼 프로그램
================================
'Unreal > Inflearn' 카테고리의 다른 글
Part1.10 언리얼 컨테이너 라이브러리 Ⅰ - Array (0) | 2024.04.11 |
---|---|
Part1.9 언리얼 C++ 설계 Ⅲ - 델리게이트(Delegate)⭕ (2) | 2024.04.11 |
Part1.7 언리얼 C++ 설계 Ⅰ - 인터페이스⭕ (0) | 2024.04.09 |
Part1.6 언리얼 오브젝트 리플렉션 시스템 Ⅱ⭕ (0) | 2024.04.08 |
Part1.5 언리얼 오브젝트 리플렉션 시스템 Ⅰ⭕ (3) | 2024.04.08 |