From af723c944b9e7031067ee9e7fd29782cabc3e20d Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Mon, 16 Mar 2026 18:25:54 +0100 Subject: [PATCH] Rework adaptive extrapolation: deceleration detection + dead zone + debug HUD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Tools/analyze_antirecoil.py | 96 ++++++------ .../Private/AntiRecoilPredict.cpp | 137 +++++++++--------- .../EasyBallistics/Private/EBBarrel.cpp | 57 ++++++++ .../Source/EasyBallistics/Public/EBBarrel.h | 26 +++- 4 files changed, 197 insertions(+), 119 deletions(-) diff --git a/Tools/analyze_antirecoil.py b/Tools/analyze_antirecoil.py index 8ae3947..695ab30 100644 --- a/Tools/analyze_antirecoil.py +++ b/Tools/analyze_antirecoil.py @@ -131,75 +131,71 @@ def simulate_adaptive(frames: List[Frame], sensitivity: float) -> List[Tuple[flo pos_errors = [] aim_errors = [] - # Sliding window for velocity estimation + # Sliding window for velocity estimation (matches C++ safe window ~18 samples) window_size = 12 SMALL = 1e-10 for i in range(window_size + 1, len(frames) - 1): # Build velocity pairs from recent real positions and aims - pos_velocities = [] - aim_velocities = [] - weights = [] + pos_vels = [] + aim_vels = [] for j in range(1, min(window_size, i)): dt = frames[i - j].timestamp - frames[i - j - 1].timestamp if dt > 1e-6: 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) - pos_velocities.append(pos_vel) - aim_velocities.append(aim_vel) - w = (window_size - j) ** 2 - weights.append(w) + pos_vels.append(pos_vel) + aim_vels.append(aim_vel) - if len(pos_velocities) < 2: + if len(pos_vels) < 4: 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_aim_vel = (0.0, 0.0, 0.0) - for pv, av, w in zip(pos_velocities, aim_velocities, weights): - avg_pos_vel = vec_add(avg_pos_vel, vec_scale(pv, w / total_w)) - avg_aim_vel = vec_add(avg_aim_vel, vec_scale(av, w / total_w)) + for k in range(n): + w = (k + 1) ** 2 + 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) - pos_var_sum = 0.0 - aim_var_sum = 0.0 - for pv, av, w in zip(pos_velocities, aim_velocities, weights): - pd = vec_sub(pv, avg_pos_vel) - ad = vec_sub(av, avg_aim_vel) - pos_var_sum += (pd[0]**2 + pd[1]**2 + pd[2]**2) * w - aim_var_sum += (ad[0]**2 + ad[1]**2 + ad[2]**2) * w - pos_variance = pos_var_sum / total_w - aim_variance = aim_var_sum / total_w + # Deceleration detection: recent speed (last 25%) vs average speed + recent_start = max(0, n - max(1, n // 4)) + recent_count = n - recent_start + recent_pos_vel = (0.0, 0.0, 0.0) + recent_aim_vel = (0.0, 0.0, 0.0) + for k in range(recent_start, n): + recent_pos_vel = vec_add(recent_pos_vel, pos_vels[k]) + recent_aim_vel = vec_add(recent_aim_vel, aim_vels[k]) + recent_pos_vel = vec_scale(recent_pos_vel, 1.0 / recent_count) + recent_aim_vel = vec_scale(recent_aim_vel, 1.0 / recent_count) - # Separate confidences with RELATIVE variance (matching C++ code) - pos_speed2 = sum(v**2 for v in avg_pos_vel) - aim_speed2 = sum(v**2 for v in avg_aim_vel) + avg_pos_speed = vec_len(avg_pos_vel) + avg_aim_speed = vec_len(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 extrap_dt = frames[i].extrap_time if frames[i].extrap_time > 0 else 0.011 - pos_confidence = 1.0 - 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 + # Predict from last safe position 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 = 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: - """Search for optimal AdaptiveSensitivity (no scaling factor needed).""" - print("\nSearching for optimal AdaptiveSensitivity...") + """Search for optimal AdaptiveSensitivity (power curve exponent for deceleration detection).""" + print("\nSearching for optimal AdaptiveSensitivity (power exponent)...") print("-" * 60) best_score = float('inf') best_sensitivity = 1.0 - # Search grid — only sensitivity, no more scaling factor - 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] + # Search grid — exponent range 0.1 to 5.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 = [] diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/AntiRecoilPredict.cpp b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/AntiRecoilPredict.cpp index e687988..be5423a 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/AntiRecoilPredict.cpp +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/AntiRecoilPredict.cpp @@ -717,107 +717,112 @@ void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLoc // Compute weighted velocities from consecutive safe sample pairs // Weight recent pairs more heavily TArray PosVelocities; - TArray AimVelocities; - TArray Weights; - PosVelocities.Reserve(SafeN - 1); - AimVelocities.Reserve(SafeN - 1); - Weights.Reserve(SafeN - 1); + // Compute per-pair velocities + TArray PosVels; + TArray AimVels; + PosVels.Reserve(SafeN - 1); + AimVels.Reserve(SafeN - 1); for (int32 i = 1; i < SafeN; i++) { double dt = TransformHistory[i].Timestamp - TransformHistory[i - 1].Timestamp; if (dt > SMALL_NUMBER) { - FVector PosVel = (TransformHistory[i].Location - TransformHistory[i - 1].Location) / dt; - FVector AimVel = (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); + PosVels.Add((TransformHistory[i].Location - TransformHistory[i - 1].Location) / dt); + AimVels.Add((TransformHistory[i].Aim - TransformHistory[i - 1].Aim) / dt); } } - if (PosVelocities.Num() == 0) + if (PosVels.Num() == 0) { OutLocation = TransformHistory[SafeN - 1].Location; OutAim = TransformHistory[SafeN - 1].Aim; return; } - // Compute weighted average velocity + // Compute overall weighted average velocity (recent samples weighted more) double TotalWeight = 0.0; FVector AvgPosVel = 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]; - AvgAimVel += AimVelocities[i] * Weights[i]; - TotalWeight += Weights[i]; + double w = FMath::Pow((double)(i + 1), 2.0); + AvgPosVel += PosVels[i] * w; + AvgAimVel += AimVels[i] * w; + TotalWeight += w; } AvgPosVel /= TotalWeight; AvgAimVel /= TotalWeight; - // Compute weighted variance of velocity (how consistent is the velocity?) - FVector PosVelVariance = FVector::ZeroVector; - FVector AimVelVariance = FVector::ZeroVector; + // Deceleration detection: compare recent speed (last 25% of pairs) vs overall speed. + // If the user is stopping, recent speed will drop toward 0 while avg is still high. + // 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; - FVector AimDiff = AimVelocities[i] - AvgAimVel; - 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 += PosVels[i]; + RecentAimVel += AimVels[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; - AimVelVariance /= TotalWeight; - - // 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). - // Using relative variance (normalized by speed^2) so both are scale-independent. - double PosSpeed2 = AvgPosVel.SizeSquared(); - double AimSpeed2 = AvgAimVel.SizeSquared(); - - double PosAbsVariance = PosVelVariance.X + PosVelVariance.Y + PosVelVariance.Z; - double AimAbsVariance = AimVelVariance.X + AimVelVariance.Y + AimVelVariance.Z; + float AimRatio = 1.0f; + float AimConfidence = 1.0f; + if (AvgAimSpeed > SMALL_NUMBER) + { + AimRatio = FMath::Clamp(RecentAimSpeed / AvgAimSpeed, 0.0f, 1.0f); + float AimRemapped = (AdaptiveDeadZone < 1.0f) + ? FMath::Clamp((AimRatio - AdaptiveDeadZone) / (1.0f - AdaptiveDeadZone), 0.0f, 1.0f) + : (AimRatio >= 1.0f ? 1.0f : 0.0f); + AimConfidence = FMath::Pow(AimRemapped, AdaptiveSensitivity); + } // Extrapolate from last safe sample const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1]; double ExtrapolationTime = CurrentTime - LastSafe.Timestamp; - // Relative variance = variance / speed^2. When speed is ~0, extrapolation offset - // is negligible anyway so confidence doesn't matter — set to 1. - // Low-speed protection: at low speed the extrapolation offset (speed * dt) is tiny, - // so there's no overshoot to prevent. We blend confidence back toward 1.0 when the - // potential extrapolation offset is small (< 1cm for position, < 0.01 for aim direction). - float PosConfidence = 1.0f; - if (PosSpeed2 > SMALL_NUMBER) - { - double PosRelVar = PosAbsVariance / PosSpeed2; - PosConfidence = 1.0f / (1.0f + (float)PosRelVar * AdaptiveSensitivity); + // Write debug values for HUD display + DbgPosRatio = PosRatio; + DbgAimRatio = AimRatio; + DbgPosConfidence = PosConfidence; + DbgAimConfidence = AimConfidence; + DbgAvgPosSpeed = AvgPosSpeed; + DbgAvgAimSpeed = AvgAimSpeed; + DbgRecentPosSpeed = RecentPosSpeed; + DbgRecentAimSpeed = RecentAimSpeed; + DbgExtrapolationTime = (float)ExtrapolationTime; - // Low-speed protection: if extrapolation offset < 1cm, blend confidence toward 1 - 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 + // Apply optional damping float DampingScale = 1.0f; if (ExtrapolationDamping > 0.0f) { diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/EBBarrel.cpp b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/EBBarrel.cpp index 34741a0..0fcb7b6 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/EBBarrel.cpp +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/EBBarrel.cpp @@ -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) { 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 if (CalibrateAntiRecoil && GEngine) { diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Public/EBBarrel.h b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Public/EBBarrel.h index 18996c9..057e3f9 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Public/EBBarrel.h +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Public/EBBarrel.h @@ -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_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_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() @@ -82,6 +82,7 @@ public: UEBBarrel(); virtual void BeginPlay() override; + virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; // Called every frame 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")) 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.")) bool RecordPredictionCSV = false; @@ -113,6 +117,19 @@ public: FString CSVFilePath; 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")) bool DebugSimulateIMUShock = false; 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")) 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")) - float AdaptiveSensitivity = 100.0f; + 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 = 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")) float ExtrapolationDamping = 0.0f;