UniversalBars plugin: PEAK-style universal UMG bars
Procedural M_UI_UniversalBar material (SDF rounded-rect, fill/delayed/shield/ penalty-zones/stripes/segments/flash/pulse), 6 preset instances, animated UUniversalBarWidget + UUniversalHUDWidget + global UUniversalBarsSubsystem API, UUniversalBarsTheme DataAsset, and a ready WBP_HUD_InGame with zone-tracking status chips. See README.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
345
Source/UniversalBars/Private/UniversalBarWidget.cpp
Normal file
345
Source/UniversalBars/Private/UniversalBarWidget.cpp
Normal file
@ -0,0 +1,345 @@
|
||||
// Copyright IHY. Universal Bars plugin.
|
||||
#include "UniversalBarWidget.h"
|
||||
#include "Components/Image.h"
|
||||
#include "Materials/MaterialInstanceDynamic.h"
|
||||
|
||||
UUniversalBarWidget::UUniversalBarWidget(const FObjectInitializer& ObjectInitializer)
|
||||
: Super(ObjectInitializer)
|
||||
{
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
EnsureMID();
|
||||
if (BarMID)
|
||||
{
|
||||
BarMID->SetScalarParameterValue(TEXT("FillPercent"), DisplayFill);
|
||||
BarMID->SetScalarParameterValue(TEXT("DelayedFillPercent"), DelayedFill);
|
||||
}
|
||||
PushZones(true);
|
||||
}
|
||||
|
||||
UMaterialInstanceDynamic* UUniversalBarWidget::EnsureMID()
|
||||
{
|
||||
if (BarMID || !BarImage)
|
||||
{
|
||||
return BarMID;
|
||||
}
|
||||
BarMID = BarImage->GetDynamicMaterial();
|
||||
return BarMID;
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::SetFillPercent(float NewValue, bool bAnimate)
|
||||
{
|
||||
EnsureMID();
|
||||
NewValue = FMath::Clamp(NewValue, 0.f, 1.f);
|
||||
|
||||
if (!bAnimate)
|
||||
{
|
||||
TargetFill = DisplayFill = DelayedFill = NewValue;
|
||||
if (BarMID)
|
||||
{
|
||||
BarMID->SetScalarParameterValue(TEXT("FillPercent"), DisplayFill);
|
||||
BarMID->SetScalarParameterValue(TEXT("DelayedFillPercent"), DelayedFill);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bool bDown = NewValue < TargetFill - KINDA_SMALL_NUMBER;
|
||||
const bool bUp = NewValue > TargetFill + KINDA_SMALL_NUMBER;
|
||||
TargetFill = NewValue;
|
||||
|
||||
if (bDown)
|
||||
{
|
||||
// instant bright drop; the lighter tail holds, then drains (damage lag)
|
||||
DisplayFill = NewValue;
|
||||
DelayTimer = DelayedDelay;
|
||||
if (BarMID)
|
||||
{
|
||||
BarMID->SetScalarParameterValue(TEXT("FillPercent"), DisplayFill);
|
||||
}
|
||||
if (bAutoFlashOnChange)
|
||||
{
|
||||
PlayDamageFlash();
|
||||
}
|
||||
}
|
||||
else if (bUp)
|
||||
{
|
||||
// show the incoming amount immediately as the lighter band; bright fill eases up into it
|
||||
DelayedFill = FMath::Max(DelayedFill, NewValue);
|
||||
if (BarMID)
|
||||
{
|
||||
BarMID->SetScalarParameterValue(TEXT("DelayedFillPercent"), DelayedFill);
|
||||
}
|
||||
if (bAutoFlashOnChange)
|
||||
{
|
||||
PlayHealFlash();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::SetShieldPercent(float NewValue)
|
||||
{
|
||||
EnsureMID();
|
||||
if (BarMID)
|
||||
{
|
||||
BarMID->SetScalarParameterValue(TEXT("ShieldPercent"), FMath::Clamp(NewValue, 0.f, 1.f));
|
||||
}
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::PlayDamageFlash()
|
||||
{
|
||||
PlayFlash(DamageFlashColor);
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::PlayHealFlash()
|
||||
{
|
||||
PlayFlash(HealFlashColor);
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::PlayFlash(FLinearColor Color)
|
||||
{
|
||||
EnsureMID();
|
||||
PendingFlashColor = Color;
|
||||
if (BarMID)
|
||||
{
|
||||
BarMID->SetVectorParameterValue(TEXT("FlashColor"), Color);
|
||||
}
|
||||
FlashAmount = 1.f;
|
||||
bFlashing = true;
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::SetBarColors(FLinearColor Fill, FLinearColor Empty, FLinearColor Border, FLinearColor Delayed)
|
||||
{
|
||||
EnsureMID();
|
||||
if (!BarMID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BarMID->SetVectorParameterValue(TEXT("FillColor"), Fill);
|
||||
BarMID->SetVectorParameterValue(TEXT("EmptyColor"), Empty);
|
||||
BarMID->SetVectorParameterValue(TEXT("BorderColor"), Border);
|
||||
BarMID->SetVectorParameterValue(TEXT("DelayedFillColor"), Delayed);
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::SetSegmentCount(int32 Count)
|
||||
{
|
||||
EnsureMID();
|
||||
if (!BarMID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
BarMID->SetScalarParameterValue(TEXT("SegmentCount"), static_cast<float>(FMath::Max(0, Count)));
|
||||
BarMID->SetScalarParameterValue(TEXT("SegmentDividerOpacity"), Count > 0 ? 0.85f : 0.f);
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::SetPenaltyZone(FName Id, float Width, FLinearColor Color)
|
||||
{
|
||||
Width = FMath::Clamp(Width, 0.f, 1.f);
|
||||
for (FBarZone& Zone : PenaltyZones)
|
||||
{
|
||||
if (Zone.Id == Id)
|
||||
{
|
||||
Zone.Width = Width;
|
||||
Zone.Color = Color;
|
||||
return; // tick eases DisplayWidth toward the new Width
|
||||
}
|
||||
}
|
||||
FBarZone& NewZone = PenaltyZones[PenaltyZones.AddDefaulted()];
|
||||
NewZone.Id = Id;
|
||||
NewZone.Width = Width;
|
||||
NewZone.Color = Color;
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::SetPenaltyZoneWidth(FName Id, float Width)
|
||||
{
|
||||
Width = FMath::Clamp(Width, 0.f, 1.f);
|
||||
for (FBarZone& Zone : PenaltyZones)
|
||||
{
|
||||
if (Zone.Id == Id)
|
||||
{
|
||||
Zone.Width = Width;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::RemovePenaltyZone(FName Id)
|
||||
{
|
||||
// Ease to zero, then drop on next tick when fully collapsed.
|
||||
for (FBarZone& Zone : PenaltyZones)
|
||||
{
|
||||
if (Zone.Id == Id)
|
||||
{
|
||||
Zone.Width = 0.f;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::ClearPenaltyZones()
|
||||
{
|
||||
for (FBarZone& Zone : PenaltyZones)
|
||||
{
|
||||
Zone.Width = 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::PushZones(bool bImmediate)
|
||||
{
|
||||
EnsureMID();
|
||||
if (!BarMID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const int32 Count = FMath::Min(PenaltyZones.Num(), MaxMaterialZones);
|
||||
float Total = 0.f;
|
||||
for (int32 i = 0; i < Count; ++i)
|
||||
{
|
||||
if (bImmediate)
|
||||
{
|
||||
PenaltyZones[i].DisplayWidth = PenaltyZones[i].Width;
|
||||
}
|
||||
Total += FMath::Clamp(PenaltyZones[i].DisplayWidth, 0.f, 1.f);
|
||||
}
|
||||
Total = FMath::Clamp(Total, 0.f, 1.f);
|
||||
|
||||
const float MaxFill = 1.f - Total;
|
||||
BarMID->SetScalarParameterValue(TEXT("MaxFillPercent"), MaxFill);
|
||||
BarMID->SetScalarParameterValue(TEXT("PenaltyCount"), static_cast<float>(Count));
|
||||
|
||||
float Bound = MaxFill;
|
||||
static const TCHAR* EndNames[MaxMaterialZones] = { TEXT("PenaltyEnd1"), TEXT("PenaltyEnd2"), TEXT("PenaltyEnd3"), TEXT("PenaltyEnd4") };
|
||||
static const TCHAR* ColorNames[MaxMaterialZones] = { TEXT("PenaltyColor1"), TEXT("PenaltyColor2"), TEXT("PenaltyColor3"), TEXT("PenaltyColor4") };
|
||||
for (int32 i = 0; i < MaxMaterialZones; ++i)
|
||||
{
|
||||
if (i < Count)
|
||||
{
|
||||
Bound = FMath::Min(1.f, Bound + FMath::Clamp(PenaltyZones[i].DisplayWidth, 0.f, 1.f));
|
||||
BarMID->SetScalarParameterValue(EndNames[i], Bound);
|
||||
BarMID->SetVectorParameterValue(ColorNames[i], PenaltyZones[i].Color);
|
||||
}
|
||||
else
|
||||
{
|
||||
BarMID->SetScalarParameterValue(EndNames[i], 1.f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float UUniversalBarWidget::GetUsableEnd() const
|
||||
{
|
||||
float Total = 0.f;
|
||||
const int32 N = FMath::Min(PenaltyZones.Num(), MaxMaterialZones);
|
||||
for (int32 i = 0; i < N; ++i)
|
||||
{
|
||||
Total += FMath::Clamp(PenaltyZones[i].DisplayWidth, 0.f, 1.f);
|
||||
}
|
||||
return FMath::Clamp(1.f - Total, 0.f, 1.f);
|
||||
}
|
||||
|
||||
float UUniversalBarWidget::GetZoneCenter(FName Id) const
|
||||
{
|
||||
float Edge = GetUsableEnd();
|
||||
const int32 N = FMath::Min(PenaltyZones.Num(), MaxMaterialZones);
|
||||
for (int32 i = 0; i < N; ++i)
|
||||
{
|
||||
const float W = FMath::Clamp(PenaltyZones[i].DisplayWidth, 0.f, 1.f);
|
||||
if (PenaltyZones[i].Id == Id)
|
||||
{
|
||||
return Edge + W * 0.5f;
|
||||
}
|
||||
Edge += W;
|
||||
}
|
||||
return -1.f;
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::SetScalar(FName Param, float Value)
|
||||
{
|
||||
EnsureMID();
|
||||
if (BarMID) { BarMID->SetScalarParameterValue(Param, Value); }
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::SetVector(FName Param, FLinearColor Value)
|
||||
{
|
||||
EnsureMID();
|
||||
if (BarMID) { BarMID->SetVectorParameterValue(Param, Value); }
|
||||
}
|
||||
|
||||
void UUniversalBarWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||
{
|
||||
Super::NativeTick(MyGeometry, InDeltaTime);
|
||||
if (!BarMID)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// bright fill eases toward target (smooth rise on heal; already snapped on damage)
|
||||
if (!FMath::IsNearlyEqual(DisplayFill, TargetFill, 1e-3f))
|
||||
{
|
||||
DisplayFill = FMath::FInterpTo(DisplayFill, TargetFill, InDeltaTime, FillRiseSpeed);
|
||||
BarMID->SetScalarParameterValue(TEXT("FillPercent"), DisplayFill);
|
||||
}
|
||||
|
||||
// lighter "lag" tail: holds, then drains toward target (damage). Never below the bright fill.
|
||||
const float DelayedTarget = FMath::Max(TargetFill, DisplayFill);
|
||||
if (!FMath::IsNearlyEqual(DelayedFill, DelayedTarget, 1e-3f))
|
||||
{
|
||||
if (DelayTimer > 0.f)
|
||||
{
|
||||
DelayTimer -= InDeltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
DelayedFill = FMath::FInterpTo(DelayedFill, DelayedTarget, InDeltaTime, DelayedFallSpeed);
|
||||
BarMID->SetScalarParameterValue(TEXT("DelayedFillPercent"), DelayedFill);
|
||||
}
|
||||
}
|
||||
|
||||
// flash decay
|
||||
if (bFlashing)
|
||||
{
|
||||
FlashAmount = FMath::FInterpTo(FlashAmount, 0.f, InDeltaTime, FlashSpeed);
|
||||
BarMID->SetScalarParameterValue(TEXT("FlashAmount"), FlashAmount);
|
||||
if (FlashAmount < 0.01f)
|
||||
{
|
||||
FlashAmount = 0.f;
|
||||
bFlashing = false;
|
||||
BarMID->SetScalarParameterValue(TEXT("FlashAmount"), 0.f);
|
||||
}
|
||||
}
|
||||
|
||||
// low-value pulse
|
||||
const float PulseTarget = (TargetFill < LowThreshold) ? 1.f : 0.f;
|
||||
if (!FMath::IsNearlyEqual(PulseAmount, PulseTarget, 1e-3f))
|
||||
{
|
||||
PulseAmount = FMath::FInterpTo(PulseAmount, PulseTarget, InDeltaTime, 6.f);
|
||||
BarMID->SetScalarParameterValue(TEXT("LowHPPulseAmount"), PulseAmount);
|
||||
}
|
||||
|
||||
// penalty zone widths ease toward target; drop fully-collapsed removed zones
|
||||
bool bZonesDirty = false;
|
||||
bool bNeedCompact = false;
|
||||
for (FBarZone& Zone : PenaltyZones)
|
||||
{
|
||||
if (!FMath::IsNearlyEqual(Zone.DisplayWidth, Zone.Width, 1e-4f))
|
||||
{
|
||||
Zone.DisplayWidth = FMath::FInterpTo(Zone.DisplayWidth, Zone.Width, InDeltaTime, ZoneInterpSpeed);
|
||||
bZonesDirty = true;
|
||||
}
|
||||
if (Zone.Width <= 0.f && Zone.DisplayWidth <= 1e-3f)
|
||||
{
|
||||
bNeedCompact = true;
|
||||
}
|
||||
}
|
||||
if (bZonesDirty)
|
||||
{
|
||||
PushZones(false);
|
||||
}
|
||||
if (bNeedCompact)
|
||||
{
|
||||
PenaltyZones.RemoveAll([](const FBarZone& Z) { return Z.Width <= 0.f && Z.DisplayWidth <= 1e-3f; });
|
||||
PushZones(false);
|
||||
}
|
||||
}
|
||||
4
Source/UniversalBars/Private/UniversalBarsModule.cpp
Normal file
4
Source/UniversalBars/Private/UniversalBarsModule.cpp
Normal file
@ -0,0 +1,4 @@
|
||||
// Copyright IHY. Universal Bars plugin.
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_MODULE(FDefaultModuleImpl, UniversalBars);
|
||||
59
Source/UniversalBars/Private/UniversalBarsSubsystem.cpp
Normal file
59
Source/UniversalBars/Private/UniversalBarsSubsystem.cpp
Normal file
@ -0,0 +1,59 @@
|
||||
// Copyright IHY. Universal Bars plugin.
|
||||
#include "UniversalBarsSubsystem.h"
|
||||
#include "UniversalHUDWidget.h"
|
||||
#include "Blueprint/UserWidget.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
|
||||
UUniversalBarsSubsystem* UUniversalBarsSubsystem::Get(const UObject* WorldContextObject)
|
||||
{
|
||||
if (!WorldContextObject)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
if (const UWorld* World = WorldContextObject->GetWorld())
|
||||
{
|
||||
return World->GetSubsystem<UUniversalBarsSubsystem>();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
UUniversalHUDWidget* UUniversalBarsSubsystem::CreateHUD(APlayerController* Owner, TSubclassOf<UUniversalHUDWidget> HUDClass, int32 ZOrder)
|
||||
{
|
||||
if (!HUDClass)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
UUniversalHUDWidget* Widget = CreateWidget<UUniversalHUDWidget>(Owner ? Cast<APlayerController>(Owner) : GetWorld()->GetFirstPlayerController(), HUDClass);
|
||||
if (Widget)
|
||||
{
|
||||
Widget->AddToViewport(ZOrder);
|
||||
RegisterHUD(Widget); // also registered in NativeConstruct; idempotent
|
||||
}
|
||||
return Widget;
|
||||
}
|
||||
|
||||
void UUniversalBarsSubsystem::RegisterHUD(UUniversalHUDWidget* InHUD)
|
||||
{
|
||||
if (InHUD)
|
||||
{
|
||||
HUD = InHUD;
|
||||
}
|
||||
}
|
||||
|
||||
void UUniversalBarsSubsystem::UnregisterHUD(UUniversalHUDWidget* InHUD)
|
||||
{
|
||||
if (HUD.Get() == InHUD)
|
||||
{
|
||||
HUD.Reset();
|
||||
}
|
||||
}
|
||||
|
||||
void UUniversalBarsSubsystem::SetHealth(float Percent) { if (UUniversalHUDWidget* H = HUD.Get()) { H->SetHealth(Percent); } }
|
||||
void UUniversalBarsSubsystem::SetStamina(float Percent) { if (UUniversalHUDWidget* H = HUD.Get()) { H->SetStamina(Percent); } }
|
||||
void UUniversalBarsSubsystem::SetShield(float Percent) { if (UUniversalHUDWidget* H = HUD.Get()) { H->SetShield(Percent); } }
|
||||
void UUniversalBarsSubsystem::SetHunger(float Amount) { if (UUniversalHUDWidget* H = HUD.Get()) { H->SetHunger(Amount); } }
|
||||
void UUniversalBarsSubsystem::SetWeight(float Amount) { if (UUniversalHUDWidget* H = HUD.Get()) { H->SetWeight(Amount); } }
|
||||
void UUniversalBarsSubsystem::SetCold(float Amount) { if (UUniversalHUDWidget* H = HUD.Get()) { H->SetCold(Amount); } }
|
||||
void UUniversalBarsSubsystem::DamageHealth(float Delta) { if (UUniversalHUDWidget* H = HUD.Get()) { H->DamageHealth(Delta); } }
|
||||
void UUniversalBarsSubsystem::HealHealth(float Delta) { if (UUniversalHUDWidget* H = HUD.Get()) { H->HealHealth(Delta); } }
|
||||
217
Source/UniversalBars/Private/UniversalHUDWidget.cpp
Normal file
217
Source/UniversalBars/Private/UniversalHUDWidget.cpp
Normal file
@ -0,0 +1,217 @@
|
||||
// Copyright IHY. Universal Bars plugin.
|
||||
#include "UniversalHUDWidget.h"
|
||||
#include "UniversalBarWidget.h"
|
||||
#include "UniversalBarsSubsystem.h"
|
||||
#include "UniversalBarsTheme.h"
|
||||
#include "Components/Image.h"
|
||||
#include "Components/PanelWidget.h"
|
||||
#include "Components/CanvasPanel.h"
|
||||
#include "Components/CanvasPanelSlot.h"
|
||||
|
||||
void UUniversalHUDWidget::NativeConstruct()
|
||||
{
|
||||
Super::NativeConstruct();
|
||||
ApplyTheme();
|
||||
if (UUniversalBarsSubsystem* Sub = UUniversalBarsSubsystem::Get(this))
|
||||
{
|
||||
Sub->RegisterHUD(this);
|
||||
}
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::ApplyTheme()
|
||||
{
|
||||
if (!Theme)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HungerColor = Theme->Hunger;
|
||||
WeightColor = Theme->Weight;
|
||||
ColdColor = Theme->Cold;
|
||||
|
||||
auto StyleBar = [this](UUniversalBarWidget* Bar, FLinearColor Fill)
|
||||
{
|
||||
if (!Bar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Bar->SetBarColors(Fill, Theme->Empty, Theme->Border, Theme->Delayed);
|
||||
Bar->SetVector(TEXT("ShieldColor"), Theme->Shield);
|
||||
Bar->DamageFlashColor = Theme->DamageFlash;
|
||||
Bar->HealFlashColor = Theme->HealFlash;
|
||||
Bar->SetScalar(TEXT("CornerRadius"), Theme->CornerRadius);
|
||||
Bar->SetScalar(TEXT("BorderSize"), Theme->BorderSize);
|
||||
Bar->SetScalar(TEXT("InnerPadding"), Theme->InnerPadding);
|
||||
};
|
||||
StyleBar(HealthBar, Theme->HealthFill);
|
||||
StyleBar(StaminaBar, Theme->StaminaFill);
|
||||
|
||||
if (ChipsPanel && Theme->ChipColors.Num() > 0)
|
||||
{
|
||||
const int32 N = ChipsPanel->GetChildrenCount();
|
||||
for (int32 i = 0; i < N; ++i)
|
||||
{
|
||||
if (UImage* Img = Cast<UImage>(ChipsPanel->GetChildAt(i)))
|
||||
{
|
||||
Img->SetBrushTintColor(FSlateColor(Theme->ChipColors[i % Theme->ChipColors.Num()]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tracking chips: tint by what they represent.
|
||||
if (ChipHeart) { ChipHeart->SetBrushTintColor(FSlateColor(Theme->HealthFill)); }
|
||||
if (ChipHunger) { ChipHunger->SetBrushTintColor(FSlateColor(Theme->Hunger)); }
|
||||
if (ChipWeight) { ChipWeight->SetBrushTintColor(FSlateColor(Theme->Weight)); }
|
||||
if (ChipCold) { ChipCold->SetBrushTintColor(FSlateColor(Theme->Cold)); }
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::NativeDestruct()
|
||||
{
|
||||
if (UUniversalBarsSubsystem* Sub = UUniversalBarsSubsystem::Get(this))
|
||||
{
|
||||
Sub->UnregisterHUD(this);
|
||||
}
|
||||
Super::NativeDestruct();
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::SetHealth(float Percent)
|
||||
{
|
||||
if (HealthBar) { HealthBar->SetFillPercent(Percent, true); }
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::SetStamina(float Percent)
|
||||
{
|
||||
if (StaminaBar) { StaminaBar->SetFillPercent(Percent, true); }
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::SetShield(float Percent)
|
||||
{
|
||||
if (HealthBar) { HealthBar->SetShieldPercent(Percent); }
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::SetHunger(float Amount)
|
||||
{
|
||||
if (HealthBar) { HealthBar->SetPenaltyZone(TEXT("Hunger"), Amount, HungerColor); }
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::SetWeight(float Amount)
|
||||
{
|
||||
if (HealthBar) { HealthBar->SetPenaltyZone(TEXT("Weight"), Amount, WeightColor); }
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::SetCold(float Amount)
|
||||
{
|
||||
if (HealthBar) { HealthBar->SetPenaltyZone(TEXT("Cold"), Amount, ColdColor); }
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::DamageHealth(float Delta)
|
||||
{
|
||||
if (HealthBar) { HealthBar->SetFillPercent(HealthBar->GetFillPercent() - FMath::Abs(Delta), true); }
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::HealHealth(float Delta)
|
||||
{
|
||||
if (HealthBar) { HealthBar->SetFillPercent(HealthBar->GetFillPercent() + FMath::Abs(Delta), true); }
|
||||
}
|
||||
|
||||
float UUniversalHUDWidget::GetHealth() const
|
||||
{
|
||||
return HealthBar ? HealthBar->GetFillPercent() : 0.f;
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::SetDemoMode(bool bEnable)
|
||||
{
|
||||
bDemoMode = bEnable;
|
||||
bDemoInit = false;
|
||||
DemoTimer = 0.f;
|
||||
DemoStep = 0;
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::DemoStepNow()
|
||||
{
|
||||
// Each step shows a different animation; between steps the bars ease on their own.
|
||||
switch (DemoStep % 10)
|
||||
{
|
||||
case 0: DamageHealth(0.25f); break; // red flash + lag tail
|
||||
case 1: SetStamina(0.30f); break; // stamina drop
|
||||
case 2: HealHealth(0.20f); break; // green flash + smooth rise
|
||||
case 3: SetHunger(0.18f); break; // hunger zone grows in
|
||||
case 4: SetWeight(0.14f); break; // weight zone grows in
|
||||
case 5: SetStamina(0.95f); break; // stamina refill
|
||||
case 6: HealHealth(0.30f); break;
|
||||
case 7: SetHunger(0.0f); break; // hunger zone retracts
|
||||
case 8: SetWeight(0.0f); break; // weight zone retracts
|
||||
case 9: DamageHealth(0.45f); break; // big hit -> low-HP pulse
|
||||
}
|
||||
++DemoStep;
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::PositionChips()
|
||||
{
|
||||
if (!HealthBar)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Width of the bar in local pixels — ChipCanvas is overlaid on HealthBar so
|
||||
// the same width maps zone-U (0..1) straight to chip X.
|
||||
const float W = HealthBar->GetCachedGeometry().GetLocalSize().X;
|
||||
if (W <= 1.f)
|
||||
{
|
||||
return; // not laid out yet this frame
|
||||
}
|
||||
|
||||
auto Place = [W](UImage* Chip, float U)
|
||||
{
|
||||
if (!Chip)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (U < 0.f)
|
||||
{
|
||||
Chip->SetVisibility(ESlateVisibility::Collapsed); // zone absent
|
||||
return;
|
||||
}
|
||||
Chip->SetVisibility(ESlateVisibility::HitTestInvisible);
|
||||
if (UCanvasPanelSlot* CSlot = Cast<UCanvasPanelSlot>(Chip->Slot))
|
||||
{
|
||||
const float ChipW = CSlot->GetSize().X;
|
||||
// centre over the zone, sitting just above the bar (negative Y)
|
||||
CSlot->SetPosition(FVector2D(U * W - ChipW * 0.5f, -ChipW - 6.f));
|
||||
}
|
||||
};
|
||||
|
||||
Place(ChipHeart, HealthBar->GetUsableEnd());
|
||||
Place(ChipHunger, HealthBar->GetZoneCenter(TEXT("Hunger")));
|
||||
Place(ChipWeight, HealthBar->GetZoneCenter(TEXT("Weight")));
|
||||
Place(ChipCold, HealthBar->GetZoneCenter(TEXT("Cold")));
|
||||
}
|
||||
|
||||
void UUniversalHUDWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime)
|
||||
{
|
||||
Super::NativeTick(MyGeometry, InDeltaTime);
|
||||
|
||||
PositionChips(); // always track the bar, demo or not
|
||||
|
||||
if (!bDemoMode)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bDemoInit)
|
||||
{
|
||||
bDemoInit = true;
|
||||
SetHealth(1.f);
|
||||
SetStamina(1.f);
|
||||
SetHunger(0.f);
|
||||
SetWeight(0.f);
|
||||
DemoTimer = 0.f;
|
||||
DemoStep = 0;
|
||||
}
|
||||
|
||||
DemoTimer += InDeltaTime;
|
||||
if (DemoTimer >= DemoStepInterval)
|
||||
{
|
||||
DemoTimer = 0.f;
|
||||
DemoStepNow();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user