총기, 총알, 발사방식 정보가 나오는 인게임 UI를 만들었는데 총기가 준모님의 데이터 테이블로 연결되어 있어서 연결이 안됐다.
데이터 테이블이 내 담당이 아니어서 건들지 않는 방법으로...
블루프린트로 만들었던 Primary Data Asset을 C++ 클래스로 만들고 DT Row가 PDA를 참조(Soft)를 했다.
그리고 UI로는 PDA만 사용하기로 했다.
[DT_Weapon(Row)]
└─ WeaponData(Soft Ref to PDA_XXX)
▼
[UWeaponDataAsset(네 C++)]
▼
픽업/이큅 마스터가 BeginPlay에 PDA 캐시
▼
캐릭터 스폰/UI는 PDA만 참조
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "WeaponTray.generated.h"
// 클래스 전방 선언
class UTextBlock;
class UImage;
class UWidgetAnimation;
class UWidget;
class UTexture2D;
class APppCharacter;
class AEquipWeaponMaster;
UCLASS(BlueprintType, Blueprintable)
class PPP_API UWeaponTray : public UUserWidget
{
GENERATED_BODY()
public:
// 위젯 생성 시 호출되는 함수 (델리게이트 바인드)
virtual void NativeConstruct() override;
// 위젯 파괴 시 호출되는 함수 (델리게이트 해제)
virtual void NativeDestruct() override;
// 위젯이 보이는지 여부를 설정하는 함수
UFUNCTION(BlueprintCallable, Category="UI")
void SetHudVisible(bool bVisible);
// 무기 정보를 업데이트 하는 함수
// 이름 + 애니만 처리
UFUNCTION(BlueprintCallable, Category = "UI")
void UpdateWeaponInfo(const FText& NewWeaponName, UTexture2D* NewWeaponImage);
UFUNCTION(BlueprintCallable, Category="UI")
void UpdateAmmoText(int32 NewAmmoInMag, int32 NewReserveAmmo);
protected:
// 블루프린트 위젯 바인딩
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> WeaponNameText;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UImage> WeaponImage;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> CurrentAmmoText;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> ReserveAmmoText;
UPROPERTY(meta = (BindWidget))
TObjectPtr<UTextBlock> FireModeText;
// 시작 시 숨김/표시 전환용 루트
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UWidget> TrayAnchor;
// 애니메이션 바인딩
// UPROPERTY(meta = (BindWidgetAnim), Transient)
// UWidgetAnimation* WeaponSwap = nullptr;
private:
// 델리게이트 바인드용
UFUNCTION()
void HandleWeaponChanged(AEquipWeaponMaster* NewWeapon);
UFUNCTION()
void HandleAmmoChanged(int32 InMag, int32 Reserve);
// Fire Mode UI 갱신
void UpdateFireModeTextFromWeapon(AEquipWeaponMaster* Weapon);
// 캐릭터 참조를 저장
UPROPERTY() APppCharacter* CachedCharacter = nullptr;
// 무기 보유 여부 가드
bool bHasWeapon = false;
};
#include "WeaponTray.h"
#include "Components/TextBlock.h"
#include "Components/Image.h"
#include "Components/Widget.h"
#include "Kismet/GameplayStatics.h"
#include "PppCharacter.h"
#include "EquipWeaponMaster.h"
#include "Engine/Texture2D.h"
#include "PPP/GameMOde/GameDefines.h"
void UWeaponTray::SetHudVisible(bool bVisible)
{
if (TrayAnchor)
{
TrayAnchor->SetVisibility
(
bVisible ? ESlateVisibility::SelfHitTestInvisible
: ESlateVisibility::Collapsed
);
}
}
void UWeaponTray::NativeConstruct()
{
Super::NativeConstruct();
// 처음엔 무기 UI 숨김
SetHudVisible(false);
bHasWeapon = false;
// 플레이어 캐릭터 캐싱
CachedCharacter = Cast<APppCharacter>(UGameplayStatics::GetPlayerCharacter(this, 0));
if (CachedCharacter)
{
CachedCharacter->OnWeaponChanged.AddDynamic(this, &UWeaponTray::HandleWeaponChanged);
CachedCharacter->OnAmmoChanged.AddDynamic(this, &UWeaponTray::HandleAmmoChanged);
}
}
void UWeaponTray::NativeDestruct()
{
CachedCharacter = Cast<APppCharacter>(UGameplayStatics::GetPlayerCharacter(this, 0));
if (CachedCharacter)
{
CachedCharacter->OnWeaponChanged.AddDynamic(this, &UWeaponTray::HandleWeaponChanged);
CachedCharacter->OnAmmoChanged.AddDynamic(this, &UWeaponTray::HandleAmmoChanged);
}
Super::NativeDestruct();
}
void UWeaponTray::UpdateWeaponInfo(const FText& NewWeaponName, UTexture2D* NewWeaponImage)
{
// 이름만 갱신
if (WeaponNameText)
{
WeaponNameText->SetText(NewWeaponName);
}
if (WeaponImage)
{
if (NewWeaponImage)
{
WeaponImage->SetBrushFromTexture(NewWeaponImage /*, true*/);
}
else
{
WeaponImage->SetBrushFromTexture(nullptr);
}
}
}
void UWeaponTray::UpdateAmmoText(int32 NewAmmoInMag, int32 NewReserveAmmo)
{
if (CurrentAmmoText != nullptr)
{
CurrentAmmoText->SetText(FText::AsNumber(NewAmmoInMag));
}
if (ReserveAmmoText != nullptr)
{
ReserveAmmoText->SetText(FText::AsNumber(NewReserveAmmo));
}
}
void UWeaponTray::HandleWeaponChanged(AEquipWeaponMaster* NewWeapon)
{
if (!NewWeapon)
{
// 무기 없음 → 숨김 + 초기화
bHasWeapon = false;
SetHudVisible(false);
UpdateWeaponInfo(FText::GetEmpty(), nullptr);
if (CurrentAmmoText) CurrentAmmoText->SetText(FText::FromString(TEXT("0")));
if (ReserveAmmoText) ReserveAmmoText->SetText(FText::FromString(TEXT("0")));
if (FireModeText) FireModeText->SetText(FText());
if (WeaponImage) WeaponImage->SetBrushFromTexture(nullptr);
return;
}
// 무기 있음 → 표시
bHasWeapon = true;
SetHudVisible(true);
const FText WeaponName =
!NewWeapon->WeaponDisplayName.IsEmpty()
? NewWeapon->WeaponDisplayName
: FText::FromName(NewWeapon->GetFName());
UpdateWeaponInfo(WeaponName, NewWeapon->GetWeaponIcon());
UpdateFireModeTextFromWeapon(NewWeapon);
HandleAmmoChanged(NewWeapon->CurrentAmmoInMag, NewWeapon->ReserveAmmo);
}
void UWeaponTray::HandleAmmoChanged
(
int32 InMag,
int32 Reserve
)
{
if (!bHasWeapon)
{
return; // 무기 없으면 무시
}
UpdateAmmoText(InMag, Reserve);
}
void UWeaponTray::UpdateFireModeTextFromWeapon
(
AEquipWeaponMaster* Weapon
)
{
if (!Weapon || !FireModeText)
{
return;
}
FireModeText->SetText(Weapon->GetFireModeText());
}
DA_Assault_Rifle1) 열기UWeaponDataAsset로 바꾸기UWeaponDataAsset 타입으로 4개 만들고 값만 복붙) ✔️WeaponName / MaxMagSize / FireMode / ThumbnailsEquipWeaponClass = 무기담당 장착용 BPPickupWeaponClass = 무기담당 픽업용 BPSpawnActor( Data->EquipWeaponClass ) → 소켓 부착EquippedWeapon->GetWeaponData()(또는 바로 Data)에서 이름/모드/아이콘 읽기AEquipWeaponMaster / APickUpWeaponMaster 맞는지Data->EquipWeaponClass를 쓰는지#pragma once
#include "CoreMinimal.h"
#include "Engine/DataTable.h" // 데이터 테이블 사용
#include "WeaponTypes.h" // EWeaponType Enum 사용
#include "WeaponRow.generated.h"
class UWeaponDataAsset; // 무기 데이터 에셋 사용
UENUM(BlueprintType)
enum class EWeaponType : uint8
{
// enum 설정
Pistol UMETA(DisplayName = "Pistol"),
Assault_Rifle UMETA(DisplayName = "Assault_Rifle"),
Shotgun UMETA(DisplayName = "Shotgun"),
Rocket_Launcher UMETA(DisplayName = "Rocket_Launcher"),
UnArmed UMETA(DisplayName = "UnArmed")
};
USTRUCT(BlueprintType)
struct FWeaponRow : public FTableRowBase
{
GENERATED_BODY()
// ...
// PDA SoftRef: 런타임은 PDA만 진실원으로 사용
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category="Data")
TSoftObjectPtr<UWeaponDataAsset> WeaponData;
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<class APickUpWeaponMaster> PickUpWeapon; // 줍는 무기 종류, Static Mesh 타입
UPROPERTY(EditAnywhere, BlueprintReadWrite)
TSubclassOf<class AEquipWeaponMaster> EquipWeapon; // 장착 중인 무기, Skeletal Mesh 타입
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FVector WeaponOffset = FVector::ZeroVector; // 무기 잡는 손 위치 값 저장 타입
UPROPERTY(EditAnywhere, BlueprintReadWrite)
FRotator WeaponRotation = FRotator::ZeroRotator; // 무기 잡는 손 위치 값 저장 타입
// 예비 탄약 수, 예) 10 / 1000 이라 가정할 때 1000에 해당됨.
UPROPERTY(EditAnywhere, BlueprintReadWrite)
int32 ReserveAmmo = 0;; // 나중에 데이터 테이블에서 5000으로 변경
};
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Sound/SoundBase.h"
#include "WeaponRow.h"
#include "WeaponTypes.h" // EFireMode Enum 사용
#include "EquipWeaponMaster.generated.h"
// 전방 선언
class UTexture2D;
class UWeaponDataAsset;
// ...
// 탄약 변경 델리게이트
DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FOnWeaponAmmoChanged, int32, AmmoInMag, int32, Reserve);
// 플레이어가 장착하는 무기(장착/발사/드랍 동작과 스탯 적용을 담당)
UCLASS()
class PPP_API AEquipWeaponMaster : public AActor
{
GENERATED_BODY()
public:
AEquipWeaponMaster();
// ...
// 탄약 변경 이벤트: 블루프린트에서 바인딩 가능
UPROPERTY(BlueprintAssignable, Category = "Weapon")
FOnWeaponAmmoChanged OnAmmoChanged;
// 재장전 함수
UFUNCTION(BlueprintCallable)
void Reload();
// 캐시된 PDA(런타임 진실원)
UFUNCTION(BlueprintPure, Category="Data")
UWeaponDataAsset* GetWeaponData() const;
UFUNCTION(BlueprintPure, Category="Weapon")
EFireMode GetFireMode() const;
// UI 편의 (있으면 유지)
UFUNCTION(BlueprintPure, Category="UI", meta=(DisplayName="GetIcon")) // BP에서는 GetIcon으로 보임
UTexture2D* GetWeaponIcon() const;
UFUNCTION(BlueprintPure, Category="UI")
UTexture2D* GetAmmoIcon() const;
// ...
// 탄약 변수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
int32 CurrentAmmoInMag;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Weapon")
int32 ReserveAmmo;
// UI 표시용
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
UTexture2D* WeaponImage = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
UTexture2D* AmmoImage = nullptr;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "UI")
FText WeaponDisplayName;
UFUNCTION(BlueprintPure, Category="Weapon")
FText GetFireModeText() const;
private:
// 여기로 PDA 캐시
UPROPERTY() UWeaponDataAsset* CachedData = nullptr;
};
#include "WeaponRow.h" // FWeaponRow 구조체 사용
#include "WeaponTypes.h" // EFireMode Enum 사용
#include "../InGame/WeaponDataAsset.h" // 무기 데이터 에셋 사용
// ...
// 재장전 함수
void AEquipWeaponMaster::Reload()
{
// PDA > DT > 멤버 순으로 최대 탄창 크기 결정
const int32 MaxAmmoInMag =
(CachedData && CachedData->MaxMagSize > 0) ? CachedData->MaxMagSize :
(WeaponDataRow.MagazineSize > 0) ? WeaponDataRow.MagazineSize :
MagazineSize;
const int32 AmmoNeeded = MaxAmmoInMag - CurrentAmmoInMag;
// 예비탄이 없거나 이미 가득 차면 재장전 불필요 → 즉시 종료
if (ReserveAmmo <= 0 || AmmoNeeded <= 0)
{
UE_LOG(LogTemp, Warning, TEXT("재장전할 탄약이 없거나, 탄창이 이미 가득 찼습니다."));
OnAmmoChanged.Broadcast(CurrentAmmoInMag, ReserveAmmo);
return;
}
const int32 Use = FMath::Min(ReserveAmmo, AmmoNeeded);
CurrentAmmoInMag += Use;
ReserveAmmo -= Use;
// 탄약 변경 델리게이트 호출
OnAmmoChanged.Broadcast(CurrentAmmoInMag, ReserveAmmo);
}
UWeaponDataAsset* AEquipWeaponMaster::GetWeaponData() const
{
return CachedData;
}
EFireMode AEquipWeaponMaster::GetFireMode() const
{
return CachedData ? CachedData->FireMode : EFireMode::Single;
}
UTexture2D* AEquipWeaponMaster::GetWeaponIcon() const
{
return CachedData ? CachedData->WeaponThumbnail : WeaponImage;
}
UTexture2D* AEquipWeaponMaster::GetAmmoIcon() const
{
return CachedData ? CachedData->AmmoThumbnail : AmmoImage; // PDA 우선
}
FText AEquipWeaponMaster::GetFireModeText() const
{
switch (GetFireMode())
{
case EFireMode::Single: return NSLOCTEXT("Weapon", "Single", "Single");
case EFireMode::Auto: return NSLOCTEXT("Weapon", "Auto", "Auto");
default: return NSLOCTEXT("Weapon", "Unknown","Unknown");
}
}
// 신규 장비가 존재하면 스폰 후 장착 및 OnEquipped 호출 (무기 장착 기능)
if (WeaponRow.EquipWeapon)
{
UE_LOG(LogTemp, Warning, TEXT("스폰 시도 - Skeletal Weapon: %s"), *WeaponRow.EquipWeapon->GetName());
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = Character;
AEquipWeaponMaster* NewWeapon = Character->GetWorld()->SpawnActor<AEquipWeaponMaster>(
WeaponRow.EquipWeapon,
FVector::ZeroVector,
FRotator::ZeroRotator,
SpawnParams
);
if (NewWeapon)
{
UE_LOG(LogTemp, Warning, TEXT("NewWeapon 스폰 성공! OnEquipped 호출"));
NewWeapon->OnEquipped(Character, WeaponRow);
// Character->EquippedWeapon = NewWeapon; // 준모님 아랫줄의 세터로 바꿔도 돼요?
// by Yeoul
UE_LOG(LogTemp, Warning, TEXT("[PickUp] After OnEquipped: Icon=%s, Ammo=%s"),
*GetNameSafe(NewWeapon->GetWeaponIcon()),
*GetNameSafe(NewWeapon->GetAmmoIcon()));
// 세터로 교체 (여기서 OnWeaponChanced 브로드캐스트됨)
Character->SetEquippedWeapon(NewWeapon);
}
else
{
UE_LOG(LogTemp, Error, TEXT("NewWeapon SK 스폰 실패"));
}
}
void APppCharacter::BeginPlay()
{
Super::BeginPlay();
ToggleCamera();
// ...
// 시작 시 UI에 '무기 없음' 신호
OnWeaponChanged.Broadcast(nullptr);
}
// 무기 변경 델리게이트
void APppCharacter::SetEquippedWeapon(AEquipWeaponMaster* NewWeapon)
{
// 현재 무기와 새로 지정하려는 무기가 같으면 아무것도 안 함
if (EquippedWeapon == NewWeapon)
{
return; // 중복 변경 방지
}
// 기존 무기 델리게이트 언바인드
if (EquippedWeapon != nullptr)
{
EquippedWeapon->OnAmmoChanged.RemoveDynamic(this, &APppCharacter::OnWeaponAmmoChanged);
// 정현성
// 기존 무기 히트, 킬 델리게이트 언바인드
EquippedWeapon->OnWeaponHit.RemoveDynamic(this, &APppCharacter::ShowHitMarker);
EquippedWeapon->OnWeaponKilled.RemoveDynamic(this, &APppCharacter::ShowKillMarker);
}
// 교체
EquippedWeapon = NewWeapon;
// 무기 변경 알림
OnWeaponChanged.Broadcast(NewWeapon);
// 새 무기 델리게이트 바인드
if (EquippedWeapon != nullptr)
{
EquippedWeapon->OnAmmoChanged.AddDynamic(this, &APppCharacter::OnWeaponAmmoChanged);
// 정현성
// 새 무기 히트, 킬 델리게이트 바인드
EquippedWeapon->OnWeaponHit.AddDynamic(this, &APppCharacter::ShowHitMarker);
EquippedWeapon->OnWeaponKilled.AddDynamic(this, &APppCharacter::ShowKillMarker);
// 즉시 현재 탄약 상태를 UI에 반영 (안전빵)
OnWeaponAmmoChanged(EquippedWeapon->CurrentAmmoInMag,
EquippedWeapon->ReserveAmmo);
}
else
{
// 맨손 등
OnWeaponAmmoChanged(0, 0);
}
}
/**무기 관련 */
void APppCharacter::OnInteract()
{
UE_LOG(LogTemp, Warning, TEXT("OnInteract() 호출됨"));
if (OverlappingPickUpActor && OverlappingPickUpActor->PickUpComp)
{
UE_LOG(LogTemp, Warning, TEXT("OverlappingPickUpActor 존재, TryPickUp 실행"));
OverlappingPickUpActor->PickUpComp->TryPickUp(this);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("F key pressed but no weapon overlapped"));
}
}
// 탄약 변경 이벤트 콜백 구현
void APppCharacter::OnWeaponAmmoChanged(int32 CurrentAmmoInMag, int32 ReserveAmmo)
{
// 탄약 변경 이벤트 발생
OnAmmoChanged.Broadcast(CurrentAmmoInMag, ReserveAmmo);
UE_LOG(LogTemp, Warning, TEXT("탄약 변경: 현재 탄창 %d, 예비 탄약 %d"), CurrentAmmoInMag, ReserveAmmo);
}
// 성준모, 장전 입력 시 호출되는 함수 구현
void APppCharacter::OnReload()
{
UE_LOG(LogTemp, Warning, TEXT("재장전 R키 입력하셨습니다."));
//UAnimMontage* MontageToPlay;
// 장착된 무기가 없을 때 실행
if (!EquippedWeapon)
{
UE_LOG(LogTemp, Warning, TEXT("Reload 실패 : 장착된 무기가 없습니다."));
return;
}
// 재장전 중복 방지
if (bIsReloading)
{
// Verbose : 상세 로그 레벨로, 매우 세부적인 진단용 메세지.
UE_LOG(LogTemp, Verbose, TEXT("이미 재장전 중입니다."));
return;
}
int32 MaxAmmoInMag = EquippedWeapon->WeaponDataRow.MagazineSize;
// PDA 값이 유효하면 MagazineSize 덮어씀
if (EquippedWeapon->GetWeaponData() && EquippedWeapon->GetWeaponData()->MaxMagSize > 0)
{
MaxAmmoInMag = EquippedWeapon->GetWeaponData()->MaxMagSize;
}
// 여기서 재장전 필요 여부 체크
if (EquippedWeapon->CurrentAmmoInMag >= MaxAmmoInMag ||
EquippedWeapon->ReserveAmmo <= 0)
{
UE_LOG(LogTemp, Log, TEXT("재장전 불필요: 탄창 가득 또는 예비탄 없음"));
return;
}
// 데이터 테이블에서 온 ReloadTime 사용
float ReloadTime = EquippedWeapon->WeaponDataRow.ReloadTime;
bIsReloading = true; // 핵심 함수, 무기 재장전 (무기 Reload 호출)
// 유효한 장전 시간이면 타이머, 아니면 즉시 완료
if (ReloadTime > 0.f)
{
EquippedWeapon->PlayReloadAnimation(); // 무기 내부에서 SkeletalMesh로 재생
GetWorldTimerManager().SetTimer(ReloadTimerHandle, this, &APppCharacter::FinishReload, ReloadTime, false);
}
else
{
// 장전 시간이 0이거나 잘못된 경우 즉시 완료 처리
FinishReload();
}
}
각 무기 PDA 생성: DA_AssaultRifle1, DA_Shotgun1 …
EquipWeaponClass = 무기담당 장착 BPPickupWeaponClass = 무기담당 픽업 BP→ 기존 데이터 에셋은 연결이 안되므로 새로 만들어서 데이터 테이블의 WeaponData에 추가하기
DT_Weapon 열기 → 각 Row의 WeaponData 칼럼에 해당 PDA 지정

픽업/이큅 액터(C++ 클래스 또는 BP 자식)의 WeaponRow에 DT_Weapon + RowName 선택