게임 개발/학습 일지

언리얼 내배캠 마스터반 5주차 - 인터페이스, 델리게이트 실습

이개 2026. 5. 14. 20:09

UObject는 다중 상속 불가

따라서 액터 아래에서 상속을 해주어야한다.

 

 

Interface를 활용하여 불에 타는 것과 불에 타지않는 것을 만들기

C++ 클래스로 언리얼 인터페이스 생성

이름은 TestMyImterface

 

C++ 클래스로 ItemBase 액터를 만든다.

C++ 클래스로 MyTorchlight 액터를 만든다.

 

ItemBase의 파생 C++클래스 Wood, Cloth, Steel을 만들어준다.

 

 

Wood 헤더 파일에 인터페이스의 헤더를 넣어준다.

 

 

인터페이스 코드를 구현한다.

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

#pragma once

#include "CoreMinimal.h"
#include "ItemBase.h"

#include "TestMyInterface.h"
#include "Wood.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT_API AWood : public AItemBase, public ITestMyInterface
{
	GENERATED_BODY()
		
public:
	virtual void OnFireDetected(float Temperature, FVector HitLocation) override;

protected:
	UPROPERTY(EditAnywhere, Category = "Effects")
	TObjectPtr<class UParticleSystem> FireEffect;

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


#include "Wood.h"
#include "Kismet/GameplayStatics.h"
#include "Particles/ParticleSystem.h"

void AWood::OnFireDetected(float Temperature, FVector HitLocation)
{
	if (FireEffect)

	{
		UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			FireEffect,
			GetActorLocation(),
			GetActorRotation(),
			FVector(1.f)
		);
	}
}

 

 

에디터 개인설정에서 시작시 강제 컴파일 체크하면 에디터와 C++ 클래스가 연동된다.

 

 

MyTorchlight 코드를 작성한다.

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

#pragma once

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

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

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

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

public:
	UPROPERTY(EditAnywhere)
	TArray<TWeakObjectPtr<AActor>> Items;

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


#include "MyTorchlight.h"
#include "TestMyInterface.h"
#include "Kismet/KismetSystemLibrary.h"

// Sets default values
AMyTorchlight::AMyTorchlight()
{
 	// 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;

}

// Called when the game starts or when spawned
void AMyTorchlight::BeginPlay()
{
	Super::BeginPlay();
	for (const TWeakObjectPtr<AActor>& Item : Items)
	{
		ITestMyInterface* MyInterface = Cast<ITestMyInterface>(Item.Get());
		if (MyInterface)
		{
			MyInterface->OnFireDetected(100.f, FVector::ZeroVector);
		}
	}
	
}

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

}

 

 

뷰포트에서 테스트하기 위해 액터에 도형을 달고 뷰포트에 넣는다.

Fire Effect에는 시작용 콘텐츠의 이펙트를 넣어준다.

 

MyTorchlight를 배치하고 Items에 액터를 추가해준다.

Wood와 Cloth애먼 효과가 적용된다.

 

 

블루프린트와 연동하여 사용하기

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

#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "TestMyInterface.generated.h"

// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class UTestMyInterface : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class MYPROJECT_API ITestMyInterface
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	
	UFUNCTION(BlueprintCallable, BlueprintNativeEvent, Category = "Interface")
	void OnFireDetected(float Temperature, FVector HitLocation);

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

#pragma once

#include "CoreMinimal.h"
#include "ItemBase.h"

#include "TestMyInterface.h"
#include "Wood.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT_API AWood : public AItemBase, public ITestMyInterface
{
	GENERATED_BODY()
		
public:
	virtual void OnFireDetected_Implementation(float Temperature, FVector HitLocation);

protected:
	UPROPERTY(EditAnywhere, Category = "Effects")
	TObjectPtr<class UParticleSystem> FireEffect;

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

#pragma once

#include "CoreMinimal.h"
#include "ItemBase.h"

#include "TestMyInterface.h"
#include "Cloth.generated.h"

/**
 * 
 */
UCLASS()
class MYPROJECT_API ACloth : public AItemBase, public ITestMyInterface
{
	GENERATED_BODY()

public:
	virtual void OnFireDetected_Implementation(float Temperature, FVector HitLocation);

protected:
	UPROPERTY(EditAnywhere, Category = "Effects")
	TObjectPtr<class UParticleSystem> FireEffect;
};
// Fill out your copyright notice in the Description page of Project Settings.


#include "MyTorchlight.h"
#include "TestMyInterface.h"
#include "Kismet/KismetSystemLibrary.h"

// Sets default values
AMyTorchlight::AMyTorchlight()
{
 	// 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;

}

// Called when the game starts or when spawned
void AMyTorchlight::BeginPlay()
{
	Super::BeginPlay();
	for (const TWeakObjectPtr<AActor>& Item : Items)
	{
		if (UKismetSystemLibrary::DoesImplementInterface(Item.Get(), UTestMyInterface::StaticClass())) // ITextMyInterface가 아니고 UTextMyInterface의 StaticClass를 가져와야한다.
		{
			ITestMyInterface::Execute_OnFireDetected(Item.Get(), 100.f, FVector::ZeroVector);
		}
	}
	
}

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

}

 

 

하지만 이방식은 호출 접근성을 보장하지 않고, Item.Get()할 때 타입 검사를 하지 않으며 크래시가 날 수 있다는 문제점이 있다.

 

 

Delegate

옵저버 패턴을 구현할 수 있게 해준다.

// C++ 형태로 1대1만 지원한다
// DECLARE_DELEGATE

// C++ 1대 다수
// DECLARE_MULTICAST

// 1대1 형태 블루프린트까지 호출
// DECLARE_DYNAMIC

// 1대 다수로 블루프린트까지 지원
// DECLARE_DYNAMIC_MULTICAST

// 바인드 되기 전까지 1바이트도 차지하지 않는다. 사용하면 느려진다. 거의 바인딩 되지 않으면 효율적이다.
// DECLARE_SPARSE

// 반환값 / 매개변수
// 파라미터 하나
// DECLARE_DELEGATE_OneParam
// 반환값이 있고, 파라미터가 세 개
// DECLARE_DELEGATE_RetVal_ThreeParams

// MULTICAST는 반환값을 지원하지 않는다.

// 바인딩
// C++ 전용이랑 블루프린트용이랑 방식이 다르다.

// C++ 전용은 AddUObject로 바인딩
// 블루프린트까지 허용 -> AddDynamic 블루프린트 이벤트 디스패처와 연동이 된다.

// 싱글 -> Bind로 시작
// 멀티 -> Add로 시작

// 앞부분 Bind/Add
// 뒷부분 UObejct/Dynamic

// 오브젝트, SharedPtr, Lamd, 전역함수, Static, UFUNCTION

// 오브젝트 = 델리게이트.BindUObject(객체, &UMyObject::함수);
// 스마트 포인터 = 델리게이트.BindSP(객체, &UMyObject::함수);
// 람다 = 델리게이트.BindLamda([]() {});
// 스태틱 = 델리게이트.BindStatic(객체, &UMyObject::함수);
// UFUNCTION = 델리게이트.BindUFunction(객체, TEXT("함수이름"));

// 싱글 C++ -> BindUObject
// 멀티 C++ -> AddUObject
// 싱글 Dynamic -> BindDynamic
// 멀티 Dynamic -> AddDynamic

// 블루프린트와 연동하려면 리플렉션 UFUNCTION 매크로를 넣어야한다.

// 싱글 1대1 대응 = Execute(); 
// 싱글은 바인딩이 안 되어 있으면 크래시가 난다.
// 그래서 사용 전에 반드시 bool값인 MySingleDelegate.IsBound();로 검사해주고 진행한다.
// 멀티 1대 다수 = BroadCast();

// 블루프린트로 받아올 경우
// 이쪽에서 객체로 만들고 그 객체를 통해서 블루프린트 델리게이트를 만들어준다.
// UPROPERTY(BlueprintAssignable)
// FDeath OnDeath

 

 

액터컴포넌트를 하나 만든다.

 

 

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

#pragma once

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

// 1대 다수로 블루프린트까지 지원하는 죽었을 때
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FHealthDeadSignature, AController*, Instigator);// 델리게이트 선언. 
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FHealthDamagedSignature, float, NewHealth, float, MaxHealth, float, HealthChange);

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

public:	
	// Sets default values for this component's properties
	UMyActorComponent();

	virtual void TickComponent(float DeltaTime,ELevelTick TickType,FActorComponentTickFunction* ThisTickFunction) override;

	UPROPERTY(BlueprintAssignable)
	FHealthDeadSignature OnHealthDead;

	UPROPERTY(BlueprintAssignable)
	FHealthDamagedSignature OnHealthDamaged;

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

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	float CurrentHealth;

	UPROPERTY(VisibleAnywhere, BlueprintReadOnly)
	float MaxHealth;

private:
	// TakeDamage 바인딩
	UFUNCTION()
	void DamageTake(AActor* DamagedActor, float Damage, const UDamageType* DamageType, AController* Instigator, AActor* Causer);

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


#include "MyActorComponent.h"

// Sets default values for this component's properties
UMyActorComponent::UMyActorComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = true;

	MaxHealth = 100.f;
}


// Called when the game starts
void UMyActorComponent::BeginPlay()
{
	Super::BeginPlay();

	CurrentHealth = MaxHealth;

	GetOwner()->OnTakeAnyDamage.AddDynamic(this, &UMyActorComponent::DamageTake);
}


void UMyActorComponent::DamageTake(AActor* DamagedActor, float Damage, const UDamageType* DamageType, AController* Instigator, AActor* Causer)
{
	float FinalDamage = FMath::Min(Damage, CurrentHealth); // 0 이하로 떨어지지 않도록 처리
	CurrentHealth -= FinalDamage;

	OnHealthDamaged.Broadcast(CurrentHealth, MaxHealth, FinalDamage);
	if (CurrentHealth <= 0.f)
	{
		OnHealthDead.Broadcast(Instigator);
	}
}

// Called every frame
void UMyActorComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	GEngine->AddOnScreenDebugMessage(-1, 0.f, FColor::Green, FString::Printf(TEXT("HP : %f"), CurrentHealth));
}

 

 

캐릭터 블루프린트 창에서 MyActorComponent를 추가해서 확인한다.

MyActor 컴포넌트를 추가한다.

 

 

트리거 박스 설치

트리거박스를 하나 배치한다.

레벨 블루프린트를 연다.

Add on Actor Begin Ovelap 노드를 생성한다.
Apply Damage를 생성하고 Base Damage를 설정한다.
오버랩 될 때마다 데미지가 들어온다.

 

 

실습

캐릭터가 대미지를 받아서 죽을 때

interface 실습 때 마든 레벨에 배치되어있는 모든 아이템들이 Destory 되도록 만들기

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


#include "MyActorComponent.h"

// Sets default values for this component's properties
UMyActorComponent::UMyActorComponent()
{
	// Set this component to be initialized when the game starts, and to be ticked every frame.  You can turn these features
	// off to improve performance if you don't need them.
	PrimaryComponentTick.bCanEverTick = true;

	MaxHealth = 100.f;
}


// Called when the game starts
void UMyActorComponent::BeginPlay()
{
	Super::BeginPlay();

	CurrentHealth = MaxHealth;

	GetOwner()->OnTakeAnyDamage.AddDynamic(this, &UMyActorComponent::DamageTake);

}


void UMyActorComponent::DamageTake(AActor* DamagedActor, float Damage, const UDamageType* DamageType, AController* Instigator, AActor* Causer)
{
	float FinalDamage = FMath::Min(Damage, CurrentHealth); // 0 이하로 떨어지지 않도록 처리
	CurrentHealth -= FinalDamage;
	UE_LOG(LogTemp, Warning, TEXT("DamageTake Called! Damage: %f"), Damage);
	OnHealthDamaged.Broadcast(CurrentHealth, MaxHealth, FinalDamage);
	if (CurrentHealth <= 0.f)
	{
		OnHealthDead.Broadcast(Instigator);
	}
}

// Called every frame
void UMyActorComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	GEngine->AddOnScreenDebugMessage(-1, 0.f, FColor::Green, FString::Printf(TEXT("HP : %f"), CurrentHealth));
}
// Fill out your copyright notice in the Description page of Project Settings.


#include "ItemBase.h"
#include "Kismet/GameplayStatics.h"
#include "GameFramework/Character.h"
#include "MyActorComponent.h"

// Sets default values
AItemBase::AItemBase()
{
 	// 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;

}

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

	ACharacter* PlayerCharacter =
		UGameplayStatics::GetPlayerCharacter(GetWorld(), 0);

    UMyActorComponent* HealthComp = PlayerCharacter->FindComponentByClass<UMyActorComponent>();

    if (HealthComp)
    {
        UE_LOG(LogTemp, Error, TEXT("Success: HealthComp Found and Binding..."));
        HealthComp->OnHealthDead.AddDynamic(this, &AItemBase::ItemDestroyed);
    }
    else
    {
        UE_LOG(LogTemp, Warning, TEXT("Failure: HealthComp NOT Found on this Actor!"));
    }
}

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

}

void AItemBase::ItemDestroyed(AController* Inst)
{
	GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Green, FString::Printf(TEXT("Dead!")));
	this->Destroy();
}