본 개발노트는 혼자서 언리얼 게임개발 독학하는 과정에서
공부한 내용들을 기록&공유하는 게시글이며
부족한점이 있을 수 있어 참고해서 봐주시면 감사하겠습니다
👉 이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해
9강에서는 언리얼 인터페이스에 대해서 학습했습니다. 아래는 필기를 한 강의 노트입니다
📖 7강 필기 노트
인터페이스란?
- 객체가 반드시 구현해야 할 행동을 지정하는데 활용되는 타입
- 다형성의 구현, 의놎ㄴ성이 분리된 설계에 유용하게 활용
언리얼 C++ 인터페이스 특징
- 인터페이스를 생성하면 두 개의 클래스가 생성됨
- U로 시작: 클래스 타입 정보의 제공
- I로 시작: 실질적인 설계 및 구현
- 객체를 설계할 때 I 인터페이스 클래스를 사용
- 추상 타입으로만 선언할 수 있는 Java, C#과 달리 언리얼은 인터페이스에도 구현이 가능함
언리얼 C++ 인터페이스
- 클래스가 반드시 구현해야 하는 기능을 지정하는데 사용함
- C++은 기본적으로 다중상속을 지원하지만, 언리얼 C++의 인터페이스를 사용해 가급적 축소된 다중상속의 형태로 구현하는 것이 향후 유지보수에 도움된다
- 언리얼 C++ 인터페이스는 두 개의 클래스를 생성한다
- 언리얼 C++ 인터페이스는 추상 타입으로 강제되지 않고, 내부에 기본 함수를 구현할 수 있다
8강에서는 언리얼 컴포지션에 대해서 학습했습니다. 아래는 필기를 한 강의 노트입니다
📖 8강 필기 노트
컴포지션을 활용한 언리얼 오브젝트 설계
- 언리얼 C++ 컴포지션을 구현하는 독특한 패턴이 있다
- 클래스 기본 객체를 생성하는 생성자 코드를 사용해 복잡한 언리얼 오브젝트를 생성할 수 있음
- 언리얼 C++ 컴포지션의 Has-A 관계에 사용되는 용어
- 언리얼 C++이 제공하는 확장 열거형을 사용해 다양한 메타 정보를 넣고 활용할 수 있다
컴포지션(Composition)
- 컴포지션은 객체 지향 설계에서 Has-A 관계를 구현하는 설계 방법
- 성질이 다른 객체에서 어떤 객체가 다른 객체를 소유하는 방식
- 복합적인 기능을 거대한 클래스를 효과적으로 설계하는데 유용하게 사용할 수 있음
언리얼 엔진에서의 컴포지션 구현 방법
- 하나의 언리얼 오브젝트에는 항상 클래스 기본 오브젝트 CDO가 있다
- 다음의 선택지가 존재한다
- 방법 1: CDO에 미리 언리얼 오브젝트를 생성해 조합 (필수적 포함)
- 방법 2: CDO에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합한다 (선택적 포함)
- 내가 소유한 언리얼 오브젝트를 SubObject라고 한다
- 나를 소유한 언리얼 오브젝트를 Outer라고 한다
9강에서는 언리얼 델리게이트 시스템 설명 및 오브젝트의 구성에 대해서 학습했습니다. 아래는 필기를 한 강의 노트입니다
📖 9강 필기 노트
강한 결합과 느슨한 결합
- 강한 결합: 클래스들이 서로 의존성을 가지는 경우
- 느슨한 결합: 실물에 의존하지 않고 추상적 설계에 의존하라
델리게이트(Delegate)
- 함수를 오브젝트처럼 관리하면 어떨까?
- C에서 함수를 다루는 방법
- 함수 포인터를 활용한 콜백함수의 구현
- 가능은 하나 이를 정의하고 사용하는 과정이 꽤나 복잡함
- 안정성을 스스로 검증해주어야 함
- C#의 델리게이트
- 함수를 마치 객체처럼 다룰 수 있ㅇ음
- 안정적이고 간편한 선언
- 언리얼 C++도 델리게이트를 지원함
- 느슨한 결합 구조를 간편하고 안정적으로 구현할 수 있음
발행 구독 디자인 패턴
- 푸시 형태의 알림을 구현하는데 적합한 디자인 패턴
- 발행자와 구독자로 구분된다
- 콘텐츠 제작자는 콘텐츠를 생산한다
- 발행자는 콘텐츠를 배포한다
- 구독자는 배포된 콘텐츠를 받아 소비한다
- 제작자와 구독자가 서로를 몰라도, 발행자를 통해 콘텐츠를 생산하고 전달할 수 있다
- 발행 구독 디자인 패턴의 장점
- 유지보수가 쉽고, 유연하게 활용될 수 있으며, 테스트가 쉬워진다
- 시스템이 커져도 유연하게 조절할 수 있으며, 기능확장이 용이하다
- 느슨한 결합
언리얼 델리게이트 선언시 고려사항
- 어떤 데이터를 전달하고 받을것인가? 인자의 수와 각각의 타입을 설계
-
- 일대일 OR 일대다
- 프로그래밍 환경 설정
-
- C++ 프로그래밍에서만 사용할 것인지
- UFUNCTION으로 지정된 블루프린트 함수와 사용할 것인지
- 어떤 함수와 연결할 것인지
-
- 클래스 외부에 설계된 C++ 함수
- 전역 설계된 정적 함수
- 언리얼 오브젝트의 멤버 함수와 연결 (대부분이 이 경우)
선언 매크로
DECLARE_{델리게이트유형}DELEGATE{함수정보}
- 델리게이트 유형
-
- 일대일 C++ : DECLARE_DELEGATE
- 일대다 C++ : DECLARE_MULTICAST
- 일대일 블루프린트 : DECLARE_DYNAMIC
- 일대다 블루프린트 : DECLARE_DYNAMIC_MULTICAST
- 함수 정보 : 연동 될 함수 형태를 지정한다
-
- 인자X 반환값X, 공란: DECLARE_DELEGATE
- 인자1 반환값X, OneParam: DELCARE_DELEGATE_OneParam
- 인자3 반환값O, RetVal_ThreeParams: DECLARE_DELEGATE_RetVal_ThreeParams (MULTICAST는 반환값 지원X)
- 최대 9개까지 지원
언리얼 C++ 델리게이트
- 느슨한 결합이 가지는 장점: 향후 시스템 변경 사항에 대해 손쉽게 대처할 수 있음
- 발행 구독 모델의 장점
-
- 클래스는 자신이 해야 할 작업에만 집중할 수 있음
- 외부에서 발생한 변경사항에 대해 영향받지 않음
- 자신의 기능을 확장하더라도 다른 모듈에 영향을 주지 않음
- 언리얼 C++의 델리게이트의 선언 방법과 활용
데이터 기반의 디자인 패턴을 설계할 때 유용하게 사용
// Interface Class 생성
class UNREALINTERFACE_API ILessonInterface
{
GENERATED_BODY()
public:
virtual void DoLesson()
{
UE_LOG(LogTemp, Log, TEXT("수업에 입장합니다."));
}
};
// Student.h 클래스에서 상속
UCLASS()
class UNREALINTERFACE_API UStudent : public UPerson, public ILessonInterface
{
GENERATED_BODY()
public:
UStudent();
virtual void DoLesson() override;
};
// Student.cpp 구현
void UStudent::DoLesson()
{
ILessonInterface::DoLesson();
UE_LOG(LogTemp, Log, TEXT("%s님은 공부합니다."), *Name);
}
// MyGameInstance에서 활용
void UMyGameInstance::Init()
{
Super::Init();
UE_LOG(LogTemp, Log, TEXT("============================"));
TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() };
for (const auto Person : Persons)
{
UE_LOG(LogTemp, Log, TEXT("구성원 이름 : %s"), *Person->GetName());
}
UE_LOG(LogTemp, Log, TEXT("============================"));
for (const auto Person : Persons)
{
ILessonInterface* LessonInterface = Cast<ILessonInterface>(Person);
if (LessonInterface)
{
UE_LOG(LogTemp, Log, TEXT("%s님은 수업에 참여할 수 있습니다."), *Person->GetName());
LessonInterface->DoLesson();
}
else
{
UE_LOG(LogTemp, Log, TEXT("%s님은 수업에 참여할 수 없습니다."), *Person->GetName());
}
}
}
//Card 컴포지션 클래스 구현 및 ENUM 클래스 선언
UENUM()
enum class ECardType : uint8
{
Stdent = 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;
};
// MyGameInstance에서 활용
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();
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);
}
}
}
// Card 클래스를 활용하는 클래스에서 선언하는 방법(Person.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;
};
// CourseInfo 클래스 선언 및 델리게이트 선언
// CourseInfo.h
DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);
UCLASS()
class UNREALDELEGATE_API UCourseInfo : public UObject
{
GENERATED_BODY()
public:
UCourseInfo();
FCourseInfoOnChangedSignature OnChanged;
void ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents);
private:
FString Contents;
};
// CourseInfo.cpp
void UCourseInfo::ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
Contents = InNewContents;
UE_LOG(LogTemp, Log, TEXT("[CourseInfo] 학사 정보가 변경되어 알림을 발송합니다."));
OnChanged.Broadcast(InSchoolName, Contents);
}
// MyGameInstance에서 활용
void UMyGameInstance::Init()
{
Super::Init();
CourseInfo = NewObject<UCourseInfo>(this);
UE_LOG(LogTemp, Log, TEXT("============================"));
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->ChangeCourseInfo(SchoolName, TEXT("변경된 학사 정보"));
UE_LOG(LogTemp, Log, TEXT("============================"));
}