Unreal5 Delegate

이승한·2024년 3월 31일
0

Unreal5

목록 보기
7/11

Delegate

  • 느슨한 결합의 장점과 이를 편리하게 구현하도록 도와주는 델리게이트의 이해
  • 발행 구독 디자인 패턴의 이해
  • 언리얼 델리게이트를 활용한 느슨한 결합의 설계와 구현의 학습

강한 결합

  • 클래스들이 서로 강한 의존성을 가짐
  • 아래 예시에서 Card가 없는 경우 Person이 만들어질 수 없다.
  • 이 때 Person은 Card에 대한 의존성을 가진다고 한다.
  • 핸드폰에서도 인증할 수 있는 새로운 카드가 도입된다면?
class Card
{
public:
	Card(int InId) : id(InId) {}
    int Id = 0;
};

class Person
{
public:
	Person(Card InCard) : ICard(InCard) {}
   
protected:
	Card IdCard;
}

느슨한 결합

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

class Card : public ICheck
{
public:
	Card(int InId) : Id(Inid) {}
    
    virtual bool check() { return true;}
private:
	int Id = 0;
};

class Person
{
public:
	Person(ICheck* InCheck) : Check(InCheck) {}
protected:
	ICheck* Check;
};

하지만 행동의 중심을 둔 추상화 작업을 위해서 매번 인터페이스를 만드는 것이 번거롭다.

행동에 대해서 오브젝트로 관리하면 어떨까?

함수를 다루는 방법

  • 함수 포인터를 활용한 콜백(callback) 함수의 구현
  • 가능은 하나 이를 정의하고 사용하는 과정이 복잡
  • 안정성을 스스로 검증
  • C++ 17 구약의 std::bind와 std::function 활용은 느림

C#의 델리게이트(delegate) 키워드

  • 함수를 마치 객체처럼 다룰 수 있음
  • 안정적이고 간편한 선언

언리얼C++도 델리게이트 지원

  • 느슨한 결합 구조를 간편하고 안정적으로 구현가능
public 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;
};
  1. 함수에 대한 것을 마치 객체 처럼 선언
    -> public delegate bool CheckDelegate();
  2. 객체를 변수로 선언
    -> public CheckDelegate Check;
  3. Card는 델리게이트와 똑같은 유형의 함수만 지정
    -> public bool CardCheck() {return true;}
  4. Person과 Card를 묶어주기만 하면 원하는 기능 구현 가능

발행 구독 디자인 패턴

  • 푸시(Push)형태의 알림(Notification)을 구현하는데 적합한 디자인 패턴
  • 발행자와 구독자로 구분
    -> 콘텐츠 제작자는 콘텐츠를 생상
    -> 발행자는 콘텐츠를 배포
    -> 구독자는 배포된 콘텐츠를 받아 소비
    -> 제작자와 구독자가 서로를 몰라도, 발행자를 통해 콘텐츠를 생상하고 전달 가능 (느슨한 결합)

  • 발행 구독 디자인 패턴의 장점
    -> 제작자와 구독자는 서로를 모르기 때문에 느슨한 결합
    -> 유지 보수가 쉽고, 유연하게 활용, 테스트가 쉬워짐
    -> 시스템 스케일을 유연하게 조절할 수 있으며, 기능 확장이 용이하다


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

  • 고려사항

  1. 몇개의 인자를 전달
  2. 어떤 방식으로 전달
  3. 일대일로 전달
  4. 일대다로 전달
  • 프로그래밍 환경 설정

  1. C++ 프로그래밍에서만 사용할 것인가?
  2. UFUNCTION으로 지정된 블루프린트 함수와 사용할 것인가?
  • 어떤 함수와 연결할 것인가?

  1. 클래스 외부에 설계된 C++함수와 연결
  2. 전역에 설계된 정적 함수와 연결
  3. 언리얼 오브젝트의 멤버 함수와 연결 (대부분의 경우에 이 방식을 사용)

델리게이트 선언 매크로

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

  • 델리게이트 유형

  1. 일대일 형태로 C++만 지원한다면 유형은 공란으로 둔다
    -> DECLARE_DELEGATE
  2. 일대다 형태로 C++만 지원한다면 MULTICAST를 선언한다
    -> DECLARE_MULTICAST
  3. 일대일 형태로 블루프린트를 지원한다면 DYNAMIC을 선언한다
    -> DECLARE_DYNAMIC
  4. 일대 다 형태로 블루프린트를 지원한다면 DYNAMIC과 MULTICAST를 조합한다
    -> DECLARE_DYNAMIC_MULTICAST
  • 함수 정보: 연동될 함수 형태를 지정한다

  1. 인자가 없고 반환값도 없으면 공란으로 둔다
    -> DECLARE_DELEGATE
  2. 인자가 하나고 반환값이 없으면 OneParam으로 지정한다
    -> DECLARE_DELEGATE_OneParam
  3. 인자가 세 개고 반환값이 있으면 RetVal_ThreeParams로 지정한다
    -> DECLARE_DELEGATE_RetVal_ThreeParams (MULTICAST는 반환값을 지원하지 않음)
  4. 최대 9개까지 지원

시나리오

  • 학교에서 진행하는 온라인 수업 활동 예시

  • 학사 정보(CourseInfo)와 학생(Student)
    -> 학교는 학사 정보를 관리
    -> 학사 정보가 변경되면 자동으로 학생에게 알려준다.
    -> 학생은 학사 정보의 알림 구독을 해지할 수 있다.

  • 시나리오

  1. 학사 정보와 3명의 학생이 있따.
  2. 시스템에서 학사 정보를 변경한다.
  3. 학사 정보가 변경되면 알림 구독한 학생들에게 변경 내용을 자동으로 전달한다.
  • 학사 정보가 변경되면 알림 주체와 내용을 학생에게 전달
    -> 두 개의 인자를 가짐
  • 변경된 학사 정보는 다수 인원을 대상으로 발송
    -> MULTICAST 사용
  • 오직 C++ 프로그래밍에서만 사용한다.
    -> DYNAMIC은 사용하지 않음

즉, 우리가 사용할 매크로는 DECLARE_MULTICAST_DELEGATE_TwoParams 매크로 사용


CourseInfo.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

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

//1.
DECLARE_MULTICAST_DELEGATE_TwoParams(FCourseInfoOnChangedSignature, const FString&, const FString&);

/**
 * 
 */
UCLASS()
class UNREALDELEGATE_API UCourseInfo : public UObject
{
	GENERATED_BODY()
public:
	UCourseInfo();
	//2.
	FCourseInfoOnChangedSignature OnChanged;
	//3.
	void ChangedCourseInfo(const FString& InSchoolName, const FString& InNewContents);

private:
	//4.
	FString Contents;
};

CourseInfo.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "CourseInfo.h"

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

void UCourseInfo::ChangedCourseInfo(const FString& InSchoolName, const FString& InNewContents)
{
	Contents = InNewContents;
	
	UE_LOG(LogTemp, Log, TEXT("[CourseInfo] 학사 정보가 변경되어 알림을 발송합니다."));
	OnChanged.Broadcast(InSchoolName,Contents);
}

Student.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Person.h"
#include "LessonInterface.h"
#include "Student.generated.h"

/**
 * 
 */
UCLASS()
class UNREALDELEGATE_API UStudent : public UPerson,public ILessonInterface
{
	GENERATED_BODY()
	
public:
	UStudent();

	virtual void DoLesson() override;

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

Student.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Student.h"
#include "Card.h"
UStudent::UStudent()
{
	Name = TEXT("이학생");
	Card->SetCardType(ECardType::Student);
}

void UStudent::DoLesson()
{
	ILessonInterface::DoLesson();
	UE_LOG(LogTemp, Log, TEXT("%s님은 공부합니다."), *Name);
}

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

MyGameInstance.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "MyGameInstance.generated.h"

/**
 * 
 */
UCLASS()
class UNREALDELEGATE_API UMyGameInstance : public UGameInstance
{

	GENERATED_BODY()
	
public:
	UMyGameInstance();

	virtual void Init() override;

private:

	// 학교를 우리가 지금 대상으로 선언했고
	//학교는 학사 시스템을 소유해줘야 하므로, MyGameInstance에서도 학사 시스템을 소유하도록 CourseInfo 를 선언하는데
	// CourseInfo는 언리얼 오브젝트이고, 포인터로 관리를 해야하는데 포인터로 관리하기에 전방 선언이 가능
	// 전방 선언으로 멤버 변수로 지정할때는 TObjectPtr을 사용
	UPROPERTY()
	TObjectPtr<class UCourseInfo> CourseInfo;

	UPROPERTY()
	FString SchoolName;
	
};

MyGameInstance.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#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();
	//NewObject<UCourseInf>(아우터 지정); -> 이전 시간에 했던 컴포지션 관계에서 아우터를 지정 가능
    // 우리가 생성한 객체는 클래스 멤버 변수에 들어가서 앞으로 관리를 받고있고, 특별한 일이 있지 않는 한 메모리에 계속 유지가 될텐데
    // 이떄 MyGameInstance는 CourseInfo를 포함해줘야 한다.
    //this를 넣어주면 CourseInfo는 MyGameInstance의 서브 오브젝트가 되고
    // MyGameInstance는 CourseInfo의 Outer가 된다. -> 컴포지션 관계 성립
	CourseInfo = NewObject<UCourseInfo>(this);

	UE_LOG(LogTemp, Log, TEXT("========================"));
    //밑에 Student를 생성하는 구문은 실행되면 오브젝트가 자동으로 소멸되기에 굳이 Outer를 설정해줄 필요 없다.
    UStudent* Student1 = NewObject<UStudent>();
	Student1->SetName(TEXT("학생1"));
	UStudent* Student2 = NewObject<UStudent>();
	Student2->SetName(TEXT("학생2"));
	UStudent* Student3 = NewObject<UStudent>();
	Student3->SetName(TEXT("학생3"));
    
    //AddUObject 함수를 사용하면, 첫번쨰 인자로 어떠한 클래스 인스턴스를 지정하고, 멤버 함수를 직접 묶을 수 있다.
    CourseInfo->OnChanged.AddUObject(Student1, &UStudent::GetNotification);
    CourseInfo->OnChanged.AddUObject(Student2, &UStudent::GetNotification);
	CourseInfo->OnChanged.AddUObject(Student3, &UStudent::GetNotification);
    
    // 마지막으로 학사 정보 변경
    CourseInfo->ChangedCourseInfo(SchoolName, TEXT("변경된 학사 정보"));
    
	UE_LOG(LogTemp, Log, TEXT("========================"));

}

출력 결과:

LogTemp: ========================
LogTemp: [CourseInfo] 학사 정보가 변경되어 알림을 발송합니다.
LogTemp: [Student] 학생3님이 학교로부터 받은 메시지 : 변경된 학사 정보
LogTemp: [Student] 학생2님이 학교로부터 받은 메시지 : 변경된 학사 정보
LogTemp: [Student] 학생1님이 학교로부터 받은 메시지 : 변경된 학사 정보
LogTemp: ========================

  1. 학사정보가 변경되어 알림을 발송한다는 메시지가 먼저 나옴
  2. 이 후에 Broardcasting
  3. 학생 3명이 학교로부터 메시지를 받게 됌

이로써, 학사 정보를 학생에게 전달하는 기능을 만들었는데

학사정보와 하생은 어떠한 의존 관계도 가지지 않는다.

이게 느슨한 결합구조가 가지는 장점이다.

0개의 댓글