diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.dll b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.dll index 03b4356..d15d2bc 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.dll and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.dll differ diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.exp b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.exp index 21a82fe..adc67f9 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.exp and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.exp differ diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.exe b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.exe index 9e5965a..60a2577 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.exe and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.exe differ diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.exp b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.exp index a324a26..902045a 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.exp and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.exp differ diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.pdb b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.pdb index 18be167..e7a36be 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.pdb and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_0.pdb differ diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.exe b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.exe index 1e89b90..05285e9 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.exe and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.exe differ diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.exp b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.exp index e9c9516..dae2909 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.exp and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.exp differ diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.pdb b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.pdb index fddac82..3b416f5 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.pdb and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.patch_1.pdb differ diff --git a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.pdb b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.pdb index 710acc0..3549419 100644 Binary files a/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.pdb and b/Unreal/Plugins/PS_Ballistics/Binaries/Win64/UnrealEditor-EasyBallistics.pdb differ diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/AntiRecoilPredict.cpp b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/AntiRecoilPredict.cpp index 0b56857..be5423a 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/AntiRecoilPredict.cpp +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/AntiRecoilPredict.cpp @@ -29,6 +29,28 @@ static int32 GetSafeCount(const TArray& History, double C return SafeN; } +void UEBBarrel::TriggerDebugIMUShock() +{ + if (!DebugSimulateIMUShock) return; + + DebugIMUShockActive = true; + DebugIMUShockLineCaptured = false; // Reset so the new shock replaces the previous yellow line + DebugIMUShockStartTime = GetWorld()->GetTimeSeconds(); + + // Generate a random shock direction (sharp upward + random lateral, simulating recoil kick) + FVector RandomDir = FMath::VRand(); + // Bias upward to simulate typical recoil pattern + RandomDir.Z = FMath::Abs(RandomDir.Z) * 2.0f; + RandomDir.Normalize(); + + DebugIMUShockAimOffset = RandomDir * FMath::DegreesToRadians(DebugIMUShockAngle); + DebugIMUShockPosOffset = RandomDir * DebugIMUShockPosition; + + // Recoil timing: for simulated shocks, the corruption will appear in the buffer + // on subsequent ticks. No need to capture here — the continuous analysis in + // TickComponent will detect it automatically. +} + void UEBBarrel::UpdateTransformHistory() { if (AntiRecoilMode == EAntiRecoilMode::ARM_None) @@ -43,11 +65,39 @@ void UEBBarrel::UpdateTransformHistory() Sample.Location = GetComponentTransform().GetLocation(); Sample.Aim = GetComponentTransform().GetUnitAxis(EAxis::X); + // Apply simulated IMU shock perturbation if active + if (DebugSimulateIMUShock && DebugIMUShockActive) + { + float ElapsedShock = (float)(CurrentTime - DebugIMUShockStartTime); + if (ElapsedShock < DebugIMUShockDuration) + { + // Decaying shock: intensity decreases over the shock duration + float ShockAlpha = 1.0f - (ElapsedShock / DebugIMUShockDuration); + // Add some high-frequency noise to simulate IMU vibration + FVector FrameNoise = FMath::VRand() * 0.3f; + + // Perturb aim direction + FVector AimPerturbation = (DebugIMUShockAimOffset + FrameNoise * FMath::DegreesToRadians(DebugIMUShockAngle)) * ShockAlpha; + Sample.Aim = (Sample.Aim + AimPerturbation).GetSafeNormal(); + + // Perturb position + FVector PosPerturbation = (DebugIMUShockPosOffset + FrameNoise * DebugIMUShockPosition) * ShockAlpha; + Sample.Location += PosPerturbation; + } + else + { + DebugIMUShockActive = false; + } + } + TransformHistory.Add(Sample); - // Trim buffer: remove samples older than AntiRecoilBufferTimeMs - const float AntiRecoilBufferTime = AntiRecoilBufferTimeMs / 1000.0f; - double OldestAllowed = CurrentTime - FMath::Max(0.05f, AntiRecoilBufferTime); + // Trim buffer: remove samples older than AntiRecoilBufferTime + // During calibration, keep a larger buffer (0.5s min) for reliable 3-sigma analysis + float EffectiveBufferTime = CalibrateAntiRecoil + ? FMath::Max(AntiRecoilBufferTime, 0.5f) + : AntiRecoilBufferTime; + double OldestAllowed = CurrentTime - FMath::Max(0.05f, EffectiveBufferTime); while (TransformHistory.Num() > 0 && TransformHistory[0].Timestamp < OldestAllowed) { TransformHistory.RemoveAt(0); @@ -78,7 +128,7 @@ void UEBBarrel::ComputeAntiRecoilTransform() case EAntiRecoilMode::ARM_LinearExtrapolation: { - int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f); + int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); if (SafeN >= 2) { PredictLinearExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim); @@ -96,9 +146,29 @@ void UEBBarrel::ComputeAntiRecoilTransform() } break; + case EAntiRecoilMode::ARM_WeightedRegression: + { + int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); + if (SafeN >= 2) + { + PredictWeightedRegression(GetWorld()->GetTimeSeconds(), Location, Aim); + } + else if (TransformHistory.Num() > 0) + { + Aim = TransformHistory[0].Aim; + Location = TransformHistory[0].Location; + } + else + { + Aim = GetComponentTransform().GetUnitAxis(EAxis::X); + Location = GetComponentTransform().GetLocation(); + } + } + break; + case EAntiRecoilMode::ARM_WeightedLinearRegression: { - int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f); + int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); if (SafeN >= 2) { PredictWeightedLinearRegression(GetWorld()->GetTimeSeconds(), Location, Aim); @@ -118,7 +188,7 @@ void UEBBarrel::ComputeAntiRecoilTransform() case EAntiRecoilMode::ARM_KalmanFilter: { - int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f); + int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); if (SafeN > 0) { // Feed only the latest SAFE sample to the Kalman filter @@ -141,7 +211,7 @@ void UEBBarrel::ComputeAntiRecoilTransform() case EAntiRecoilMode::ARM_AdaptiveExtrapolation: { - int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f); + int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); if (SafeN >= 2) { PredictAdaptiveExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim); @@ -167,7 +237,7 @@ void UEBBarrel::ComputeAntiRecoilTransform() void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const { - const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f); + const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); if (SafeN < 2) { OutLocation = TransformHistory[0].Location; @@ -247,7 +317,7 @@ void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocat void UEBBarrel::PredictWeightedLinearRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const { - const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f); + const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); if (SafeN < 2) { OutLocation = TransformHistory[0].Location; @@ -313,6 +383,226 @@ void UEBBarrel::PredictWeightedLinearRegression(double CurrentTime, FVector& Out } } +// --- Weighted Quadratic Regression --- +// Fits a weighted least-squares quadratic (y = a + bt + ct^2) through SAFE samples. +// Captures deceleration naturally: if user stops before firing, c < 0 curves prediction toward stop. +// Falls back to linear fit if < 3 samples or ill-conditioned 3x3 system. + +void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const +{ + const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); + if (SafeN < 2) + { + OutLocation = TransformHistory[0].Location; + OutAim = TransformHistory[0].Aim; + return; + } + + // Use timestamps relative to the first safe sample to avoid precision issues + double T0 = TransformHistory[0].Timestamp; + + // Accumulate weighted sums for quadratic regression: y = a + b*t + c*t^2 + double SumW = 0.0; + double SumWT = 0.0; + double SumWTT = 0.0; + double SumWTTT = 0.0; + double SumWTTTT = 0.0; + FVector SumWY = FVector::ZeroVector; + FVector SumWTY = FVector::ZeroVector; + FVector SumWTTY = FVector::ZeroVector; + FVector SumWAim = FVector::ZeroVector; + FVector SumWTAim = FVector::ZeroVector; + FVector SumWTTAim = FVector::ZeroVector; + + for (int32 i = 0; i < SafeN; i++) + { + double w = FMath::Pow((double)(i + 1), (double)RegressionWeightExponent); + double t = TransformHistory[i].Timestamp - T0; + double tt = t * t; + + SumW += w; + SumWT += w * t; + SumWTT += w * tt; + SumWTTT += w * tt * t; + SumWTTTT += w * tt * tt; + SumWY += TransformHistory[i].Location * w; + SumWTY += TransformHistory[i].Location * (w * t); + SumWTTY += TransformHistory[i].Location * (w * tt); + SumWAim += TransformHistory[i].Aim * w; + SumWTAim += TransformHistory[i].Aim * (w * t); + SumWTTAim += TransformHistory[i].Aim * (w * tt); + } + + double TPred = CurrentTime - T0; + double TLastSafe = TransformHistory[SafeN - 1].Timestamp - T0; + + // Try quadratic fit (3x3 system) if we have enough samples + bool bUseQuadratic = false; + FVector PosA, PosB, PosC; + FVector AimA, AimB, AimC; + + if (SafeN >= 3) + { + // Normal equations for weighted quadratic: M * [a,b,c]^T = R + // M = | SumW SumWT SumWTT | + // | SumWT SumWTT SumWTTT | + // | SumWTT SumWTTT SumWTTTT | + // Solve by Cramer's rule (3x3 determinant) + double M00 = SumW, M01 = SumWT, M02 = SumWTT; + double M10 = SumWT, M11 = SumWTT, M12 = SumWTTT; + double M20 = SumWTT, M21 = SumWTTT, M22 = SumWTTTT; + + double Det3 = M00 * (M11 * M22 - M12 * M21) + - M01 * (M10 * M22 - M12 * M20) + + M02 * (M10 * M21 - M11 * M20); + + if (FMath::Abs(Det3) > SMALL_NUMBER) + { + bUseQuadratic = true; + double InvDet = 1.0 / Det3; + + // Cofactors for Cramer's rule + double C00 = M11 * M22 - M12 * M21; + double C01 = -(M10 * M22 - M12 * M20); + double C02 = M10 * M21 - M11 * M20; + double C10 = -(M01 * M22 - M02 * M21); + double C11 = M00 * M22 - M02 * M20; + double C12 = -(M00 * M21 - M01 * M20); + double C20 = M01 * M12 - M02 * M11; + double C21 = -(M00 * M12 - M02 * M10); + double C22 = M00 * M11 - M01 * M10; + + // Solve for position coefficients: a, b, c + PosA = (SumWY * C00 + SumWTY * C10 + SumWTTY * C20) * InvDet; + PosB = (SumWY * C01 + SumWTY * C11 + SumWTTY * C21) * InvDet; + PosC = (SumWY * C02 + SumWTY * C12 + SumWTTY * C22) * InvDet; + + // Solve for aim coefficients: a, b, c + AimA = (SumWAim * C00 + SumWTAim * C10 + SumWTTAim * C20) * InvDet; + AimB = (SumWAim * C01 + SumWTAim * C11 + SumWTTAim * C21) * InvDet; + AimC = (SumWAim * C02 + SumWTAim * C12 + SumWTTAim * C22) * InvDet; + + } + } + + // Always compute linear result (2x2 system) + FVector LinearLocation, LinearAim; + { + double Det = SumW * SumWTT - SumWT * SumWT; + if (FMath::Abs(Det) <= SMALL_NUMBER) + { + OutLocation = TransformHistory[SafeN - 1].Location; + OutAim = TransformHistory[SafeN - 1].Aim; + return; + } + + FVector PosIntercept = (SumWY * SumWTT - SumWTY * SumWT) / Det; + FVector PosSlope = (SumWTY * SumW - SumWY * SumWT) / Det; + FVector AimIntercept = (SumWAim * SumWTT - SumWTAim * SumWT) / Det; + FVector AimSlope = (SumWTAim * SumW - SumWAim * SumWT) / Det; + + LinearLocation = PosIntercept + PosSlope * TPred; + FVector PredAim = AimIntercept + AimSlope * TPred; + LinearAim = PredAim.GetSafeNormal(); + if (LinearAim.IsNearlyZero()) + { + LinearAim = TransformHistory[SafeN - 1].Aim; + } + } + + if (bUseQuadratic) + { + // Compute quadratic result with velocity-reversal clamping + FVector QuadLocation; + for (int32 Axis = 0; Axis < 3; Axis++) + { + double b = PosB[Axis]; + double c = PosC[Axis]; + double VelAtLastSafe = b + 2.0 * c * TLastSafe; + double VelAtPred = b + 2.0 * c * TPred; + double TUse = TPred; + + if (VelAtLastSafe * VelAtPred < 0.0 && FMath::Abs(c) > SMALL_NUMBER) + { + double TStop = -b / (2.0 * c); + if (TStop > TLastSafe && TStop < TPred) + { + TUse = TStop; + } + } + + QuadLocation[Axis] = PosA[Axis] + b * TUse + c * TUse * TUse; + } + + FVector QuadAimVec; + for (int32 Axis = 0; Axis < 3; Axis++) + { + double b = AimB[Axis]; + double c = AimC[Axis]; + double VelAtLastSafe = b + 2.0 * c * TLastSafe; + double VelAtPred = b + 2.0 * c * TPred; + double TUse = TPred; + + if (VelAtLastSafe * VelAtPred < 0.0 && FMath::Abs(c) > SMALL_NUMBER) + { + double TStop = -b / (2.0 * c); + if (TStop > TLastSafe && TStop < TPred) + { + TUse = TStop; + } + } + + QuadAimVec[Axis] = AimA[Axis] + b * TUse + c * TUse * TUse; + } + + FVector QuadAim = QuadAimVec.GetSafeNormal(); + if (QuadAim.IsNearlyZero()) + { + QuadAim = TransformHistory[SafeN - 1].Aim; + } + + // Smooth blend between linear and quadratic based on significance of c. + // Alpha = 0 → pure linear, Alpha = 1 → pure quadratic. + // Ramp from 0 to 1 as max(|c*t^2| / |b*t|) goes from 0.05 to 0.20. + double PosCRatio = (FMath::Max(PosB.GetAbsMax() * TPred, SMALL_NUMBER) > SMALL_NUMBER) + ? (PosC.GetAbsMax() * TPred * TPred) / (PosB.GetAbsMax() * TPred) : 0.0; + double AimCRatio = (FMath::Max(AimB.GetAbsMax() * TPred, SMALL_NUMBER) > SMALL_NUMBER) + ? (AimC.GetAbsMax() * TPred * TPred) / (AimB.GetAbsMax() * TPred) : 0.0; + double MaxRatio = FMath::Max(PosCRatio, AimCRatio); + float BlendAlpha = (float)FMath::Clamp((MaxRatio - 0.05) / (0.20 - 0.05), 0.0, 1.0); + + OutLocation = FMath::Lerp(LinearLocation, QuadLocation, BlendAlpha); + OutAim = FMath::Lerp(LinearAim, QuadAim, BlendAlpha).GetSafeNormal(); + if (OutAim.IsNearlyZero()) + { + OutAim = TransformHistory[SafeN - 1].Aim; + } + } + else + { + OutLocation = LinearLocation; + OutAim = LinearAim; + } + + // Apply optional velocity damping on the final result + // Blend toward the last safe sample position (i.e., reduce the extrapolation offset) + if (ExtrapolationDamping > 0.0f) + { + float ExtrapolationTime = (float)(TPred - TLastSafe); + float DampingScale = FMath::Exp(-ExtrapolationDamping * ExtrapolationTime); + FVector LastSafeLocation = TransformHistory[SafeN - 1].Location; + FVector LastSafeAim = TransformHistory[SafeN - 1].Aim; + + // Damping blends from full extrapolation (DampingScale=1) toward last safe sample (DampingScale=0) + OutLocation = LastSafeLocation + (OutLocation - LastSafeLocation) * DampingScale; + OutAim = FMath::Lerp(LastSafeAim, OutAim, DampingScale).GetSafeNormal(); + if (OutAim.IsNearlyZero()) + { + OutAim = LastSafeAim; + } + } +} + // --- Simplified Kalman Filter --- // Maintains state estimate [position, velocity, aim, angular_velocity] // Only fed with SAFE (non-contaminated) measurements, predicts forward to current time @@ -416,7 +706,7 @@ void UEBBarrel::PredictKalmanFilter(double CurrentTime, FVector& OutLocation, FV void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const { - const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f); + const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); if (SafeN < 2) { OutLocation = TransformHistory[0].Location; @@ -492,65 +782,46 @@ void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLoc float RecentAimSpeed = RecentAimVel.Size(); // Confidence: ratio of recent speed to average speed. - // Dead zone: ratios above AdaptiveDeadZone are treated as 1.0 (no correction). - // Only ratios BELOW AdaptiveDeadZone trigger extrapolation reduction. - // Below dead zone: remap [0, deadzone] → [0, 1] then apply sensitivity exponent. - // Minimum speed threshold: below this, speed ratios are unreliable (noise dominates), - // so we keep full confidence to avoid false deceleration detection. - const float MinSpeedThreshold = SMALL_NUMBER; - + // 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; - float PosRemapped = 1.0f; - if (AvgPosSpeed > MinSpeedThreshold) + if (AvgPosSpeed > SMALL_NUMBER) { PosRatio = FMath::Clamp(RecentPosSpeed / AvgPosSpeed, 0.0f, 1.0f); - if (PosRatio >= AdaptiveDeadZone) - { - // Inside dead zone: no correction, full confidence - PosRemapped = 1.0f; - PosConfidence = 1.0f; - } - else - { - // Below dead zone: real deceleration detected - // Remap [0, deadzone] → [0, 1] - PosRemapped = (AdaptiveDeadZone > SMALL_NUMBER) - ? FMath::Clamp(PosRatio / AdaptiveDeadZone, 0.0f, 1.0f) - : 0.0f; - PosConfidence = FMath::Pow(PosRemapped, AdaptiveSensitivity); - } + 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); } - // else: AvgPosSpeed <= MinSpeedThreshold → keep defaults (Ratio=1, Conf=1) float AimRatio = 1.0f; float AimConfidence = 1.0f; - float AimRemapped = 1.0f; - if (AvgAimSpeed > MinSpeedThreshold) + if (AvgAimSpeed > SMALL_NUMBER) { AimRatio = FMath::Clamp(RecentAimSpeed / AvgAimSpeed, 0.0f, 1.0f); - if (AimRatio >= AdaptiveDeadZone) - { - // Inside dead zone: no correction, full confidence - AimRemapped = 1.0f; - AimConfidence = 1.0f; - } - else - { - // Below dead zone: real deceleration detected - // Remap [0, deadzone] → [0, 1] - AimRemapped = (AdaptiveDeadZone > SMALL_NUMBER) - ? FMath::Clamp(AimRatio / AdaptiveDeadZone, 0.0f, 1.0f) - : 0.0f; - AimConfidence = FMath::Pow(AimRemapped, AdaptiveSensitivity); - } + 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); } - // else: AvgAimSpeed <= MinSpeedThreshold → keep defaults (Ratio=1, Conf=1) // Extrapolate from last safe sample const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1]; double ExtrapolationTime = CurrentTime - LastSafe.Timestamp; + // 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; + // Apply optional damping float DampingScale = 1.0f; if (ExtrapolationDamping > 0.0f) diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/BarrelProxy.cpp b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/BarrelProxy.cpp index 6bc31f8..e9f8f1b 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/BarrelProxy.cpp +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/BarrelProxy.cpp @@ -29,7 +29,7 @@ FPrimitiveSceneProxy* UEBBarrel::CreateSceneProxy() { const FLinearColor DrawColor = GetViewSelectionColor(FColor::Green, *View, IsSelected(), IsHovered(), true, IsIndividuallySelected()); FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex); - DrawDirectionalArrow(PDI, Transform, DrawColor, Component->DebugArrowSize, Component->DebugArrowSize*0.1f, 16, 0.0f); + DrawDirectionalArrow(PDI, Transform, DrawColor, Component->DebugArrowSize, Component->DebugArrowSize*0.1f, 16, Component->DebugArrowSize*0.01f); } } } diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/EBBarrel.cpp b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/EBBarrel.cpp index 2f06af8..0fcb7b6 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/EBBarrel.cpp +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/EBBarrel.cpp @@ -98,15 +98,62 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon // Green line: raw tracker data (potentially shocked if IMU simulation is active) DrawDebugLine(GetWorld(), RawLocation, RawLocation + RawAim * DebugAntiRecoilLineLength, - FColor::Green, false, -1.0f, 0, 0.0f); + FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness); // Red line: anti-recoil predicted aim (what would be used if shooting now) DrawDebugLine(GetWorld(), Location, Location + Aim * DebugAntiRecoilLineLength, - FColor::Red, false, -1.0f, 0, 0.0f); + FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness); - // Small dots at origins for clarity - DrawDebugPoint(GetWorld(), RawLocation, 3.0f, FColor::Green, false, -1.0f, 0); - DrawDebugPoint(GetWorld(), Location, 3.0f, FColor::Red, false, -1.0f, 0); + // Small spheres at origins for clarity + DrawDebugSphere(GetWorld(), RawLocation, 1.5f, 6, FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f); + DrawDebugSphere(GetWorld(), Location, 1.5f, 6, FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f); + + // Yellow line: shows where shot would land WITHOUT anti-recoil correction + // Captures raw aim at shock onset and persists for DebugIMUShockDisplayTime seconds + if (DebugIMUShockActive && !DebugIMUShockLineCaptured) + { + // Capture the raw (shocked) aim at the first frame of shock + DebugIMUShockCapturedLocation = RawLocation; + DebugIMUShockCapturedAim = RawAim; + DebugIMUShockLineCaptured = true; + DebugIMUShockLineEndTime = GetWorld()->GetTimeSeconds() + DebugIMUShockDisplayTime; + } + if (!DebugIMUShockActive && DebugIMUShockLineCaptured) + { + // Shock ended: keep displaying but update captured aim to worst-case (peak shock) + // which was already captured at onset + } + if (DebugIMUShockLineCaptured) + { + if (GetWorld()->GetTimeSeconds() < DebugIMUShockLineEndTime) + { + // Green persistent line: raw tracker aim at moment of shot (same color as real-time green) + DrawDebugLine(GetWorld(), DebugIMUShockCapturedLocation, + DebugIMUShockCapturedLocation + DebugIMUShockCapturedAim * DebugAntiRecoilLineLength, + FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness); + DrawDebugSphere(GetWorld(), DebugIMUShockCapturedLocation, 3.0f, 8, FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness); + } + else + { + DebugIMUShockLineCaptured = false; + } + } + + // Red persistent line: corrected aim retained for the shot (same color as real-time red) + if (DebugCorrectedShotLineCaptured) + { + if (GetWorld()->GetTimeSeconds() < DebugCorrectedShotLineEndTime) + { + DrawDebugLine(GetWorld(), DebugCorrectedShotCapturedLocation, + DebugCorrectedShotCapturedLocation + DebugCorrectedShotCapturedAim * DebugAntiRecoilLineLength, + FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness); + DrawDebugSphere(GetWorld(), DebugCorrectedShotCapturedLocation, 3.0f, 8, FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness); + } + else + { + DebugCorrectedShotLineCaptured = false; + } + } } // CSV Prediction Recording @@ -121,7 +168,7 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon if (CSVFileHandle) { bCSVFileOpen = true; - FString Header = TEXT("Timestamp,RealPosX,RealPosY,RealPosZ,RealAimX,RealAimY,RealAimZ,PredPosX,PredPosY,PredPosZ,PredAimX,PredAimY,PredAimZ,SafeCount,BufferCount,ExtrapolationTime,ShotFired\n"); + FString Header = TEXT("Timestamp,RealPosX,RealPosY,RealPosZ,RealAimX,RealAimY,RealAimZ,PredPosX,PredPosY,PredPosZ,PredAimX,PredAimY,PredAimZ,SafeCount,BufferCount,ExtrapolationTime\n"); auto HeaderUtf8 = StringCast(*Header); CSVFileHandle->Write((const uint8*)HeaderUtf8.Get(), HeaderUtf8.Length()); @@ -138,7 +185,7 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon FVector RealPos = GetComponentTransform().GetLocation(); FVector RealAim = GetComponentTransform().GetUnitAxis(EAxis::X); // Count safe samples (same logic as GetSafeCount in AntiRecoilPredict.cpp) - double SafeCutoff = GetWorld()->GetTimeSeconds() - (AntiRecoilDiscardTimeMs / 1000.0f); + double SafeCutoff = GetWorld()->GetTimeSeconds() - AntiRecoilDiscardTime; int32 SafeN = 0; for (int32 si = 0; si < TransformHistory.Num(); si++) { @@ -146,17 +193,15 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon } double ExtrapTime = (SafeN > 0) ? (GetWorld()->GetTimeSeconds() - TransformHistory[SafeN - 1].Timestamp) : 0.0; - FString Line = FString::Printf(TEXT("%.6f,%.4f,%.4f,%.4f,%.6f,%.6f,%.6f,%.4f,%.4f,%.4f,%.6f,%.6f,%.6f,%d,%d,%.6f,%d\n"), + FString Line = FString::Printf(TEXT("%.6f,%.4f,%.4f,%.4f,%.6f,%.6f,%.6f,%.4f,%.4f,%.4f,%.6f,%.6f,%.6f,%d,%d,%.6f\n"), GetWorld()->GetTimeSeconds(), RealPos.X, RealPos.Y, RealPos.Z, RealAim.X, RealAim.Y, RealAim.Z, Location.X, Location.Y, Location.Z, Aim.X, Aim.Y, Aim.Z, - SafeN, TransformHistory.Num(), ExtrapTime, - bShotFiredThisFrame ? 1 : 0); + SafeN, TransformHistory.Num(), ExtrapTime); auto LineUtf8 = StringCast(*Line); CSVFileHandle->Write((const uint8*)LineUtf8.Get(), LineUtf8.Length()); - bShotFiredThisFrame = false; } } else if (bCSVFileOpen) @@ -176,6 +221,122 @@ 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) + { + int32 CalKey = -600; + FColor CalTitle = FColor::Magenta; + FColor CalValue = FColor::White; + FColor CalGood = FColor::Green; + FColor CalWarn = FColor::Yellow; + + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalTitle, + TEXT("====== ANTI-RECOIL CALIBRATION ======")); + + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalValue, + FString::Printf(TEXT(" Collecting: %d / %d shots"), + CalibrationShotsCollected, CalibrationShotCount)); + + // Running stats from current sequence + if (CalibrationShots.Num() > 0) + { + // Quick re-analysis of already captured shots for running display + float RunMax = 0.0f, RunSum = 0.0f; + int32 RunCount = 0; + for (const FCalibrationShotData& S : CalibrationShots) + { + // Simplified: use buffer size as rough proxy until full analysis + // We'll show "pending analysis" for individual shots + RunCount++; + } + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalValue, + FString::Printf(TEXT(" %d shots captured, awaiting sequence completion..."), RunCount)); + } + else + { + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalValue, + TEXT(" Fire weapon to collect data...")); + } + + // Show last completed sequence results + if (LastCalibrationResult.bValid) + { + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalTitle, + TEXT(" --- Last Sequence Results ---")); + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalGood, + FString::Printf(TEXT(" Shots: %d (outliers removed: %d)"), + LastCalibrationResult.TotalShots, LastCalibrationResult.OutliersRemoved)); + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalGood, + FString::Printf(TEXT(" Corruption: Med %.3fs | P95 %.3fs | Max %.3fs"), + LastCalibrationResult.MedianCorruptionDuration, + LastCalibrationResult.P95CorruptionDuration, + LastCalibrationResult.MaxCorruptionDuration)); + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalGood, + FString::Printf(TEXT(" Peak deviations: %.2f deg | %.2f cm"), + LastCalibrationResult.AvgPeakAngleDeviation, + LastCalibrationResult.AvgPeakPositionDeviation)); + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn, + FString::Printf(TEXT(" >> DiscardTime: %.4fs"), + LastCalibrationResult.RecommendedDiscardTime)); + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn, + FString::Printf(TEXT(" >> BufferTime: %.4fs"), + LastCalibrationResult.RecommendedBufferTime)); + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn, + FString::Printf(TEXT(" >> KalmanProcessNoise: %.3f"), + LastCalibrationResult.RecommendedKalmanProcessNoise)); + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn, + FString::Printf(TEXT(" >> KalmanMeasurementNoise: %.4f"), + LastCalibrationResult.RecommendedKalmanMeasurementNoise)); + } + + GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalTitle, + TEXT("=================================")); + } + //Only server can tick if (GetOwner()->GetLocalRole() == ROLE_Authority){ @@ -301,6 +462,39 @@ void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim, in } } + // Capture yellow debug line on real shot: shows raw tracker aim at firing moment + // (where the bullet would go WITHOUT anti-recoil correction) + if (DebugAntiRecoil && TransformHistory.Num() > 0) + { + DebugIMUShockCapturedLocation = TransformHistory.Last().Location; + DebugIMUShockCapturedAim = TransformHistory.Last().Aim; + DebugIMUShockLineCaptured = true; + DebugIMUShockLineEndTime = GetWorld()->GetTimeSeconds() + DebugIMUShockDisplayTime; + + // Blue line: corrected aim direction retained for this shot (after anti-recoil filtering) + DebugCorrectedShotCapturedLocation = OutLocation; + DebugCorrectedShotCapturedAim = OutAim.GetSafeNormal(); + DebugCorrectedShotLineCaptured = true; + DebugCorrectedShotLineEndTime = GetWorld()->GetTimeSeconds() + DebugIMUShockDisplayTime; + } + + // Calibration: snapshot the entire buffer for offline analysis + if (CalibrateAntiRecoil && TransformHistory.Num() > 0) + { + FCalibrationShotData ShotData; + ShotData.BufferSnapshot = TransformHistory; + ShotData.ShotTime = GetWorld()->GetTimeSeconds(); + CalibrationShots.Add(MoveTemp(ShotData)); + CalibrationShotsCollected++; + + if (CalibrationShotsCollected >= CalibrationShotCount) + { + ComputeCalibrationResult(); + CalibrationShots.Empty(); + CalibrationShotsCollected = 0; + } + } + BeforeShotFired.Broadcast(); #ifdef WITH_EDITOR if (shotTrace) { @@ -364,8 +558,6 @@ void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim, in } } - bShotFiredThisFrame = true; - if (ReplicateShotFiredEvents) { ShotFiredMulticast(); } @@ -386,3 +578,258 @@ void UEBBarrel::ApplyRecoil_Implementation(UPrimitiveComponent* Component, FVect } } +void UEBBarrel::ComputeCalibrationResult() +{ + LastCalibrationResult = FCalibrationResult(); + LastCalibrationResult.TotalShots = CalibrationShots.Num(); + + if (CalibrationShots.Num() < 3) + { + return; + } + + TArray CorruptionDurations; + TArray PeakAngles; + TArray PeakPositions; + + // Collect all clean-sample acceleration variances and residuals for Kalman estimation + TArray AllAccelerationVariances; + TArray AllResidualVariances; + + for (const FCalibrationShotData& Shot : CalibrationShots) + { + const TArray& Buffer = Shot.BufferSnapshot; + if (Buffer.Num() < 5) + { + CorruptionDurations.Add(0.0f); + PeakAngles.Add(0.0f); + PeakPositions.Add(0.0f); + continue; + } + + // --- Auto-seuil 3-sigma corruption detection --- + // Baseline: 30% oldest samples (min 3) + int32 BaselineCount = FMath::Max(3, Buffer.Num() * 3 / 10); + + // Fit linear trend on baseline (position and aim vs time) + double T0 = Buffer[0].Timestamp; + + // Weighted means for linear regression on baseline + double SumT = 0.0, SumTT = 0.0; + FVector SumPos = FVector::ZeroVector, SumTPos = FVector::ZeroVector; + FVector SumAim = FVector::ZeroVector, SumTAim = FVector::ZeroVector; + + for (int32 i = 0; i < BaselineCount; i++) + { + double t = Buffer[i].Timestamp - T0; + SumT += t; + SumTT += t * t; + SumPos += Buffer[i].Location; + SumTPos += Buffer[i].Location * t; + SumAim += Buffer[i].Aim; + SumTAim += Buffer[i].Aim * t; + } + + double N = (double)BaselineCount; + double Det = N * SumTT - SumT * SumT; + + FVector PosIntercept, PosSlope, AimIntercept, AimSlope; + if (FMath::Abs(Det) > SMALL_NUMBER) + { + PosIntercept = (SumPos * SumTT - SumTPos * SumT) / Det; + PosSlope = (SumTPos * N - SumPos * SumT) / Det; + AimIntercept = (SumAim * SumTT - SumTAim * SumT) / Det; + AimSlope = (SumTAim * N - SumAim * SumT) / Det; + } + else + { + PosIntercept = SumPos / N; + PosSlope = FVector::ZeroVector; + AimIntercept = SumAim / N; + AimSlope = FVector::ZeroVector; + } + + // Compute residuals on baseline to get sigma + float SumAngleResidSq = 0.0f; + float SumPosResidSq = 0.0f; + + for (int32 i = 0; i < BaselineCount; i++) + { + double t = Buffer[i].Timestamp - T0; + FVector PredAim = (AimIntercept + AimSlope * t).GetSafeNormal(); + FVector PredPos = PosIntercept + PosSlope * t; + + float Dot = FVector::DotProduct(Buffer[i].Aim.GetSafeNormal(), PredAim); + float AngleResid = FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(Dot, -1.0f, 1.0f))); + float PosResid = FVector::Dist(Buffer[i].Location, PredPos); + + SumAngleResidSq += AngleResid * AngleResid; + SumPosResidSq += PosResid * PosResid; + } + + float SigmaAngle = FMath::Sqrt(SumAngleResidSq / FMath::Max(1.0f, N - 2.0f)); + float SigmaPos = FMath::Sqrt(SumPosResidSq / FMath::Max(1.0f, N - 2.0f)); + + // Minimum sigma to avoid zero-threshold (perfectly still tracker) + SigmaAngle = FMath::Max(SigmaAngle, 0.05f); + SigmaPos = FMath::Max(SigmaPos, 0.01f); + + float AngleThreshold = SigmaAngle * 3.0f; + float PosThreshold = SigmaPos * 3.0f; + + // Detect corruption in all samples after baseline + float ShotCorruptionDuration = 0.0f; + float ShotPeakAngle = 0.0f; + float ShotPeakPos = 0.0f; + double FirstCorruptedTime = 0.0; + + for (int32 i = BaselineCount; i < Buffer.Num(); i++) + { + double t = Buffer[i].Timestamp - T0; + FVector PredAim = (AimIntercept + AimSlope * t).GetSafeNormal(); + FVector PredPos = PosIntercept + PosSlope * t; + + float Dot = FVector::DotProduct(Buffer[i].Aim.GetSafeNormal(), PredAim); + float AngleDev = FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(Dot, -1.0f, 1.0f))); + float PosDev = FVector::Dist(Buffer[i].Location, PredPos); + + if (AngleDev > AngleThreshold || PosDev > PosThreshold) + { + if (FirstCorruptedTime == 0.0) + { + FirstCorruptedTime = Buffer[i].Timestamp; + } + if (AngleDev > ShotPeakAngle) ShotPeakAngle = AngleDev; + if (PosDev > ShotPeakPos) ShotPeakPos = PosDev; + } + } + + if (FirstCorruptedTime > 0.0) + { + ShotCorruptionDuration = (float)(Shot.ShotTime - FirstCorruptedTime); + } + + CorruptionDurations.Add(ShotCorruptionDuration); + PeakAngles.Add(ShotPeakAngle); + PeakPositions.Add(ShotPeakPos); + + // --- Kalman parameter estimation from clean baseline samples --- + // ProcessNoise: variance of acceleration (velocity changes between consecutive samples) + if (BaselineCount >= 3) + { + float SumAccelSq = 0.0f; + int32 AccelCount = 0; + FVector PrevVel = FVector::ZeroVector; + bool bHasPrevVel = false; + + for (int32 i = 1; i < BaselineCount; i++) + { + double dt = Buffer[i].Timestamp - Buffer[i - 1].Timestamp; + if (dt > SMALL_NUMBER) + { + FVector Vel = (Buffer[i].Location - Buffer[i - 1].Location) / dt; + if (bHasPrevVel) + { + FVector Accel = (Vel - PrevVel) / dt; + SumAccelSq += Accel.SizeSquared(); + AccelCount++; + } + PrevVel = Vel; + bHasPrevVel = true; + } + } + if (AccelCount > 0) + { + AllAccelerationVariances.Add(SumAccelSq / AccelCount); + } + } + + // MeasurementNoise: variance of residuals from linear trend + if (SumPosResidSq > 0.0f) + { + AllResidualVariances.Add(SumPosResidSq / FMath::Max(1.0f, N - 2.0f)); + } + } + + // --- Aggregate timing statistics --- + CorruptionDurations.Sort(); + int32 Num = CorruptionDurations.Num(); + + // IQR outlier removal + float Q1 = CorruptionDurations[Num / 4]; + float Q3 = CorruptionDurations[(3 * Num) / 4]; + float IQR = Q3 - Q1; + float UpperFence = Q3 + 1.5f * IQR; + + TArray CleanDurations; + int32 OutlierCount = 0; + for (float D : CorruptionDurations) + { + if (D <= UpperFence) + CleanDurations.Add(D); + else + OutlierCount++; + } + + if (CleanDurations.Num() < 3) + { + CleanDurations = CorruptionDurations; + OutlierCount = 0; + } + + CleanDurations.Sort(); + int32 CN = CleanDurations.Num(); + + float Median = CleanDurations[CN / 2]; + int32 P95Index = FMath::Min((int32)(CN * 0.95f), CN - 1); + float P95 = CleanDurations[P95Index]; + float Max = CleanDurations.Last(); + + float RecommendedDiscard = P95 * 1.3f; + RecommendedDiscard = FMath::Clamp(RecommendedDiscard, 0.011f, 0.2f); + + float SafeWindow = FMath::Max(0.05f, RecommendedDiscard * 0.5f); + float RecommendedBuffer = RecommendedDiscard + SafeWindow; + + // Average peak deviations + float SumAngle = 0.0f, SumPos = 0.0f; + for (int32 i = 0; i < PeakAngles.Num(); i++) + { + SumAngle += PeakAngles[i]; + SumPos += PeakPositions[i]; + } + + // --- Kalman parameter recommendations --- + float RecommendedProcessNoise = 200.0f; // default fallback + float RecommendedMeasurementNoise = 0.01f; // default fallback + + if (AllAccelerationVariances.Num() > 0) + { + float SumAccelVar = 0.0f; + for (float V : AllAccelerationVariances) SumAccelVar += V; + RecommendedProcessNoise = SumAccelVar / AllAccelerationVariances.Num(); + // Clamp to sane range + RecommendedProcessNoise = FMath::Clamp(RecommendedProcessNoise, 0.1f, 10000.0f); + } + + if (AllResidualVariances.Num() > 0) + { + float SumResidVar = 0.0f; + for (float V : AllResidualVariances) SumResidVar += V; + RecommendedMeasurementNoise = SumResidVar / AllResidualVariances.Num(); + RecommendedMeasurementNoise = FMath::Clamp(RecommendedMeasurementNoise, 0.001f, 100.0f); + } + + // Populate result + LastCalibrationResult.RecommendedDiscardTime = RecommendedDiscard; + LastCalibrationResult.RecommendedBufferTime = RecommendedBuffer; + LastCalibrationResult.RecommendedKalmanProcessNoise = RecommendedProcessNoise; + LastCalibrationResult.RecommendedKalmanMeasurementNoise = RecommendedMeasurementNoise; + LastCalibrationResult.MedianCorruptionDuration = Median; + LastCalibrationResult.P95CorruptionDuration = P95; + LastCalibrationResult.MaxCorruptionDuration = Max; + LastCalibrationResult.AvgPeakAngleDeviation = SumAngle / PeakAngles.Num(); + LastCalibrationResult.AvgPeakPositionDeviation = SumPos / PeakPositions.Num(); + LastCalibrationResult.OutliersRemoved = OutlierCount; + LastCalibrationResult.bValid = true; +} \ No newline at end of file diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/Trace.cpp b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/Trace.cpp index 173ece1..5fee6c4 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/Trace.cpp +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Private/Trace.cpp @@ -80,58 +80,22 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn } if (MaterialDensityControlsPenetrationDepth) { - float SafeDensity = FMath::Max(PhysMaterial->Density, 0.001f); - penDepthMultiplier /= SafeDensity; + penDepthMultiplier /= PhysMaterial->Density; } if (MaterialRestitutionControlsRicochet) { - RicochetRestitution *= FMath::Clamp(PhysMaterial->Restitution, 0.0f, 1.0f); + RicochetRestitution *= PhysMaterial->Restitution; } } - // Guard: if bullet has near-zero velocity, stop it immediately - if (Velocity.SizeSquared() < SMALL_NUMBER) - { - SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin); - FVector Impulse = Velocity * Mass * ImpulseMultiplier; - if (AddImpulse && HitResult.Component->IsSimulatingPhysics()) { - HitResult.Component->AddImpulseAtLocation(Impulse, HitResult.Location, HitResult.BoneName); - } - if (HasAuthority()) { - OnImpact(false, false, HitResult.Location, Velocity, HitResult.Normal, GetActorLocation(), FVector::ZeroVector, Impulse, 0.0f, HitResult.GetActor(), HitResult.Component.Get(), HitResult.BoneName, PhysMaterial, HitResult, fireEventID); - } else { - OnNetPredictedImpact(false, false, HitResult.Location, Velocity, HitResult.Normal, GetActorLocation(), FVector::ZeroVector, Impulse, 0.0f, HitResult.GetActor(), HitResult.Component.Get(), HitResult.BoneName, PhysMaterial, HitResult, fireEventID); - } - Velocity = FVector::ZeroVector; - Deactivate(); - return 0.0f; - } - float dot = FVector::DotProduct(Velocity.GetSafeNormal(), HitResult.Normal) + 1.0f; FVector cross = FVector::CrossProduct(Velocity.GetSafeNormal(), HitResult.Normal); - - // Guard: near-zero cross product at very shallow grazing angles - FVector flat; - if (cross.SizeSquared() < SMALL_NUMBER) - { - // Bullet nearly parallel to surface: project velocity onto surface plane - flat = FVector::VectorPlaneProject(Velocity.GetSafeNormal(), HitResult.Normal).GetSafeNormal(); - if (flat.IsNearlyZero()) - { - flat = FMath::Abs(HitResult.Normal.Z) < 0.9f - ? FVector::CrossProduct(HitResult.Normal, FVector::UpVector).GetSafeNormal() - : FVector::CrossProduct(HitResult.Normal, FVector::RightVector).GetSafeNormal(); - } - } - else - { - flat = HitResult.Normal.RotateAngleAxis(-90.0f, cross); - } + FVector flat = HitResult.Normal.RotateAngleAxis(-90.0f, cross); #ifdef WITH_EDITOR if (DebugEnabled) { - FColor DebugColor = FColor::MakeRedToGreenColorFromScalar(Velocity.Size() / FMath::Max(MuzzleVelocityMax, 1.0f)); + FColor DebugColor = FColor::MakeRedToGreenColorFromScalar(Velocity.Size() / MuzzleVelocityMax); DrawDebugLine(GetWorld(), start, HitResult.Location, DebugColor, false, DebugTrailTime, 0, DebugTrailWidth); }; #endif @@ -139,43 +103,32 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn float GrazingAngle = FMath::Pow(dot, GrazingAngleExponent); FVector PenetrationVector = RandomStream.VRandCone(Velocity, penEnterSpread); PenetrationVector = FMath::Lerp(PenetrationVector, -HitResult.Normal, FMath::Lerp(penNormalization, penNormalizationGrazing, GrazingAngle)); - float AvgMuzzleVelocity = FMath::Max((MuzzleVelocityMin + MuzzleVelocityMax) * 0.5f, 1.0f); - // Incidence angle factor: head-on (dot=2) -> full depth, grazing (dot=0) -> minimal depth - float IncidenceFactor = FMath::Clamp(dot * 0.5f, 0.05f, 1.0f); - float PenetrationDistance = FMath::Lerp(MinPenetration, MaxPenetration, RandomStream.FRand()) * FMath::Pow((Velocity.Size() / AvgMuzzleVelocity), 2.0f) * penDepthMultiplier * IncidenceFactor; + float PenetrationDistance = FMath::Lerp(MinPenetration, MaxPenetration, RandomStream.FRand()) * FMath::Pow((Velocity.Size() / ((MuzzleVelocityMin + MuzzleVelocityMax) * 0.5f)), 2.0f) * penDepthMultiplier; float PenetrationDepth = -FVector::DotProduct(PenetrationVector, HitResult.Normal) * PenetrationDistance; - float BlockTime = 1.0f; + float BlockTIme = 1.0f; if (PenetrationDistance > 0.0f) { if (!neverPenetrate) { - BlockTime = PenetrationTrace(HitResult.Location - (HitResult.Normal * CollisionMargin), HitResult.Location + PenetrationVector * PenetrationDistance, HitResult.Component, PenTraceType, CollisionChannel, exitLoc, exitNormal); + BlockTIme = PenetrationTrace(HitResult.Location - (HitResult.Normal * CollisionMargin), HitResult.Location + PenetrationVector * PenetrationDistance, HitResult.Component, PenTraceType, CollisionChannel, exitLoc, exitNormal); } } - if (BlockTime >= 0.999999f) { + if (BlockTIme >= 0.999999f) { //no pen SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin); float ricThreshold = 1.0f; - if (SpeedControlsRicochetProbability) { ricThreshold *= Velocity.Size() / FMath::Max(MuzzleVelocityMax, 1.0f); }; + if (SpeedControlsRicochetProbability) { ricThreshold *= Velocity.Size() / MuzzleVelocityMax; }; if (!neverRicochet && RandomStream.FRand() * ricThreshold < FMath::Lerp(RicochetProbability * ricProbMultiplier, RicochetProbabilityGrazing * ricProbMultiplier, GrazingAngle)) { //bounce FVector bounceAngle = flat * dot * (1.0f - ricFriction); bounceAngle += HitResult.Normal * (1.0f - dot) * ricRestitution; - float bounceSize = bounceAngle.Size(); - if (bounceSize > SMALL_NUMBER) - { - bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceSize; - NewVelocity = bounceAngle * Velocity.Size(); - } - else - { - // bounceAngle is zero (head-on + high friction + low restitution): reflect off normal - NewVelocity = RandomStream.VRandCone(HitResult.Normal, ricSpread) * Velocity.Size() * ricRestitution; - } + bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceAngle.Size(); + + NewVelocity = bounceAngle * Velocity.Size(); Ricochet = true; OwnerSafe = false; } @@ -186,7 +139,7 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn } else { //penetration - float RemainingEnergy = FMath::Pow(1.0f - BlockTime, 2.0f); + float RemainingEnergy = FMath::Pow(1.0f - BlockTIme, 2.0f); SetActorLocation(exitLoc + exitNormal * CollisionMargin); NewVelocity = RandomStream.VRandCone(PenetrationVector, penExitSpread * (1.0f - RemainingEnergy)); NewVelocity = FMath::Lerp(NewVelocity, Velocity.GetSafeNormal(), RemainingEnergy); @@ -236,7 +189,7 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn #ifdef WITH_EDITOR if (DebugEnabled) { - FLinearColor Color = GetDebugColor(Velocity.Size() / FMath::Max((MuzzleVelocityMin + MuzzleVelocityMax)*0.5f, 1.0f)); + FLinearColor Color = GetDebugColor(Velocity.Size() / ((MuzzleVelocityMin + MuzzleVelocityMax)*0.5f)); DrawDebugLine(GetWorld(), start, start + TraceDistance, Color.ToFColor(true), false, DebugTrailTime, 0, 0); } } diff --git a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Public/EBBarrel.h b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Public/EBBarrel.h index 15ea0c4..057e3f9 100644 --- a/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Public/EBBarrel.h +++ b/Unreal/Plugins/PS_Ballistics/Source/EasyBallistics/Public/EBBarrel.h @@ -29,6 +29,7 @@ enum class EAntiRecoilMode : uint8 ARM_Buffer UMETA(DisplayName = "Buffer (No Prediction)", ToolTip = "Legacy mode. Returns the oldest sample in the buffer, guaranteed to be pre-shock. Simple and reliable but introduces a fixed time delay equal to BufferTime. Best for static or slow-moving aiming."), ARM_LinearExtrapolation UMETA(DisplayName = "Linear Extrapolation", ToolTip = "Computes average linear and angular velocity from consecutive safe (pre-shock) samples, then extrapolates forward to the current time. Good balance of simplicity and accuracy for steady movements. May overshoot on sudden direction changes."), 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 = "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).") }; @@ -43,6 +44,33 @@ struct FTimestampedTransform FVector Aim = FVector::ForwardVector; }; +// Stores calibration measurement results from a sequence of shots +USTRUCT() +struct FCalibrationResult +{ + GENERATED_BODY() + + float RecommendedDiscardTime = 0.0f; + float RecommendedBufferTime = 0.0f; + float RecommendedKalmanProcessNoise = 0.0f; + float RecommendedKalmanMeasurementNoise = 0.0f; + float MedianCorruptionDuration = 0.0f; + float P95CorruptionDuration = 0.0f; + float MaxCorruptionDuration = 0.0f; + float AvgPeakAngleDeviation = 0.0f; + float AvgPeakPositionDeviation = 0.0f; + int32 TotalShots = 0; + int32 OutliersRemoved = 0; + bool bValid = false; +}; + +// Raw buffer snapshot captured at shot time for offline analysis +struct FCalibrationShotData +{ + TArray BufferSnapshot; + double ShotTime = 0.0; +}; + UCLASS(Blueprintable, ClassGroup = (Custom), hidecategories = (Object, LOD, Physics, Lighting, TextureStreaming, Collision, HLOD, Mobile, VirtualTexture, ComponentReplication), editinlinenew, meta = (BlueprintSpawnableComponent)) class EASYBALLISTICS_API UEBBarrel : public UPrimitiveComponent { @@ -70,6 +98,16 @@ public: bool DebugAntiRecoil = false; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Length of the debug aim lines (cm)", EditCondition = "DebugAntiRecoil")) float DebugAntiRecoilLineLength = 400.0f; + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Thickness of the debug aim lines", EditCondition = "DebugAntiRecoil")) + float DebugAntiRecoilLineThickness = 0.0f; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Calibration", meta = (ToolTip = "When enabled, automatically measures recoil corruption over sequences of CalibrationShotCount shots. After each sequence, displays recommended DiscardTime, BufferTime, and Kalman parameters on HUD. Loops automatically until disabled. No thresholds needed — uses statistical 3-sigma auto-detection.")) + bool CalibrateAntiRecoil = false; + 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; @@ -78,18 +116,41 @@ public: bool bCSVFileOpen = false; FString CSVFilePath; IFileHandle* CSVFileHandle = nullptr; - bool bShotFiredThisFrame = false; + + // 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")) + float DebugIMUShockAngle = 15.0f; + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Position perturbation intensity in cm", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.0")) + float DebugIMUShockPosition = 2.0f; + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Duration of the simulated shock in seconds", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.01")) + float DebugIMUShockDuration = 0.08f; + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "How long (seconds) the yellow debug line persists after a shock, showing where the shot would have gone without anti-recoil correction", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.5")) + float DebugIMUShockDisplayTime = 3.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Selects the anti-recoil compensation algorithm. Hover over each option in the dropdown for a detailed description of how it works.")) - EAntiRecoilMode AntiRecoilMode = EAntiRecoilMode::ARM_AdaptiveExtrapolation; + EAntiRecoilMode AntiRecoilMode = EAntiRecoilMode::ARM_KalmanFilter; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Total time window (ms) of tracker history to keep. Determines how far back in time samples are stored. Must be greater than DiscardTime. Example: 200ms at 60fps stores ~12 samples.", ClampMin = "5")) - float AntiRecoilBufferTimeMs = 200.0f; + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Total time window (seconds) of tracker history to keep. Determines how far back in time samples are stored. Must be greater than DiscardTime. Example: 0.2s at 60fps stores ~12 samples.", ClampMin = "0.05")) + float AntiRecoilBufferTime = 0.15f; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Time window (ms) of most recent samples to exclude as potentially contaminated by IMU recoil shock. The prediction algorithms only use samples older than this. Increase if the shock lasts longer. Safe window = BufferTime - DiscardTime.", ClampMin = "0.0")) - float AntiRecoilDiscardTimeMs = 30.0f; + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Time window (seconds) of most recent samples to exclude as potentially contaminated by IMU recoil shock. The prediction algorithms only use samples older than this. Increase if the shock lasts longer. Safe window = BufferTime - DiscardTime.", ClampMin = "0.0")) + float AntiRecoilDiscardTime = 0.03f; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Controls how the weight curve grows across safe samples in regression modes. 1.0 = linear growth, >1.0 = recent samples weighted much more heavily (convex curve), <1.0 = more uniform weighting (concave curve), 0.0 = all samples weighted equally. Formula: weight = pow(sampleIndex+1, exponent).", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_WeightedLinearRegression", ClampMin = "0.0", ClampMax = "5.0")) + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Controls how the weight curve grows across safe samples in regression modes. 1.0 = linear growth, >1.0 = recent samples weighted much more heavily (convex curve), <1.0 = more uniform weighting (concave curve), 0.0 = all samples weighted equally. Formula: weight = pow(sampleIndex+1, exponent).", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_WeightedRegression || AntiRecoilMode == EAntiRecoilMode::ARM_WeightedLinearRegression", ClampMin = "0.0", ClampMax = "5.0")) float RegressionWeightExponent = 2.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Kalman filter process noise (higher = more responsive to movement changes, lower = smoother). Since safe samples are already filtered by DiscardTime, this should be high enough to track aiming movements.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_KalmanFilter", ClampMin = "0.01")) @@ -99,13 +160,13 @@ public: float KalmanMeasurementNoise = 0.01f; 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 = 3.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.95f; + 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 = 5.0f; + float ExtrapolationDamping = 0.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Velocity", meta = (ToolTip = "Bullet inherits barrel velocity, only works with physics enabled or with additional velocity set")) float InheritVelocity = 1.0f; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Velocity", meta = (ToolTip = "Amount of recoil applied to the barrel, only works with physics enabled")) float RecoilMultiplier = 1.0f; @@ -182,6 +243,9 @@ public: UFUNCTION(Server, Reliable, WithValidation, BlueprintCallable, Category = "Shooting") void GatlingSpool(bool Spool); UFUNCTION(BlueprintCallable, Category = "Shooting") void Shoot(bool Trigger, int nextEventFire); + UFUNCTION(BlueprintCallable, Category = "Debug", meta = (ToolTip = "Trigger a simulated IMU shock for testing anti-recoil prediction. Requires DebugSimulateIMUShock to be enabled.")) + void TriggerDebugIMUShock(); + UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Prediction") void PredictHit(bool& Hit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, AActor*& HitActor, TArray& Trajectory, TSubclassOf BulletClass, TArrayIgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const; UFUNCTION(BlueprintCallable, meta = (AutoCreateRefTerm = "IgnoredActors"), Category = "Prediction") void PredictHitFromLocation(bool &Hit, FHitResult& TraceResult, FVector& HitLocation, float& HitTime, AActor*& HitActor, TArray& Trajectory, TSubclassOf BulletClass, FVector StartLocation, FVector AimDirection, TArrayIgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const; UFUNCTION(BlueprintCallable, Category = "Prediction") void CalculateAimDirection(TSubclassOf BulletClass, FVector TargetLocation, FVector TargetVelocity, FVector& AimDirection, FVector& PredictedTargetLocation, FVector& PredictedIntersectionLocation, float& PredictedFlightTime, float& Error, float MaxTime = 10.0f, float Step = 0.1f, int NumIterations = 4) const; @@ -234,6 +298,30 @@ private: TArray TransformHistory; + // Debug IMU shock simulation state + double DebugIMUShockStartTime = 0.0; + bool DebugIMUShockActive = false; + FVector DebugIMUShockAimOffset = FVector::ZeroVector; + FVector DebugIMUShockPosOffset = FVector::ZeroVector; + + // Debug yellow line persistence (shows uncorrected raw aim after shock) + bool DebugIMUShockLineCaptured = false; + double DebugIMUShockLineEndTime = 0.0; + FVector DebugIMUShockCapturedLocation = FVector::ZeroVector; + FVector DebugIMUShockCapturedAim = FVector::ForwardVector; + + // Debug blue line persistence (shows corrected aim retained for the shot) + bool DebugCorrectedShotLineCaptured = false; + double DebugCorrectedShotLineEndTime = 0.0; + FVector DebugCorrectedShotCapturedLocation = FVector::ZeroVector; + FVector DebugCorrectedShotCapturedAim = FVector::ForwardVector; + + // Calibration state + int32 CalibrationShotsCollected = 0; + TArray CalibrationShots; + FCalibrationResult LastCalibrationResult; + void ComputeCalibrationResult(); + // Kalman filter state FVector KalmanPosition = FVector::ZeroVector; FVector KalmanVelocity = FVector::ZeroVector; @@ -250,6 +338,7 @@ private: void ComputeAntiRecoilTransform(); void PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const; void PredictWeightedLinearRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const; + void PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const; void PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const; void UpdateKalmanFilter(double CurrentTime, const FVector& MeasuredLocation, const FVector& MeasuredAim); void PredictKalmanFilter(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;