ArchitectureBP_StaminaSystemComponent (Stamina/Tension) – Tracks Left/Right Hand Stamina, IsLeft/RightKeyPressed, LastPushHand, MuscleTension. Fires OnTensionMax/OnPlayerDeath.
BP_SysyphusChar (Player) – maintains spacing/alignment with the Rock.
UI_StaminaHUD – Radial stamina slider (value + color lerp), twin Area Bars with moving markers, overlay detection/tinting, and mode switching (Normal / Volatile) per area.
Part I: Area Bars
Both-hands rules
Left and right hands are rhythm inputs. A single-hand hit advances; a both-hands overlap can be configured as either a short burst or a penalty. The core decision is to allow advance only when there is input, the input is inside at least one window, and the “both hands overlapping” rule does not block movement.
Pseudocode
Advance Rule
hasInput = IsLeftKeyPressed || IsRightKeyPressed
leftOK = DetectOverlayMarker(LeftMarker)
rightOK = DetectOverlayMarker(RightMarker)
bothHandsOverlay = leftOK && rightOK && hasInput
canAdvance = hasInput && !bothHandsOverlay && (leftOK || rightOK)
if canAdvance:
dir = normalized_tangent_or_world_dir()
ChildRock.AddMovementInput(dir, Force * dt)
DistanceAlongSpline += step
Rock.RollTimeline.Play()
Rock.DustFX.Play()
else:
Rock.RollTimeline.Stop()
Rock.DustFX.Stop()On widget construct the system reads each widget’s current transform and caches baseline Y positions: L Area Bar Pos Y, R Area Bar Pos Y, L Player Marker Pos Y, and R Player Marker Pos Y. These baselines are used by the “Update Bar Left/Right” logic to compute clamped positions, pulses, and easing relative to a stable origin. We prefer widget RenderTransform translation; if the parent is a Canvas Panel, this can be swapped for CanvasSlot position.
Area change binding
At construction, the HUD binds OnGameModeChanged by getting the player character, casting to BP_SysyphusChar, and attaching a callback that switches a runtime variable Bar Locomotion Mode between NORMAL_MODE and VOLATILE_MODE via a Switch on Area (VILLAGE, MUD, FOREST, SNOWLAND, VOLCANO, BUMPY).
Calmer surfaces (Village, Forest) map to NORMAL_MODE; unstable ones (Mud, Snowland, Volcano, Bumpy) map to VOLATILE_MODE.
The selected mode is consumed every frame by the bar and marker updaters to pick movement speeds, easing coefficients, jitter amplitude, color curves, and any shake timelines.
Area bars and markers with modes
Purpose: each tick the HUD moves the left and right Area Bars vertically. The motion style depends on Bar Locomotion Mode (NORMAL or VOLATILE). For each bar, the graph computes a target Y, clamps it to bounds, eases toward it, tags whether the bar is moving down, and writes the result back to the widget. Optional FollowLeft/FollowRight helpers let other UI elements track the bar.
Key variables
Bar Locomotion Mode ∈ { NORMAL_MODE, VOLATILE_MODE }.
L_Area_Bar_Pos_Y and R_Area_Bar_Pos_Y: cached current Y for left and right bars.
IsMovingDown_L and IsMovingDown_R: booleans derived from the direction of motion.
Mode parameters (selected per mode): AreaBarSpeed_L/R, InterpSpeed_L/R, AreaBarBounce, MinY, MaxY.
CurrentY_L/R: read from each widget’s RenderTransform.Translation.Y (or CanvasSlot position).
Per-frame logic
- On Tick, select the parameter set by Bar Locomotion Mode. NORMAL uses gentler speeds and stronger smoothing; VOLATILE uses higher speeds, lower smoothing, and larger bounce.
- Update Bar Left:
- Read CurrentY_L and IsMovingDown_L.
- Compute TargetY_L by adding or subtracting AreaBarSpeed_L * dt depending on IsMovingDown_L. Optionally add a small signed AreaBarBounce.
- Clamp TargetY_L to [MinY, MaxY].
- Ease with FInterpTo: NewY_L = FInterpTo(CurrentY_L, TargetY_L, dt, InterpSpeed_L).
- Direction flag: IsMovingDown_L = (NewY_L > CurrentY_L + epsilon).
- Write back: set RenderTransform.Translation.Y to NewY_L; store L_Area_Bar_Pos_Y = NewY_L; call FollowLeft(NewY_L) if other elements should track it.
- Read CurrentY_L and IsMovingDown_L.
- Update Bar Right: repeat the same sequence with CurrentY_R, IsMovingDown_R, and right-side parameters.
Pseudocode — Area Bars Tick
function SelectParams(mode):
if mode == NORMAL_MODE:
return {speedL:S_Ln, speedR:S_Rn, interpL:K_Ln, interpR:K_Rn, bounce:B_n, minY:MinY, maxY:MaxY}
else:
return {speedL:S_Lv, speedR:S_Rv, interpL:K_Lv, interpR:K_Rv, bounce:B_v, minY:MinY, maxY:MaxY}
function TickUpdate(dt):
P = SelectParams(BarLocomotionMode)
# Left bar
curL = GetWidgetY(LeftAreaBar)
tgtL = curL + (IsMovingDown_L ? +P.speedL : -P.speedL) * dt
tgtL += (IsMovingDown_L ? +1 : -1) * P.bounce
tgtL = clamp(tgtL, P.minY, P.maxY)
newL = FInterpTo(curL, tgtL, dt, P.interpL)
IsMovingDown_L = (newL > curL + 1e-3)
SetWidgetY(LeftAreaBar, newL)
L_Area_Bar_Pos_Y = newL
FollowLeft(newL) # optional
# Right bar
curR = GetWidgetY(RightAreaBar)
tgtR = curR + (IsMovingDown_R ? +P.speedR : -P.speedR) * dt
tgtR += (IsMovingDown_R ? +1 : -1) * P.bounce
tgtR = clamp(tgtR, P.minY, P.maxY)
newR = FInterpTo(curR, tgtR, dt, P.interpR)
IsMovingDown_R = (newR > curR + 1e-3)
SetWidgetY(RightAreaBar, newR)
R_Area_Bar_Pos_Y = newR
FollowRight(newR) # optional
function GetWidgetY(widget):
return widget.RenderTransform.Translation.Y
function SetWidgetY(widget, y):
t = widget.RenderTransform
t.Translation.Y = y
widget.RenderTransform = tIt drives the left-hand player marker (the small cursor on the rail) up or down every frame based on input. When the player is pressing the left key and input is enabled, the marker rises; otherwise it falls. The Y value is clamped to UI bounds and written back to the widget. A custom event FalldownLeft can immediately trigger the falling behavior.
Inputs and state
Player Input (bool) from BP_StaminaSystemComponent: whether input is globally allowed.
Is Left Key Pressed (bool): current left-hand press.
Left-hand Player Marker widget: read and write its transform.
Speeds: PlayerMarkerMovementSpeed (rise) and PlayerMarkerFallSpeed (fall).
Cached Y: L_Player Marker Pos Y (last frame’s value).
Helper macro: LimitMarkerWithinBounds (clamps Y to [MinY, MaxY]).
Step-by-step
-
Read the current Y of the left marker (Break Widget Transform → Translation.Y) and the cached L_Player Marker Pos Y.
-
Gate by Player Input AND Is Left Key Pressed.
True (pressing): compute an upward step using PlayerMarkerMovementSpeed (usually subtract from Y because UMG +Y is down).
False (not pressing or disabled): compute a downward step using PlayerMarkerFallSpeed.
-
Clamp the proposed Y with LimitMarkerWithinBounds to keep it inside the rail.
-
Update L_Player Marker Pos Y with the clamped value.
-
Build a new widget transform with the updated translation and call Set Render Transform on the marker.
-
Optional Print String nodes log the value for debugging.
- FalldownLeft can call this path with the “false” branch to force a fall tick (punish, timeout, or exhaustion).
Update Left Marker
function UpdateLeftMarker(dt):
curY = GetWidgetY(LeftMarker)
hasInput = PlayerInput && IsLeftKeyPressed
if hasInput:
targetY = curY - PlayerMarkerMovementSpeed * dt
else:
targetY = curY + PlayerMarkerFallSpeed * dt
y = LimitMarkerWithinBounds(targetY)
L_PlayerMarkerPosY = y
SetWidgetY(LeftMarker, y)Alpha 0.8 keeps the overlay semi-transparent so it does not overpower other UI. For softer transitions, replace the direct Set Color and Opacity with a short color Lerp or per-channel FInterpTo.
Part II: Radial Tension Bar
Updates the circular HUD bar to show muscle tension as a percentage and fades its progress color from green → red as tension rises (0 → 1).
Flow
-
Custom Event UpdateRadialTensionBar fires (e.g., on tick or when tension changes).
-
Get Player Character → Cast to BP_SysyphusChar to access the character’s BP_StaminaSystemComponent.
- Call Get Tension Percent on the component → returns p ∈ [0,1].
- Write p into the UI Radial Slider via Set Value (numeric fill).
-
Compute the progress tint: Lerp(LinearColor) between Green and Red with Alpha = p.
- Apply the tint with Set Slider Progress Color on the same Radial Slider.
Radial Tension Bar
event UpdateRadialTensionBar():comp = GetPlayerCharacter().BP_StaminaSystemComponent
p = comp.GetTensionPercent() # 0.0 (rest) -> 1.0 (max)
RadialSlider.SetValue(p) # update fill amount
color = Lerp(Green, Red, p) # 0->green, 1->red
RadialSlider.SetProgressColor(color) # update visual state