Unreal Project Acero#29(Store System)

공팡팡·2025년 4월 24일

Unreal - Project Acero

목록 보기
5/10

Shop Overlay 구성

여러모로 머리가 아팠던 구간이다.

Shop Overlay의 구성은 다음과 같다. 일단 상점의 로직자체는 크게 어렵지 않다. 버튼을 누를경우 해당 웨폰의 Cost를 플레이어가 가지고 있는 골드와 비교해서, 구매가 가능하다면 감소시키고 무기를 장착시키면 된다.

유튜브와 여러 튜토리얼에서는 이러한 상점시스템을 하드코딩식으로 다루었다. 즉 기존에 정의한 버튼을 일일이 바인딩해서, 웨폰을 소환하고 이를 플레이어에게 달아주는 식이다. 이러한 하드코딩 방식은 구현은 쉬우나 이 경우 유연성도 떨어질 뿐더러, 확장도 불가능하다.

때문에 좀 더 깔끔하고 이쁜 코드가 없을까 고민을 거쳤다.

Weapon Struct

일단 구조체가 필요하다고 판단, Weapon Struct를 정의 및 구현하였다.

사진에 보이듯이 무기를 고를때 필요한건, 무기의 이름, 종류, Cost, 약간의 설명과 Icon이다. 나의 경우 Icon은 수동으로 작업해주었다.(만약 Dynamic한 구성을 원한다면 Icon도 매핑이 가능하다.)


USTRUCT()
struct FWeaponStruct: public FTableRowBase
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Weapons)
	FName WeaponName;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Weapons)
	int32 Cost;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Weapons)
	TSubclassOf<class AWeapon> WeaponClass;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category=Weapons)
	FText Description;	
};

이제 버튼에 매핑을 할 차례이다. 로직은 버튼을 누르면 버튼에 매핑되어 있는 구조체를 넘겨주는 방식으로 진행하려고 하였다. 결과는 아래와 같다.


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

	if (!WeaponDataTable) return;

	FWeaponStruct* Rifle = WeaponDataTable->FindRow<FWeaponStruct>(FName("Rifle"), TEXT(""));
	FWeaponStruct* Sniper = WeaponDataTable->FindRow<FWeaponStruct>(FName("Sniper"), TEXT(""));
	FWeaponStruct* Shotgun = WeaponDataTable->FindRow<FWeaponStruct>(FName("Shotgun"), TEXT(""));
	FWeaponStruct* SMG = WeaponDataTable->FindRow<FWeaponStruct>(FName("SMG"), TEXT(""));

	if (Rifle) ButtonWeaponMap.Add(RifleButton1, *Rifle);
	if (Sniper) ButtonWeaponMap.Add(SniperButton1, *Sniper);
	if (Shotgun) ButtonWeaponMap.Add(ShotgunButton1, *Shotgun);
	if (SMG) ButtonWeaponMap.Add(SMGButton1, *SMG);
	
	if (Rifle && RifleButton1) {
		ButtonWeaponMap.Add(RifleButton1, *Rifle);
		RifleButton1->OnClicked.AddDynamic(this, &UShopOverlay::OnAnyButtonClicked);
	}
	if (Sniper && SniperButton1) {
		ButtonWeaponMap.Add(SniperButton1, *Sniper);
		SniperButton1->OnClicked.AddDynamic(this, &UShopOverlay::OnAnyButtonClicked);
	}
	if (Shotgun && ShotgunButton1) {
		ButtonWeaponMap.Add(ShotgunButton1, *Shotgun);
		ShotgunButton1->OnClicked.AddDynamic(this, &UShopOverlay::OnAnyButtonClicked);
	}
	if (SMG && SMGButton1) {
		ButtonWeaponMap.Add(SMGButton1, *SMG);
		SMGButton1->OnClicked.AddDynamic(this, &UShopOverlay::OnAnyButtonClicked);
	}

}

처음에 택했던 방식이다. Rifle과 Sniper등의 구조체를 언리얼의 Data Table에서 미리 만든 후에 넘겨주는 방식이다. 그리고서 NativeConstruct를 통해 구조체 인스턴스를 Map형태의 Button에 넣어주게 하였다.

그러나 이 경우 구조체 인스턴스를 다시 버튼에 넘겨주기가 다소 더럽고, 무엇보다 유연성이 크게 떨어졌다.

더 깔끔한 방법은 없나 찾다가 커스텀버튼을 사용하고, 이를 델리게이트 하면 구조체 정보도 넘길 수 있을 뿐더러, 유연한 구성이 가능해짐을 알았다.

CustomButton

DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnShopButtonClicked, const FWeaponStruct&, WeaponData);

UCLASS()
class CDPROJECT_API UShopButton : public UUserWidget
{
	GENERATED_BODY()

public:
	UPROPERTY(meta=(BindWidget))
	class UButton* Button;

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	FWeaponStruct WeaponData;

	UPROPERTY(BlueprintAssignable)
	FOnShopButtonClicked OnShopButtonClicked;

	virtual void NativeConstruct() override;
	
UFUNCTION()
	void OnClicked();
};

아예 버튼안에 구조체정보를 담고, 이를 다시 넘겨주는 방식이다.

FWeapon구조체를 FOnButtonClicked라는 함수에 바인딩하고, ShopOverlay에서 커스텀된 버튼을 끌어다 썼다.

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

	if (RifleButton1)
	{
		RifleButton1->OnShopButtonClicked.AddDynamic(this, &UShopOverlay::OnShopButtonClicked);
	}
}

void UShopOverlay::OnShopButtonClicked(const FWeaponStruct& WeaponData)
{
	if (!CanPurchase(WeaponData)) return;
	GiveWeaponToPlayer(WeaponData);
}

극단적으로 코드의 양이 줄어들었음을 알 수 있다.

아래의 사진처럼 무기에 해당하는 WBP_Button을 구성하고 원하는 구조체를 넘겨주기만 한다면, 버튼을 눌렀을 때 해당 구조체를 ShopOverlay에 넘겨줄 수 있었다.

결론적으로 지금의 구조가 가장 깔끔하고, 유연하게 확장 가능한 방식이었다. 버튼마다 별도로 OnClicked()를 만들 필요 없이 공통처리가 가능하다는 점에서 만족하고 넘어간 코드가 완성되었다.

profile
공부 정리 노트

0개의 댓글