게임 엔진/Unreal

[Unreal Engine] Delegate

Henzee 2025. 6. 16. 23:47

 

 


 

 

1. 느슨한 결합

1) 강한 결합과 느슨한 결합

강한 결합의 예시

  • 강한 결합 (Tight Coupling)
    • 클래스들이 서로 의존성을 가지는 경우를 의미함
    • 위 예시에서 Card가 없는 경우 Person이 만들어질 수 없음
    • 이때 Person은 Card에 대한 의존성을 가진다고 함
    • 핸드폰에서도 인증할 수 있는 새로운 카드가 도입된다면?

 

느슨한 결합의 예시

  • 느슨한 결합 (Loose Coupling)
    • 실물에 의존하지 말고 추상적 설계에 의존하라. (DIP 원칙)
    • 왜 Person은 Card가 필요한가? 출입을 확인해야 하기 때문
    • 출입에 관련된 추상적인 설계에 의존해야 함
    • ICheck를 상속받은 새로운 카드 인터페이스를 선언해 해결
    • 이러한 느슨한 결합 구조는 유지 보수를 손쉽게 만들어 줌

 

 

2) 느슨한 결합의 간편한 구현 - 델리게이트 (Delegate)

문서 링크: 언리얼 델리게이트 

 

  • 함수를 오브젝트처럼 관리하는 아이디어
  • 함수를 다루는 방법
    • 함수 포인터를 활용한 콜백(callback) 함수의 구현
    • 가능은 하나 이를 정의하고 사용하는 과정이 꽤나 복잡함
    • 안정성을 스스로 검증해줘야 함
    • C++ 17 규약의 std::bind와 std::function 활용은 느림
  • C#의 델리게이트(delegate) 키워드
    • 함수를 마치 객체처럼 다룰 수 있음
    • 안정적이고 간편한 선언
  • 언리얼 C++도 델리게이트를 지원
    • 느슨한 결합 구조를 간편하고 안정적으로 구현 가능
class Card 
{
	public int Id;
	public bool CardCheck() { return true; }
};

public delegate bool CheckDelegate();
public class Person
{
	Person(CheckDelegate InCheckDelegate)
	{
		this.Check = InCheckDelegate;
	}

	public CheckDelegate Check;
};

 

 


 

 

2. 발행-구독 디자인 패턴

1) 발행 구독 디자인 패턴

발행-구독 구조

 

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

 

 

2) 예제를 위한 클래스 다이어그램과 시나리오

클래스 다이어그램

 

  • 학교에서 진행하는 온라인 수업 활동 예시
  • 학사정보(CourseInfo)와 학생(Student)
    • 학교는 학사 정보를 관리
    • 학사 정보가 변경되면 자동으로 학생에게 알림
    • 학생은 학사 정보의 알림 구독을 해지 가능
  • 시나리오
    1. 학사 정보와 3명의 학생이 존재
    2. 시스템에서 학사 정보를 변경
    3. 학사 정보가 변경되면 알림 구독한 학생들에게 변경 내용을 자동으로 전달

 

 

3) 언리얼 델리게이트를 활용한 예제 설계

 

  • 언리얼 엔진은 발행 구독 패턴 구현을 위해 델리게이트 기능을 제공
  • 델리게이트의 사전적 의미는 대리자
    • 학사 정보의 구독과 알림을 대리해주는 객체
  • 시나리오 구현을 위한 설계
    • 학사 정보는 구독과 알림을 대행할 델리게이트를 선언
    • 학생은 학사 정보의 델리게이트를 통해 알림을 구독
    • 학사 정보는 내용 변경 시 델리게이트를 사용해 등록한 학생들에게 알림
  • 학사 정보 클래스와 학생 클래스의 상호 의존성을 최대한 없앰
    • 하나의 클래스는 하나의 작업에만 집중하도록 설계
    • 학사 정보 클래스는 델리게이트를 선언하고 알림에만 집중
    • 학생 클래스는 알림을 수신하는데만 집중
    • 학사 정보와 학생은 서로 헤더를 참조하지 않도록 주의
  • 이를 위해 발행과 구독을 컨트롤하는 주체를 설정
    • 학사 정보에서 선언한 델리게이트를 중심으로 구독과 알림을 컨트롤하는 주체 설정

 

 


 

 

3. 언리얼 델리게이트의 선언

1) 언리얼 델리게이트 선언 시 고려사항

  • 델리게이트를 설계하기 위한 고려사항
    • 어떤 데이터를 전달하고 받을 것인가? 인자의 수와 각각의 타입을 설계
      • 몇 개의 인자를 전달할 것인지
      • 어떤 방식으로 전달할 것인지
      • 일대일로 전달
      • 일대다로 전달
    • 프로그래밍 환경 설정
      • C++ 프로그래밍에서만 사용할 것인가?
      • UFUNCTION으로 지정된 블루프린트 함수와 사용할 것인가?
    • 어떤 함수와 연결할 것인가?
      • 클래스 외부에 설계된 C++ 함수와 연결
      • 전역에 설계된 정적 함수와 연결
      • 언리얼 오브젝트의 멤버 함수와 연결 (대부분의 경우에 이 방식을 사용)

 

 

2) 언리얼 델리게이트 선언 매크로

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

  • 델리게이트 유형 : 어떤 유형의 델리게이트인지 구상
    • 일대일 형태로 C++만 지원한다면 유형은 공란 : DECLARE_DELEGATE
    • 일대다 형태로 C++만 지원한다면 MULTICAST를 선언 : DECLARE_MULTICAST
    • 일대일 형태로 블루프린트를 지원한다면 DYNAMIC을 선언 : DECLARE_DYNAMIC
    • 일대다 형태로 블루프린트를 지원한다면 DYNAMIC과 MULTICAST를 조합 : DECLARE_DYNAMIC_MULTICAST
  • 함수 정보 : 연동될 함수 형태를 지정
    • 인자가 없고 반환값도 없으면 공란 : DECLARE_DELEGATE
    • 인자가 하나고 반환값이 없으면 OneParam으로 지정 : DECLARE_DELEGATE_OneParam
    • 인자가 세 개고 반환값이 있으면 RetVal_ThreeParams로 지정 : DECLARE_DELEGATE_RetVal_ThreeParams (MULTICAST는 반환값을 지원하지 않음)
    • 최대 9개까지 지원

 

 

3) 언리얼 델리게이트 매크로 선정 예시

DECLARE_MULTICAST_DELEGATE_TwoParams

  • 학사 정보가 변경되면 알림 주체와 내용을 학생에게 전달
    • 두 개의 인자
  • 변경된 학사 정보는 다수 인원을 대상으로 발송
    • MULTICAST 사용
  • 오직 C++ 프로그래밍에서만 사용
    • DYNAMIC 사용 안함

 

 


 

 

4. 델리게이트를 활용한 예제 구현

1) CourseInfo 클래스 생성 및 델리게이트 구현

Object 상속

 

// Header File

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "CourseInfo.generated.h"

// 델리게이트 선언
DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&)

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

	// 델리게이트 정보를 멤버 변수로 등록
	FCourseInfoOnChangedSignature OnChanged;

	// 외부에서 학사 정보를 변경할 때 사용할 함수
	void ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents);

private:
	// C++ 에서만 사용할 것이므로 UPROPERTY()로 메모리 관리 필요없음
	FString Contents;
};


// Cpp File

#include "CourseInfo.h"

UCourseInfo::UCourseInfo()
{
	Contents = TEXT("기존 학사 정보");
}

void UCourseInfo::ChangeCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
	Contents = InNewContents;

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

 

 

2) Student 클래스에 델리게이트 구현

// Header File

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

	virtual void DoLesson() override;

	void GetNotification(const FString& School, const FString& NewCourseInfo);
};


// Cpp File

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

 

 

3) MyGameInstance에서 델리게이트 연결

// Header File

UCLASS()
class HELLOUNREAL_API UMyGameInstance : public UGameInstance
{
	GENERATED_BODY()

public:
	UMyGameInstance();

	virtual void Init() override;
	
private:
	UPROPERTY()
	TObjectPtr<class UCourseInfo> CourseInfo;

	UPROPERTY()
	FString SchoolName;
};


// Cpp File

#include "MyGameInstance.h"
#include "Student.h"
#include "Teacher.h"
#include "Staff.h"
#include "Card.h"
#include "CourseInfo.h"

UMyGameInstance::UMyGameInstance()
{
	SchoolName = TEXT("학교");
}

void UMyGameInstance::Init()
{
	Super::Init();

	CourseInfo = NewObject<UCourseInfo>(this); // CourseInfo 객체의 Outer를 MyGameInstance로 지정 (컴포지션 관계 설정)

	UE_LOG(LogTemp, Log, TEXT("============================"));

	UStudent* Student1 = NewObject<UStudent>(); // 바로 사라질 객체라서 Outer 지정 안함
	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 변경
	CourseInfo->ChangeCourseInfo(SchoolName, TEXT("변경된 학사 정보"));

	UE_LOG(LogTemp, Log, TEXT("============================"));
}

 

출력 결과