[UE] 언리얼 C++ 설계 2 - 컴포지션
언리얼 C++만의 컴포지션 기법을 사용해 복잡한 언리얼 오브젝트를 효과적으로 생성하자
- 언리얼 C++ 컴포지션 기법을 사용해 오브젝트의 포함 관계를 설계하는 방법의 학습
- 언리얼 C++이 제공하는 확장 열거형 타입의 선언과 활용 방법의 학습
인프런 이득우님의 '언리얼 프로그래밍 Part1 - 언리얼 C++의 이해' 강의를 참고하였습니다.
😎 [이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해] 강의 들으러 가기!
목차
언리얼 오브젝트의 컴포지션
컴포지션 ( Composition )
객체 지향 프로그래밍의 설계는 상속과 컴포지션으로 나눌 수 있다.
- 상속이란 같은 성질을 가진 부모, 자식 객체 간의 Is-A 관계만 의존해서는 설계와 유지보수가 어려움.
- 컴포지션은 객체 지향 설계에서 Has-A 관계를 구현하는 설계 방법
- 컴포지션의 활용
- 복합적인 기능을 거대한 클래스를 효과적으로 설계하는데 유용하게 사용할 수 있음.
모던 객체 설계 기법과 컴포지션
S O L I D | |
Single Responsibility Principle 단일 책임 원칙 |
하나의 객체는 하나의 의무(기능)만 가지도록 설계한다. |
Open-Closed Principle 개방 폐쇄 원칙 |
기존 구현된 코드를 변경하지 않으면서 새로운 기능을 추가할 수 있도록 설계한다. |
Liskov Substitution Principle 리스코프 치환 원칙 |
자식 객체를 부모 객체로 변경해도 작동에 문제가 없을 정도로 상속을 단순하게 사용한다. |
Interface Segregation Design 인터페이스 분리 원칙 |
객체가 구현해야 할 기능이 많다면 여러 개의 단순한 인터페이스로 분리해 설계한다. |
Dependency Injection Principle 의존성 역전의 원칙 |
구현된 실물보다 구축해야 할 추상적 개념에 의존한다. |
모던 객체 설계 기법의 설계 핵심은 상속을 단순화하고,
단순한 기능을 가진 다수의 객체를 조합해 복잡한 객체를 구성하는데 있음.
언리얼 엔진에서의 컴포지션 구현 방법
- 하나의 언리얼 오브젝트(UObject이하 계층)에는 항상 클래스 기본 오브젝트 CDO가 1:1로 매칭 되어있다.
- 언리얼 오브젝트간의 컴포지션은 어떻게 구현할 것인가?
- 언리얼 오브젝트에 다른 언리얼 오브젝트를 조합할 때 다음의 선택지가 존재한다.
- 방법 1: CDO에 미리 언리얼 오브젝트를 생성해 조합한다. CreateDefaultSubObject() 를 사용한다.
- 필수적 포함: 언제나 조합되어있어야 한다
- ex. 캐릭터의 Mesh. (항상 필요한 컴포넌트 초기화)
- 방법 2: CDO에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합한다. NewObject() 를 사용한다.
- 선택적 포함: 내가 필요할 때 조합하겠다 ⇒ 런타임에서 동적 생성하겠다.
- ex. 특정 상황에서만 필요한 오브젝트 생성
- 방법 1: CDO에 미리 언리얼 오브젝트를 생성해 조합한다. CreateDefaultSubObject() 를 사용한다.
- 언리얼 오브젝트를 생성할 때 컴포지션 정보를 자동으로 구축할 수 있다.
- 내가 소유한 언리얼 오브젝트를 SubObject라고 한다.
- 나를 소유한 언리얼 오브젝트를 Outer라고 한다.
SubObject가 필수적 포함인지, 아니면 선택적 포함인지 파악하고 사용하자!
예시
컴포지션 설계 예시
- 학교 구성원 시스템의 설계 예시
- 학교 구성원을 위해 출입증을 만들기로 한다.
- 출입증은 Person에서 구현해 상속시킬 것인가? 아니면 컴포지션으로 분리할 것인가?
- Person에서 직접 구현해서 상속시키는 경우의 문제
- 새로운 형태의 구성원이 등장한다면(예를 들어 출입증이 없는 외부 연수생) Peson을 수정할 것인가?
- 상위 클래스 Person을 수정하면, 하위 클래스들의 동작은 문제 없음을 보장할 수 있는가?
- 따라서 설계적으로 출입증은 컴포지션으로 분리하는 것이 바람직하다.
- 그렇다면 컴포지션으로만 포함시키면 모든 것이 해결되는가?
모던 객체 지향 언어가 제공하는 고급 기법을 활용해야 한다.
예제를 위한 클래스 다이어그램
학교 구성원임을 증명하는 출입증 카드의 부여
- 학생, 교사, 직원 모두가 상시 지니고 있음.
- 향후 확장성을 고려해 컴포지션으로 구현함.
예제 코드
Card.h
#pragma once
#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Card.generated.h"
enum class ECardType : uint8 // 1바이트 크기의 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();
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();
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; // 컴포지션 관계에 있을때는 헤더가 아닌 전방선언(class OO)을 하는게 좋다. 이렇게하면 헤더를 선언하는것보다 의존성을 줄일 수 있다.
};
UPROPERTY()
TObjectPtr<class UCard> Card;
- 컴포지션 관계에 있을때는 헤더가 아닌 전방선언(class UCard)을 하는게 좋다.
- 이렇게 전방선언을 사용하면 헤더를 선언하는것보다 의존성을 줄일 수 있다.
Person.cpp
#include "Person.h"
#include "Card.h"
UPerson::UPerson(){
Name = TEXT("홍길동");
Card = CreateDefaultSubobject<UCard>(TEXT("NAME_Card"));
}
Teacher.h
#pragma once
#include "CoreMinimal.h"
#include "Person.h"
#include "LessonInterface.h"
#include "Teacher.generated.h"
UCLASS()
class UNREALCOMPOSITION_API UTeacher : public UPerson, public ILessonInterface
{
GENERATED_BODY()
public:
UTeacher();
virtual void DoLesson() override;
};
Teacher.cpp
#include "Teacher.h"
#include "Card.h"
UTeacher::UTeacher(){
Name = TEXT("이선생");
Card->SetCardType(ECardType::Teacher);
}
void UTeacher::DoLesson(){
ILessonInterface::DoLesson();
UE_LOG(LogTemp, Log, TEXT("%s님은 가르칩니다."), *Name);
}
MyGameInstance.h
#pragma once
#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"
UCLASS()
class UNREALCOMPOSITION_API UMyGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
UMyGameInstance();
virtual void Init() override;
private:
UPROPERTY()
FString SchoolName;
};
MyGameInstance.cpp
#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 auto 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("============================"));
}
실행화면
참고사항 & 정리
언리얼 공식 가이드
UPROPERTY 변수 사용시 TObjectPtr이라고 하는 템플릿 클래스로 감싸서 사용해라.
구현부에는 원시 포인터를 사용해도 상관없다.
이유는?
- 언리얼 엔진이 더 이상 32비트를 지원하지 않아 64비트 포인터인 TObjectPtr 사용을 권장하고 있다.
정리: 컴포지션을 활용한 언리얼 오브젝트 설계
- 언리얼 C++ 은 컴포지션을 구현하는 독특한 패턴이 있다.
- 클래스 기본 객체를 생성하는 생성자 코드를 사용해 복잡한 언리얼 오브젝트를 구축 후 한 번에 생성할 수 있음.
- 언리얼 C++ 컴포지션의 Has-A 관계에 사용되는 용어
- 내가 소유한 하위 오브젝트는 SubObject
- 나를 소유한 상위 오브젝트는 Outer
- 언리얼 C++이 제공하는 확장 열거형을 사용해 다양한 메타 정보를 넣고 활용할 수 있다.
언리얼 C++의 컴포지션 기법은
게임의 복잡한 객체를 설계하고 생성할 때 유용하게 사용된다.
'⭐ Unreal Engine > UE 개념정리 - 언리얼의 이해' 카테고리의 다른 글
[UE] 언리얼 C++의 TArray, TSet, TMap 자료구조 라이브러리와 활용방법 (0) | 2024.04.20 |
---|---|
[UE] 언리얼 C++ 설계 3 - 델리게이트 Delegate (0) | 2024.04.05 |
[UE] 언리얼 C++ 설계 1 - 인터페이스 (0) | 2024.04.05 |
[UE] 언리얼 리플렉션 Unreal Reflection II (0) | 2024.03.21 |
[UE] 언리얼 리플렉션 Unreal Reflection I (0) | 2024.03.18 |
댓글
이 글 공유하기
다른 글
-
[UE] 언리얼 C++의 TArray, TSet, TMap 자료구조 라이브러리와 활용방법
[UE] 언리얼 C++의 TArray, TSet, TMap 자료구조 라이브러리와 활용방법
2024.04.20 -
[UE] 언리얼 C++ 설계 3 - 델리게이트 Delegate
[UE] 언리얼 C++ 설계 3 - 델리게이트 Delegate
2024.04.05 -
[UE] 언리얼 C++ 설계 1 - 인터페이스
[UE] 언리얼 C++ 설계 1 - 인터페이스
2024.04.05 -
[UE] 언리얼 리플렉션 Unreal Reflection II
[UE] 언리얼 리플렉션 Unreal Reflection II
2024.03.21