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>
346 lines
8.4 KiB
C++
346 lines
8.4 KiB
C++
// 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);
|
|
}
|
|
}
|