Tune adaptive extrapolation defaults, add AdaptiveMinSpeed property, fix debug visuals

- Add AdaptiveMinSpeed UPROPERTY (default 30 cm/s) to avoid false deceleration at low speeds
- Update default values: BufferTime=300ms, DiscardTime=40ms, Sensitivity=1.5, Damping=8.0
- Replace debug spheres with points to not obstruct aiming view
- Add detailed debug logs with [LOW]/[DZ]/[DEC] tags for dead zone diagnosis
- Convert buffer/discard time units to milliseconds
- Set AdaptiveExtrapolation as default AntiRecoil mode
- Fix DLL copy error handling in DinkeyPlugin and ViveVBS build scripts
- Add AimStabilization dead zone with smooth transition (no hard jumps)
- Add AimSmoothingSpeed property for temporal aim smoothing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-17 19:50:39 +01:00
parent 83188b1fa1
commit 48737b60c9
5 changed files with 149 additions and 47 deletions

View File

@ -92,8 +92,9 @@ void UEBBarrel::UpdateTransformHistory()
TransformHistory.Add(Sample); TransformHistory.Add(Sample);
// Trim buffer: remove samples older than AntiRecoilBufferTime // Trim buffer: remove samples older than AntiRecoilBufferTimeMs
// During calibration, keep a larger buffer (0.5s min) for reliable 3-sigma analysis // During calibration, keep a larger buffer (0.5s min) for reliable 3-sigma analysis
const float AntiRecoilBufferTime = AntiRecoilBufferTimeMs / 1000.0f;
float EffectiveBufferTime = CalibrateAntiRecoil float EffectiveBufferTime = CalibrateAntiRecoil
? FMath::Max(AntiRecoilBufferTime, 0.5f) ? FMath::Max(AntiRecoilBufferTime, 0.5f)
: AntiRecoilBufferTime; : AntiRecoilBufferTime;
@ -128,7 +129,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
case EAntiRecoilMode::ARM_LinearExtrapolation: case EAntiRecoilMode::ARM_LinearExtrapolation:
{ {
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN >= 2) if (SafeN >= 2)
{ {
PredictLinearExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim); PredictLinearExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim);
@ -148,7 +149,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
case EAntiRecoilMode::ARM_WeightedRegression: case EAntiRecoilMode::ARM_WeightedRegression:
{ {
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN >= 2) if (SafeN >= 2)
{ {
PredictWeightedRegression(GetWorld()->GetTimeSeconds(), Location, Aim); PredictWeightedRegression(GetWorld()->GetTimeSeconds(), Location, Aim);
@ -168,7 +169,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
case EAntiRecoilMode::ARM_WeightedLinearRegression: case EAntiRecoilMode::ARM_WeightedLinearRegression:
{ {
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN >= 2) if (SafeN >= 2)
{ {
PredictWeightedLinearRegression(GetWorld()->GetTimeSeconds(), Location, Aim); PredictWeightedLinearRegression(GetWorld()->GetTimeSeconds(), Location, Aim);
@ -188,7 +189,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
case EAntiRecoilMode::ARM_KalmanFilter: case EAntiRecoilMode::ARM_KalmanFilter:
{ {
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN > 0) if (SafeN > 0)
{ {
// Feed only the latest SAFE sample to the Kalman filter // Feed only the latest SAFE sample to the Kalman filter
@ -211,7 +212,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
case EAntiRecoilMode::ARM_AdaptiveExtrapolation: case EAntiRecoilMode::ARM_AdaptiveExtrapolation:
{ {
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN >= 2) if (SafeN >= 2)
{ {
PredictAdaptiveExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim); PredictAdaptiveExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim);
@ -237,7 +238,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
{ {
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN < 2) if (SafeN < 2)
{ {
OutLocation = TransformHistory[0].Location; OutLocation = TransformHistory[0].Location;
@ -317,7 +318,7 @@ void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocat
void UEBBarrel::PredictWeightedLinearRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const void UEBBarrel::PredictWeightedLinearRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
{ {
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN < 2) if (SafeN < 2)
{ {
OutLocation = TransformHistory[0].Location; OutLocation = TransformHistory[0].Location;
@ -390,7 +391,7 @@ void UEBBarrel::PredictWeightedLinearRegression(double CurrentTime, FVector& Out
void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
{ {
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN < 2) if (SafeN < 2)
{ {
OutLocation = TransformHistory[0].Location; OutLocation = TransformHistory[0].Location;
@ -706,7 +707,7 @@ void UEBBarrel::PredictKalmanFilter(double CurrentTime, FVector& OutLocation, FV
void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
{ {
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime); const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
if (SafeN < 2) if (SafeN < 2)
{ {
OutLocation = TransformHistory[0].Location; OutLocation = TransformHistory[0].Location;
@ -782,30 +783,68 @@ void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLoc
float RecentAimSpeed = RecentAimVel.Size(); float RecentAimSpeed = RecentAimVel.Size();
// Confidence: ratio of recent speed to average speed. // Confidence: ratio of recent speed to average speed.
// Dead zone: ratios above AdaptiveDeadZone are treated as 1.0 (normal fluctuations). // Dead zone: ratios above AdaptiveDeadZone are treated as 1.0 (no correction).
// Remapped ratio: (ratio - deadzone) / (1 - deadzone), clamped to [0, 1]. // Only ratios BELOW AdaptiveDeadZone trigger extrapolation reduction.
// AdaptiveSensitivity is the power exponent on the remapped ratio. // 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 = AdaptiveMinSpeed;
float PosRatio = 1.0f; float PosRatio = 1.0f;
float PosConfidence = 1.0f; float PosConfidence = 1.0f;
if (AvgPosSpeed > SMALL_NUMBER) float PosRemapped = 1.0f;
if (AvgPosSpeed > MinSpeedThreshold)
{ {
PosRatio = FMath::Clamp(RecentPosSpeed / AvgPosSpeed, 0.0f, 1.0f); PosRatio = FMath::Clamp(RecentPosSpeed / AvgPosSpeed, 0.0f, 1.0f);
float PosRemapped = (AdaptiveDeadZone < 1.0f) if (PosRatio >= AdaptiveDeadZone)
? FMath::Clamp((PosRatio - AdaptiveDeadZone) / (1.0f - AdaptiveDeadZone), 0.0f, 1.0f) {
: (PosRatio >= 1.0f ? 1.0f : 0.0f); // Inside dead zone: no correction, full confidence
PosConfidence = FMath::Pow(PosRemapped, AdaptiveSensitivity); 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);
}
} }
// else: AvgPosSpeed <= MinSpeedThreshold → keep defaults (Ratio=1, Conf=1)
float AimRatio = 1.0f; float AimRatio = 1.0f;
float AimConfidence = 1.0f; float AimConfidence = 1.0f;
if (AvgAimSpeed > SMALL_NUMBER) float AimRemapped = 1.0f;
if (AvgAimSpeed > MinSpeedThreshold)
{ {
AimRatio = FMath::Clamp(RecentAimSpeed / AvgAimSpeed, 0.0f, 1.0f); AimRatio = FMath::Clamp(RecentAimSpeed / AvgAimSpeed, 0.0f, 1.0f);
float AimRemapped = (AdaptiveDeadZone < 1.0f) if (AimRatio >= AdaptiveDeadZone)
? FMath::Clamp((AimRatio - AdaptiveDeadZone) / (1.0f - AdaptiveDeadZone), 0.0f, 1.0f) {
: (AimRatio >= 1.0f ? 1.0f : 0.0f); // Inside dead zone: no correction, full confidence
AimConfidence = FMath::Pow(AimRemapped, AdaptiveSensitivity); 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);
}
} }
// else: AvgAimSpeed <= MinSpeedThreshold → keep defaults (Ratio=1, Conf=1)
// Debug logs for dead zone diagnosis
UE_LOG(LogTemp, Log, TEXT("[AdaptiveExtrap] DZ=%.3f Sens=%.2f MinSpd=%.1f | Pos: Ratio=%.4f Remap=%.4f Conf=%.4f (Avg=%.2f Recent=%.2f %s) | Aim: Ratio=%.4f Remap=%.4f Conf=%.4f (Avg=%.2f Recent=%.2f %s)"),
AdaptiveDeadZone, AdaptiveSensitivity, MinSpeedThreshold,
PosRatio, PosRemapped, PosConfidence, AvgPosSpeed, RecentPosSpeed,
(AvgPosSpeed <= MinSpeedThreshold) ? TEXT("[LOW]") : (PosRatio >= AdaptiveDeadZone ? TEXT("[DZ]") : TEXT("[DEC]")),
AimRatio, AimRemapped, AimConfidence, AvgAimSpeed, RecentAimSpeed,
(AvgAimSpeed <= MinSpeedThreshold) ? TEXT("[LOW]") : (AimRatio >= AdaptiveDeadZone ? TEXT("[DZ]") : TEXT("[DEC]")));
// Extrapolate from last safe sample // Extrapolate from last safe sample
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1]; const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
@ -814,6 +853,8 @@ void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLoc
// Write debug values for HUD display // Write debug values for HUD display
DbgPosRatio = PosRatio; DbgPosRatio = PosRatio;
DbgAimRatio = AimRatio; DbgAimRatio = AimRatio;
DbgPosRemapped = PosRemapped;
DbgAimRemapped = AimRemapped;
DbgPosConfidence = PosConfidence; DbgPosConfidence = PosConfidence;
DbgAimConfidence = AimConfidence; DbgAimConfidence = AimConfidence;
DbgAvgPosSpeed = AvgPosSpeed; DbgAvgPosSpeed = AvgPosSpeed;

View File

@ -17,6 +17,7 @@ void UEBBarrel::Shoot(bool Trigger, int nextFireID) {
if (ClientSideAim && GetOwner()->GetRemoteRole() == ROLE_Authority && Trigger) { if (ClientSideAim && GetOwner()->GetRemoteRole() == ROLE_Authority && Trigger) {
Aim = GetComponentTransform().GetUnitAxis(EAxis::X); Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation(); Location = GetComponentTransform().GetLocation();
ApplyAimStabilization();
nextFireEventID = nextFireID; nextFireEventID = nextFireID;
ShootRepCSA(Trigger, UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(), Location), Aim, nextFireID); ShootRepCSA(Trigger, UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(), Location), Aim, nextFireID);
} }

View File

@ -29,7 +29,7 @@ FPrimitiveSceneProxy* UEBBarrel::CreateSceneProxy() {
const FLinearColor DrawColor = GetViewSelectionColor(FColor::Green, *View, IsSelected(), IsHovered(), true, IsIndividuallySelected()); const FLinearColor DrawColor = GetViewSelectionColor(FColor::Green, *View, IsSelected(), IsHovered(), true, IsIndividuallySelected());
FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex); FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex);
DrawDirectionalArrow(PDI, Transform, DrawColor, Component->DebugArrowSize, Component->DebugArrowSize*0.1f, 16, Component->DebugArrowSize*0.01f); DrawDirectionalArrow(PDI, Transform, DrawColor, Component->DebugArrowSize, Component->DebugArrowSize*0.1f, 16, 0.0f);
} }
} }
} }

View File

@ -45,6 +45,41 @@ void UEBBarrel::EndPlay(const EEndPlayReason::Type EndPlayReason)
Super::EndPlay(EndPlayReason); Super::EndPlay(EndPlayReason);
} }
void UEBBarrel::ApplyAimStabilization()
{
if (AimDeadZoneDegrees <= 0.0f)
{
return;
}
if (!bStabilizedAimInitialized)
{
StabilizedAim = Aim;
bStabilizedAimInitialized = true;
}
else
{
// Compute angle between current aim and last stable aim
float AngleDeg = FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(FVector::DotProduct(Aim, StabilizedAim), -1.0f, 1.0f)));
if (AngleDeg <= AimDeadZoneDegrees)
{
// Within dead zone: keep previous stable aim (filter jitter)
Aim = StabilizedAim;
}
else
{
// Outside dead zone: smooth transition to avoid jumps
// Subtract the dead zone from the angle so movement starts from zero
float ExcessDeg = AngleDeg - AimDeadZoneDegrees;
float BlendAlpha = FMath::Clamp(ExcessDeg / AngleDeg, 0.0f, 1.0f);
// Slerp from stabilized aim toward real aim, removing the dead zone portion
StabilizedAim = FMath::Lerp(StabilizedAim, Aim, BlendAlpha).GetSafeNormal();
Aim = StabilizedAim;
}
}
}
void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{ {
Super::TickComponent(DeltaTime, TickType, ThisTickFunction); Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
@ -58,6 +93,7 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) { if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) {
ComputeAntiRecoilTransform(); ComputeAntiRecoilTransform();
ApplyAimStabilization();
ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim); ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim);
TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency); TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency);
@ -65,6 +101,7 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
}else{ }else{
if (!RemoteAimReceived) { if (!RemoteAimReceived) {
ComputeAntiRecoilTransform(); ComputeAntiRecoilTransform();
ApplyAimStabilization();
} }
else { else {
FVector LocOffset = (Location - GetComponentLocation()); FVector LocOffset = (Location - GetComponentLocation());
@ -77,6 +114,7 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
} }
else { else {
ComputeAntiRecoilTransform(); ComputeAntiRecoilTransform();
ApplyAimStabilization();
} }
// Debug visualization: raw tracker (green) vs predicted aim (red) // Debug visualization: raw tracker (green) vs predicted aim (red)
@ -104,9 +142,9 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
DrawDebugLine(GetWorld(), Location, Location + Aim * DebugAntiRecoilLineLength, DrawDebugLine(GetWorld(), Location, Location + Aim * DebugAntiRecoilLineLength,
FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness); FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness);
// Small spheres at origins for clarity // Small dots at origins for clarity
DrawDebugSphere(GetWorld(), RawLocation, 1.5f, 6, FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f); DrawDebugPoint(GetWorld(), RawLocation, 3.0f, FColor::Green, false, -1.0f, 0);
DrawDebugSphere(GetWorld(), Location, 1.5f, 6, FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f); DrawDebugPoint(GetWorld(), Location, 3.0f, FColor::Red, false, -1.0f, 0);
// Yellow line: shows where shot would land WITHOUT anti-recoil correction // Yellow line: shows where shot would land WITHOUT anti-recoil correction
// Captures raw aim at shock onset and persists for DebugIMUShockDisplayTime seconds // Captures raw aim at shock onset and persists for DebugIMUShockDisplayTime seconds
@ -131,7 +169,7 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
DrawDebugLine(GetWorld(), DebugIMUShockCapturedLocation, DrawDebugLine(GetWorld(), DebugIMUShockCapturedLocation,
DebugIMUShockCapturedLocation + DebugIMUShockCapturedAim * DebugAntiRecoilLineLength, DebugIMUShockCapturedLocation + DebugIMUShockCapturedAim * DebugAntiRecoilLineLength,
FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness); FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness);
DrawDebugSphere(GetWorld(), DebugIMUShockCapturedLocation, 3.0f, 8, FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness); DrawDebugPoint(GetWorld(), DebugIMUShockCapturedLocation, 3.0f, FColor::Green, false, -1.0f, 0);
} }
else else
{ {
@ -147,7 +185,7 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
DrawDebugLine(GetWorld(), DebugCorrectedShotCapturedLocation, DrawDebugLine(GetWorld(), DebugCorrectedShotCapturedLocation,
DebugCorrectedShotCapturedLocation + DebugCorrectedShotCapturedAim * DebugAntiRecoilLineLength, DebugCorrectedShotCapturedLocation + DebugCorrectedShotCapturedAim * DebugAntiRecoilLineLength,
FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness); FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness);
DrawDebugSphere(GetWorld(), DebugCorrectedShotCapturedLocation, 3.0f, 8, FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness); DrawDebugPoint(GetWorld(), DebugCorrectedShotCapturedLocation, 3.0f, FColor::Red, false, -1.0f, 0);
} }
else else
{ {
@ -185,7 +223,7 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
FVector RealPos = GetComponentTransform().GetLocation(); FVector RealPos = GetComponentTransform().GetLocation();
FVector RealAim = GetComponentTransform().GetUnitAxis(EAxis::X); FVector RealAim = GetComponentTransform().GetUnitAxis(EAxis::X);
// Count safe samples (same logic as GetSafeCount in AntiRecoilPredict.cpp) // Count safe samples (same logic as GetSafeCount in AntiRecoilPredict.cpp)
double SafeCutoff = GetWorld()->GetTimeSeconds() - AntiRecoilDiscardTime; double SafeCutoff = GetWorld()->GetTimeSeconds() - (AntiRecoilDiscardTimeMs / 1000.0f);
int32 SafeN = 0; int32 SafeN = 0;
for (int32 si = 0; si < TransformHistory.Num(); si++) for (int32 si = 0; si < TransformHistory.Num(); si++)
{ {
@ -240,17 +278,26 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudTitle, GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudTitle,
TEXT("====== ADAPTIVE EXTRAPOLATION ======")); TEXT("====== ADAPTIVE EXTRAPOLATION ======"));
// Dead zone info
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudVal,
FString::Printf(TEXT(" DeadZone=%.2f Sensitivity=%.1f"),
AdaptiveDeadZone, AdaptiveSensitivity));
// Speed ratio + confidence (position) // Speed ratio + confidence (position)
FColor PosColor = (DbgPosConfidence > 0.9f) ? HudGood : HudWarn; bool bPosInDeadZone = (DbgPosRatio >= AdaptiveDeadZone);
FColor PosColor = bPosInDeadZone ? HudGood : HudWarn;
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, PosColor, GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, PosColor,
FString::Printf(TEXT(" Pos: ratio=%.2f conf=%.2f speed=%.1f/%.1f cm/s"), FString::Printf(TEXT(" Pos: ratio=%.3f %s remap=%.3f conf=%.3f spd=%.1f/%.1f"),
DbgPosRatio, DbgPosConfidence, DbgRecentPosSpeed, DbgAvgPosSpeed)); DbgPosRatio, bPosInDeadZone ? TEXT("[DZ]") : TEXT("[!!]"),
DbgPosRemapped, DbgPosConfidence, DbgRecentPosSpeed, DbgAvgPosSpeed));
// Speed ratio + confidence (aim) // Speed ratio + confidence (aim)
FColor AimColor = (DbgAimConfidence > 0.9f) ? HudGood : HudWarn; bool bAimInDeadZone = (DbgAimRatio >= AdaptiveDeadZone);
FColor AimColor = bAimInDeadZone ? HudGood : HudWarn;
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, AimColor, GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, AimColor,
FString::Printf(TEXT(" Aim: ratio=%.2f conf=%.2f speed=%.4f/%.4f /s"), FString::Printf(TEXT(" Aim: ratio=%.3f %s remap=%.3f conf=%.3f spd=%.4f/%.4f"),
DbgAimRatio, DbgAimConfidence, DbgRecentAimSpeed, DbgAvgAimSpeed)); DbgAimRatio, bAimInDeadZone ? TEXT("[DZ]") : TEXT("[!!]"),
DbgAimRemapped, DbgAimConfidence, DbgRecentAimSpeed, DbgAvgAimSpeed));
// Extrapolation time // Extrapolation time
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudVal, GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudVal,
@ -320,11 +367,11 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
LastCalibrationResult.AvgPeakAngleDeviation, LastCalibrationResult.AvgPeakAngleDeviation,
LastCalibrationResult.AvgPeakPositionDeviation)); LastCalibrationResult.AvgPeakPositionDeviation));
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn, GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn,
FString::Printf(TEXT(" >> DiscardTime: %.4fs"), FString::Printf(TEXT(" >> DiscardTime: %.1f ms"),
LastCalibrationResult.RecommendedDiscardTime)); LastCalibrationResult.RecommendedDiscardTime * 1000.0f));
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn, GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn,
FString::Printf(TEXT(" >> BufferTime: %.4fs"), FString::Printf(TEXT(" >> BufferTime: %.1f ms"),
LastCalibrationResult.RecommendedBufferTime)); LastCalibrationResult.RecommendedBufferTime * 1000.0f));
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn, GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn,
FString::Printf(TEXT(" >> KalmanProcessNoise: %.3f"), FString::Printf(TEXT(" >> KalmanProcessNoise: %.3f"),
LastCalibrationResult.RecommendedKalmanProcessNoise)); LastCalibrationResult.RecommendedKalmanProcessNoise));

View File

@ -120,6 +120,8 @@ public:
// Debug HUD state (written by const prediction functions, read by TickComponent) // Debug HUD state (written by const prediction functions, read by TickComponent)
mutable float DbgPosRatio = 0.0f; mutable float DbgPosRatio = 0.0f;
mutable float DbgAimRatio = 0.0f; mutable float DbgAimRatio = 0.0f;
mutable float DbgPosRemapped = 0.0f;
mutable float DbgAimRemapped = 0.0f;
mutable float DbgPosConfidence = 0.0f; mutable float DbgPosConfidence = 0.0f;
mutable float DbgAimConfidence = 0.0f; mutable float DbgAimConfidence = 0.0f;
mutable float DbgAvgPosSpeed = 0.0f; mutable float DbgAvgPosSpeed = 0.0f;
@ -142,13 +144,13 @@ public:
float DebugIMUShockDisplayTime = 3.0f; 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.")) 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_KalmanFilter; EAntiRecoilMode AntiRecoilMode = EAntiRecoilMode::ARM_AdaptiveExtrapolation;
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")) 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 AntiRecoilBufferTime = 0.15f; float AntiRecoilBufferTimeMs = 300.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")) 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 AntiRecoilDiscardTime = 0.03f; float AntiRecoilDiscardTimeMs = 40.0f;
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")) 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; float RegressionWeightExponent = 2.0f;
@ -160,13 +162,19 @@ public:
float KalmanMeasurementNoise = 0.01f; 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")) 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; float AdaptiveSensitivity = 1.5f;
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")) 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; float AdaptiveDeadZone = 0.8f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Minimum average speed (cm/s) required for deceleration detection. Below this threshold, speed ratios are unreliable due to noise, so confidence stays at 1.0 (full extrapolation). Prevents false deceleration detection during slow/small movements. 0 = disabled.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation", ClampMin = "0.0", ClampMax = "200.0"))
float AdaptiveMinSpeed = 30.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Velocity damping during extrapolation. 0 = disabled (default). Higher values cause extrapolated velocity to decay exponentially toward zero over the discard window. Reduces overshoot on fast draw-aim-fire sequences where the user stops moving before firing. Applies to all prediction modes except Buffer. Typical range: 5-15.", ClampMin = "0.0", ClampMax = "50.0")) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Velocity damping during extrapolation. 0 = disabled (default). Higher values cause extrapolated velocity to decay exponentially toward zero over the discard window. Reduces overshoot on fast draw-aim-fire sequences where the user stops moving before firing. Applies to all prediction modes except Buffer. Typical range: 5-15.", ClampMin = "0.0", ClampMax = "50.0"))
float ExtrapolationDamping = 0.0f; float ExtrapolationDamping = 8.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AimStabilization", meta = (ToolTip = "Angular dead zone in degrees. If the aim direction changes by less than this angle since the last stable aim, the change is ignored (aim stays locked). Eliminates micro-jitter from VR tracker vibrations. 0 = disabled. Typical: 0.1 to 0.5 degrees.", ClampMin = "0.0", ClampMax = "5.0"))
float AimDeadZoneDegrees = 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 = "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; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Velocity", meta = (ToolTip = "Amount of recoil applied to the barrel, only works with physics enabled")) float RecoilMultiplier = 1.0f;
@ -296,6 +304,10 @@ private:
FVector Aim; FVector Aim;
FVector Location; FVector Location;
// Aim stabilization: last accepted aim direction (outside dead zone)
FVector StabilizedAim = FVector::ForwardVector;
bool bStabilizedAimInitialized = false;
TArray<FTimestampedTransform> TransformHistory; TArray<FTimestampedTransform> TransformHistory;
// Debug IMU shock simulation state // Debug IMU shock simulation state
@ -336,6 +348,7 @@ private:
void UpdateTransformHistory(); void UpdateTransformHistory();
void ComputeAntiRecoilTransform(); void ComputeAntiRecoilTransform();
void ApplyAimStabilization();
void PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const; void PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
void PredictWeightedLinearRegression(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 PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;