강의내용
언리얼 C++만의 컴포지션 기법을 사용해 복잡한 언리얼 오브젝트를 효과적으로 생성하기
강의 목표
언리얼 C++의 컴포지션 기법을 사용해 오브젝트의 포함 관계를 설계하는 방법의 학습
언리얼 C++이 제공하는 확장 열거형 타입의 선언과 활용 방법의 학습
언리얼 오브젝트의 컴포지션
Has-A 즉 다른 클래스 객체를 변수로 들고 있는 설계 방법
현대 설계 기법 SOLID
Single Responsibility - 단일 책임 원칙
Open-Closed - 개방 폐쇄 원칙
Liskov substitution - 리스코프 치환(자식 객체를 부모로 변경해도 작동에 문제 없을 정도로 상속 단순히)
Interface Segregation - 단순한 인터페이스로 분리
Dependency Injection - 추상적 개념에 의존
예) Person에 학생증을 넣어서 상속하면 학생증 없는 구성원 등장하면 Person 수정해야 하기 때문에 컴포지션으로 하는게 바람직
Card라는 클래스를 컴포지션으로 만들어 구현해 보자.
언리얼에서 컴포지션 구현 방법 2가지
1. CreateDefaultSubobject라는 함수를 이용해 미리 오브젝트를 생성해 조합
2. 일단 빈 포인터만 넣고 런타임에서 NewObject()로 생성해 조합
일단 이번 시간에는 첫번째 방법을 해본다.
실습
UnrealComposition이라는 새로운 프로젝트를 생성해보자.
그리고 지난 강의에서 작업한 UnrealInterface의 코드를 복붙해서 옮기자.
그 이후에 언리얼 편집기에서 Tools → refresh visual studio 2022 project 를 실행한다.
각 코드에서 UNREALINTERFACE_API라고 되어 있는 걸 지금 프로젝트 이름인 UNREALCOMPOSITION_API으로 변경을 해준다.
다시 언리얼 에디터로 돌아가서 빌드를 해준다.
빌드 이후 프로젝트 설정으로 가서 Map들을 clear 하고, GameInstance도 변경한다.
새롭게 Composition으로 조합시킬 Card 클래스를 생성해본다. Object 클래스를 Card라는 이름으로생성한다.
기본적인 Card에 대한 코드를 작성한다.
Card 코드 작성
#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();
ECardType GetCardType() const { return CardType; }
void SetCardType(ECardType InCardType) { CardType = InCardType; }
private:
UPROPERTY()
ECardType CardType;
UPROPERTY()
uint32 Id;
};
그리고 생성자 코드를 작성한다.
#include "Card.h"
UCard::UCard()
{
CardType = ECardType::Invalid;
Id = 0;
}
그리고 빌드를 한다. (언리얼 에디터는 끄고 작업)
빌드가 됐으면, Person으로 가서 카드를 가지도록 개념을 확장해보자.
Person 에 Card 추가
선언에서 전방선언을 해주면 좋다. Card에 대한 포인터 변수를 선언하려면 Card에 대한 헤더를 포함을 해주는게 보통이다. 근데 컴포지션 관계에 있을 때는 선언에서 전방선언을 해주는 것이 좋다. 이를 통해 의존성을 최대한 없앨 수가 있다.
#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 FString& GetName() { return Name; }
FORCEINLINE void SetName(const FString& InName) { Name = InName; }
protected:
UPROPERTY()
FString Name;
UPROPERTY()
class UCard* Card;
};
이렇게 Unreal 오브젝트를 이용해서 컴포지션 관계를 구축할 수 있다.
이 방식은 Unreal4까지는 정석이었으나 5부터는 새로운 표준화가 되었어.
https://docs.unrealengine.com/5.0/ko/unreal-engine-5-migration-guide/
UPROPERTY 변수에 원시 포인터가 있었던 많은 엔진 클래스에서 이제 TObjectPtr 를 사용해서 변경하라는 내용이다.
선택 사항이기는 하지만, UObject 포인터 프로퍼티에 대한 T* 와 UCLASS 및 USTRUCT 타입에 있는 컨테이너 클래스를 통해 TObjectPtr<T> 를 사용하는 것이 좋습니다.
선언에 대한 부분만 TObjectPtr<T>를 사용하고, 구현에서는 그냥 포인터를 사용하면 된다.
UPROPERTY()
TObjectPtr<class UCard> Card;
이렇게 감싸주면 된다 자체가 포인터라 *는 뺐다.
그리고 getter와 setter를 선언해 본다.
#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; } // 반환값이 레퍼런스면 반환 형에도 const를 붙여야 함
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;
};
이제 구현부로 가서 구현을 해본다.
#include "Person.h"
#include "Card.h"
UPerson::UPerson()
{
Name = TEXT("홍길동");
Card = CreateDefaultSubobject<UCard>(TEXT("NAME_Card"));
}
CreateDefaultSubobject의 첫번째 인자는 FName을 넣어 줘야 하는데 FName이라는 걸 알려주려면 NAME_이라는 접두사를 써주는게 하나의 방법이다. 코드를 보는 사람이 이건 string이 아니고, FName이구나 인식할 수 있다.
Teacher의 생성자에도
#include "Teacher.h"
#include "Card.h"
UTeacher::UTeacher()
{
Name = TEXT("이선생");
Card->SetCardType(ECardType::Teacher);
}
이렇게 선생님용 카드라고 넣어주고,
Student의 생성자 에서도
#include "Student.h"
#include "Card.h"
UStudent::UStudent()
{
Name = TEXT("이학생");
Card->SetCardType(ECardType::Student);
}
Staff의 생성자에서도
#include "Staff.h"
#include "Card.h"
UStaff::UStaff()
{
Name = TEXT("이직원");
Card->SetCardType(ECardType::Staff);
}
MyGameInstance에서 CardType 출력
MyGameInstance로 가서
#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);
}
UE_LOG(LogTemp, Log, TEXT("==================================="));
}
rebuild로 빌드를 하고 디버깅 없이 실행하기를 해본다.

이렇게 나온다.
ECardType의 메타 정보 출력
이번엔 ECardType 에서 메타 정보를 빼와서 DisplayName을 출력해보자.
이를 위해 FindObject를 쓸거야.
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")); // 절대주소를 TEXT에 넣는다. Script/프로젝트이름.열거형이름
if (CardEnumType)
{
FString CardMetaData = CardEnumType->GetDisplayNameTextByValue((int64)CardType).ToString();
UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 %s"), *Person->GetName(), *CardMetaData);
}
}
UE_LOG(LogTemp, Log, TEXT("==================================="));
}
이렇게 하고 언리얼에서 컴파일 한 후 확인해보자.

이렇게 문자열을 출력하는 걸 볼 수 있다.
정리
컴포지션을 활용한 언리얼 오브젝트 설계
1. 언리얼 C++은 컴포지션을 구현하는 독특한 패턴이 있다.
2. 클래스 기본 객체를 생성하는 생성자 코드를 사용해 복잡한 언리얼 오브젝트를 생성할 수 있음.
3. 언리얼 C++ 컴포지션의 Has-A 관계에 사용되는 용어
- 내가 소유한 하위 오브젝트: Subobject
- 나를 소유한 상위 오브젝트: Outer
4. 언리얼 C++이 제공하는 확장 열거형을 사용해 다양한 메타 정보를 넣고 활용할 수 있다.
언리얼 C++의 컴포지션 기법은 게임의 복잡한 객체를 설계하고 생성할 때 유용하게 사용된다.
'Unreal engine > Unreal C++' 카테고리의 다른 글
| 3_1_언리얼 컨테이너 TArray와 TSet (0) | 2023.09.29 |
|---|---|
| 2_3_언리얼 C++ 델리게이트로 구현하는 느슨한 결합 (0) | 2023.09.28 |
| 2_1_언리얼 C++ 모던객체지향 설계_인터페이스 (0) | 2023.09.22 |
| 1_6_언리얼 오브젝트의 이해_언리얼 오브젝트 리플렉션 시스템2 (0) | 2023.09.18 |
| 1_5_언리얼 오브젝트의 이해_언리얼 오브젝트 리플렉션 시스템1 (0) | 2023.09.18 |
댓글