개발 이야기 안하는 개발자

언리얼5_1_2 : 언리얼 C++ 모던객체지향 설계 본문

Unreal/이득우의 언리얼 프로그래밍

언리얼5_1_2 : 언리얼 C++ 모던객체지향 설계

07e 2024. 2. 16. 20:30
반응형

UInterface

언리얼 Interface는 기존 C++와 다르게 설계되어 있다.

기본 클래스에 가까운 형태를 띄고 있다.

 

Uinterface를 제작하면 2개의 클래스가 생성이 되는데, 이때 UINTERFACE라는 매크로를 받는 클래스는 타입정보를 관리하기 위해 언리얼 에디터가 제작한 클래스이다. 해당 클래스는 수정할 게 없다.

그 하단에 있는 클래스(접두사 I가 붙어있는 클래스_인터페이스)를 수정하면 된다.

언리얼이 사용하는 인터페이스는 정의가 가능하다. (기본 틀이 클래스이기 때문)

	virtual void DoLesson()
	{
		UE_LOG(LogTemp, Log, TEXT("수업에 입장을 합니다"));
	}

 

 

이를 상속받는 자식 클래스에서는 override한다.

UCLASS()
class UNREALINTERFACE_API UStudent : public UPerson, public ILessonInterface
{
	GENERATED_BODY()
	
public :

	UStudent();
	virtual void DoLesson() override;
private :
};

 

이때, DoLesson의 인터페이스 정의 부분을 호출하려고 할 때 Super는 사용할 수 없다. 왜냐하면 Super가 가르키는 대상은 UPerson이기 때문이다. 따라서 ILessonInterface가 정의하는 DoLesson()을 호출하고 싶다면 아래와 같이 호출해야 한다.

ILessonInterface::DoLesson();

 

UInterface의 형변환

	TArray<UPerson*> Person = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };

	for (const auto ThePerson : Person)
	{
		ILessonInterface* LessonInterface = Cast<ILessonInterface>(ThePerson);
		if (LessonInterface)
		{
			UE_LOG(LogTemp, Log, TEXT("%s님은 수업에 참여할 수 있습니다."), *ThePerson->GetName());
			LessonInterface->DoLesson();
		}
		else
		{
			UE_LOG(LogTemp, Log, TEXT("%s님은 수업에 참여할 수 없습니다."), *ThePerson->GetName());
		}
	}

 

 

 

 

컴포지션(Composition)

객체지향설계에서 상속을 가진 Is_A 관계만으론 설계와 유지보수가 어려움.

다른 성질을 가진 클래스를 가진 Has_A 관계를 컴포지션이라 함.

 

언리얼에선 컴포지션을 구현하기 위해선 2가지 방법이 있다. 

1. CDO에 미리 생성하기 (필수적 포함)

2. CDO에 빈 포인터만 넣고 런타임에 생성하기 (선택적 포함)

서브오브젝트

언리얼 오브젝트가 가지고 있는 다른 언리얼 오브젝트.

아우터

서브오브젝트(본인)을 가지고 있는 다른 언리얼 오브젝트.

 

Enum

앞에 접두사 E 붙여줄 것.

UENUM으로 매크로를 붙여줘야 하며, UMETA라는 메타 정보를 집어놓고 데이터를 활용 할 수 있다.

 

CDO에 미리 생성하는 예제를 보기 위해 학생, 선생, 스탭이라는 Person의 부모 클래스에 서브오브젝트인 Card 클래스를 만들어 보자.

/**
 * 
 */
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;
};
UCLASS()
class UNREALCOMPOSITION_API UPerson : public UObject
{
	GENERATED_BODY()
	
public :
	UPerson();
protected :
	UPROPERTY()
	FString Name;

	UPROPERTY()
	TObjectPtr<class UCard> Card;

private :
};

 

 

여기서 중요한 점은 Card를 서브오브젝트를 가진 Person에서 class UCard* Card 가 아닌 TObjectPtr<class UCard> Card 라고 선언한 점이다.

언리얼 5로 넘어오면서 TObjectPtr이란 탬플릿으로 감싸서 사용하는 것을 선택사항이지만 권장한다.

이는 빌드할때는 큰 차이가 없지만 에디터에서 사용할땐 더 나은 환경에서 테스트를 진행하게 도와준다고 명시되어 있다.

 

따라서 언리얼5에선 더 나은 경험을 위해선 TObjectPtr<class T> val 로 선언하는게 좋다.

 

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

 

CDO에서 미리 선언하는 방식인데 CreateDefaultSubobject라는 메소드를 활용해서 생성하게 된다.

그리고 Person을 상속받는 Student의 생성자 이다.

UStudent::UStudent()
{
	Name = TEXT("최학생");
	Card->SetCardType(ECardType::Studen);
}

 

같은 방식으로 Teacher와 Staff를 정의하고 테스트를 진행한다.

	TArray<UPerson*> Person = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };

	for (const auto ThePerson : Person)
	{
		const UCard* OwnCard = ThePerson->GetCard();
		check(OwnCard);
		ECardType CardType = OwnCard->GetCardType();
		UE_LOG(LogTemp, Log, TEXT("%s님이 소유한 카드 종류 %d"), *ThePerson->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"), *ThePerson->GetName(), *CardMetaData);
		}
	}

출력 :

LogTemp: 최학생님이 소유한 카드 종류 1
LogTemp: 최학생님이 소유한 카드 종류 For Student
LogTemp: 이선생님이 소유한 카드 종류 2
LogTemp: 이선생님이 소유한 카드 종류 For Teacher
LogTemp: 이직원님이 소유한 카드 종류 3
LogTemp: 이직원님이 소유한 카드 종류 For Staff

 

Enum을 사용하는데 사용된 FindObject는 찾고자 하는 EnumClass의 위치를 가져와서 읽어 들인다.

해당 Person이 가지고 있는 Enum타입이 어떤 메타데이터를 가지고 있는지 검색해서 가져오는 방식이다.

 

 

델리게이트

함수를 오브젝트처럼 관리해서 느스한 결합 구조를 간편하고 안정적으로 구현하는데 사용된다.

델리게이트 문서 : https://docs.unrealengine.com/5.1/ko/delegates-and-lamba-functions-in-unreal-engine/

 

강한 결합 : 클래스들이 서로 의존성을 가지는 경우

느슨한 결합 : 실물에 의존하지 말고 추상적 설계에 의존하는 경우 (DIP 원칙)

 

델리게이트 매크로 선언

DECLARE_{델리게이트 유형}DELEGATE{함수정보}

 

일대일 형태면서 C++만 지원한다 - DECLARE_DELEGATE

일대다 형태면서 C++만 지원한다 - DECLARE_MULTICAST

일대일 형태면서 블루프린트를 지원한다 - DECLARE_DYNAMIC

일대다 형태면서 블루프린트를 지원한다 - DECLARE_DYNAMIC_MULTICAST 

 

인자가 없고 반환값이 없다 - DECLARE_DELEGATE

인자가 하나고 반환값이 없다 - DECLARE_DELEGATE_OneParam

인자가 세개고 반환값이 있다 - DECLARE_DELEGATE_RetVal_ThreeParams (MULTICAST는 반환값을 지원안함)

최대 9개까지 인자를 받는다.

 

예시 - 다수 인원을 대상으로 발송, 오직 C++에서만, 인자 2개

DECLARE_MULTICAST_DELEGATE_TwoParams

 

델리게이트를 사용하는 클래스 제작

DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);

/**
 * 
 */
UCLASS()
class UNREALDELEGATE_API UCourseInfo : public UObject
{
	GENERATED_BODY()
	
public :
	UCourseInfo();

	FCourseInfoOnChangedSignature OnChanged;

	void ChnageCourseInfo(const FString& InSchoolName, const FString& InNewContents);

private:
	FString Contents;
};
void UCourseInfo::ChnageCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
	Contents = InNewContents;

	UE_LOG(LogTemp, Log, TEXT("[CourseInfo] 학사 정보가 변경되어 알림을 발송합니다"));
	OnChanged.Broadcast(InSchoolName, Contents);
}

 

CourseInfo 클래스는 DECLARE_MULTICAST_DELEGATE_TwoParams라는 메크로로 델리게이트를 사용할 예정이다.

해당 델리게이트의 이름은 FCourseInfoOnChangedSignature 이다.

보통 언리얼에선 델리게이트 끝에(접미사에) Signature를 붙인다.

 

해당 ChangeCourseInfo 메소드는 변경사항이 생겼을 경우 델리게이트를 호출하겠다라는 의미를 포함하고 있다.

델리게이트로 제작된 멤버변수 OnChaged 를 Broadcast라는 메소드를 통해 변경사항이 생긴것을 알린다.

 

 

	CourseInfo = NewObject<UCourseInfo>(this);

	UStudent* Student1 = NewObject<UStudent>();
	Student1->SetName(TEXT("학생1"));
	UStudent* Student2 = NewObject<UStudent>();
	Student2->SetName(TEXT("학생2"));
	UStudent* Student3 = NewObject<UStudent>();
	Student3->SetName(TEXT("학생3"));

	CourseInfo->OnChanged.AddUObject(Student1, &UStudent::GetNotification);
	CourseInfo->OnChanged.AddUObject(Student2, &UStudent::GetNotification);
	CourseInfo->OnChanged.AddUObject(Student3, &UStudent::GetNotification);

	CourseInfo->ChnageCourseInfo(SchoolName, TEXT("변경된 학사 정보"));
    
    출력 :
LogTemp: [CourseInfo] 학사 정보가 변경되어 알림을 발송합니다
LogTemp: [Student]학생3님이 기본 학교로부터 받은 메세지 : 변경된 학사 정보
LogTemp: [Student]학생2님이 기본 학교로부터 받은 메세지 : 변경된 학사 정보
LogTemp: [Student]학생1님이 기본 학교로부터 받은 메세지 : 변경된 학사 정보
void UStudent::GetNotification(const FString& School, const FString& NewCourseInfo)
{
	UE_LOG(LogTemp, Log, TEXT("[Student]%s님이 %s로부터 받은 메세지 : %s"), *Name, *School, *NewCourseInfo);
}

 

OnChanged에 AddUObject를 통해 델리게이트에 등록한다.

마지막에 등록된 순서대로 (LIFO) 호출된다.

반응형