Unreal

5. 컴포지션

튀김족발 2023. 9. 11. 20:37

컴포지션

  • 객체 지향 설계에서 상속이 가진 Is-A 관계만 의존해서는 설계와 유지보수가 어려움.
  • 컴포지션은 객체 지향 설계에서 Has-A관계를 구현하는 설계 방법
  • 컴포지션의 활용
    • 복합적인 기능을 거대한 클래스를 효과적으로 설계하는데 유용하게 사용할 수 있음.

모던 객체 설계 기법과 컴포지션

  • 좋은 객체지향 설계 패턴을 제작하기 위한 모던 객체 설계 기법 (SOLID)
  • Single Responsivility Principle (단일 책임의 원칙)
    • 하나의 객체는 하나의 의무만 가지도록 설계한다.
  • Open-Closed Principle (개방 폐쇄 원칙)
    • 기존에 구현된 코드를 변경하지 않으면서 새로운 기능을 추가할 수 있도록 설계한다.
  • Liskov Substitution Principle (리스코프 치환의 법칙)
    • 자식 객체를 부모 객체로 변경해도 작동에 문제 없을 정도로 상속을 단순히 사용한다.
  • Interface Segregation Design (인터페이스 분리 원칙)
    • 객체가 구현해야 할 긴응이 많다면 이들을 여러 개의 단순한 인터페이스들로 분리해 설계한다.
  • Dependency Injection Principle (의존성 역전 원칙)
    • 구현된 실물ㄷ보다 구축해야 할 추상적 개념에 의존한다.

따라서 모던 객체 설계 기법의 설계 핵심은 상속을 단순화하고, 단순한 기능을 가진 다수의 객체를 조합해 복잡한 객체를 구성하는데 있음.

 

컴포지션 설계 예시

  • 학교 구성원 시스템의 설계 예시
    • 학교 구성원을 위해 출입증을 만들기로 한다.
    • 출입증은 Person에서 구현해 상속시킬 것인가? 아니면 컴포지션으로 분리할 것인가?
  • Person에서 직접 구현해 상속시키는 경우의 문제
    • 새로운 형태의 구성원이 등장한다면(예를 들어 출입증이 없는 외부 연수생) Person을 수정할 것인가?
    • 상위 클래스 Person을 수정하면, 하위 클래스들의 동작은 문제 없음을 보장할 수 있는가?
  • 따라서 설계적으로 출입증은 컴포지션으로 분리하는 것이 바람직함.
  • 그렇다면 컴포지션으로만 포함시키면 모든 것이 해결될 수 있는가?

효과적인 설계를 위해 프로그래밍 언어가 제공하는 고급 기법을 활용해야 함.

 

예제를 위한 설계

  • 학교 구성원임을 증명하는 출입증 카드의 부여
    • 학생, 교사, 직원 모두가 상시 지니고 있음.
    • 향후 확장성을 고려해 컴포지션으로 구현함.

 

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

  • 하나의 언리얼 오브젝트에는 항상 클래스 기본 오브젝트 CDO가 있다.
  • 언리얼 오브젝트간의 컴포지션은 어떻게 구현할 것인가?
  • 언리얼 오브젝트에 다른 언리얼 오브젝트를 조합할 때 다음의 선택지가 존재한다.
    • 방법 1 : CDO에 미리 언리얼 오브젝트를 생성해 조합한다. (필수적 포함)
    • 방법 2 : CDO에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합한다. (선택적 포함)
  • 언리얼 오브젝트를 생성할 때 컴포지션 정보를 구축할 수 있다.
    • 내가 소유한 언리얼 오브젝트를 Subobject라고 한다.
    • 나를 소유한 언리얼 오브젝트를 Outer라고 한다.

 

코드

Card

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Card.generated.h"

UENUM()
enum class ECardType : uint8
{
	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();

	FORCEINLINE ECardType GetCardType() const {return CardType; }
	FORCEINLINE void SetCardType(const ECardType& InCardType) { CardType = InCardType; }

private:
	UPROPERTY()
	ECardType CardType;

	UPROPERTY()
	uint32 Id;
};

enum으로 ECardType을 정의해주고 Student, Teacher, Staff를 선언해준다. 보통 enum은 8바이트 형태로 선언해준다. 옆에 있는 UMETA는 메타 정보를 포함시켜 줄수 있다.
그리고 CardType과 Id를 만들어준다.

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "Card.h"

UCard::UCard()
{
	CardType = ECardType::Invalid;
	Id = 0;
}

CPP 파일은 간단히 생성자만 만들어준다.

 

Person

설명하기 전에 이번 언리얼5에 오면서 바뀐 기능을 살펴보자
https://docs.unrealengine.com/5.0/ko/unreal-engine-5-migration-guide/

 

언리얼 엔진 5 마이그레이션 가이드

언리얼 엔진 4 프로젝트로 언리얼 엔진 5로 이주하는 방법 및 요구 사항.

docs.unrealengine.com

여기 중간에 C++ 오브젝트 포인터 프로퍼티를 살펴보면 기존에 사용하던 *을 사용하지 않고 TObjectPtr<T>을 사용하는것을 권고하고있다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Person.generated.h"

/**
 * 
 */
UCLASS()
class UNREALCOMPOSITION_API UPerson : public UObject
{
	GENERATED_BODY()
	
public:
	UPerson();

	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;
	TObjectPtr<class UCard> Card;
};

따라서 Card를 TObjectPtr로 선언해준다.

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "Person.h"
#include "Card.h"

UPerson::UPerson()
{
	Name = TEXT("홍길동");
	Card = CreateDefaultSubobject<UCard>(TEXT("NAME_Card"));
}

CreateDefaultSubobject의 첫번째 인자는 이 오브젝트의 고유한 이름으로 넣어준다.

 

그리고 Card를 출력 해보자

// Fill out your copyright notice in the Description page of Project Settings.

#include "MyGameInstance.h"
#include "Student.h"
#include "Teacher.h"
#include "Staff.h"
#include "Card.h"

UMyGameInstance::UMyGameInstance()
{
	SchoolName = TEXT("기본학교");
}

void UMyGameInstance::Init()
{
	Super::Init();

	UE_LOG(LogTemp, Log, TEXT("================================="));

	TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>()};
	for(const TArray<UPerson*>::ElementType Person : Persons)
	{
		const UCard* OwnCard = Person->GetCard();
		check(OwnCard);
		ECardType CardType = OwnCard->GetCardType();
		UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 %d"), *Person->GetName(), CardType);
	}
	UE_LOG(LogTemp, Log, TEXT("================================="));
}

이렇게 Person의 각 카드 종류를 출력해준다.

LogTemp: =================================
LogTemp: 학생님이 소유한 카드 종류 1
LogTemp: 스앵님님이 소유한 카드 종류 2
LogTemp: 스태프님이 소유한 카드 종류 3
LogTemp: =================================

출력은 됐..다... 그런데 뭔가 이상하다. enum 값이 아닌 int 값으로 출력되고 있다.

 

// Fill out your copyright notice in the Description page of Project Settings.

#include "MyGameInstance.h"
#include "Student.h"
#include "Teacher.h"
#include "Staff.h"
#include "Card.h"

UMyGameInstance::UMyGameInstance()
{
	SchoolName = TEXT("기본학교");
}

void UMyGameInstance::Init()
{
	Super::Init();

	UE_LOG(LogTemp, Log, TEXT("================================="));

	TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>()};
	for(const TArray<UPerson*>::ElementType Person : Persons)
	{
		const UCard* OwnCard = Person->GetCard();
		check(OwnCard);
		ECardType CardType = OwnCard->GetCardType();
		//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(); 
			UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 %s"), *Person->GetName(), *CardMetaData);
		}
	}
	UE_LOG(LogTemp, Log, TEXT("================================="));
}

const UEnum* CardEnumType = FindObject<UEnum>(nullptr, TEXT("/Script/UnrealComposition.ECardType"));
이 코드로 현재 enum 값을 갖고올 수 있다. 그리고 GetDisplayNameTextByValue으로 UMETA로 정의된 메타 데이터를 갖고온다.

이렇게 하고 출력을 하면,

LogTemp: =================================
LogTemp: 학생님이 소유한 카드 종류 For Student
LogTemp: 스앵님님이 소유한 카드 종류 For Teacher
LogTemp: 스태프님이 소유한 카드 종류 For Staff
LogTemp: =================================

와 된다!