[ Unreal Engine 5 / #19 Delegate]

SeungWoo·2024년 10월 11일

[ Ureal Engine 5 / 수업 ]

목록 보기
20/31
post-thumbnail

Delegate ( 대리자 )

Delegate는 하나 이상의 함수를 호출할 수 있는 일종의 함수 포인터이며,
다른 오브젝트가 이벤트에 반응할 수 있도록 설계된 메커니즘

  • Delegate는 크게 세 가지로 분류
    • Single-cast Delegate: 하나의 함수만 바인딩할 수 있음.
    • Multi-cast Delegate: 여러 개의 함수를 동시에 바인딩하고 호출할 수 있음.
    • Dynamic Delegate: 블루프린트에서 바인딩할 수 있는 Delegate

Single_Cast Delegate

#pragma once

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

// Single-cast Delegate 선언
DECLARE_DELEGATE(FSimpleDelegate);

UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
    GENERATED_BODY()

public:
    AMyActor();

    // Single-cast 델리게이트 인스턴스
    FSimpleDelegate MyDelegate;

    void TriggerAction();
    void HandleAction();
};
#include "MyActor.h"

AMyActor::AMyActor()
{
    // 생성자에서 함수 바인딩
    MyDelegate.BindUObject(this, &AMyActor::HandleAction);
}

void AMyActor::TriggerAction()
{
    // 델리게이트가 유효하면 호출
    if (MyDelegate.IsBound())
    {
        MyDelegate.Execute();
    }
}

void AMyActor::HandleAction()
{
    UE_LOG(LogTemp, Warning, TEXT("Action has been handled!"));
}
  • Single-cast Delegate
    • Single-cast Delegate는 한 번에 하나의 함수만 호출할 수 있습니다.
    • 일반적인 C++ 함수 포인터와 비슷하게 동작하지만, 바인딩 및 호출이 더 안전하고 직관적입니다.
    • 순서
      • DECLARE_DELEGATE : Delegate를 선언하는 매크로입니다. 파라미터가 없는 Simple Delegate를 선언했습니다.
      • BindUObject : 클래스를 바인딩하는 메서드로, 여기서는 this 객체와 HandleAction함수를 바인딩했습니다.
      • Execute : Delegate에 바인딩된 함수를 호출합니다.

Multi_cast Delegate

#pragma once

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

// Multicast Delegate 선언
DECLARE_MULTICAST_DELEGATE(FMulticastDelegate);

UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
    GENERATED_BODY()

public:
    AMyActor();

    // Multicast 델리게이트 인스턴스
    FMulticastDelegate MyDelegate;

    void TriggerEvent();
    void FirstHandler();
    void SecondHandler();
};
#include "MyActor.h"

AMyActor::AMyActor()
{
    // 생성자에서 여러 함수 바인딩
    MyDelegate.AddUObject(this, &AMyActor::FirstHandler);
    MyDelegate.AddUObject(this, &AMyActor::SecondHandler);
}

void AMyActor::TriggerEvent()
{
    // 모든 바인딩된 함수들을 호출
    MyDelegate.Broadcast();
}

void AMyActor::FirstHandler()
{
    UE_LOG(LogTemp, Warning, TEXT("First handler called!"));
}

void AMyActor::SecondHandler()
{
    UE_LOG(LogTemp, Warning, TEXT("Second handler called!"));
}
  • Multi-cast Delegate
    • Multi-cast Delegate는 여러 함수를 바인딩할 수 있으며, 호출할 때 모든 바인딩된 함수들이 실행됩니다.
    • 순서
      • DECLARE_MULTICAST_DELEGATE: 여러 함수를 바인딩할 수 있는 Multi-cast Delegate를 선언합니다.
      • AddUObject: 여러 함수를 Delegate에 추가할 수 있습니다.
      • Broadcast: Multi-cast Delegate에 바인딩된 모든 함수를 호출합니다.

  • Dynamic Delegate
    • Dynamic Delegate는 블루프린트에서도 사용할 수 있는 Delegate입니다. UFUNCTION과 함께 사용되어야 하며, Blueprint 노드에서 함수 바인딩이 가능합니다.
    • 순서
      • DECLARE_DYNAMIC_DELEGATE: 블루프린트에서 사용할 수 있는 Dynamic Delegate를 선언합니다.
      • UPROPERTY(BlueprintAssignable): 블루프린트에서도 이 Delegate를 사용할 수 있도록 선언합니다.
      • ExecuteIfBound: Delegate가 바인딩된 함수가 있으면 실행하는 함수입니다.

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam 예시
InventoryComponent.h

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"

// Dynamic Multicast Delegate 선언 (파라미터로 bool 전달)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryToggled, bool, bIsOpen);

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class MYPROJECT_API UInventoryComponent : public UActorComponent
{
    GENERATED_BODY()

public:	
    UInventoryComponent();

    // Dynamic Multicast Delegate 인스턴스
    UPROPERTY(BlueprintAssignable, Category = "Inventory")
    FOnInventoryToggled OnInventoryToggled;

    void ToggleInventory();
    
private:
    bool bIsInventoryOpen;

    void OpenInventory();
    void CloseInventory();
};

InventoryComponent.cpp

#include "InventoryComponent.h"

UInventoryComponent::UInventoryComponent()
{
    bIsInventoryOpen = false;
}

void UInventoryComponent::ToggleInventory()
{
    if (bIsInventoryOpen)
    {
        CloseInventory();
    }
    else
    {
        OpenInventory();
    }

    // 델리게이트 호출
    OnInventoryToggled.Broadcast(bIsInventoryOpen);
}

void UInventoryComponent::OpenInventory()
{
    bIsInventoryOpen = true;
    UE_LOG(LogTemp, Warning, TEXT("Inventory opened."));
}

void UInventoryComponent::CloseInventory()
{
    bIsInventoryOpen = false;
    UE_LOG(LogTemp, Warning, TEXT("Inventory closed."));
}
  • Dynamic Multicast Delegate 선언 :
    • DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryToggled, bool, bIsOpen);를 사용하여 bool 타입의 파라미터를 전달하는 델리게이트를 선언합니다.
  • Delegate 인스턴스 생성 :
    • UInventoryComponent 클래스의 멤버 변수로 FOnInventoryToggled 타입의 델리게이트를 선언하고, BlueprintAssignable로 지정하여 블루프린트에서도 접근할 수 있도록 했습니다.
  • 델리게이트 호출 :
    • ToggleInventory 함수에서 인벤토리 상태를 열거나 닫은 후, OnInventoryToggled.Broadcast(bIsInventoryOpen);를 호출하여 바인딩된 모든 함수(리스너)에게 인벤토리 상태가 변경되었음을 알립니다.
  • 블루프린트 통합 :
    • 이 델리게이트는 블루프린트에서도 OnInventoryToggled 이벤트를 바인딩할 수 있습니다. 예를 들어, 블루프린트에서 이 컴포넌트를 가진 액터의 OnInventoryToggled 이벤트에 UI를 업데이트하는 함수를 연결할 수 있습니다.

  • Delegate와 파라미터
    • DECLARE_DELEGATE_OneParam : 하나의 파라미터를 받는 Delegate를 선언합니다.
    • BindUObject : 파라미터를 받는 함수를 Delegate에 바인딩합니다.
    • Execute : 파라미터를 전달하여 바인딩된 함수를 호출합니다.

  • C++ 코드에서 바인딩하는 방법
    MyActor.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InventoryComponent.h"
#include "MyActor.generated.h"

UCLASS()
class MYPROJECT_API AMyActor : public AActor
{
    GENERATED_BODY()

public:
    AMyActor();

protected:
    virtual void BeginPlay() override;

    UFUNCTION()
    void HandleInventoryToggle(bool bIsOpen);

private:
    UPROPERTY(VisibleAnywhere)
    UInventoryComponent* InventoryComponent;
};

MyActor.cpp

#include "MyActor.h"

AMyActor::AMyActor()
{
    // InventoryComponent 초기화
    InventoryComponent = CreateDefaultSubobject<UInventoryComponent>(TEXT("InventoryComponent"));
}

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

    // InventoryComponent의 델리게이트에 함수 바인딩
    if (InventoryComponent)
    {
        InventoryComponent->OnInventoryToggled.AddDynamic(this, &AMyActor::HandleInventoryToggle);
    }
}

void AMyActor::HandleInventoryToggle(bool bIsOpen)
{
    if (bIsOpen)
    {
        UE_LOG(LogTemp, Warning, TEXT("The inventory is now open."));
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("The inventory is now closed."));
    }
}
  • AddDynamic 함수 사용 :
    • BeginPlay 함수에서 AddDynamic을 사용하여 HandleInventoryToggle 함수를 OnInventoryToggled 델리게이트에 바인딩합니다.
    • 첫 번째 인자는 델리게이트를 바인딩할 객체(this), 두 번째 인자는 호출할 함수의 주소입니다.
  • HandleInventoryToggle 함수 :
    • 델리게이트가 호출될 때 실행되는 함수입니다. bIsOpen 값에 따라 인벤토리가 열렸는지 닫혔는지 로그를 출력합니다
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    // InventoryComponent의 델리게이트에 함수 바인딩
    if (InventoryComponent)
    {
        InventoryComponent->OnInventoryToggled.AddDynamic(this, &AMyActor::HandleInventoryToggle);
    }
}
  • 이렇게 AddDynamic로 바인딩한 부분은 Broadcast로 호출시 해당 바인딩부분도 호출된다.
void UInventoryComponent::ToggleInventory()
{
    // 인벤토리 열기 또는 닫기
    if (bIsInventoryOpen)
    {
        CloseInventory();
    }
    else
    {
        OpenInventory();
    }

    // 델리게이트 호출 (바인딩된 모든 함수 호출됨)
    OnInventoryToggled.Broadcast(bIsInventoryOpen);
}

  • Delegate와 Event 간 차이점
    • Unreal Engine에는 Delegate와 유사한 Event 시스템도 존재합니다. 둘은 비슷해 보이지만, Event는 BlueprintCallable 함수의 확장형으로 주로 Blueprint에서 많이 사용되며, Delegate는 코드 레벨에서 더 세밀하게 제어할 수 있습니다. Delegate는 다중 함수 바인딩, 파라미터 지원, 동적 및 정적 바인딩 등의 강력한 기능을 제공
  • Delegate의 취소
    • // Delegate 바인딩 해제 : MyDelegate.Unbind();

해당 로직을 만들어보자

  • Inventory Widget (UserWidget) 클래스 생성

  • UserWidget 형태의 C++ 클래스를 하나더 만든다

  • TPSPlayer 에 바인딩 하기

  • 이렇게 되면 불가피하게 의존성이 또 생김


코드 정리

UImanager.h

#pragma once

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

class UInventoryWidget;

UCLASS()
class MAGICIAN_PROJECT_API AUIManager : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AUIManager();

	// 인벤토리 위젯을 생성하는 함수
	void CreatInventoryWidget();

	// 인벤토리 위젯의 Delegate를 구동하는 함수
	void BindInventoryToggleDelegate();

	// 제작된 위젯 정확한 지칭 한 주소
	UPROPERTY()
	UInventoryWidget* InventoryWidget;

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

	// static 클래스 골라줘서 -> 해당 위젯형태로 제작
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "UI")
	TSubclassOf<UInventoryWidget> InventoryWidgetClass;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;
};

UImanager.cpp

#include "UIManager.h"
#include "InventoryWidget.h"
#include "CTPSPlayer.h"
#include "Blueprint\UserWidget.h"


// Sets default values
AUIManager::AUIManager()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

}

void AUIManager::CreatInventoryWidget()
{
	if (InventoryWidgetClass)
	{
		// 인벤토리 위젯을 생성
		InventoryWidget = CreateWidget<UInventoryWidget>(GetWorld(), InventoryWidgetClass);

		if (InventoryWidget)
		{
			// Delegate 연결
			BindInventoryToggleDelegate();
		}
	}

}

void AUIManager::BindInventoryToggleDelegate()
{
	// Delegate를 구독할 대상 플레이어를 찾음
	if (ACTPSPlayer* player = Cast<ACTPSPlayer>(GetWorld()->GetFirstPlayerController()->GetPawn()))
	{
		// 플레이어가 Delegate를 구독할 수 있도록 설정
		InventoryWidget->OnInventoryToggled.AddDynamic(player, &ACTPSPlayer::HandleInventoryToggled);
	}

}

// Called when the game starts or when spawned
void AUIManager::BeginPlay()
{
	Super::BeginPlay();
	
	CreatInventoryWidget();
}

// Called every frame
void AUIManager::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

InventoryWidget.h

#pragma once

#include "CoreMinimal.h"
#include "ItemData.h"
#include "Blueprint/UserWidget.h"
#include "InventoryWidget.generated.h"

class UGridPanel;
class UInventorySlot;
class UCanvasPanel;
class UInventoryActorComponent;

// 델리게이트 ( 대리자 ) -> 인벤토리 On / Off, 의존성 줄일려고
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInventoryToggled, bool, bIsOpen);

UCLASS()
class MAGICIAN_PROJECT_API UInventoryWidget : public UUserWidget
{
	GENERATED_BODY()
	

public:
	// Delegate 인스턴스 생성 ( 인벤토리 열림/ 닫힘을 알림 )
	UPROPERTY(BlueprintAssignable, Category = "Inventory")
	FOnInventoryToggled OnInventoryToggled;

	// 인벤토리 여/닫 함수
	UFUNCTION(BlueprintCallable, Category = "Inventroy")
	void ToggleInventory();


	UFUNCTION(BlueprintCallable, Category = "Inventroy")
	void SetInventoryItem(UInventoryActorComponent* InventoryComp);

public:
	// 인벤토리 상태를 저장 ( 열림 또는 닫힘 )
	bool bIsInventoryOpen = false;

	// 해당 아이템 Array 데이터들이 --> Inventory Actor Component에서 가져오기
	/*UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory")
	TArray<FItemData> InventoryItem;*/

	// 그리드 패널 ( 아이템 배치를 위한 GridPanel )
	UPROPERTY(meta = (BindWidget))
	UGridPanel* GridPanel;

	UPROPERTY(meta = (BindWidget))
	UCanvasPanel* CanvasPanel;

	// 그리드 업데이트 함수
	void UpdateInventoryGrid(UInventoryActorComponent* InventoryComp);

	// 
	UPROPERTY(EditAnywhere, Category = "Inventory")
	TSubclassOf<UInventorySlot> SlotWidgetClass;


	// 위젯을 열고 닫는 함수
	virtual void NativeConstruct() override;
	virtual void NativeDestruct() override;
	
};

InventoryWidget.cpp

#include "InventoryWidget.h"
#include "InventorySlot.h"
#include "Components\CanvasPanel.h"
#include "Components\GridPanel.h"
#include "Blueprint\WidgetTree.h"
#include "InventoryActorComponent.h"


void UInventoryWidget::ToggleInventory()
{
	// 열려 있으면 ~
	if (bIsInventoryOpen)
	{
		RemoveFromParent();
	}
	else
	{
		AddToViewport();
	}

	// 상태를 토글
	bIsInventoryOpen = !bIsInventoryOpen;

	// 델리게이트 호출
	OnInventoryToggled.Broadcast(bIsInventoryOpen);
}

void UInventoryWidget::SetInventoryItem(UInventoryActorComponent* InventoryComp)
{
	// 인벤토리 컴포넌트로부터 아이템 데이터를 받아와서 그리드를 업데이트
	if (InventoryComp)
	{
		UpdateInventoryGrid(InventoryComp);
	}
	// 인벤토리 그리드 업데이트
}

/*
 위젯이 생성될 때 필요한 초기화 작업을 설정할 떄 사용합니다
 예를 들어, 위젯의 상태를 초기화하거나, 위젯 내부의 다른 컴포넌트를 초기화하는 작업 등을 할
*/

void UInventoryWidget::UpdateInventoryGrid(UInventoryActorComponent* InventoryComp)
{
	// 인벤토리 컴포넌트 또는 슬롯 위젯 클래스, 그리드 패널이 유효하지 않으면 리턴
	if (!InventoryComp || !SlotWidgetClass || !GridPanel)
		return;

	GridPanel->ClearChildren();

	const int32 NumColums = 5; // 그리드 열 수 설정
	int32 row = 0;
	int32 colum = 0;

	//인벤토리 맵을 순회하며 각 아이템을 그리드에 추가
	// Map - > Key & Value  -> Pair
	for (const TPair<FName, FInventoryItem>& InventoryItem : InventoryComp->Inventory)
	{
		// 슬롯 위젯 생성 
		if (UInventorySlot* SlotWidget = CreateWidget<UInventorySlot>(this, SlotWidgetClass))
		{
			// InventoryItem 구조체에서 FItemData와 수량을 가져와서 슬롯에 설정
			const FItemData& ItemData = InventoryItem.Value.ItemData;
			int32 Quantity = InventoryItem.Value.Quantity;

			// 슬롯 위젯에 아이템 데이터와 수량 설정
			SlotWidget->SetItemData(ItemData, Quantity);

			// GridPanel에 아이템 추가
			GridPanel->AddChildToGrid(SlotWidget, row, colum);

			colum++;
			if (colum >= NumColums)
			{
				colum = 0;
				row++;
			}
		}
	}
}

void UInventoryWidget::NativeConstruct()
{
	Super::NativeConstruct();

	// UCanvasPanel 생성 및 위젯 트리에 추가
	CanvasPanel = WidgetTree->ConstructWidget<UCanvasPanel>(UCanvasPanel::StaticClass(), TEXT("CanvasPanel"));
	if (CanvasPanel)
	{
		// 위젯 트리의 루트로 설정
		WidgetTree->RootWidget = CanvasPanel;
	}

	GridPanel = WidgetTree->ConstructWidget<UGridPanel>(UGridPanel::StaticClass(), TEXT("GridPanel"));
	if (GridPanel && CanvasPanel)
	{
		CanvasPanel->AddChild(GridPanel);
	}
}

void UInventoryWidget::NativeDestruct()
{
	Super::NativeDestruct();
	
	UE_LOG(LogTemp, Warning, TEXT("Inventory Widget Destruct"));
}

InventorySlot.h

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

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "InventorySlot.generated.h"

class UImage;
class UTextBlock;

UCLASS()
class MAGICIAN_PROJECT_API UInventorySlot : public UUserWidget
{
	GENERATED_BODY()

protected:
	// 아이템 썸네일 이미지
	UPROPERTY(meta = (BindWidget))
	UImage* ItemThumnail;
	// 아이템 이름
	UPROPERTY(meta = (BindWidget))
	UTextBlock* ItemName;
	// 아이템 수량
	UPROPERTY(meta = (BindWidget))
	UTextBlock* stackCount;


public:
	UFUNCTION(BlueprintCallable, Category = "Inventory Slot")
	void SetItemData(const FItemData& ItemData, int32 Quantity);

};

InventorySlot.cpp

#include "InventorySlot.h"

#include "ItemData.h"
#include "Components\Image.h"
#include "Components\TextBlock.h"


void UInventorySlot::SetItemData(const FItemData& ItemData, int32 Quantity)
{
	// 아이템 썸네일 설정
	if (ItemThumnail && ItemData.ItemThumbnail)
	{
		ItemThumnail->SetBrushFromTexture(ItemData.ItemThumbnail);
	}

	// 아이템 이름 설정
	if (ItemName)
	{
		ItemName->SetText(ItemData.ItemName);
	}

	// 스택 가능한 아이템의 경우 수량 설정
	if (stackCount)
	{
		stackCount->SetText(FText::AsNumber(Quantity));
	}
}

TPSPlayer.cpp

void ACTPSPlayer::BeginPlay()
{
	... 생략
    
	// UIManager를 찾아 참조
	UIManagerRef = Cast<AUIManager>(UGameplayStatics::GetActorOfClass(GetWorld(), AUIManager::StaticClass()));
}
void ACTPSPlayer::ToggleInventory(const FInputActionValue& value)
{
	if (UIManagerRef)
	{
		if (UIManagerRef->InventoryWidget)
		{
			UIManagerRef->InventoryWidget->ToggleInventory();
		}
	}
}

void ACTPSPlayer::HandleInventoryToggled(bool bIsOpen)
{
	bIsInventoryOpen = bIsOpen;

	if (bIsInventoryOpen)
	{
		UE_LOG(LogTemp, Warning, TEXT("Inventory Opened"));
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Inventory close"));
	}
}

  • 해당 C++ 클래스를 블루프린트로 만든 후, TPS에 맞게 이름에 맞게 넣어주면 작동이 된다
profile
This is my study archive

0개의 댓글