언리얼 엔진 공부하기 #10 [UE5]

신지한·2024년 6월 26일
0

개발노트

목록 보기
16/17
post-thumbnail

📢 개발노트에 앞서서

본 개발노트는 혼자서 언리얼 게임개발 독학하는 과정에서
공부한 내용들을 기록&공유하는 게시글이며
부족한점이 있을 수 있어 참고해서 봐주시면 감사하겠습니다

👉 이득우의 언리얼 프로그래밍 Part1 - 언리얼 C++의 이해

❗ 본 게시글은 위 강의의 내용을 정리 및 실습한 게시글로 모든 내용은 대부분의 내용은 책이 출처입니다


📚 스터디 내용

9강에서는 언리얼 인터페이스에 대해서 학습했습니다. 아래는 필기를 한 강의 노트입니다

📖 7강 필기 노트

인터페이스란?

  • 객체가 반드시 구현해야 할 행동을 지정하는데 활용되는 타입
  • 다형성의 구현, 의놎ㄴ성이 분리된 설계에 유용하게 활용

언리얼 C++ 인터페이스 특징

  • 인터페이스를 생성하면 두 개의 클래스가 생성됨
    • U로 시작: 클래스 타입 정보의 제공
    • I로 시작: 실질적인 설계 및 구현
  • 객체를 설계할 때 I 인터페이스 클래스를 사용
  • 추상 타입으로만 선언할 수 있는 Java, C#과 달리 언리얼은 인터페이스에도 구현이 가능함

언리얼 C++ 인터페이스

  1. 클래스가 반드시 구현해야 하는 기능을 지정하는데 사용함
  2. C++은 기본적으로 다중상속을 지원하지만, 언리얼 C++의 인터페이스를 사용해 가급적 축소된 다중상속의 형태로 구현하는 것이 향후 유지보수에 도움된다
  3. 언리얼 C++ 인터페이스는 두 개의 클래스를 생성한다
  4. 언리얼 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++의 델리게이트의 선언 방법과 활용

데이터 기반의 디자인 패턴을 설계할 때 유용하게 사용

👉 실습코드

❗ 실습 프로젝트가 많아 코드 일부를 발췌했습니다

📌 7강 실습코드

// 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());
		}
	}
}

📌 8강 실습코드

//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;
};

📌 9강 실습코드

// 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("============================"));
}

😅 강의 들으면서 필기한거라 급하게 작성해서 내용이 중구난방일 수 있습니다..!

profile
게임 개발자

0개의 댓글