언리얼 엔진5 Basic - 모던객체지향 설계 구현

타입·2024년 3월 21일
0

언리얼 강의

목록 보기
2/47

인터페이스 (Interface)

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

  • 언리얼의 인터페이스
    • 인터페이스를 생성하면 두 개의 클래스가 생성됨
      U로 시작하는 타입 클래스
      I로 시작하는 인터페이스 클래스
    • 객체를 설계할 때 I 인터페이스 클래스를 사용
      U타입 클래스 정보는 런타임에서 인터페이스 구현 여부를 파악하는 용도로 사용됨
      U타입 클래스에서 작업할 일은 없고 I 인터페이스 클래스에서 구현
    • C++ 인터페이스의 특징
      추상 타입으로만 선언할 수 있는 Java와 C#과 달리 언리얼은 인터페이스에도 구현 가능

Source 폴더로 복사한 코드를 프로젝트에 포함시키려면
언리얼 에디터에서 'Tools-Referesh Visual Studio 2022 Project' 선택

  • Unreal Interface를 상속받아 인터페이스 추가

인터페이스의 함수를 추상 함수로 두어 구현을 강제할 수도 있고, 미리 정의해두고 필요한 경우 재정의
Super 키워드로 인터페이스의 부모를 가져올 수 없음 (클래스 정보는 단일 상속만 지원)
대신 IUserInterface::Func(); 처럼 인터페이스의 타입을 직접 입력하여 가능

캐스팅으로 인터페이스 상속 여부를 확인할 수 있음

TArray<UPerson*> Persons = { NewObject<UStudent>(), NewObject<UTeacher>(), NewObject<UStaff>() }
for (const auto Person : Persons)
{
	ILessonInterface* LessonInterface = Cast<ILessonInterface>(Person);
    if (LessonInterface)
    {
    	LessonInterface->DoLesson(); // (인터페이스 상속)
    }
    else
    {
    	// 수업에 참여 불가 (인터페이스 상속 안함)
    }
}

언리얼 C++ 인터페이스를 사용하면, 클래스가 수행해야 할 의무를 명시적으로 지정할 수 있어 좋은 객체 설계를 만드는데 도움을 줄 수 있다.


컴포지션 (Composition)

상속: 성질이 같은 부모-자식 관계를 의미하는 Is-A 관계
컴포지션: 성질이 다른 두 객체에서 어느 객체를 소유하는 Has-A 관계

class Card
{
public:
	Card(int InId) : Id(InId) {}
    int id = 0;
};

class Person
{
public:
	Person(Card InCard) : IdCard(InCard) {}
protected:
	Card IdCard; // 카드 소유
};
  • 언리얼 오브젝트에 다른 언리얼 오브젝트를 조합할 때
    방법 1: CDO에 미리 언리얼 오브젝트를 생성해 조합 (필수적 포함)
    방법 2: CDO에 빈 포인터만 넣고 런타임에서 언리얼 오브젝트를 생성해 조합 (선택적 조합)

  • 언리얼 오브젝트를 생성할 때 컴포지션 정보를 구축할 수 있음
    내가 소유한 언리얼 오브젝트는 Subobject
    나를 소유한 언리얼 오브젝트는 Outer

전방 선언으로 클래스 구조는 모르지만 클래스 만큼의 크기를 미리 확보 (의존성도 감소)
언리얼5부턴 선언 시 TObjectPtr로 감싸는 걸 추천 [공식 문서 링크]

언리얼 C++의 컴포지션 기법은 게임의 복잡한 객체를 설계하고 생성할 때 유용하게 사용된다.


델리게이트 (Delegate)

  • 강한 결합과 느슨한 결합
    • 강한 결합 (Tight Coupling)
      클래스들이 서로 의존성을 가지는 경우를 의미
    • 느슨한 결합 (Loose Coupling)
      실물에 의존하지 말고 추상적 설계에 의존하라 (DIP 원칙)
      느슨한 결합 구조는 유지 보수를 손쉽게 만듦
  • 함수를 다루는 방법
    • 함수 포인터를 활용한 콜백 함수의 구현
      정의하고 사용하는 과정이 복잡하고 안정성 검증 필요
      C++17의 std::bind와 std::function 활용은 느림
    • C#의 델리게이트 키워드
      함수를 마치 객체처럼 다룰 수 있음
      안정적이고 간편한 선언
    • 언리얼 C++도 델리게이트를 지원
      느슨한 결합 구조를 간편하고 안정적으로 구현 가능
  • 발행 구독 디자인 패턴
    Push 형태의 알림을 구현하는데 적합한 디자인 패턴

    • 발행자와 구독자로 구분
      콘텐츠 제작자는 콘텐츠를 생산
      발행자는 콘텐츠를 배포
      구독자는 배포된 콘텐츠를 받아 소비
      제작자와 구독자가 서로를 몰라도, 발행자를 통해 콘텐츠를 생산하고 전달 가능 (느슨한 결합)
    • 발행 구독 디자인 패턴의 장점
      제작자와 구독자는 서로를 모르기 때문에 느슨한 결합으로 구성
      유지 보수(Maintenance)가 쉽고 유연하게 활용될 수 있고(Flexibility) 테스트가 쉬워짐
      시스템 스케일을 유연하게 조절할 수 있으며(Scalability), 기능 확장(Extensibility)이 용이
  • 델리게이트 예시

// CourseInfo.h
DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);
UCLASS()
class UNREALDELEGATE_API UCourseInfo : public UObject
{
	...
    FCourseInfoOnChangedSignature OnChanged;
};

// CourseInfo.cpp
void UCourseInfo::ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
	Contents = InNewContents;
    
    OnChanged.Broadcast(InSchoolName, Contents);
}

// Student.cpp
void UStudent::GetNotification(const FString& School, const FString& NewCourseInfo)
{
	UE_LOG(LogTemp, Log, TEXT("[Student] %s님이 %s로부터 받은 메시지 : %s"), *Name, *School, *NewCourseInfo);
}

// MyGameInstance.cpp
void UMyGameInstance::Init()
{
	Super::Init();
    
    // 학사 정보는 발행자가 소유
    CourseInfo = NewObject<UCourseInfo>(this);
    
    UStudent* Student1 = NewObject<UStudent>();
    UStudent* Student2 = NewObject<UStudent>();
    UStudent* Student3 = NewObject<UStudent>();
    
    // 구독
    CourseInfo->OnChanged.AddUObject(Student1, &UStudent::GetNotification);
    CourseInfo->OnChanged.AddUObject(Student2, &UStudent::GetNotification);
    CourseInfo->OnChanged.AddUObject(Student3, &UStudent::GetNotification);
    
    // 발행
    CourseInfo->ChangeCourseInfo(SchoolName, TEXT("변경된 학사 정보"));
}

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

profile
주니어 언리얼 프로그래머

0개의 댓글