리얼 개발

[UE5] 무기 선택 창으로 무기 교체하기 본문

Unreal Engine5

[UE5] 무기 선택 창으로 무기 교체하기

econo-my 2024. 9. 11. 17:43

 

GTA5 무기 선택 창처럼 무기를 교체하는 시스템을 만들어보자.

 

 

무기 클래스 제작

바꿀 무기를 먼저 제작해주기로 했다. 

 

WeaponBase 부모 클래스를 만들고 Sword, Bow, Staff 클래스들이 WeaponBase를 상속받도록 설계했다. 업캐스팅을 통해 관리하기 쉽고 공통된 코드를 줄이고자 함이다.

 

 

 

UI 클래스 제작

UserWidget 클래스를 상속받아 제작했다. Sword, Bow, Staff 가 적힌 버튼 3개를 만들고 버튼 클릭시 해당하는 무기로 교체할 수 있도록 해보자.

#include "UI/WeaponChoiceUI.h"
#include "Components/Button.h"
#include "Player/CPlayerController.h"
#include "Interface/TakeWeaponInterface.h"

UWeaponChoiceUI::UWeaponChoiceUI(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
}

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

	SwordButtonPtr = Cast<UButton>(GetWidgetFromName(TEXT("SwordButton")));
	if (SwordButtonPtr == nullptr) return;

	BowButtonPtr = Cast<UButton>(GetWidgetFromName(TEXT("BowButton")));
	if (BowButtonPtr == nullptr) return;

	StaffButtonPtr = Cast<UButton>(GetWidgetFromName(TEXT("StaffButton")));
	if (StaffButtonPtr == nullptr) return;

	SwordButtonPtr->OnClicked.AddDynamic(this, &UWeaponChoiceUI::OnClickButtonSword);
	BowButtonPtr->OnClicked.AddDynamic(this, &UWeaponChoiceUI::OnClickButtonBow);
	StaffButtonPtr->OnClicked.AddDynamic(this, &UWeaponChoiceUI::OnClickButtonStaff);

	UE_LOG(LogTemp, Warning, TEXT("NativeConstruct 호출"));
}

void UWeaponChoiceUI::OnClickButtonSword()
{
	UE_LOG(LogTemp, Display, TEXT("ClickButton Sword"));
	ITakeWeaponInterface* Interface = Cast<ITakeWeaponInterface>(GetWorld()->GetFirstPlayerController()->GetPawn());
	if (Interface)
	{
		Interface->TakeWeapon(EWeaponType::Sword);
		Interface->CloseWeaponChoiceUI();
	}
}

void UWeaponChoiceUI::OnClickButtonBow()
{
	UE_LOG(LogTemp, Display, TEXT("ClickButton Bow"));
	ITakeWeaponInterface* Interface = Cast<ITakeWeaponInterface>(GetWorld()->GetFirstPlayerController()->GetPawn());
	if (Interface)
	{
		Interface->TakeWeapon(EWeaponType::Bow);
		Interface->CloseWeaponChoiceUI();
	}
}

void UWeaponChoiceUI::OnClickButtonStaff()
{
	UE_LOG(LogTemp, Display, TEXT("ClickButton Staff"));
	ITakeWeaponInterface* Interface = Cast<ITakeWeaponInterface>(GetWorld()->GetFirstPlayerController()->GetPawn());
	if (Interface)
	{
		Interface->TakeWeapon(EWeaponType::Staff);
		Interface->CloseWeaponChoiceUI();
	}
}

 

UButton 클래스에 선언되어 있는 OnClicked 델리게이트에 버튼 클릭시 실행될 콜백 함수들을 등록했다. 이때 캐릭터 클래스와 연결될 수 있도록 인터페이스를 만들었다. 

 

버튼 클릭시 버튼에 해당하는 무기 타입을 인터페이스를 구현하는 클래스에게 넘겨줘 어떤 무기로 교체해야하는지 알려준다. 그리고 버튼이 클릭될 때 UI가 닫힐 수 있도록 CloseWeaponChoiceUI() 함수를 호출해준다.

 

 

인터페이스

인터페이스는 UI와 캐릭터를 연결해주는 클래스이며 컴파일 의존성을 줄이는데 효과적이다.

UENUM()
enum class EWeaponType : uint8
{
	Sword = 0,
	Bow,
	Staff
};

class CAPSTONEPROJECT_API ITakeWeaponInterface
{
	GENERATED_BODY()

public:
	virtual void TakeWeapon(EWeaponType WeaponType) = 0;
	virtual void CloseWeaponChoiceUI() = 0;
};

무기 타입을 넘겨주는 TakeWeapon 함수와 버튼 클릭시 UI가 닫히도록 신호를 보내는 CloseWeaponChoiceUI 순수 가상 함수가 선언되어 있다. 또한 인터페이스에 enum class 타입으로 무기 타입을 선언함으로써 UI 클래스와 캐릭터 클래스 모두 무기 타입 변수를 가질 수 있다.

 

 

캐릭터

어떤 무기 타입이 들어왔는지 switch나 if 를 통해 분기를 만들어 처리할 수 있겠으나, 델리게이트를 구조체로 감싸는 방법으로 구현해보았다.

DECLARE_DELEGATE(FTakeItemDelegate)

USTRUCT()
struct FTakeItemDelegateWrapper
{
	GENERATED_BODY()

	FTakeItemDelegateWrapper() {}
	FTakeItemDelegateWrapper(const FTakeItemDelegate& InTakeItemDelegate) : TakeItemDelegate(InTakeItemDelegate) {}

	FTakeItemDelegate TakeItemDelegate;
};

UPROPERTY()
TArray<FTakeItemDelegateWrapper> TakeItemDelegateArray;

먼저 캐릭터 클래스에 델리게이트와 델리게이트를 감쌀 구조체를 선언해주고 TArray를 이용해 구조체를 관리해준다.

 

void OpenWeaponChoiceUI(); //UI를 띄우기 위한 함수
virtual void CloseWeaponChoiceUI() override; //인터페이스 구현
virtual void TakeWeapon(EWeaponType WeaponType) override; //인터페이스 구현

void EquipSword(); //선택한 무기가 Sword 일 때 호출될 함수
void EquipBow(); //선택한 무기가 Bow 일 때 호출될 함수
void EquipStaff(); //선택한 무기가 Staff 일 때 호출될 함수

그리고 무기 교체에 필요한 함수들을 선언해준다.

 

cpp로 와서 델리게이트를 등록한 뒤 함수들을 정의해주면 된다.

TakeItemDelegateArray.Add(FTakeItemDelegateWrapper(FTakeItemDelegate::CreateUObject(this, &ACharacterBase::EquipSword)));
TakeItemDelegateArray.Add(FTakeItemDelegateWrapper(FTakeItemDelegate::CreateUObject(this, &ACharacterBase::EquipBow)));
TakeItemDelegateArray.Add(FTakeItemDelegateWrapper(FTakeItemDelegate::CreateUObject(this, &ACharacterBase::EquipStaff)));

void ACharacterBase::TakeWeapon(EWeaponType WeaponType)
{
	TakeItemDelegateArray[(uint8)WeaponType].TakeItemDelegate.ExecuteIfBound();
}

void ACharacterBase::EquipSword()
{
	UE_LOG(LogTemp, Display, TEXT("Equip Sword"));

	if (WeaponBase)
	{
		WeaponBase->Destroy();
	}
	
	FVector SpawnLocation = GetMesh()->GetSocketLocation(TEXT("hand_rSocket"));
	FRotator SpawnRotation = GetMesh()->GetSocketRotation(TEXT("hand_rSocket"));

	WeaponBase = GetWorld()->SpawnActor<ASword>(SwordClass, SpawnLocation, SpawnRotation);
	WeaponBase->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, TEXT("hand_rSocket"));
}

void ACharacterBase::EquipBow()
{
	UE_LOG(LogTemp, Display, TEXT("Equip Bow"));

	if (WeaponBase)
	{
		WeaponBase->Destroy();
	}

	FVector SpawnLocation = GetMesh()->GetSocketLocation(TEXT("hand_rSocket"));
	FRotator SpawnRotation = GetMesh()->GetSocketRotation(TEXT("hand_rSocket"));

	WeaponBase = GetWorld()->SpawnActor<ABow>(BowClass, SpawnLocation, SpawnRotation);
	WeaponBase->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, TEXT("hand_rSocket"));
}

void ACharacterBase::EquipStaff()
{
	UE_LOG(LogTemp, Display, TEXT("Equip Staff"));

	if (WeaponBase)
	{
		WeaponBase->Destroy();
	}

	FVector SpawnLocation = GetMesh()->GetSocketLocation(TEXT("hand_rSocket"));
	FRotator SpawnRotation = GetMesh()->GetSocketRotation(TEXT("hand_rSocket"));

	WeaponBase = GetWorld()->SpawnActor<AStaff>(StaffClass, SpawnLocation, SpawnRotation);
	WeaponBase->AttachToComponent(GetMesh(), FAttachmentTransformRules::SnapToTargetNotIncludingScale, TEXT("hand_rSocket"));
}

TakeWeapon은 UI 버튼이 눌릴 때 실행되는 함수로, TArray에 등록된 델리게이트들을 인자로 들어오는 무기 타입으로 구분하여 델리게이트의 실행 함수를 호출한다.

 

델리게이트의 실행 함수가 호출되면 무기 타입에 해당하는 함수가 호출되며, 먼저 이미 다른 무기를 들고있는지 검사 후 들고 있다면 삭제 후 들고, 아니라면 바로 무기를 들게 만든다. 이 부분에서 내가 구현한 것처럼 삭제 후 스폰하는 것과 다른 무기의 콜리전과 시야에 보이는 것을 숨기는 두 가지 방법이 있는 것 같은데 상황에 따라 구현하면 될 것 같다.

 

마지막으로 UI를 띄워주면 끝이다.

void ACharacterBase::OpenWeaponChoiceUI()
{
	if (!WeaponChoiceUIPtr->IsInViewport())
	{
		WeaponChoiceUIPtr->AddToViewport();
	}
}

void ACharacterBase::CloseWeaponChoiceUI()
{
	WeaponChoiceUIPtr->RemoveFromViewport();
}

 

 

UI 호출 버튼 클릭 시

 

 

Sword 버튼 클릭 시

 

Bow 버튼 클릭 시

'Unreal Engine5' 카테고리의 다른 글

[UE5] 모션워핑과 풀바디 IK  (0) 2024.10.16
[UE5] 언리얼 빌드 시스템  (1) 2024.09.12
[UE5] Vector 클래스 분석  (0) 2024.09.04
[UE5] 보간 함수를 이용한 회전  (1) 2024.09.02
[UE5] 용어 정리  (4) 2024.09.02