Taene's

[U C++] Composition 본문

Unreal5/Unreal C++

[U C++] Composition

taene_ 2024. 7. 8. 18:38

객체 지향 설계의 Is-A, Has-A 관계

  • Is-A 관계(상속): 사과는 과일이다(O) 과일은 사과다(X)와 같이 부모 자식의 상속 관계를 쉽게 생각할 수 있다.
  • Has-A 관계(컴포지션): '차는 엔진을 가지고 있다'와 같이 기능을 포함시키고 싶을 때 사용한다.
  • 객체 지향 프로그래밍의 설계는 상속과 컴포지션의 활용이라 요악할 수 있다.

 

언리얼 엔진에서의 컴포지션 구현 방법

  • 하나의 언리얼 오브젝트에는 항상 클래스 기본 오브젝트 CDO가 있다.
  • 언리얼 오브젝트간의 컴포지션을 어떻게 구현할 것인가?
  • 언리얼 오브젝트에 다른 언리얼 오브젝트를 조합할 때 다음의 선택지가 존재한다.
    • 방법 1: CDO에 미리 언리얼 오브젝트를 생성해 조합한다. ( 필수적 포함 )
      • 생성자 코드에 작성하고, CreateDefaultSubobject()라는 API를 사용한다.
    • 방법 2: CDO에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합한다. ( 선택적 포함 )
      • 게임 콘텐츠를 제작할 때 동작하는 런타임 코드에 작성하고, NewObject()라는 API를 사용한다.
  • 언리얼 오브젝트를 생성할 때 컴포지션 정보를 구축할 수 있다.
    • 내가 소유한 언리얼 오브젝트를 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("=========================="));

}

 

컴포지션을 활용한 언리얼 오브젝트 설계

  1. 언리얼 C++은 컴포지션을 구현하는 독특한 패턴이 있다.
  2. 클래스 기본 객체를 생성하는 생성자 코드를 사용해 복잡한 언리얼 오브젝트를 생성할 수 있다.
  3. 언리얼 C++ 컴포지션의 Has-A 관계에 사용되는 용어
    • 내가 소유한 하위 오브젝트: Subobject
    • 나를 소유한 상위 오브젝트: Outer
  4. 언리얼 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