본문 바로가기

Unreal Engine 4/C++

<Unreal C++> 15 - Delegate

 

필요한 개념


언리얼에는 트리거가 이미 만들어져 있지만 액터 배치에 있는 트리거를 사용하기 보다는 직접 정의해서 사용하는 경우가 많다.

트리거(방아쇠) : 어떤 이벤트를 발생시키기 위한 충돌체

델리게이트는 일반 델리게이트건 다이나믹 델리게이트건 싱글과 멀티라는 게 있다.
싱글캐스트(Singlecast) : 델리게이트와 함수의 관계는 1 : 1(하나에 델리게이트에 하나의 함수포인터를 연결)
멀티캐스트(Multicast) : 델리게이트 하나에 여러개의 함수를 연결해 연쇄 콜하기 위해 사용

// 충돌체 정의 용도로 씀
// 요걸 해가면서 우리가 원하는 델리게이트를 만듬
// 기본 델리게이트를 어떻게 정의하는지
// 이벤트 델리게이트 어떻게 정의하는지
// 델리게이션 내부 콜, 외부 콜 정리
// BP랑 오버라이드 하는 함수를 어떻게 정의할 수 있는지

// 델리게이트 정의 매크로
// 1 : 델리게이트 이름 2 : void 아무것도 없음
// 파라미터 없으면 원래 void를 넣어줌(C문법) -> 우리는 생략을 할뿐
// 2번째 void가 그런 의미
// #define DECLARE_DELEGATE( DelegateName ) FUNC_DECLARE_DELEGATE( DelegateName, void )
// 1 : 자료형명

// 델리게이션 자료형 선언
DECLARE_DELEGATE(FBoxLightBeginOverlap); //void__(void) -> 반환타입 함수타입, 파라미터 void

// 파라미터도 void 비어있는 걸로 연결할 수 있는 델리게이트
// ex) 컴포넌트 오버랩
//UPROPERTY(BlueprintAssignable, Category = "Collision")
// FComponentBeginOverlapSignature OnComponentBeginOverlap;
// FComponentBeginOverlapSignature은 자료형이다.
// 말그대로 ()안에 넣은 FBoxLightBeginOverlap을 자료형으로 사용하겠단 의미

// 델리게이트는 변수지만, 보통 변수는 private에 놓고 함수는 public으로 염
// 그렇지만 편하게 외부에서 가져다 쓰라고 public으로 델리게이트만 예외로 열어놈
// 변수명은 아무거나 써도 된다. OnBoxLightBeginOverlap 얘는 어차피 변수명임
// 근데 편하게 쓸려고 F자를 On으로 바꿔서 명명하는 것도 좋은 방법
FBoxLightBeginOverlap OnBoxLightBeginOverlap;

// IsBound() : 델리게이션에 함수가 하나라도 연결되어 있는지 체크
// Execute() : 싱글캐스트에서 연결되어 있는 함수를 실행
if (OnBoxLightBeginOverlap.IsBound())
   OnBoxLightBeginOverlap.Execute();


1e-6f = 10^-6 -> 0.000001
매직 넘버로 많이 씀
PointLight->Intensity = 1e+4f; // e는 지수 -> 10^4 = 10000
);

정리
DECLARE_DELEGATE
- 델리게이션 자료형 선언
- 선언된 자료형으로부터 변수 선언
- IsBound() : 해당 변수에 연결된 함수가 있는지?
- Execute() : 해당 변수에 연결된 함수 실행

Q) 굳이 이렇게 복잡하게 델리게이트를 써야하나요?
그냥 같은 클래스에다가 켜지고 꺼지는거 넣어놓면 안되나?
근데 쓰는 이유는 어떤 이벤트가 발생했을때 어떤 해당 클래스의 함수를 호출해준다.
이게 델리게이션을 쓰지 않는다면 호출을 받으려는 객체를 소유해야한다.

Trigger class -> Light class(On, Off)의 객체를 소유해야하니까(Has-a) -> Has-a는 별로 좋지 않음(Trigger가 수정되면 라이트도 수정되어야하는 가능성이 있다.)
트리거 하나를 고치려면 has-a 관계의 소유 객체 클래스를 다 수정해야 한다.

Trigger는 함수 주소만 가지고 <- Light::On, Light::Off 함수 주소만 가짐
Trigger입장에서는 함수주소로 단지 호출만 해주는 입장(Light인지 뭔지 상관 없음)
결국 caller와 callee가 구분(분리)됨, 수정될 가능성이 없음

처음에 SpotLight로 콜 잡았는데 기획팀에서 PointLight로 바꿔달라 하면 기존에는 Trigger class가 가지고 있는 Has-a 관계의 소유 객체를 수정해야하거나 교체해야하고(소유)
이런식으로 하다보면 쓰잘데기 없는 코드가 미친듯이 느려남(버그 확률 늘어남)
근데 Trigger로 하면 상관없다. Light::On 함수주소에서-> PointLight::On의 함수 주소를 넣어주면 된다.(소유안함) -> 유지 보수 용이

결론 : 델리게이트를 적극적으로 활용하자(게임은 모두 이벤트(맞고, 때리고, 지나가고)
캐릭터가 문을 연다면 어떤 애는 문을 열였을때 불 켜지고 뭐 떨어지고...
여기서 델리게이트가 없다면(Has-a 관계의 소유해야하는 객체가 이벤트만큼 계속 늘어남, 큐브, 몹, 보스.. 다 가지고 있어야 함)

지금은 싱글캐스트(1:1)로 구현했지만,
나중에 멀티캐스트 델리게이트(델리게이트가 여러개의 함수를 가짐(형태만 맞는다면 다 가짐)) 이 델리게이트가 여러개의 함수 주소를 가지면(몹 공격, 보스 등장... 문열림)
얘들의 주소만 넣으면 됨

델리게이션은 함수포인터로 구현되어있다.
함수포인터를 편하게 다루고자 델리게이션이 구현되어 있다.

함수 포인터의 동적 배열로 가지고 있는게 멀티캐스트고
함수 포인터를 1 : 1로 쓰는게 싱글캐스트(한개만 연결)

클래스가 다른 클래스의 객체를 소유하는(Has-a) 관계는 좋지 않다. 보통은 Actor가 다른 컴포넌트들을 가지는 것만 함

델리게이트 : 해당 객체를 소유하는 것이 아니라 해당 객체에 소속되어 있는 함수의 주소만으로 관리하기 위해 사용

정리
Delegate 사용 이유
- 객체를 소유하게 되면(Has-a) 객체를 소유한 클래스를 수정할 경우 소유하고 있는 객체의 부분도 같이 수정될 가능성이 있음
- 함수의 주소만을 소유할 경우 위와 같은 작업을 줄일 수 있음
- 여러 객체의 함수를 호출하더라도 객체를 여러 개를 소유할 필요가 없으므로 관리가 편해짐
- 호출될 함수의 추가나 제거가 용이함

 

 

 

 

 

C03_Trigger.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "C03_Trigger.generated.h"

// 충돌체 정의 용도로 씀
// 요걸 해가면서 우리가 원하는 델리게이트를 만듬
// 기본 델리게이트를 어떻게 정의하는지
// 이벤트 델리게이트 어떻게 정의하는지
// 델리게이션 내부 콜, 외부 콜 정리
// BP랑 오버라이드 하는 함수를 어떻게 정의할 수 있는지

// 델리게이트 정의 매크로
// 1 : 델리게이트 이름 2 : void 아무것도 없음
// 파라미터 없으면 원래 void를 넣어줌(C문법) -> 우리는 생략을 할뿐
// 2번째 void가 그런 의미
// #define DECLARE_DELEGATE( DelegateName ) FUNC_DECLARE_DELEGATE( DelegateName, void )
// 1 : 자료형명
 
// 델리게이션 자료형 선언
DECLARE_DELEGATE(FBoxLightBeginOverlap); //void__(void) -> 반환타입 함수타입, 파라미터 void
DECLARE_DELEGATE(FBoxLightEndOverlap);

// 파라미터도 void 비어있는 걸로 연결할 수 있는 델리게이트
// ex) 컴포넌트 오버랩
//UPROPERTY(BlueprintAssignable, Category = "Collision")
//	FComponentBeginOverlapSignature OnComponentBeginOverlap;
// FComponentBeginOverlapSignature은 자료형이다.
// 말그대로 ()안에 넣은 FBoxLightBeginOverlap을 자료형으로 사용하겠단 의미


UCLASS()
class UONLINE_03_CPP_API AC03_Trigger : public AActor
{
	GENERATED_BODY()

private:
	UPROPERTY(VisibleDefaultsOnly)
		class USceneComponent* Scene;
	
	UPROPERTY(VisibleDefaultsOnly)
		class UBoxComponent* Box;

	UPROPERTY(VisibleDefaultsOnly)
		class UTextRenderComponent* Text;


public:	
	AC03_Trigger();

protected:
	virtual void BeginPlay() override;


private:
	UFUNCTION()
		void ActorBeginOverlap(AActor* OverlappedActor, AActor* OtherActor);

	UFUNCTION()
		void ActorEndOverlap(AActor* OverlappedActor, AActor* OtherActor);

	// 델리게이트는 변수지만, 보통 변수는 private에 놓고 함수는 public으로 염
	// 그렇지만 편하게 외부에서 가져다 쓰라고 public으로 델리게이트만 예외로 열어놈
	// 변수명은 아무거나 써도 된다. OnBoxLightBeginOverlap 얘는 어차피 변수명임
	// 근데 편하게 쓸려고 F자를 On으로 바꿔서 명명하는 것도 좋은 방법
public:
	FBoxLightBeginOverlap OnBoxLightBeginOverlap;
	FBoxLightBeginOverlap OnBoxLightEndOverlap;
};

 

 

 

 

 

 

 

C03_Trigger.cpp


더보기
#include "C03_Trigger.h"
#include "Global.h"
#include "Components/BoxComponent.h" // SceneComponent는 Box에 대한 부모여서 헤더를 물고들어와서 인클루드 안해도됨
#include "Components/TextRenderComponent.h"

AC03_Trigger::AC03_Trigger()
{
	CHelpers::CreateComponent<USceneComponent>(this, &Scene, "Scene");
	CHelpers::CreateComponent<UBoxComponent>(this, &Box, "Box", Scene);
	CHelpers::CreateComponent<UTextRenderComponent>(this, &Text, "Text", Scene);
	
	Box->SetRelativeScale3D(FVector(3));
	Box->bHiddenInGame = false;
	
	Text->SetRelativeLocation(FVector(0, 0, 100));
	Text->SetRelativeRotation(FRotator(0, 180, 0));
	Text->SetRelativeScale3D(FVector(2));
	Text->TextRenderColor = FColor::Red;
	Text->HorizontalAlignment = EHorizTextAligment::EHTA_Center;
	Text->Text = FText::FromString(GetName());
}

void AC03_Trigger::BeginPlay()
{
	Super::BeginPlay();

	OnActorBeginOverlap.AddDynamic(this, &AC03_Trigger::ActorBeginOverlap);
	OnActorEndOverlap.AddDynamic(this, &AC03_Trigger::ActorEndOverlap);
}

void AC03_Trigger::ActorBeginOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
	// IsBound() : 델리게이션에 함수가 하나라도 연결되어 있는지 체크
	// Execute() : 생글캐스트에서 연결되어 있는 함수를 실행
	if (OnBoxLightBeginOverlap.IsBound())
		OnBoxLightBeginOverlap.Execute();
}

void AC03_Trigger::ActorEndOverlap(AActor* OverlappedActor, AActor* OtherActor)
{
	if (OnBoxLightEndOverlap.IsBound())
		OnBoxLightEndOverlap.Execute();
}

 

 

 

C04_Light.h


더보기
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "C04_Light.generated.h"

UCLASS()
class UONLINE_03_CPP_API AC04_Light : public AActor
{
	GENERATED_BODY()

private:
	UPROPERTY(VisibleDefaultsOnly)
		class USceneComponent* Scene;

	UPROPERTY(VisibleDefaultsOnly)
		class UTextRenderComponent* Text;

	UPROPERTY(VisibleDefaultsOnly)
		class UPointLightComponent* Light;


public:	
	AC04_Light();

protected:
	virtual void BeginPlay() override;

private:
	UFUNCTION()
		void OnLight();
	UFUNCTION()
		void OffLight();
};

 

 

 

 

 

 

 

C04_Light.cpp


더보기
#include "C04_Light.h"
#include "Global.h"
#include "C03_Trigger.h"
#include "Components/TextRenderComponent.h" 
#include "Components/PointLightComponent.h"

AC04_Light::AC04_Light()
{
	CHelpers::CreateComponent<USceneComponent>(this, &Scene, "Scene");
	CHelpers::CreateComponent<UTextRenderComponent>(this, &Text, "Text", Scene);
	CHelpers::CreateComponent<UPointLightComponent>(this, &Light, "Light", Scene);
	
	Text->SetRelativeLocation(FVector(0, 0, 100));
	Text->SetRelativeRotation(FRotator(0, 180, 0));
	Text->SetRelativeScale3D(FVector(2));
	Text->TextRenderColor = FColor::Red;
	Text->HorizontalAlignment = EHorizTextAligment::EHTA_Center;
	Text->Text = FText::FromString(GetName());

	// 1e-6f = 10^-6 -> 0.000001
	// 매직 넘버로 많이 씀
	// 강도
	Light->Intensity = 1e+4f; // e는 지수 -> 10^4 = 10000

	// 반경
	Light->AttenuationRadius = 200;
	// 라이트의 컬러
	Light->LightColor = FColor(255, 128, 50);
}

void AC04_Light::BeginPlay()
{
	Super::BeginPlay();

	Light->SetVisibility(false);

	// 배치되어 있는 클래스 찾음
	// GetAllActorsOfClass()은 BP와 사용하던 것과 유사, 조금은 다름
	// UGamePlayStatics : 게임플레이 하는데 도움되는 클래스가 모여있음
	// 1 : 월드가 들어감(월드에 있는 것을 가져다 써서) 2 : 찾을려는 클래스 3 : 찾은 액터들을 반환
	// void UGameplayStatics::GetAllActorsOfClass(const UObject* WorldContextObject, TSubclassOf<AActor> ActorClass, TArray<AActor*>& OutActors)
	// TArray : 가변형 배열, C++ STL의 vector와 유사한 기능을 가짐
	// ::StaticClass() -> UClass 타입으로 리턴함(TSubclassOf는 UClass를 받을 수 있다.)
	
	TArray<AActor*> actors;
	UGameplayStatics::GetAllActorsOfClass(GetWorld(), AC03_Trigger::StaticClass(), actors);
	// 안에 조건이 false일때 리턴이다.
	// .Num() -> 배열의 크기
	// 0 보다 크지 않다면 함수를 빠져나옴
	CheckFalse(actors.Num() > 0);

	// 1개만 있을 거니까
	AC03_Trigger* trigger = Cast<AC03_Trigger>(actors[0]);
	// UFunction이 붙어야하는이유가 BindUFunction이어서
	// BindUFunction가 UFunction이 연결할 수 있는 함수를 사용해서 붙어야함
	// 1 : 함수를 가진 객체 2 : 연결하려는 함수명
	trigger->OnBoxLightBeginOverlap.BindUFunction(this, "OnLight");
	trigger->OnBoxLightEndOverlap.BindUFunction(this, "OffLight");
}

void AC04_Light::OnLight()
{
	Light->SetVisibility(true);
}

void AC04_Light::OffLight()
{
	Light->SetVisibility(false);
}

 

 

 

 

 

 

 

Global.h 추가된 내용


더보기
...

#include "Kismet/GameplayStatics.h" // GetAllActorsOfClass() 함수 사용 위해

 

 

 

 

 

 

 

 

 

결과


Light가 켜지고 꺼지는 것을 볼 수 있다.