Rework adaptive extrapolation: deceleration detection + dead zone + debug HUD

Replace variance-based confidence (caused constant lag) with targeted
deceleration detection: compares recent speed (last 25% of safe window)
to average speed. During steady movement ratio≈1 → zero lag.
Only reduces extrapolation when actual braking is detected.

- AdaptiveSensitivity: now a power exponent (0.1-5.0, default 1.0)
- AdaptiveDeadZone: new parameter (default 0.8) to ignore normal
  speed fluctuations and only react to real deceleration
- DebugAntiRecoilHUD: real-time display of ratio, confidence, speeds, errors
- EndPlay: auto-close CSV file when stopping play (no more locked files)
- Python script updated to match new deceleration-based algorithm

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-16 18:25:54 +01:00
parent fa257fb87b
commit af723c944b
4 changed files with 197 additions and 119 deletions

View File

@ -131,75 +131,71 @@ def simulate_adaptive(frames: List[Frame], sensitivity: float) -> List[Tuple[flo
pos_errors = [] pos_errors = []
aim_errors = [] aim_errors = []
# Sliding window for velocity estimation # Sliding window for velocity estimation (matches C++ safe window ~18 samples)
window_size = 12 window_size = 12
SMALL = 1e-10 SMALL = 1e-10
for i in range(window_size + 1, len(frames) - 1): for i in range(window_size + 1, len(frames) - 1):
# Build velocity pairs from recent real positions and aims # Build velocity pairs from recent real positions and aims
pos_velocities = [] pos_vels = []
aim_velocities = [] aim_vels = []
weights = []
for j in range(1, min(window_size, i)): for j in range(1, min(window_size, i)):
dt = frames[i - j].timestamp - frames[i - j - 1].timestamp dt = frames[i - j].timestamp - frames[i - j - 1].timestamp
if dt > 1e-6: if dt > 1e-6:
pos_vel = vec_scale(vec_sub(frames[i - j].real_pos, frames[i - j - 1].real_pos), 1.0 / dt) pos_vel = vec_scale(vec_sub(frames[i - j].real_pos, frames[i - j - 1].real_pos), 1.0 / dt)
aim_vel = vec_scale(vec_sub(frames[i - j].real_aim, frames[i - j - 1].real_aim), 1.0 / dt) aim_vel = vec_scale(vec_sub(frames[i - j].real_aim, frames[i - j - 1].real_aim), 1.0 / dt)
pos_velocities.append(pos_vel) pos_vels.append(pos_vel)
aim_velocities.append(aim_vel) aim_vels.append(aim_vel)
w = (window_size - j) ** 2
weights.append(w)
if len(pos_velocities) < 2: if len(pos_vels) < 4:
continue continue
total_w = sum(weights) n = len(pos_vels)
# Weighted average velocity (position) # Weighted average velocity (recent samples weighted more, matching C++)
total_w = 0.0
avg_pos_vel = (0.0, 0.0, 0.0) avg_pos_vel = (0.0, 0.0, 0.0)
avg_aim_vel = (0.0, 0.0, 0.0) avg_aim_vel = (0.0, 0.0, 0.0)
for pv, av, w in zip(pos_velocities, aim_velocities, weights): for k in range(n):
avg_pos_vel = vec_add(avg_pos_vel, vec_scale(pv, w / total_w)) w = (k + 1) ** 2
avg_aim_vel = vec_add(avg_aim_vel, vec_scale(av, w / total_w)) avg_pos_vel = vec_add(avg_pos_vel, vec_scale(pos_vels[k], w))
avg_aim_vel = vec_add(avg_aim_vel, vec_scale(aim_vels[k], w))
total_w += w
avg_pos_vel = vec_scale(avg_pos_vel, 1.0 / total_w)
avg_aim_vel = vec_scale(avg_aim_vel, 1.0 / total_w)
# Weighted variance (position and aim separately) # Deceleration detection: recent speed (last 25%) vs average speed
pos_var_sum = 0.0 recent_start = max(0, n - max(1, n // 4))
aim_var_sum = 0.0 recent_count = n - recent_start
for pv, av, w in zip(pos_velocities, aim_velocities, weights): recent_pos_vel = (0.0, 0.0, 0.0)
pd = vec_sub(pv, avg_pos_vel) recent_aim_vel = (0.0, 0.0, 0.0)
ad = vec_sub(av, avg_aim_vel) for k in range(recent_start, n):
pos_var_sum += (pd[0]**2 + pd[1]**2 + pd[2]**2) * w recent_pos_vel = vec_add(recent_pos_vel, pos_vels[k])
aim_var_sum += (ad[0]**2 + ad[1]**2 + ad[2]**2) * w recent_aim_vel = vec_add(recent_aim_vel, aim_vels[k])
pos_variance = pos_var_sum / total_w recent_pos_vel = vec_scale(recent_pos_vel, 1.0 / recent_count)
aim_variance = aim_var_sum / total_w recent_aim_vel = vec_scale(recent_aim_vel, 1.0 / recent_count)
# Separate confidences with RELATIVE variance (matching C++ code) avg_pos_speed = vec_len(avg_pos_vel)
pos_speed2 = sum(v**2 for v in avg_pos_vel) avg_aim_speed = vec_len(avg_aim_vel)
aim_speed2 = sum(v**2 for v in avg_aim_vel) recent_pos_speed = vec_len(recent_pos_vel)
recent_aim_speed = vec_len(recent_aim_vel)
# Confidence = (recentSpeed / avgSpeed) ^ sensitivity
pos_confidence = 1.0
if avg_pos_speed > SMALL:
pos_ratio = min(recent_pos_speed / avg_pos_speed, 1.0)
pos_confidence = pos_ratio ** sensitivity
aim_confidence = 1.0
if avg_aim_speed > SMALL:
aim_ratio = min(recent_aim_speed / avg_aim_speed, 1.0)
aim_confidence = aim_ratio ** sensitivity
# Extrapolation time # Extrapolation time
extrap_dt = frames[i].extrap_time if frames[i].extrap_time > 0 else 0.011 extrap_dt = frames[i].extrap_time if frames[i].extrap_time > 0 else 0.011
pos_confidence = 1.0 # Predict from last safe position
if pos_speed2 > SMALL:
pos_rel_var = pos_variance / pos_speed2
pos_confidence = 1.0 / (1.0 + pos_rel_var * sensitivity)
# Low-speed protection: if extrapolation offset < 1cm, blend confidence toward 1
pos_extrap_offset = math.sqrt(pos_speed2) * extrap_dt
pos_offset_significance = max(0.0, min(pos_extrap_offset / 1.0, 1.0)) # 1cm threshold
pos_confidence = 1.0 + (pos_confidence - 1.0) * pos_offset_significance
aim_confidence = 1.0
if aim_speed2 > SMALL:
aim_rel_var = aim_variance / aim_speed2
aim_confidence = 1.0 / (1.0 + aim_rel_var * sensitivity)
# Low-speed protection: if aim extrapolation offset is tiny, blend toward 1
aim_extrap_offset = math.sqrt(aim_speed2) * extrap_dt
aim_offset_significance = max(0.0, min(aim_extrap_offset / 0.01, 1.0)) # 0.01 unit threshold
aim_confidence = 1.0 + (aim_confidence - 1.0) * aim_offset_significance
# Predict
pred_pos = vec_add(frames[i - 1].real_pos, vec_scale(avg_pos_vel, extrap_dt * pos_confidence)) pred_pos = vec_add(frames[i - 1].real_pos, vec_scale(avg_pos_vel, extrap_dt * pos_confidence))
pred_aim_raw = vec_add(frames[i - 1].real_aim, vec_scale(avg_aim_vel, extrap_dt * aim_confidence)) pred_aim_raw = vec_add(frames[i - 1].real_aim, vec_scale(avg_aim_vel, extrap_dt * aim_confidence))
pred_aim = vec_normalize(pred_aim_raw) pred_aim = vec_normalize(pred_aim_raw)
@ -214,15 +210,15 @@ def simulate_adaptive(frames: List[Frame], sensitivity: float) -> List[Tuple[flo
def find_optimal_parameters(frames: List[Frame]) -> dict: def find_optimal_parameters(frames: List[Frame]) -> dict:
"""Search for optimal AdaptiveSensitivity (no scaling factor needed).""" """Search for optimal AdaptiveSensitivity (power curve exponent for deceleration detection)."""
print("\nSearching for optimal AdaptiveSensitivity...") print("\nSearching for optimal AdaptiveSensitivity (power exponent)...")
print("-" * 60) print("-" * 60)
best_score = float('inf') best_score = float('inf')
best_sensitivity = 1.0 best_sensitivity = 1.0
# Search grid — only sensitivity, no more scaling factor # Search grid — exponent range 0.1 to 5.0
sensitivities = [0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 50.0, 75.0, 100.0, 150.0, 200.0] sensitivities = [0.1, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]
results = [] results = []

View File

@ -717,107 +717,112 @@ void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLoc
// Compute weighted velocities from consecutive safe sample pairs // Compute weighted velocities from consecutive safe sample pairs
// Weight recent pairs more heavily // Weight recent pairs more heavily
TArray<FVector> PosVelocities; TArray<FVector> PosVelocities;
TArray<FVector> AimVelocities; // Compute per-pair velocities
TArray<double> Weights; TArray<FVector> PosVels;
PosVelocities.Reserve(SafeN - 1); TArray<FVector> AimVels;
AimVelocities.Reserve(SafeN - 1); PosVels.Reserve(SafeN - 1);
Weights.Reserve(SafeN - 1); AimVels.Reserve(SafeN - 1);
for (int32 i = 1; i < SafeN; i++) for (int32 i = 1; i < SafeN; i++)
{ {
double dt = TransformHistory[i].Timestamp - TransformHistory[i - 1].Timestamp; double dt = TransformHistory[i].Timestamp - TransformHistory[i - 1].Timestamp;
if (dt > SMALL_NUMBER) if (dt > SMALL_NUMBER)
{ {
FVector PosVel = (TransformHistory[i].Location - TransformHistory[i - 1].Location) / dt; PosVels.Add((TransformHistory[i].Location - TransformHistory[i - 1].Location) / dt);
FVector AimVel = (TransformHistory[i].Aim - TransformHistory[i - 1].Aim) / dt; AimVels.Add((TransformHistory[i].Aim - TransformHistory[i - 1].Aim) / dt);
// Weight: recent pairs get exponentially more weight
double w = FMath::Pow((double)i, 2.0);
PosVelocities.Add(PosVel);
AimVelocities.Add(AimVel);
Weights.Add(w);
} }
} }
if (PosVelocities.Num() == 0) if (PosVels.Num() == 0)
{ {
OutLocation = TransformHistory[SafeN - 1].Location; OutLocation = TransformHistory[SafeN - 1].Location;
OutAim = TransformHistory[SafeN - 1].Aim; OutAim = TransformHistory[SafeN - 1].Aim;
return; return;
} }
// Compute weighted average velocity // Compute overall weighted average velocity (recent samples weighted more)
double TotalWeight = 0.0; double TotalWeight = 0.0;
FVector AvgPosVel = FVector::ZeroVector; FVector AvgPosVel = FVector::ZeroVector;
FVector AvgAimVel = FVector::ZeroVector; FVector AvgAimVel = FVector::ZeroVector;
for (int32 i = 0; i < PosVelocities.Num(); i++) for (int32 i = 0; i < PosVels.Num(); i++)
{ {
AvgPosVel += PosVelocities[i] * Weights[i]; double w = FMath::Pow((double)(i + 1), 2.0);
AvgAimVel += AimVelocities[i] * Weights[i]; AvgPosVel += PosVels[i] * w;
TotalWeight += Weights[i]; AvgAimVel += AimVels[i] * w;
TotalWeight += w;
} }
AvgPosVel /= TotalWeight; AvgPosVel /= TotalWeight;
AvgAimVel /= TotalWeight; AvgAimVel /= TotalWeight;
// Compute weighted variance of velocity (how consistent is the velocity?) // Deceleration detection: compare recent speed (last 25% of pairs) vs overall speed.
FVector PosVelVariance = FVector::ZeroVector; // If the user is stopping, recent speed will drop toward 0 while avg is still high.
FVector AimVelVariance = FVector::ZeroVector; // Confidence = recentSpeed / avgSpeed, clamped to [0, 1].
// During steady movement: ratio ≈ 1 → full extrapolation, zero lag.
// During deceleration: ratio < 1 → reduced extrapolation, prevents overshoot.
int32 RecentStart = FMath::Max(0, PosVels.Num() - FMath::Max(1, PosVels.Num() / 4));
int32 RecentCount = PosVels.Num() - RecentStart;
for (int32 i = 0; i < PosVelocities.Num(); i++) // Recent average velocity (unweighted, just the latest samples)
FVector RecentPosVel = FVector::ZeroVector;
FVector RecentAimVel = FVector::ZeroVector;
for (int32 i = RecentStart; i < PosVels.Num(); i++)
{ {
FVector PosDiff = PosVelocities[i] - AvgPosVel; RecentPosVel += PosVels[i];
FVector AimDiff = AimVelocities[i] - AvgAimVel; RecentAimVel += AimVels[i];
PosVelVariance += FVector(PosDiff.X * PosDiff.X, PosDiff.Y * PosDiff.Y, PosDiff.Z * PosDiff.Z) * Weights[i]; }
AimVelVariance += FVector(AimDiff.X * AimDiff.X, AimDiff.Y * AimDiff.Y, AimDiff.Z * AimDiff.Z) * Weights[i]; RecentPosVel /= (double)RecentCount;
RecentAimVel /= (double)RecentCount;
// Compute speed ratio: recent / average
float AvgPosSpeed = AvgPosVel.Size();
float AvgAimSpeed = AvgAimVel.Size();
float RecentPosSpeed = RecentPosVel.Size();
float RecentAimSpeed = RecentAimVel.Size();
// Confidence: ratio of recent speed to average speed.
// Dead zone: ratios above AdaptiveDeadZone are treated as 1.0 (normal fluctuations).
// Remapped ratio: (ratio - deadzone) / (1 - deadzone), clamped to [0, 1].
// AdaptiveSensitivity is the power exponent on the remapped ratio.
float PosRatio = 1.0f;
float PosConfidence = 1.0f;
if (AvgPosSpeed > SMALL_NUMBER)
{
PosRatio = FMath::Clamp(RecentPosSpeed / AvgPosSpeed, 0.0f, 1.0f);
float PosRemapped = (AdaptiveDeadZone < 1.0f)
? FMath::Clamp((PosRatio - AdaptiveDeadZone) / (1.0f - AdaptiveDeadZone), 0.0f, 1.0f)
: (PosRatio >= 1.0f ? 1.0f : 0.0f);
PosConfidence = FMath::Pow(PosRemapped, AdaptiveSensitivity);
} }
PosVelVariance /= TotalWeight; float AimRatio = 1.0f;
AimVelVariance /= TotalWeight; float AimConfidence = 1.0f;
if (AvgAimSpeed > SMALL_NUMBER)
// Compute separate confidence for position and aim. {
// Position variance is in cm^2/s^2 (large values), aim variance is in unit^2/s^2 (small values). AimRatio = FMath::Clamp(RecentAimSpeed / AvgAimSpeed, 0.0f, 1.0f);
// Using relative variance (normalized by speed^2) so both are scale-independent. float AimRemapped = (AdaptiveDeadZone < 1.0f)
double PosSpeed2 = AvgPosVel.SizeSquared(); ? FMath::Clamp((AimRatio - AdaptiveDeadZone) / (1.0f - AdaptiveDeadZone), 0.0f, 1.0f)
double AimSpeed2 = AvgAimVel.SizeSquared(); : (AimRatio >= 1.0f ? 1.0f : 0.0f);
AimConfidence = FMath::Pow(AimRemapped, AdaptiveSensitivity);
double PosAbsVariance = PosVelVariance.X + PosVelVariance.Y + PosVelVariance.Z; }
double AimAbsVariance = AimVelVariance.X + AimVelVariance.Y + AimVelVariance.Z;
// Extrapolate from last safe sample // Extrapolate from last safe sample
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1]; const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
double ExtrapolationTime = CurrentTime - LastSafe.Timestamp; double ExtrapolationTime = CurrentTime - LastSafe.Timestamp;
// Relative variance = variance / speed^2. When speed is ~0, extrapolation offset // Write debug values for HUD display
// is negligible anyway so confidence doesn't matter — set to 1. DbgPosRatio = PosRatio;
// Low-speed protection: at low speed the extrapolation offset (speed * dt) is tiny, DbgAimRatio = AimRatio;
// so there's no overshoot to prevent. We blend confidence back toward 1.0 when the DbgPosConfidence = PosConfidence;
// potential extrapolation offset is small (< 1cm for position, < 0.01 for aim direction). DbgAimConfidence = AimConfidence;
float PosConfidence = 1.0f; DbgAvgPosSpeed = AvgPosSpeed;
if (PosSpeed2 > SMALL_NUMBER) DbgAvgAimSpeed = AvgAimSpeed;
{ DbgRecentPosSpeed = RecentPosSpeed;
double PosRelVar = PosAbsVariance / PosSpeed2; DbgRecentAimSpeed = RecentAimSpeed;
PosConfidence = 1.0f / (1.0f + (float)PosRelVar * AdaptiveSensitivity); DbgExtrapolationTime = (float)ExtrapolationTime;
// Low-speed protection: if extrapolation offset < 1cm, blend confidence toward 1 // Apply optional damping
float PosExtrapOffset = FMath::Sqrt((float)PosSpeed2) * (float)ExtrapolationTime;
float PosOffsetSignificance = FMath::Clamp(PosExtrapOffset / 1.0f, 0.0f, 1.0f); // 1cm threshold
PosConfidence = FMath::Lerp(1.0f, PosConfidence, PosOffsetSignificance);
}
float AimConfidence = 1.0f;
if (AimSpeed2 > SMALL_NUMBER)
{
double AimRelVar = AimAbsVariance / AimSpeed2;
AimConfidence = 1.0f / (1.0f + (float)AimRelVar * AdaptiveSensitivity);
// Low-speed protection: if aim extrapolation offset is tiny, blend toward 1
float AimExtrapOffset = FMath::Sqrt((float)AimSpeed2) * (float)ExtrapolationTime;
float AimOffsetSignificance = FMath::Clamp(AimExtrapOffset / 0.01f, 0.0f, 1.0f); // 0.01 unit threshold
AimConfidence = FMath::Lerp(1.0f, AimConfidence, AimOffsetSignificance);
}
// Apply optional damping on top of confidence
float DampingScale = 1.0f; float DampingScale = 1.0f;
if (ExtrapolationDamping > 0.0f) if (ExtrapolationDamping > 0.0f)
{ {

View File

@ -32,6 +32,19 @@ void UEBBarrel::BeginPlay()
} }
} }
void UEBBarrel::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// Close CSV file on stop/exit so the file isn't left locked
if (bCSVFileOpen && CSVFileHandle)
{
delete CSVFileHandle;
CSVFileHandle = nullptr;
bCSVFileOpen = false;
}
Super::EndPlay(EndPlayReason);
}
void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{ {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction); Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
@ -208,6 +221,50 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
} }
} }
// Anti-Recoil Debug HUD
if (DebugAntiRecoilHUD && GEngine && AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation)
{
// Compute live errors
FVector RealPos = GetComponentTransform().GetLocation();
FVector RealAim = GetComponentTransform().GetUnitAxis(EAxis::X);
DbgPosError = FVector::Dist(Location, RealPos);
float AimDot = FMath::Clamp(FVector::DotProduct(Aim, RealAim), -1.0f, 1.0f);
DbgAimError = FMath::RadiansToDegrees(FMath::Acos(AimDot));
int32 HudKey = -500;
FColor HudTitle = FColor::Yellow;
FColor HudVal = FColor::White;
FColor HudGood = FColor::Green;
FColor HudWarn = FColor::Orange;
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudTitle,
TEXT("====== ADAPTIVE EXTRAPOLATION ======"));
// Speed ratio + confidence (position)
FColor PosColor = (DbgPosConfidence > 0.9f) ? HudGood : HudWarn;
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, PosColor,
FString::Printf(TEXT(" Pos: ratio=%.2f conf=%.2f speed=%.1f/%.1f cm/s"),
DbgPosRatio, DbgPosConfidence, DbgRecentPosSpeed, DbgAvgPosSpeed));
// Speed ratio + confidence (aim)
FColor AimColor = (DbgAimConfidence > 0.9f) ? HudGood : HudWarn;
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, AimColor,
FString::Printf(TEXT(" Aim: ratio=%.2f conf=%.2f speed=%.4f/%.4f /s"),
DbgAimRatio, DbgAimConfidence, DbgRecentAimSpeed, DbgAvgAimSpeed));
// Extrapolation time
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudVal,
FString::Printf(TEXT(" Extrap: %.0f ms"), DbgExtrapolationTime * 1000.0f));
// Errors
FColor PosErrColor = (DbgPosError < 2.0f) ? HudGood : (DbgPosError < 5.0f) ? HudVal : HudWarn;
FColor AimErrColor = (DbgAimError < 2.0f) ? HudGood : (DbgAimError < 5.0f) ? HudVal : HudWarn;
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, PosErrColor,
FString::Printf(TEXT(" Pos error: %.2f cm"), DbgPosError));
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, AimErrColor,
FString::Printf(TEXT(" Aim error: %.2f deg"), DbgAimError));
}
// Calibration HUD // Calibration HUD
if (CalibrateAntiRecoil && GEngine) if (CalibrateAntiRecoil && GEngine)
{ {

View File

@ -31,7 +31,7 @@ enum class EAntiRecoilMode : uint8
ARM_WeightedLinearRegression UMETA(DisplayName = "Weighted Linear Regression", ToolTip = "Fits a weighted least-squares line (y=a+bt) through safe samples. Recent samples weighted higher (controlled by RegressionWeightExponent). Simple, stable, no oscillation. May overshoot on sudden stops since it assumes constant velocity."), ARM_WeightedLinearRegression UMETA(DisplayName = "Weighted Linear Regression", ToolTip = "Fits a weighted least-squares line (y=a+bt) through safe samples. Recent samples weighted higher (controlled by RegressionWeightExponent). Simple, stable, no oscillation. May overshoot on sudden stops since it assumes constant velocity."),
ARM_WeightedRegression UMETA(DisplayName = "Weighted Quadratic Regression", ToolTip = "Fits a weighted quadratic curve (y=a+bt+ct^2) through safe samples, capturing deceleration naturally. Blends smoothly between linear and quadratic based on acceleration significance. Falls back to linear with < 3 samples. Includes velocity-reversal clamping."), ARM_WeightedRegression UMETA(DisplayName = "Weighted Quadratic Regression", ToolTip = "Fits a weighted quadratic curve (y=a+bt+ct^2) through safe samples, capturing deceleration naturally. Blends smoothly between linear and quadratic based on acceleration significance. Falls back to linear with < 3 samples. Includes velocity-reversal clamping."),
ARM_KalmanFilter UMETA(DisplayName = "Kalman Filter", ToolTip = "Maintains an internal state model (position + velocity, aim + angular velocity) updated only with safe samples. Predicts forward using the estimated dynamics. Best for smooth continuous tracking with optimal noise rejection. Requires tuning ProcessNoise and MeasurementNoise for best results."), ARM_KalmanFilter UMETA(DisplayName = "Kalman Filter", ToolTip = "Maintains an internal state model (position + velocity, aim + angular velocity) updated only with safe samples. Predicts forward using the estimated dynamics. Best for smooth continuous tracking with optimal noise rejection. Requires tuning ProcessNoise and MeasurementNoise for best results."),
ARM_AdaptiveExtrapolation UMETA(DisplayName = "Adaptive Extrapolation", ToolTip = "Computes weighted average velocity from safe samples, then scales extrapolation by a confidence factor based on velocity consistency. When velocity is steady (low variance), full extrapolation. When velocity changes rapidly (deceleration/direction change), extrapolation is reduced automatically. One tuning parameter: AdaptiveSensitivity.") ARM_AdaptiveExtrapolation UMETA(DisplayName = "Adaptive Extrapolation", ToolTip = "Deceleration-aware linear extrapolation. Compares recent speed (last 25% of safe window) to average speed. During steady movement: full extrapolation (zero lag). During deceleration/stop: extrapolation is reduced proportionally. Prevents overshoot on fast draw-aim-fire sequences without adding lag during normal tracking. Tuning: AdaptiveSensitivity controls the power curve (1=linear, 2=aggressive, 0.5=gentle).")
}; };
USTRUCT() USTRUCT()
@ -82,6 +82,7 @@ public:
UEBBarrel(); UEBBarrel();
virtual void BeginPlay() override; virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
// Called every frame // Called every frame
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
@ -105,6 +106,9 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Calibration", meta = (ToolTip = "Number of shots per calibration sequence.", EditCondition = "CalibrateAntiRecoil", ClampMin = "3", ClampMax = "50")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Calibration", meta = (ToolTip = "Number of shots per calibration sequence.", EditCondition = "CalibrateAntiRecoil", ClampMin = "3", ClampMax = "50"))
int32 CalibrationShotCount = 10; int32 CalibrationShotCount = 10;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Show real-time anti-recoil prediction HUD: speed ratio, confidence, position/aim errors, extrapolation time."))
bool DebugAntiRecoilHUD = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|CSV Recording", meta = (ToolTip = "Record per-frame prediction data to CSV for offline analysis. File saved to project Saved/Logs/ folder. Toggle off to stop and close the file.")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|CSV Recording", meta = (ToolTip = "Record per-frame prediction data to CSV for offline analysis. File saved to project Saved/Logs/ folder. Toggle off to stop and close the file."))
bool RecordPredictionCSV = false; bool RecordPredictionCSV = false;
@ -113,6 +117,19 @@ public:
FString CSVFilePath; FString CSVFilePath;
IFileHandle* CSVFileHandle = nullptr; IFileHandle* CSVFileHandle = nullptr;
// Debug HUD state (written by const prediction functions, read by TickComponent)
mutable float DbgPosRatio = 0.0f;
mutable float DbgAimRatio = 0.0f;
mutable float DbgPosConfidence = 0.0f;
mutable float DbgAimConfidence = 0.0f;
mutable float DbgAvgPosSpeed = 0.0f;
mutable float DbgAvgAimSpeed = 0.0f;
mutable float DbgRecentPosSpeed = 0.0f;
mutable float DbgRecentAimSpeed = 0.0f;
mutable float DbgExtrapolationTime = 0.0f;
float DbgPosError = 0.0f;
float DbgAimError = 0.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Enable IMU shock simulation for testing anti-recoil prediction without firing")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Enable IMU shock simulation for testing anti-recoil prediction without firing"))
bool DebugSimulateIMUShock = false; bool DebugSimulateIMUShock = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Angular perturbation intensity in degrees", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.0")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Angular perturbation intensity in degrees", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.0"))
@ -142,8 +159,11 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Kalman filter measurement noise (higher = trusts model over measurements, lower = trusts measurements). Should be low since safe samples are clean.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_KalmanFilter", ClampMin = "0.001")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Kalman filter measurement noise (higher = trusts model over measurements, lower = trusts measurements). Should be low since safe samples are clean.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_KalmanFilter", ClampMin = "0.001"))
float KalmanMeasurementNoise = 0.01f; float KalmanMeasurementNoise = 0.01f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Controls how sensitive the adaptive extrapolation is to velocity changes. Higher = more aggressive reduction during velocity changes (less overshoot but more latency). Lower = more permissive extrapolation. Uses relative variance so values are scale-independent. Typical range: 50-200. Use the analyze_antirecoil.py tool to find the optimal value for your setup.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation", ClampMin = "0.01", ClampMax = "500.0")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Power curve exponent for deceleration detection. Controls how aggressively slowing down reduces extrapolation. confidence = (remappedRatio)^sensitivity. 1.0 = linear (gentle). 2.0 = quadratic (aggressive). 0.5 = square root (very gentle). During steady movement, ratio is ~1 so confidence is always 1 regardless of this value.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation", ClampMin = "0.1", ClampMax = "5.0"))
float AdaptiveSensitivity = 100.0f; float AdaptiveSensitivity = 1.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Dead zone for deceleration detection. Speed ratios (recent/avg) above this value are treated as 1.0 (no correction). Only ratios below trigger extrapolation reduction. Higher = more tolerant to natural speed fluctuations (less false positives). Lower = more sensitive to deceleration. 0.8 = ignore normal jitter, only react to real braking.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation", ClampMin = "0.0", ClampMax = "0.95"))
float AdaptiveDeadZone = 0.8f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Velocity damping during extrapolation. 0 = disabled (default). Higher values cause extrapolated velocity to decay exponentially toward zero over the discard window. Reduces overshoot on fast draw-aim-fire sequences where the user stops moving before firing. Applies to all prediction modes except Buffer. Typical range: 5-15.", ClampMin = "0.0", ClampMax = "50.0")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Velocity damping during extrapolation. 0 = disabled (default). Higher values cause extrapolated velocity to decay exponentially toward zero over the discard window. Reduces overshoot on fast draw-aim-fire sequences where the user stops moving before firing. Applies to all prediction modes except Buffer. Typical range: 5-15.", ClampMin = "0.0", ClampMax = "50.0"))
float ExtrapolationDamping = 0.0f; float ExtrapolationDamping = 0.0f;