HUD에 추가될 Widget 클래스 추가
SideBarWidget
//SideBarWidget.h
UCLASS()
class CCFF_API USideBarWidget : public UUserWidget
{
GENERATED_BODY()
public:
USideBarWidget(const FObjectInitializer& ObjectInitializer);
UFUNCTION(BlueprintCallable, Category = "CCFF|UI")
void UpdateBurstMeterBar(const float InPercentage);
UFUNCTION(BlueprintCallable, Category = "CCFF|UI")
void UpdateSuperMeterBar(const float InPercentage);
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> BurstMeterBar;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> SuperMeterBar;
};
//SideBarWidget.cpp
USideBarWidget::USideBarWidget(const FObjectInitializer& ObjectInitializer):Super(ObjectInitializer)
{
}
void USideBarWidget::UpdateBurstMeterBar(const float InPercentage)
{
BurstMeterBar->SetPercent(InPercentage);
}
void USideBarWidget::UpdateSuperMeterBar(const float InPercentage)
{
SuperMeterBar->SetPercent(InPercentage);
}
- Burst, Super 게이지 관련 위젯 클래스
- 게이지 상태를 나타낼 UProgressBar 2개 추가
ㅡ> 'meta=(BindWidgetOptional)' 을 통해 에디터에서 Bind가 가능하도록 설정 - 프로그레스 바 업데이트 함수 2종 추가
ProfileWidget
//ProfileWidget.h
UCLASS()
class CCFF_API UProfileWidget : public UUserWidget
{
GENERATED_BODY()
public:
UProfileWidget(const FObjectInitializer& ObjectInitializer);
UFUNCTION(BlueprintCallable, Category = "CCFF|UI")
void UpdateHealthBar(const float InPercentage);
UFUNCTION(BlueprintCallable, Category = "CCFF|UI")
void UpdateStockCount(const int32 InCount);
protected:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidgetOptional))
TObjectPtr<UProgressBar> HealthBar;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidgetOptional))
TObjectPtr<UTextBlock> StockCount;
};
//ProfileWidget.cpp
UProfileWidget::UProfileWidget(const FObjectInitializer& ObjectInitializer): Super(ObjectInitializer)
{
}
void UProfileWidget::UpdateHealthBar(const float InPercentage)
{
HealthBar->SetPercent(InPercentage);
}
void UProfileWidget::UpdateStockCount(const int32 InCount)
{
FText CountText=FText::FromString(FString::FormatAsNumber(InCount));
StockCount->SetText(CountText);
//UE_LOG(LogTemp, Display, TEXT("UProfileWidget::UpdateStockCount: %s"),*CountText.ToString());
}
- 체력 게이지 및 목숨 개수 관련 위젯 클래스
- 체력 게이지를 나타낼 UProgressBar, 목숨 개수를 나타낼 UTextBlock 추가
- 각 UI 업데이트 함수 2종 추가
HUD에 표시될 BaseInGameWidget
//BaseInGameWidget.h
UCLASS()
class CCFF_API UBaseInGameWidget : public UBaseUserWidget
{
GENERATED_BODY()
public:
virtual void NativeConstruct() override;
...
void UpdateHealthBar(const float InPercentage);
void UpdateStockCount(const int32 InCount);
void UpdateSuperMeterBar(const float InPercentage);
void UpdateBurstMeterBar(const float InPercentage);
void InitializeHUDWidget(UStatusComponent* InStatusComponent);
protected:
UPROPERTY(meta = (BindWidgetOptional))
UTextBlock* TimerText;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidgetOptional))
TObjectPtr<UProfileWidget> ProfileWidget;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, meta = (BindWidgetOptional))
TObjectPtr<USideBarWidget> SideBarWidget;
...
};
//BaseInGameWidget.cpp
void UBaseInGameWidget::NativeConstruct()
{
Super::NativeConstruct();
if (APlayerController* PC=Cast<APlayerController>(GetOwningPlayer()))
{
if (PC->IsLocalController())
{
if (ABaseCharacter* MyCharacter=Cast<ABaseCharacter>(PC->GetPawn()))
{
MyCharacter->SetHUDWidget(this);
}
}
}
}
void UBaseInGameWidget::UpdateHealthBar(const float InPercentage)
{
if (ProfileWidget)
{
ProfileWidget->UpdateHealthBar(InPercentage);
//UE_LOG(LogTemp,Log,TEXT("OnRep_CurrentHP: %0.1f"),InPercentage);
}
}
void UBaseInGameWidget::UpdateStockCount(const int32 InCount)
{
if (ProfileWidget)
{
ProfileWidget->UpdateStockCount(InCount);
}
//UE_LOG(LogTemp,Warning,TEXT("UBaseInGameWidget::UpdateStockCount: %d"),InCount);
}
void UBaseInGameWidget::UpdateSuperMeterBar(const float InPercentage)
{
if (SideBarWidget)
{
SideBarWidget->UpdateSuperMeterBar(InPercentage);
}
}
void UBaseInGameWidget::UpdateBurstMeterBar(const float InPercentage)
{
if (SideBarWidget)
{
SideBarWidget->UpdateBurstMeterBar(InPercentage);
}
}
void UBaseInGameWidget::InitializeHUDWidget(UStatusComponent* InStatusComponent)
{
float Percentage=FMath::Clamp(InStatusComponent->GetCurrentHP()/InStatusComponent->GetMaxHP(), 0, InStatusComponent->GetMaxHP());
UpdateHealthBar(Percentage);
Percentage=FMath::Clamp(InStatusComponent->GetBurstMeter()/InStatusComponent->GetMaxBurstMeter(), 0, InStatusComponent->GetMaxBurstMeter());
UpdateBurstMeterBar(Percentage);
Percentage=FMath::Clamp(InStatusComponent->GetSuperMeter()/InStatusComponent->GetMaxSuperMeter(), 0, InStatusComponent->GetMaxSuperMeter());
UpdateSuperMeterBar(Percentage);
}
- 화면에 표시되어야 하는 각 위젯 클래스들 변수로 추가
ㅡ> 마찬가지로 'meta=(BindWidgetOptional)' 을 통해 에디터에서 Bind가 가능하도록 설정 - 위젯이 생성될 때 호출되는 NativeConstruct에서 HUDWidget과 캐릭터를 연동하는 함수 호출하도록 구성
ㅡ> 게임 설계 구조상 이것만으로는 정상 작동하지 않아 추가 조치
ㅡ> 위젯보다 캐릭터가 먼저 생성되어 있다면 이것으로 충분 - 각 위젯 클래스 표기 업데이트 함수 설정
- HUDWidget 초기값 설정 관련 함수 선언
StatusComponent
//StatusComponent.h
DECLARE_MULTICAST_DELEGATE_OneParam(FOnCurrentHPChangedDelegate, float /*InPercentage*/);
DECLARE_MULTICAST_DELEGATE(FOnDeathDelegate);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnSuperMeterChangedDelegate, float /*InPercentage*/);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnBurstMeterChangedDelegate, float /*InPercentage*/);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnStockCountChangedDelegate, int32 /*InCount*/);
DECLARE_MULTICAST_DELEGATE(FOnBlockMeterChangedDelegate);
UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class CCFF_API UStatusComponent : public UActorComponent
{
GENERATED_BODY()
public:
// Sets default values for this component's properties
UStatusComponent();
#pragma region GetFunction
FORCEINLINE float GetCurrentHP() const { return CurrentHP; }
FORCEINLINE float GetMaxHP() const { return MaxHP; }
FORCEINLINE float GetBurstMeter() const { return BurstMeter; }
FORCEINLINE float GetMaxBurstMeter() const { return BurstMeter; }
FORCEINLINE float GetSuperMeter() const { return SuperMeter; }
FORCEINLINE float GetMaxSuperMeter() const { return SuperMeter; }
FORCEINLINE float GetMaxBlockMeter() const { return MaxBlockMeter; }
FORCEINLINE float GetBlockMeter() const { return BlockMeter; }
FORCEINLINE float GetCurrentStockCount() const { return CurrentStockCount; }
#pragma endregion
#pragma region SetFunction
void SetCurrentHP(const float InCurrentHP);
void SetSuperMeter(const float InSuperMeter);
void SetBurstMeter(const float InBurstMeter);
void SetBlockMeter(const float InBlockMeter);
void SetCurrentStockCount(const int32 InCount);
#pragma endregion
#pragma region AddFunction
void AddBurstMeter(const float InAmount);
void AddBlockMeter(const float InAmount);
#pragma endregion
virtual void GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const override;
protected:
UFUNCTION()
void OnRep_CurrentHP();
UFUNCTION()
void OnRep_BurstMeter();
UFUNCTION()
void OnRep_SuperMeter();
UFUNCTION()
void OnRep_StockCount();
public:
FOnCurrentHPChangedDelegate OnCurrentHPChanged;
FOnDeathDelegate OnDeathState;
FOnSuperMeterChangedDelegate OnSuperMeterChanged;
FOnBurstMeterChangedDelegate OnBurstMeterChanged;
FOnBlockMeterChangedDelegate OnGuardCrush;
FOnStockCountChangedDelegate OnStockCountChanged;
private:
UPROPERTY()
float MaxHP;
UPROPERTY(ReplicatedUsing=OnRep_CurrentHP)
float CurrentHP;
UPROPERTY()
float MaxSuperMeter;
UPROPERTY(ReplicatedUsing=OnRep_SuperMeter)
float SuperMeter;
UPROPERTY()
float MaxBurstMeter;
UPROPERTY(ReplicatedUsing=OnRep_BurstMeter)
float BurstMeter;
UPROPERTY()
float MaxBlockMeter;
UPROPERTY(Replicated)
float BlockMeter;
UPROPERTY(ReplicatedUsing=OnRep_StockCount)
int32 CurrentStockCount;
};
//StatusComponent.cpp
UStatusComponent::UStatusComponent()
{
// 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 = false;
//Initialize Status
//MaxHP=10000;
MaxHP=1000;
CurrentHP=MaxHP;
SuperMeter=0;
MaxSuperMeter=10000;
BurstMeter=0;
MaxBurstMeter=10000;
MaxBlockMeter=10000;
BlockMeter=10000;
CurrentStockCount=999;
//Set Replicate
SetIsReplicatedByDefault(true);
}
void UStatusComponent::SetCurrentHP(float InCurrentHP)
{
CurrentHP = InCurrentHP;
if (CurrentHP <= KINDA_SMALL_NUMBER)
{
CurrentHP = 0.f;
OnDeathState.Broadcast();
}
float Percentage = FMath::Clamp(CurrentHP / MaxHP,0.f,1.f);
OnCurrentHPChanged.Broadcast(Percentage);
}
void UStatusComponent::SetSuperMeter(float InSuperMeter)
{
SuperMeter = InSuperMeter;
if (SuperMeter <= KINDA_SMALL_NUMBER)
{
SuperMeter = 0.f;
}
float Percentage = FMath::Clamp(SuperMeter / MaxSuperMeter,0.f,1.f);
OnSuperMeterChanged.Broadcast(Percentage);
}
void UStatusComponent::SetBurstMeter(const float InBurstMeter)
{
BurstMeter = InBurstMeter;
if (BurstMeter <= KINDA_SMALL_NUMBER)
{
BurstMeter = 0.f;
}
float Percentage = FMath::Clamp(BurstMeter / MaxBurstMeter,0.f,1.f);
OnBurstMeterChanged.Broadcast(Percentage);
}
void UStatusComponent::SetBlockMeter(const float InBlockMeter)
{
BlockMeter = InBlockMeter;
if (BlockMeter <= KINDA_SMALL_NUMBER)
{
OnGuardCrush.Broadcast();
}
}
void UStatusComponent::AddBurstMeter(const float InAmount)
{
BurstMeter=FMath::Clamp(BurstMeter + InAmount,0.f,MaxBurstMeter);
if (BurstMeter <= KINDA_SMALL_NUMBER)
{
BurstMeter = 0.f;
}
float Percentage = FMath::Clamp(BurstMeter / MaxBurstMeter,0.f,1.f);
OnBurstMeterChanged.Broadcast(Percentage);
}
void UStatusComponent::AddBlockMeter(const float InAmount)
{
BlockMeter=FMath::Clamp(BlockMeter + InAmount,0.f,MaxBlockMeter);
if (BlockMeter <= 0.0f)
{
OnGuardCrush.Broadcast();
}
}
void UStatusComponent::SetCurrentStockCount(const int32 InCount)
{
CurrentStockCount = InCount;
OnStockCountChanged.Broadcast(CurrentStockCount);
//UE_LOG(LogTemp,Warning,TEXT("UStatusComponent::SetCurrentStockCount: %d"),InCount);
}
void UStatusComponent::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ThisClass,CurrentHP);
DOREPLIFETIME_CONDITION(ThisClass,SuperMeter,COND_OwnerOnly);
DOREPLIFETIME_CONDITION(ThisClass,BurstMeter,COND_OwnerOnly);
DOREPLIFETIME_CONDITION(ThisClass,BlockMeter,COND_OwnerOnly);
DOREPLIFETIME_CONDITION(ThisClass,CurrentStockCount,COND_OwnerOnly);
}
void UStatusComponent::OnRep_CurrentHP()
{
float Percentage = FMath::Clamp(CurrentHP / MaxHP,0.f,1.f);
//UE_LOG(LogTemp,Log,TEXT("OnRep_CurrentHP: %0.1f"),Percentage);
OnCurrentHPChanged.Broadcast(Percentage);
}
void UStatusComponent::OnRep_BurstMeter()
{
float Percentage = FMath::Clamp(BurstMeter / MaxBurstMeter,0.f,1.f);
//UE_LOG(LogTemp,Log,TEXT("OnRep_BurstMeter: %0.1f"),Percentage);
OnBurstMeterChanged.Broadcast(Percentage);
}
void UStatusComponent::OnRep_SuperMeter()
{
float Percentage = FMath::Clamp(SuperMeter / MaxSuperMeter,0.f,1.f);
//UE_LOG(LogTemp,Log,TEXT("OnRep_SuperMeter: %0.1f"),Percentage);
OnSuperMeterChanged.Broadcast(Percentage);
}
void UStatusComponent::OnRep_StockCount()
{
OnStockCountChanged.Broadcast(CurrentStockCount);
}
- 캐릭터 스테이터스를 관리하는 컴포넌트 클래스
- 서버에서 관리되어야 하는 스테이터스들을 클라에서도 접근 가능하도록 Replicated 설정
- 표기되는 스테이터스들에는 이벤트 바인딩용 델리게이트 및 변수 추가
- 스테이터스 변수 관련 Get, Set 및 Add Function 추가
- 각 스테이터스 변수들의 값이 수정되는 함수들에서는 관련 델리게이트를 Broadcast하도록 로직 구성
ㅡ> 값이 수정되면 HUD에서 해당하는 값들의 UI가 업데이트될 수 있도록 - GetLifetimeReplicatedProps에 Replicated 설정된 변수들 추가
캐릭터와 위젯 연동
void ABaseCharacter::SetHUDWidget(UUserWidget* HUDWidget)
{
if (UBaseInGameWidget* MyHUD=Cast<UBaseInGameWidget>(HUDWidget))
{
StatusComponent->OnCurrentHPChanged.AddUObject(MyHUD,&UBaseInGameWidget::UpdateHealthBar);
StatusComponent->OnSuperMeterChanged.AddUObject(MyHUD,&UBaseInGameWidget::UpdateSuperMeterBar);
StatusComponent->OnBurstMeterChanged.AddUObject(MyHUD,&UBaseInGameWidget::UpdateBurstMeterBar);
StatusComponent->OnStockCountChanged.AddUObject(MyHUD,&UBaseInGameWidget::UpdateStockCount);
//UE_LOG(LogTemp,Display,TEXT("CharacterController SetHud Call"));
MyHUD->InitializeHUDWidget(StatusComponent);
//UE_LOG(LogTemp,Display,TEXT("[SetHud] UpdateStockCount Called"));
UpdateStockCount();
if (UCCFFGameInstance* GI = Cast<UCCFFGameInstance>(GetGameInstance()))
{
MyHUD->UpdateCharacterImage(GI->GetSelectedCharacterID());
}
}
}
void ABaseCharacter::UpdateStockCount()
{
if (AArenaPlayerState* ArenaPS=Cast<AArenaPlayerState>(GetPlayerState()))
{
//UE_LOG(LogTemp,Warning,TEXT("[UpdateStockCount] SetCurrentStockCount UpdateStockCount Call (LocallyControlled: %d, HasAuthority: %d"),IsLocallyControlled(),HasAuthority());
StatusComponent->SetCurrentStockCount(ArenaPS->MaxLives);
}
else
{
//UE_LOG(LogTemp,Display,TEXT("[UpdateStockCount] PlayerState is NULL"));
}
}
void ABaseCharacter::BeginPlay()
{
if (ACharacterController* CC = Cast<ACharacterController>(GetController()))
{
//UE_LOG(LogTemp,Error,TEXT("ABaseCharacter::BeginPlay Called"));
if (CC->IsLocalController())
{
//UE_LOG(LogTemp,Error,TEXT("ABaseCharacter::BeginPlay Controller Is Valid"));
if (AArenaModeHUD* AM = Cast<AArenaModeHUD>(CC->GetHUD()))
{
//UE_LOG(LogTemp,Error,TEXT("ABaseCharacter::SetHUD Called"));
SetHUDWidget(AM->GetBaseInGameWidget());
}
}
}
}
- 본래는 캐릭터가 먼저 생성되어 있고 나중에 위젯이 생성되면 NativeConstruct가 호출되면서 SetHUDWidget 함수 호출
ㅡ> But, 현재 게임 설계 구조상 캐릭터가 위젯보다 나중에 생성될 수 있음
ㅡ> 따라서 관련 델리게이트에 함수를 바인딩하는 시점이 캐릭터가 완전히 생성된 시점 이후여야 함
ㅡ> NativeConstruct에서 호출한 SetHUDWidget 함수 호출만으로는 정상 연동되지 않음 - 캐릭터가 완전히 생성된 이후인 BeginPlay에서 HUD 위젯들의 업데이트 함수들을 StatusComponent의 델리게이트에 바인딩하여 연동
참고자료
'프로젝트 > CCFF' 카테고리의 다른 글
미친 선인장과 분노의 버섯 - #18 프로젝트 마무리 (0) | 2025.04.18 |
---|---|
미친 선인장과 분노의 버섯 - #17 콜리전 오버랩 이벤트 갱신 (0) | 2025.04.17 |
미친 선인장과 분노의 버섯 - #15 버퍼를 이용한 선입력(예약) & 키 조합을 통한 액션 및 상태에 따른 동작 다변화 (0) | 2025.04.15 |
미친 선인장과 분노의 버섯 - #14 더블 클릭을 통한 달리기 (0) | 2025.04.14 |
미친 선인장과 분노의 버섯 - #13 카메라 회전 트러블 슈팅 + 클라이언트 UI 상태 변화에 따른 업데이트 (0) | 2025.04.11 |