Taene's
[U C++] Composition 본문
객체 지향 설계의 Is-A, Has-A 관계
- Is-A 관계(상속): 사과는 과일이다(O) 과일은 사과다(X)와 같이 부모 자식의 상속 관계를 쉽게 생각할 수 있다.
- Has-A 관계(컴포지션): '차는 엔진을 가지고 있다'와 같이 기능을 포함시키고 싶을 때 사용한다.
- 객체 지향 프로그래밍의 설계는 상속과 컴포지션의 활용이라 요악할 수 있다.
언리얼 엔진에서의 컴포지션 구현 방법
- 하나의 언리얼 오브젝트에는 항상 클래스 기본 오브젝트 CDO가 있다.
- 언리얼 오브젝트간의 컴포지션을 어떻게 구현할 것인가?
- 언리얼 오브젝트에 다른 언리얼 오브젝트를 조합할 때 다음의 선택지가 존재한다.
- 방법 1: CDO에 미리 언리얼 오브젝트를 생성해 조합한다. ( 필수적 포함 )
- 생성자 코드에 작성하고, CreateDefaultSubobject()라는 API를 사용한다.
- 방법 2: CDO에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합한다. ( 선택적 포함 )
- 게임 콘텐츠를 제작할 때 동작하는 런타임 코드에 작성하고, NewObject()라는 API를 사용한다.
- 방법 1: CDO에 미리 언리얼 오브젝트를 생성해 조합한다. ( 필수적 포함 )
- 언리얼 오브젝트를 생성할 때 컴포지션 정보를 구축할 수 있다.
- 내가 소유한 언리얼 오브젝트를 Subobject라고 한다.
- 나를 소유한 언리얼 오브젝트를 Outer라고 한다.
Composition 구현
// Card.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Card.generated.h"
/* // 일반 C++ 방식
enum class ECardType : uint8 // enum class는 8byte, 바이트 형태를 지정해주는게 일반적이다
{
Student = 1,
Teacher,
Staff,
Invalid
};
*/
// 언리얼 C++ 방식 - 필드마다 메타정보를 넣을 수 있다.
UENUM()
enum class ECardType : uint8 // enum class는 8byte, 바이트 형태를 지정해주는게 일반적이다
{
Student = 1 UMETA(DisplayName = "For Student"),
Teacher UMETA(DisplayName = "For Teacher"),
Staff UMETA(DisplayName = "For Staff"),
Invalid
};
/**
*
*/
UCLASS()
class UNREALCOMPOSITION_API UCard : public UObject
{
GENERATED_BODY()
public:
UCard();
ECardType GetCardType() const { return CardType; }
void SetCardType(ECardType InCardType) { CardType = InCardType; }
private:
UPROPERTY()
ECardType CardType;
UPROPERTY()
uint32 Id;
};
// Card.cpp
#include "Card.h"
UCard::UCard()
{
CardType = ECardType::Invalid;
Id = 0;
}
// Person.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Person.generated.h"
/**
*
*/
UCLASS()
class UNREALCOMPOSITION_API UPerson : public UObject
{
GENERATED_BODY()
public:
UPerson();
// Get 함수는 const를 써주는것이 좋다,
// 아래는 리턴값을 레퍼런스로 받고 있어서 레퍼런스를 받은 측에서 값을 변경할 수 있기 때문에 맨앞에 const를 붙여주는 것이 좋다.
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()
// class UCard* Card; // 컴포지션 관계에서는, 헤더를 포함시키지 않고 전방 선언을 해서 의존성을 최대한 줄일 수 있다.(언리얼 엔진4 까지의 선언방법)
TObjectPtr<class UCard> Card; // 언리얼 엔진5 부터는 포인터로 선언하는 것들을 TObjectPtr<T>로 감싸서 선언하는 것이 좋다.
// (TObjectPtr이 포인터 클래스이므로 포인터 연산자는 포함하지 않는다.)
private:
};
// Person.cpp
#include "Person.h"
#include "Card.h"
UPerson::UPerson()
{
Name = TEXT("홍길동");
Card = CreateDefaultSubobject<UCard>(TEXT("NAME_Card"));
}
// Teacher.cpp
#include "Teacher.h"
#include "Card.h"
UTeacher::UTeacher()
{
Name = TEXT("O선생");
Card->SetCardType(ECardType::Teacher);
// 부모클래스의 생성자가 호출된 다음 자식클래스의 생성자가 호출되기 때문에
// CreateDefaultSubobject를 하면 중복이 된다.
}
void UTeacher::DoLesson()
{
UE_LOG(LogTemp, Log, TEXT("%s님이 가르칩니다."), *Name);
}
// MyGameInstance.cpp
#include "MyGameInstance.h"
#include "Student.h"
#include "Teacher.h"
#include "Staff.h"
#include "LessonInterface.h"
#include "Card.h"
UMyGameInstance::UMyGameInstance()
{
SchoolName = TEXT("OO대학교");
}
void UMyGameInstance::Init()
{
Super::Init();
UE_LOG(LogTemp, Log, TEXT("=========================="));
TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };
// Persons를 순회하며 어떤 카드를 갖고있는지 출력
for (const UPerson* p : Persons)
{
const UCard* OwnCard = p->GetCard();
check(OwnCard);
ECardType CardType = OwnCard->GetCardType();
FString CardTypeName = (CardType == ECardType::Student) ? TEXT("학생카드")
: (CardType == ECardType::Teacher) ? TEXT("선생카드") : TEXT("스탭카드");
UE_LOG(LogTemp, Log, TEXT("%s은(는) %s를 갖고있다."), *p->GetName(), *CardTypeName);
// 열거형의 메타정보 출력하기
const UEnum* CardEnumType = FindObject<UEnum>(nullptr, TEXT("/Script/UnrealComposition.ECardType"));
// Script/프로젝트이름(모듈이름).출력할 메타정보의 타입 이름
if (CardEnumType)
{
// GetDisplayNameTextByValue는 int64만 받으므로 형변환을 해야 한다.
FString CardMetaData = CardEnumType->GetDisplayNameTextByValue((int64)CardType).ToString();
UE_LOG(LogTemp, Log, TEXT("%s은(는) %s를 갖고있다."), *p->GetName(), *CardMetaData);
}
}
UE_LOG(LogTemp, Log, TEXT("=========================="));
}
컴포지션을 활용한 언리얼 오브젝트 설계
- 언리얼 C++은 컴포지션을 구현하는 독특한 패턴이 있다.
- 클래스 기본 객체를 생성하는 생성자 코드를 사용해 복잡한 언리얼 오브젝트를 생성할 수 있다.
- 언리얼 C++ 컴포지션의 Has-A 관계에 사용되는 용어
- 내가 소유한 하위 오브젝트: Subobject
- 나를 소유한 상위 오브젝트: Outer
- 언리얼 C++이 제공하는 확장 열거형을 사용해 다양한 메타 정보를 넣고 활용할 수 있다.
'Unreal5 > Unreal C++' 카테고리의 다른 글
[U C++] Delegate (0) | 2024.07.08 |
---|---|
[U C++] Design Pattern - 발행 구독 디자인 패턴 (0) | 2024.07.08 |
[U C++] const 선언 (0) | 2024.07.08 |
[U C++] FORCEINLINE (0) | 2024.07.08 |
[U C++] SOLID (0) | 2024.07.07 |