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:
@@ -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<FVector> PosVelocities;
|
||||
TArray<FVector> AimVelocities;
|
||||
TArray<double> Weights;
|
||||
PosVelocities.Reserve(SafeN - 1);
|
||||
AimVelocities.Reserve(SafeN - 1);
|
||||
Weights.Reserve(SafeN - 1);
|
||||
// Compute per-pair velocities
|
||||
TArray<FVector> PosVels;
|
||||
TArray<FVector> 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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user