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:
Bonchellon
2026-06-23 19:04:50 +03:00
commit 41a0cdef36
24 changed files with 1262 additions and 0 deletions

View 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);
}
}

View File

@ -0,0 +1,4 @@
// Copyright IHY. Universal Bars plugin.
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE(FDefaultModuleImpl, UniversalBars);

View 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); } }

View 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();
}
}

View File

@ -0,0 +1,157 @@
// Copyright IHY. Universal Bars plugin.
// One animated UMG bar driven by M_UI_UniversalBar. Handles a single logical
// value (HP, stamina, durability, ...) plus optional data-driven penalty zones
// for combined PEAK-style capacity bars (hunger / weight / cold eating into max).
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "UniversalBarWidget.generated.h"
class UImage;
class UMaterialInstanceDynamic;
// A penalty / reserve zone consuming capacity from the right end of the bar.
USTRUCT(BlueprintType)
struct FBarZone
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Zone")
FName Id = NAME_None;
// Target width as a fraction of the full track (0..1).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Zone", meta=(ClampMin="0", ClampMax="1"))
float Width = 0.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Zone")
FLinearColor Color = FLinearColor(0.9f, 0.6f, 0.2f, 1.f);
// Animated width that eases toward Width at runtime (not authored).
float DisplayWidth = 0.f;
};
UCLASS()
class UNIVERSALBARS_API UUniversalBarWidget : public UUserWidget
{
GENERATED_BODY()
public:
UUniversalBarWidget(const FObjectInitializer& ObjectInitializer);
// Name this "BarImage" in the WBP; assign an MI_UI_Bar_* (or the master) to its Brush.
UPROPERTY(meta=(BindWidget))
TObjectPtr<UImage> BarImage = nullptr;
// ---- animation tuning ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Anim")
float FillRiseSpeed = 7.f; // how fast the bright fill grows on increase/heal
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Anim")
float DelayedFallSpeed = 4.f; // how fast the damage-lag tail drains on decrease
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Anim")
float DelayedDelay = 0.25f; // pause before the tail starts draining (sec)
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Anim")
float FlashSpeed = 6.f; // flash decay rate
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Anim")
float ZoneInterpSpeed = 8.f; // penalty-zone width easing
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Anim")
float LowThreshold = 0.25f; // below this, low-value pulse turns on
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Anim")
bool bAutoFlashOnChange = true; // SetFillPercent auto-plays damage/heal flash
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Colors")
FLinearColor DamageFlashColor = FLinearColor(1.f, 0.12f, 0.12f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Colors")
FLinearColor HealFlashColor = FLinearColor(0.6f, 1.f, 0.6f, 1.f);
// ---- data-driven penalty zones (combined / PEAK mode) ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Bar|Zones")
TArray<FBarZone> PenaltyZones;
// ===== public API =====
// Animated set of the bar's main value (0..1). Decrease => instant drop + lag
// tail (+ damage flash). Increase => smooth rise (+ heal flash).
UFUNCTION(BlueprintCallable, Category="Bar")
void SetFillPercent(float NewValue, bool bAnimate = true);
UFUNCTION(BlueprintCallable, Category="Bar")
void SetShieldPercent(float NewValue);
UFUNCTION(BlueprintCallable, Category="Bar")
void PlayDamageFlash();
UFUNCTION(BlueprintCallable, Category="Bar")
void PlayHealFlash();
UFUNCTION(BlueprintCallable, Category="Bar")
void PlayFlash(FLinearColor Color);
UFUNCTION(BlueprintCallable, Category="Bar")
void SetBarColors(FLinearColor Fill, FLinearColor Empty, FLinearColor Border, FLinearColor Delayed);
UFUNCTION(BlueprintCallable, Category="Bar")
void SetSegmentCount(int32 Count);
// ---- penalty zone management (animated) ----
UFUNCTION(BlueprintCallable, Category="Bar|Zones")
void SetPenaltyZone(FName Id, float Width, FLinearColor Color);
UFUNCTION(BlueprintCallable, Category="Bar|Zones")
void SetPenaltyZoneWidth(FName Id, float Width);
UFUNCTION(BlueprintCallable, Category="Bar|Zones")
void RemovePenaltyZone(FName Id);
UFUNCTION(BlueprintCallable, Category="Bar|Zones")
void ClearPenaltyZones();
// Generic escape hatches.
UFUNCTION(BlueprintCallable, Category="Bar")
void SetScalar(FName Param, float Value);
UFUNCTION(BlueprintCallable, Category="Bar")
void SetVector(FName Param, FLinearColor Value);
UFUNCTION(BlueprintPure, Category="Bar")
float GetFillPercent() const { return TargetFill; }
// ---- zone layout in bar-U space (0..1), using current animated widths ----
// Right edge of the usable region (1 - sum of penalty display widths).
UFUNCTION(BlueprintPure, Category="Bar|Zones")
float GetUsableEnd() const;
// U-coordinate of a penalty zone's centre; -1 if the zone is absent.
UFUNCTION(BlueprintPure, Category="Bar|Zones")
float GetZoneCenter(FName Id) const;
protected:
virtual void NativeConstruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
UMaterialInstanceDynamic* EnsureMID();
void PushZones(bool bImmediate);
private:
UPROPERTY(Transient)
TObjectPtr<UMaterialInstanceDynamic> BarMID = nullptr;
float TargetFill = 1.f;
float DisplayFill = 1.f;
float DelayedFill = 1.f;
float DelayTimer = 0.f;
float FlashAmount = 0.f;
bool bFlashing = false;
FLinearColor PendingFlashColor = FLinearColor::Red;
float PulseAmount = 0.f;
static constexpr int32 MaxMaterialZones = 4;
};

View File

@ -0,0 +1,50 @@
// Copyright IHY. Universal Bars plugin.
// Global access point for the active HUD. From any Blueprint/C++:
// Get World Subsystem -> UniversalBarsSubsystem -> Set Health (0.5)
// No widget reference needed. The HUD widget registers itself on construct.
#pragma once
#include "CoreMinimal.h"
#include "Subsystems/WorldSubsystem.h"
#include "UniversalBarsSubsystem.generated.h"
class UUniversalHUDWidget;
class APlayerController;
UCLASS()
class UNIVERSALBARS_API UUniversalBarsSubsystem : public UWorldSubsystem
{
GENERATED_BODY()
public:
// One-call accessor from any object with a world.
UFUNCTION(BlueprintPure, Category="UniversalBars", meta=(WorldContext="WorldContextObject"))
static UUniversalBarsSubsystem* Get(const UObject* WorldContextObject);
// Spawn a HUD widget, add it to the viewport, and make it the active HUD.
UFUNCTION(BlueprintCallable, Category="UniversalBars")
UUniversalHUDWidget* CreateHUD(APlayerController* Owner, TSubclassOf<UUniversalHUDWidget> HUDClass, int32 ZOrder = 0);
UFUNCTION(BlueprintCallable, Category="UniversalBars")
void RegisterHUD(UUniversalHUDWidget* InHUD);
UFUNCTION(BlueprintCallable, Category="UniversalBars")
void UnregisterHUD(UUniversalHUDWidget* InHUD);
UFUNCTION(BlueprintPure, Category="UniversalBars")
UUniversalHUDWidget* GetHUD() const { return HUD.Get(); }
// ===== global forwarders (animated) =====
UFUNCTION(BlueprintCallable, Category="UniversalBars") void SetHealth(float Percent);
UFUNCTION(BlueprintCallable, Category="UniversalBars") void SetStamina(float Percent);
UFUNCTION(BlueprintCallable, Category="UniversalBars") void SetShield(float Percent);
UFUNCTION(BlueprintCallable, Category="UniversalBars") void SetHunger(float Amount);
UFUNCTION(BlueprintCallable, Category="UniversalBars") void SetWeight(float Amount);
UFUNCTION(BlueprintCallable, Category="UniversalBars") void SetCold(float Amount);
UFUNCTION(BlueprintCallable, Category="UniversalBars") void DamageHealth(float Delta);
UFUNCTION(BlueprintCallable, Category="UniversalBars") void HealHealth(float Delta);
private:
UPROPERTY()
TWeakObjectPtr<UUniversalHUDWidget> HUD;
};

View File

@ -0,0 +1,36 @@
// Copyright IHY. Universal Bars plugin.
// Central palette / geometry tokens for the HUD. One DataAsset to retint every
// bar and chip without touching widgets. Applied by UUniversalHUDWidget on construct.
#pragma once
#include "CoreMinimal.h"
#include "Engine/DataAsset.h"
#include "UniversalBarsTheme.generated.h"
UCLASS(BlueprintType)
class UNIVERSALBARS_API UUniversalBarsTheme : public UPrimaryDataAsset
{
GENERATED_BODY()
public:
// ---- palette ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor HealthFill = FLinearColor(0.30f, 0.85f, 0.20f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor StaminaFill = FLinearColor(0.85f, 0.95f, 0.30f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor Hunger = FLinearColor(0.90f, 0.60f, 0.20f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor Weight = FLinearColor(0.82f, 0.23f, 0.23f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor Cold = FLinearColor(0.40f, 0.70f, 1.00f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor Shield = FLinearColor(0.65f, 0.85f, 1.00f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor Border = FLinearColor(1.00f, 1.00f, 1.00f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor Empty = FLinearColor(0.05f, 0.07f, 0.05f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor Delayed = FLinearColor(1.00f, 0.95f, 0.85f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor DamageFlash = FLinearColor(1.00f, 0.12f, 0.12f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Palette") FLinearColor HealFlash = FLinearColor(0.60f, 1.00f, 0.60f, 1.f);
// ---- geometry (PEAK = thick white border, soft rounding) ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Geometry", meta=(ClampMin="0", ClampMax="1")) float CornerRadius = 0.60f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Geometry", meta=(ClampMin="0", ClampMax="0.25")) float BorderSize = 0.10f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Geometry", meta=(ClampMin="0", ClampMax="0.2")) float InnerPadding = 0.06f;
// ---- status chips ----
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Chips") TArray<FLinearColor> ChipColors;
};

View File

@ -0,0 +1,115 @@
// Copyright IHY. Universal Bars plugin.
// HUD container that stacks a combined health bar (HP + hunger/weight/cold
// penalty zones) and a stamina bar. Exposes high-level, animated stat setters
// and registers itself with UUniversalBarsSubsystem for global access.
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "UniversalHUDWidget.generated.h"
class UUniversalBarWidget;
class UPanelWidget;
class UCanvasPanel;
class UImage;
class UUniversalBarsTheme;
UCLASS()
class UNIVERSALBARS_API UUniversalHUDWidget : public UUserWidget
{
GENERATED_BODY()
public:
// Place a WBP_UniversalBar named "HealthBar" / "StaminaBar" in the HUD WBP.
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UUniversalBarWidget> HealthBar = nullptr;
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UUniversalBarWidget> StaminaBar = nullptr;
// Optional container for status chips (filled in the designer with placeholders).
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UPanelWidget> ChipsPanel = nullptr;
// PEAK-style markers that sit ABOVE the health bar, over their zone. Place
// these inside a CanvasPanel ("ChipCanvas") overlaid on HealthBar; they are
// moved every tick to track the (animated) zone boundaries.
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UCanvasPanel> ChipCanvas = nullptr;
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UImage> ChipHeart = nullptr; // tracks usable-HP end
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UImage> ChipHunger = nullptr; // tracks "Hunger" zone
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UImage> ChipWeight = nullptr; // tracks "Weight" zone
UPROPERTY(meta=(BindWidgetOptional))
TObjectPtr<UImage> ChipCold = nullptr; // tracks "Cold" zone (hidden if absent)
// Optional central palette/geometry; applied on construct when set.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HUD")
TObjectPtr<UUniversalBarsTheme> Theme = nullptr;
// Penalty-zone colours used by the hunger/weight/cold helpers (overridden by Theme if set).
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HUD|Colors")
FLinearColor HungerColor = FLinearColor(0.90f, 0.60f, 0.20f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HUD|Colors")
FLinearColor WeightColor = FLinearColor(0.82f, 0.23f, 0.23f, 1.f);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HUD|Colors")
FLinearColor ColdColor = FLinearColor(0.40f, 0.70f, 1.00f, 1.f);
// ===== high-level animated API =====
UFUNCTION(BlueprintCallable, Category="HUD") void SetHealth(float Percent); // 0..1
UFUNCTION(BlueprintCallable, Category="HUD") void SetStamina(float Percent); // 0..1
UFUNCTION(BlueprintCallable, Category="HUD") void SetShield(float Percent); // 0..1 (overlay above HP)
// Penalty fractions of the full track (eat into max HP). 0 hides the zone.
UFUNCTION(BlueprintCallable, Category="HUD") void SetHunger(float Amount);
UFUNCTION(BlueprintCallable, Category="HUD") void SetWeight(float Amount);
UFUNCTION(BlueprintCallable, Category="HUD") void SetCold(float Amount);
// Deltas (positive = heal/up, negative = damage/down) with the right flash.
UFUNCTION(BlueprintCallable, Category="HUD") void DamageHealth(float Delta);
UFUNCTION(BlueprintCallable, Category="HUD") void HealHealth(float Delta);
UFUNCTION(BlueprintPure, Category="HUD") float GetHealth() const;
// Re-tint and re-shape all bars + chips from the assigned Theme.
UFUNCTION(BlueprintCallable, Category="HUD") void ApplyTheme();
// ===== self-test / study driver =====
// When on, NativeTick cycles through every stat change so you can watch the
// animations live in PIE. Defaults ON for this study HUD — uncheck "Demo Mode"
// in the WBP Class Defaults (or call SetDemoMode(false)) for shipping.
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HUD|Demo")
bool bDemoMode = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HUD|Demo", meta=(ClampMin="0.2"))
float DemoStepInterval = 1.6f;
UFUNCTION(BlueprintCallable, Category="HUD|Demo")
void SetDemoMode(bool bEnable);
// Runs one demo step (damage / heal / hunger / weight / stamina). Called by
// the demo tick, but also handy to fire manually from BP.
UFUNCTION(BlueprintCallable, Category="HUD|Demo")
void DemoStepNow();
protected:
virtual void NativeConstruct() override;
virtual void NativeDestruct() override;
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
// Slides each chip to sit above the centre of its zone on the health bar.
void PositionChips();
private:
float DemoTimer = 0.f;
int32 DemoStep = 0;
bool bDemoInit = false;
};

View File

@ -0,0 +1,20 @@
// Copyright IHY. Universal Bars plugin.
using UnrealBuildTool;
public class UniversalBars : ModuleRules
{
public UniversalBars(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine",
"UMG",
"SlateCore",
"Slate"
});
}
}