commit 41a0cdef369e2d7a6e856a0403dce893e96bc6d3 Author: Bonchellon <64547270+bonchellon@users.noreply.github.com> Date: Tue Jun 23 19:04:50 2026 +0300 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b4562bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +# UE build artifacts +Intermediate/ +Binaries/ +DerivedDataCache/ +Saved/ +*.sln +*.suo +*.VC.db +.vs/ diff --git a/Content/Components/WBP_BarHealth.uasset b/Content/Components/WBP_BarHealth.uasset new file mode 100644 index 0000000..16cd40f Binary files /dev/null and b/Content/Components/WBP_BarHealth.uasset differ diff --git a/Content/Components/WBP_BarStamina.uasset b/Content/Components/WBP_BarStamina.uasset new file mode 100644 index 0000000..294e49f Binary files /dev/null and b/Content/Components/WBP_BarStamina.uasset differ diff --git a/Content/Components/WBP_HUD_InGame.uasset b/Content/Components/WBP_HUD_InGame.uasset new file mode 100644 index 0000000..b5c1dd9 Binary files /dev/null and b/Content/Components/WBP_HUD_InGame.uasset differ diff --git a/Content/Components/WBP_UniversalBar.uasset b/Content/Components/WBP_UniversalBar.uasset new file mode 100644 index 0000000..3e675f3 Binary files /dev/null and b/Content/Components/WBP_UniversalBar.uasset differ diff --git a/Content/Materials/MI_UI_Bar_Boss.uasset b/Content/Materials/MI_UI_Bar_Boss.uasset new file mode 100644 index 0000000..9439622 Binary files /dev/null and b/Content/Materials/MI_UI_Bar_Boss.uasset differ diff --git a/Content/Materials/MI_UI_Bar_Health.uasset b/Content/Materials/MI_UI_Bar_Health.uasset new file mode 100644 index 0000000..141d7c3 Binary files /dev/null and b/Content/Materials/MI_UI_Bar_Health.uasset differ diff --git a/Content/Materials/MI_UI_Bar_Injury.uasset b/Content/Materials/MI_UI_Bar_Injury.uasset new file mode 100644 index 0000000..a82617f Binary files /dev/null and b/Content/Materials/MI_UI_Bar_Injury.uasset differ diff --git a/Content/Materials/MI_UI_Bar_PeakCombined.uasset b/Content/Materials/MI_UI_Bar_PeakCombined.uasset new file mode 100644 index 0000000..afafef3 Binary files /dev/null and b/Content/Materials/MI_UI_Bar_PeakCombined.uasset differ diff --git a/Content/Materials/MI_UI_Bar_Shield.uasset b/Content/Materials/MI_UI_Bar_Shield.uasset new file mode 100644 index 0000000..db32865 Binary files /dev/null and b/Content/Materials/MI_UI_Bar_Shield.uasset differ diff --git a/Content/Materials/MI_UI_Bar_Stamina.uasset b/Content/Materials/MI_UI_Bar_Stamina.uasset new file mode 100644 index 0000000..e7ecd33 Binary files /dev/null and b/Content/Materials/MI_UI_Bar_Stamina.uasset differ diff --git a/Content/Materials/M_UI_UniversalBar.uasset b/Content/Materials/M_UI_UniversalBar.uasset new file mode 100644 index 0000000..51e8090 Binary files /dev/null and b/Content/Materials/M_UI_UniversalBar.uasset differ diff --git a/Content/Theme/DA_UniversalBars_Default.uasset b/Content/Theme/DA_UniversalBars_Default.uasset new file mode 100644 index 0000000..6077b72 Binary files /dev/null and b/Content/Theme/DA_UniversalBars_Default.uasset differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d2c664 --- /dev/null +++ b/README.md @@ -0,0 +1,231 @@ +# Universal Bars + +PEAK-style universal UMG bars for Unreal Engine 5.7 — one procedural material +drives every kind of stat bar (health, stamina, hunger, thirst, oxygen, cold, +shield, injury, boss HP, item durability, progress), with a global subsystem +API and animated damage / heal / low-value feedback. + +Everything is **procedural** — no bar textures required. White rounded border, +soft corners, dark translucent backdrop, bright flat fill: clean cartoon +survival HUD. + +--- + +## 1. Install / enable + +The plugin lives at `Plugins/UniversalBars/`. It is a project plugin and is +enabled by default. After pulling, regenerate project files and build the +editor target once (it adds the `UniversalBars` runtime module). Content mounts +under `/UniversalBars/`. + +> New C++ classes (a new module) cannot be hot-loaded by Live Coding — do a full +> editor build the first time. + +--- + +## 2. What's inside + +``` +/UniversalBars/ + Materials/ + M_UI_UniversalBar master material (Domain=UI, Translucent, procedural SDF) + MI_UI_Bar_Health solo green bar + MI_UI_Bar_Stamina solo yellow-green bar + MI_UI_Bar_Injury striped "broken HP" bar + MI_UI_Bar_Shield blue bar + shield overlay + MI_UI_Bar_Boss segmented boss bar + MI_UI_Bar_PeakCombined HP + hunger + weight in ONE bar (PEAK capacity) + Components/ + WBP_UniversalBar one reusable bar (parent: UUniversalBarWidget) + WBP_BarHealth combined-capacity variant + WBP_BarStamina stamina variant + WBP_HUD_InGame ready HUD: health + stamina + status chips + Theme/ put DA_UniversalBars_* theme assets here + +C++ (module UniversalBars): + UUniversalBarWidget one animated bar + UUniversalHUDWidget HUD container with high-level stat setters + UUniversalBarsSubsystem global access point (UWorldSubsystem) + UUniversalBarsTheme palette / geometry / chip-colour DataAsset +``` + +--- + +## 3. Quick start + +### Blueprint — easiest path (global API, no widget reference) + +1. On `BeginPlay` (player controller / HUD), call + `Universal Bars Subsystem → Create HUD` with `HUD Class = WBP_HUD_InGame`. +2. From **anywhere** in BP: + +``` +Get World Subsystem (UniversalBarsSubsystem) + → Set Health (0.62) // 0..1, animates + flashes automatically + → Set Stamina (0.8) + → Set Hunger (0.16) // penalty zone eats into max HP (combined bar) + → Set Weight (0.12) + → Damage Health (0.2) // red flash + instant drop + lag tail + → Heal Health (0.25) // green flash + smooth rise +``` + +Direction is detected for you: a decrease plays the damage animation, an +increase plays the heal animation. + +### C++ + +```cpp +#include "UniversalBarsSubsystem.h" + +if (UUniversalBarsSubsystem* Bars = UUniversalBarsSubsystem::Get(this)) +{ + Bars->SetHealth(0.62f); + Bars->SetHunger(0.16f); + Bars->DamageHealth(0.2f); +} +``` + +### Driving a single bar directly + +Place a `WBP_UniversalBar` (or a preset variant) anywhere and call: + +```cpp +UUniversalBarWidget* Bar = ...; +Bar->SetFillPercent(0.5f); // animated +Bar->SetShieldPercent(0.25f); +Bar->SetSegmentCount(10); // boss segments +Bar->SetPenaltyZone("Hunger", 0.16f, FLinearColor(0.9f,0.6f,0.2f,1)); // animated zone +Bar->PlayDamageFlash(); // manual flash +``` + +--- + +## 4. Animation model (same as the HTML prototype) + +| Change | Bright fill | Lag tail | Flash | +|---|---|---|---| +| **Decrease / damage** | snaps down instantly | holds `DelayedDelay`s, then drains at `DelayedFallSpeed` | red (`DamageFlashColor`) | +| **Increase / heal** | eases up at `FillRiseSpeed` | jumps to target (lighter "incoming" band) | green (`HealFlashColor`) | +| **value < `LowThreshold`** | — | — | border pulses (low-value warning) | +| **penalty zone change** | — | zone width eases at `ZoneInterpSpeed` | — | + +All tuning fields live on `UUniversalBarWidget` (Bar|Anim category). Set +`bAutoFlashOnChange=false` to drive flashes manually. + +Under the hood the material exposes `FillPercent`, `DelayedFillPercent`, +`FlashAmount`, `LowHPPulseAmount` — the widget tweens these every tick via a +Dynamic Material Instance. + +--- + +## 5. Combined "capacity" bar (PEAK) + +One bar = current value (bright) + depleted-but-recoverable zone (dark) + +striped penalty zones eating max from the right: + +- `MaxFillPercent` — right edge of the usable region (`1 - sum(penalties)`). +- `PenaltyCount` + `PenaltyEnd1..4` + `PenaltyColor1..4` — up to 4 striped zones. + +You never set these by hand — `SetHunger / SetWeight / SetCold` (HUD) or +`SetPenaltyZone(Id, Width, Color)` (bar) manage them and animate the widths. +Set a penalty to `0` to retract its zone. + +`PenaltyCount = 0` → the same material is a plain solo bar. + +--- + +## 6. Material parameter reference + +**Scalars:** `FillPercent`, `DelayedFillPercent`, `ShieldPercent`, +`MaxFillPercent`, `PenaltyCount`, `PenaltyEnd1..4`, `BarOpacity`, +`BackgroundOpacity`, `CornerRadius`, `BorderSize`, `InnerPadding`, +`EdgeSoftness`, `BarAspect`, `FlashAmount`, `LowHPPulseAmount`, `PulseSpeed`, +`StripeAmount`, `StripeScale`, `StripeSpeed`, `SegmentCount`, +`SegmentDividerSize`, `SegmentDividerOpacity`, `UseVerticalFill`, `InvertFill`, +`NoiseAmount`, `TimeOffset`. + +**Vectors:** `FillColor`, `DelayedFillColor`, `ShieldColor`, `BackgroundColor`, +`BorderColor`, `EmptyColor`, `FlashColor`, `StripeColorA`, `StripeColorB`, +`PenaltyColor1..4`. + +`BarAspect` must roughly match the on-screen width/height ratio so the rounded +corners stay circular (default 8 ≈ a 600×75 bar). + +--- + +## 7. Theme + +`UUniversalBarsTheme` (DataAsset) centralises palette + geometry + chip colours. +Create one under `/UniversalBars/Theme/` (right-click → Miscellaneous → Data +Asset → Universal Bars Theme), then assign it to `WBP_HUD_InGame`'s `Theme` +property. `UUniversalHUDWidget::ApplyTheme()` runs on construct and retints both +bars and all chips. Per-preset look still lives in the `MI_UI_Bar_*` instances; +the theme is the HUD-level override. + +--- + +## 8. Presets (recommended values) + +| Preset | Fill | Notes | +|---|---|---| +| Health | `(0.30,0.85,0.20)` | white border, low-HP pulse, damage lag | +| Stamina | `(0.85,0.95,0.30)` | thinner, no pulse | +| Injury | `(0.75,0.20,0.20)` + `StripeAmount=1` | striped "broken" HP | +| Shield | `(0.30,0.70,1.0)` + `ShieldPercent` | blue + overlay | +| Boss | `(0.85,0.15,0.15)` + `SegmentCount=10` | wide, segmented, less rounded | +| PeakCombined | green + Hunger/Weight penalties | one-bar HP+hunger+weight | + +Keep `BorderColor` pure white and `BorderSize` 0.06–0.10 for the toy/cartoon +look; `CornerRadius` 0.5–0.7 for horizontal bars. + +--- + +## 9. Demo / study mode (WBP_HUD_InGame) + +`WBP_HUD_InGame` ships with **Demo Mode ON** (`UUniversalHUDWidget::bDemoMode`). +While on, the HUD's `NativeTick` runs a 10-step cycle every `DemoStepInterval` +(1.6 s) so you can watch every animation live in PIE: + +``` +0 DamageHealth(0.25) red flash + instant drop + lag tail +1 SetStamina(0.30) stamina drop +2 HealHealth(0.20) green flash + smooth rise +3 SetHunger(0.18) amber zone eases in from the right +4 SetWeight(0.14) red zone eases in +5 SetStamina(0.95) stamina refill +6 HealHealth(0.30) +7 SetHunger(0.0) hunger zone retracts +8 SetWeight(0.0) weight zone retracts +9 DamageHealth(0.45) big hit -> low-HP border pulse +``` + +To watch: add the HUD (`Subsystem → Create HUD`, or just drop it in any widget / +PIE) and press Play. The cycle restarts each time the widget constructs. + +**Turn it off for shipping:** open `WBP_HUD_InGame` → Class Defaults → uncheck +**Demo Mode**, or call `SetDemoMode(false)`. Read `DemoStepNow()` in +`UniversalHUDWidget.cpp` to see exactly which API call drives each animation. + +## 9b. Status chips (PEAK markers) + +In `WBP_HUD_InGame` the chips live in a `CanvasPanel` (`ChipCanvas`) **overlaid on +the health bar**, not in a row below it. Each tick `UUniversalHUDWidget::PositionChips()` +slides them to sit just above the centre of their zone, using +`UUniversalBarWidget::GetUsableEnd()` / `GetZoneCenter("Hunger"|"Weight"|"Cold")`: + +- `ChipHeart` → the usable-HP / penalty boundary +- `ChipHunger` → centre of the hunger zone (hidden until `SetHunger > 0`) +- `ChipWeight` → centre of the weight zone +- `ChipCold` → centre of the cold zone + +So when hunger/weight grow or shrink, the chips ride along with the zone edges. +Add your own marker by placing an `Image` in `ChipCanvas` and binding it (or extend +`PositionChips`). Swap the placeholder tints for real icon textures. + +## 10. Notes / limits + +- The bar is a UI-domain material — it only renders inside UMG/Slate (not on + meshes). Verify visuals in the UMG designer or content-browser thumbnail. +- Up to 4 penalty zones are drawn by the material in a single draw call; the + widget warns if you push more. +- `SetFillPercent` clamps to 0..1. Feed it `CurrentHP / MaxHP`, etc. diff --git a/Source/UniversalBars/Private/UniversalBarWidget.cpp b/Source/UniversalBars/Private/UniversalBarWidget.cpp new file mode 100644 index 0000000..012b6be --- /dev/null +++ b/Source/UniversalBars/Private/UniversalBarWidget.cpp @@ -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(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(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); + } +} diff --git a/Source/UniversalBars/Private/UniversalBarsModule.cpp b/Source/UniversalBars/Private/UniversalBarsModule.cpp new file mode 100644 index 0000000..a8d70a0 --- /dev/null +++ b/Source/UniversalBars/Private/UniversalBarsModule.cpp @@ -0,0 +1,4 @@ +// Copyright IHY. Universal Bars plugin. +#include "Modules/ModuleManager.h" + +IMPLEMENT_MODULE(FDefaultModuleImpl, UniversalBars); diff --git a/Source/UniversalBars/Private/UniversalBarsSubsystem.cpp b/Source/UniversalBars/Private/UniversalBarsSubsystem.cpp new file mode 100644 index 0000000..71aee2d --- /dev/null +++ b/Source/UniversalBars/Private/UniversalBarsSubsystem.cpp @@ -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(); + } + return nullptr; +} + +UUniversalHUDWidget* UUniversalBarsSubsystem::CreateHUD(APlayerController* Owner, TSubclassOf HUDClass, int32 ZOrder) +{ + if (!HUDClass) + { + return nullptr; + } + UUniversalHUDWidget* Widget = CreateWidget(Owner ? Cast(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); } } diff --git a/Source/UniversalBars/Private/UniversalHUDWidget.cpp b/Source/UniversalBars/Private/UniversalHUDWidget.cpp new file mode 100644 index 0000000..0131e8c --- /dev/null +++ b/Source/UniversalBars/Private/UniversalHUDWidget.cpp @@ -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(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(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(); + } +} diff --git a/Source/UniversalBars/Public/UniversalBarWidget.h b/Source/UniversalBars/Public/UniversalBarWidget.h new file mode 100644 index 0000000..242c1b5 --- /dev/null +++ b/Source/UniversalBars/Public/UniversalBarWidget.h @@ -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 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 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 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; +}; diff --git a/Source/UniversalBars/Public/UniversalBarsSubsystem.h b/Source/UniversalBars/Public/UniversalBarsSubsystem.h new file mode 100644 index 0000000..032f21d --- /dev/null +++ b/Source/UniversalBars/Public/UniversalBarsSubsystem.h @@ -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 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 HUD; +}; diff --git a/Source/UniversalBars/Public/UniversalBarsTheme.h b/Source/UniversalBars/Public/UniversalBarsTheme.h new file mode 100644 index 0000000..c384db1 --- /dev/null +++ b/Source/UniversalBars/Public/UniversalBarsTheme.h @@ -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 ChipColors; +}; diff --git a/Source/UniversalBars/Public/UniversalHUDWidget.h b/Source/UniversalBars/Public/UniversalHUDWidget.h new file mode 100644 index 0000000..354cd5e --- /dev/null +++ b/Source/UniversalBars/Public/UniversalHUDWidget.h @@ -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 HealthBar = nullptr; + + UPROPERTY(meta=(BindWidgetOptional)) + TObjectPtr StaminaBar = nullptr; + + // Optional container for status chips (filled in the designer with placeholders). + UPROPERTY(meta=(BindWidgetOptional)) + TObjectPtr 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 ChipCanvas = nullptr; + + UPROPERTY(meta=(BindWidgetOptional)) + TObjectPtr ChipHeart = nullptr; // tracks usable-HP end + + UPROPERTY(meta=(BindWidgetOptional)) + TObjectPtr ChipHunger = nullptr; // tracks "Hunger" zone + + UPROPERTY(meta=(BindWidgetOptional)) + TObjectPtr ChipWeight = nullptr; // tracks "Weight" zone + + UPROPERTY(meta=(BindWidgetOptional)) + TObjectPtr ChipCold = nullptr; // tracks "Cold" zone (hidden if absent) + + // Optional central palette/geometry; applied on construct when set. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="HUD") + TObjectPtr 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; +}; diff --git a/Source/UniversalBars/UniversalBars.Build.cs b/Source/UniversalBars/UniversalBars.Build.cs new file mode 100644 index 0000000..ad9369c --- /dev/null +++ b/Source/UniversalBars/UniversalBars.Build.cs @@ -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" + }); + } +} diff --git a/UniversalBars.uplugin b/UniversalBars.uplugin new file mode 100644 index 0000000..1196620 --- /dev/null +++ b/UniversalBars.uplugin @@ -0,0 +1,19 @@ +{ + "FileVersion": 3, + "Version": 1, + "VersionName": "1.0", + "FriendlyName": "Universal Bars", + "Description": "PEAK-style universal UMG bars (health / stamina / hunger / weight / shield / boss / durability) driven by one procedural material, with a global subsystem API and animated damage / heal / low-HP feedback.", + "Category": "UI", + "CreatedBy": "IHY", + "CanContainContent": true, + "IsBetaVersion": false, + "Installed": false, + "Modules": [ + { + "Name": "UniversalBars", + "Type": "Runtime", + "LoadingPhase": "Default" + } + ] +}