We train chest-compression cadence with moving “beat balls” that pass through a fixed hit window. The player taps on the beat (like a rhythm game). A Generator blueprint spawns beats at the chosen BPM and manages UI hints; each Sphere blueprint travels along a spline, detects whether it is in the hit window, and resolves a hit or miss with feedback (VFX + camera shake).
Actors
binds player input, paces spawns, enlarges the hit window on press, and cleans up all active spheres.
one beat instance that moves at constant speed along a spline, marks when it enters/leaves the target area, and handles hit logic.
Data Flow
-
Setup & Input Binding
Cache player on BeginPlay, bind OnPlayerPressed to a window-enlarge timeline and OnPlayerEndInteraction to cleanup. -
Beat Pacing & Spawning
SetTimerByEvent loops at BeatInterval. On tick, spawn BP_Sphere with the lane spline and TravelTime = BeatInterval. -
Spline Motion at Constant Velocity
Each sphere computes MovementSpeed = SplineLength / TravelTime, advances CurrentDistance, and updates world position; an optional material scalar shows progress. -
Window Overlap Gates
Overlap begin/end with the target window flip IsSphereInTargetArea, forming the timing gate for judgement. -
Press-Time Check & Feedback
On press, if the sphere is in the window: play VFX, short camera shake, run a pop timeline, then destroy after a brief delay. -
Cleanup & Session Control
OnPlayerEndInteraction calls DestroyAllSpheres to wipe leftovers and stop the round. -
Tuning Knobs
BPM/BeatInterval set cadence and travel time. Judgement leniency = window radius × enlarge scale. Readability = sphere progress + window pulse. Feedback = VFX & camera-shake curve. Cleanup = destroy delay + lane clear.
▼ Pseudocode — Generator
// CONFIG
BPM = 110
BeatInterval = 60.0 / BPM
TargetWindow = AreaVisualizerSphere
on BeginPlay():
player = GetPlayerCharacter()
bind(player.OnPlayerPressed, onPlayerPressed)
bind(player.OnPlayerEndInteraction, onInteractionEnd)
startTimer(tickBeat, BeatInterval, loop = true)
function tickBeat():
if SpawningEnabled:
spawn BP_Sphere { TargetSpline, TravelTime = BeatInterval }
function onPlayerPressed():
playTimeline(EnlargeArea) // scales TargetWindow for feedback
// optional: player.HitCount++ or analytics
function onInteractionEnd():
DestroyAllSpheres()
▼ Pseudocode — Sphere
// INPUTS on spawn
TargetSpline : USplineComponent
TravelTime : float
// STATE
CurrentDistance = 0
IsSphereInTargetArea = false
MovementSpeed = 0
on BeginPlay():
L = TargetSpline.GetSplineLength()
MovementSpeed = L / max(TravelTime, 0.001)
on Tick(dt):
CurrentDistance += MovementSpeed * dt
pos = TargetSpline.GetLocationAtDistanceAlongSpline(CurrentDistance, World)
SetActorLocation(pos)
SetMaterialScalar("Progress", CurrentDistance / L)
if CurrentDistance >= L + KillBuffer:
DestroyActor()
on BeginOverlap(TargetWindow):
IsSphereInTargetArea = true
on EndOverlap(TargetWindow):
IsSphereInTargetArea = false
function CheckTiming(PlayerIsPressing):
if IsSphereInTargetArea and PlayerIsPressing:
SpawnVFXAt(GetActorLocation())
CameraShake()
playTimeline(ShrinkPop)
delay(1.0)
DestroyActor()
// else: optional miss cue
Constant-velocity spline motion makes arrivals stable across frame rates; a fixed window with moving cues makes rhythm legible and maps BPM directly to spawn cadence. Overlap-based checks are robust and cheap, and widening the window tunes accessibility without changing core input logic.