Add anti-recoil prediction system with 3 algorithms + debug visualization

- Add EAntiRecoilMode enum: Disabled, Buffer, Linear Extrapolation,
  Weighted Regression (default), Kalman Filter
- Replace frame-based buffer with time-based FTimestampedTransform history
- Add AntiRecoilBufferTime/DiscardTime for time-based sample management
- Implement 3 prediction methods in new AntiRecoilPredict.cpp:
  linear extrapolation, weighted least-squares regression, simplified Kalman
- Add RegressionWeightExponent parameter to control weight curve shape
- Add debug visualization: green (raw tracker), red (predicted aim),
  yellow (uncorrected aim persisting 3s after IMU shock)
- Add IMU shock simulation for testing without physical firing
- Fix shotTrace DrawDebugLine: correct endpoint and color

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
j.foucher 2026-03-13 17:55:50 +01:00
parent 41ec54f7dd
commit 72dfc39771
6 changed files with 567 additions and 53 deletions

View File

@ -0,0 +1,414 @@
// Anti-Recoil Prediction Methods for EBBarrel
// Provides linear extrapolation, weighted regression, and Kalman filter prediction
//
// Key concept: The buffer stores samples within AntiRecoilBufferTime seconds.
// Samples within the last AntiRecoilDiscardTime seconds may be contaminated by recoil shock.
// All prediction algorithms work ONLY on the "safe" (oldest) portion of the buffer
// and extrapolate forward to the current time.
#include "EBBarrel.h"
// Returns the number of safe (non-contaminated) samples based on a time threshold.
// Samples whose timestamp >= (CurrentTime - DiscardTime) are considered potentially contaminated.
static int32 GetSafeCount(const TArray<FTimestampedTransform>& History, double CurrentTime, float DiscardTime)
{
if (History.Num() == 0) return 0;
double SafeCutoff = CurrentTime - FMath::Max(0.0f, DiscardTime);
int32 SafeN = 0;
for (int32 i = 0; i < History.Num(); i++)
{
if (History[i].Timestamp < SafeCutoff)
{
SafeN = i + 1;
}
else
{
break;
}
}
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;
}
void UEBBarrel::UpdateTransformHistory()
{
if (AntiRecoilMode == EAntiRecoilMode::ARM_None)
{
return;
}
double CurrentTime = GetWorld()->GetTimeSeconds();
FTimestampedTransform Sample;
Sample.Timestamp = CurrentTime;
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 AntiRecoilBufferTime
double OldestAllowed = CurrentTime - FMath::Max(0.05f, AntiRecoilBufferTime);
while (TransformHistory.Num() > 0 && TransformHistory[0].Timestamp < OldestAllowed)
{
TransformHistory.RemoveAt(0);
}
}
void UEBBarrel::ComputeAntiRecoilTransform()
{
switch (AntiRecoilMode)
{
case EAntiRecoilMode::ARM_None:
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation();
break;
case EAntiRecoilMode::ARM_Buffer:
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_LinearExtrapolation:
{
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
if (SafeN >= 2)
{
PredictLinearExtrapolation(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_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_KalmanFilter:
{
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
if (SafeN > 0)
{
// Feed only the latest SAFE sample to the Kalman filter
const FTimestampedTransform& LatestSafe = TransformHistory[SafeN - 1];
UpdateKalmanFilter(LatestSafe.Timestamp, LatestSafe.Location, LatestSafe.Aim);
PredictKalmanFilter(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;
}
}
// --- Linear Extrapolation ---
// Computes average velocity from ALL safe samples (consecutive differences),
// then extrapolates from the last safe sample to current time.
void UEBBarrel::PredictLinearExtrapolation(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;
}
// Compute average velocity from consecutive safe sample differences
FVector AvgLinearVelocity = FVector::ZeroVector;
FVector AvgAimDelta = FVector::ZeroVector;
double TotalDt = 0.0;
int32 ValidPairs = 0;
for (int32 i = 1; i < SafeN; i++)
{
double dt = TransformHistory[i].Timestamp - TransformHistory[i - 1].Timestamp;
if (dt > SMALL_NUMBER)
{
AvgLinearVelocity += (TransformHistory[i].Location - TransformHistory[i - 1].Location) / dt;
AvgAimDelta += (TransformHistory[i].Aim - TransformHistory[i - 1].Aim) / dt;
TotalDt += dt;
ValidPairs++;
}
}
if (ValidPairs == 0)
{
OutLocation = TransformHistory[SafeN - 1].Location;
OutAim = TransformHistory[SafeN - 1].Aim;
return;
}
AvgLinearVelocity /= (double)ValidPairs;
AvgAimDelta /= (double)ValidPairs;
// Extrapolate from the last SAFE sample to current time
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
double ExtrapolationTime = CurrentTime - LastSafe.Timestamp;
OutLocation = LastSafe.Location + AvgLinearVelocity * ExtrapolationTime;
// Angular extrapolation using quaternion slerp
// Use first and last safe samples for rotation direction
const FTimestampedTransform& FirstSafe = TransformHistory[0];
double SafeDeltaT = LastSafe.Timestamp - FirstSafe.Timestamp;
if (SafeDeltaT > SMALL_NUMBER)
{
FQuat FirstQuat = FRotationMatrix::MakeFromX(FirstSafe.Aim).ToQuat();
FQuat LastQuat = FRotationMatrix::MakeFromX(LastSafe.Aim).ToQuat();
double TotalAlpha = ExtrapolationTime / SafeDeltaT;
FQuat PredictedQuat = FQuat::Slerp(FirstQuat, LastQuat, 1.0 + TotalAlpha);
OutAim = PredictedQuat.GetForwardVector().GetSafeNormal();
if (OutAim.IsNearlyZero())
{
OutAim = LastSafe.Aim;
}
}
else
{
OutAim = LastSafe.Aim;
}
}
// --- Weighted Linear Regression ---
// Fits a weighted least-squares line through only the SAFE samples, extrapolates to current time.
// More recent safe samples get higher weight.
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;
// Weighted linear regression: y = a + b * t
// Weights: linearly increasing (1, 2, 3, ..., SafeN)
double SumW = 0.0;
double SumWT = 0.0;
double SumWTT = 0.0;
FVector SumWY = FVector::ZeroVector;
FVector SumWTY = FVector::ZeroVector;
FVector SumWAim = FVector::ZeroVector;
FVector SumWTAim = FVector::ZeroVector;
for (int32 i = 0; i < SafeN; i++)
{
double w = FMath::Pow((double)(i + 1), (double)RegressionWeightExponent); // Weight curve controlled by exponent
double t = TransformHistory[i].Timestamp - T0;
SumW += w;
SumWT += w * t;
SumWTT += w * t * t;
SumWY += TransformHistory[i].Location * w;
SumWTY += TransformHistory[i].Location * (w * t);
SumWAim += TransformHistory[i].Aim * w;
SumWTAim += TransformHistory[i].Aim * (w * t);
}
double Det = SumW * SumWTT - SumWT * SumWT;
if (FMath::Abs(Det) <= SMALL_NUMBER)
{
OutLocation = TransformHistory[SafeN - 1].Location;
OutAim = TransformHistory[SafeN - 1].Aim;
return;
}
// Solve for position: intercept (a) and slope (b)
FVector PosIntercept = (SumWY * SumWTT - SumWTY * SumWT) / Det;
FVector PosSlope = (SumWTY * SumW - SumWY * SumWT) / Det;
// Solve for aim: intercept (a) and slope (b)
FVector AimIntercept = (SumWAim * SumWTT - SumWTAim * SumWT) / Det;
FVector AimSlope = (SumWTAim * SumW - SumWAim * SumWT) / Det;
// Extrapolate to current time
double TPred = CurrentTime - T0;
OutLocation = PosIntercept + PosSlope * TPred;
FVector PredAim = AimIntercept + AimSlope * TPred;
OutAim = PredAim.GetSafeNormal();
if (OutAim.IsNearlyZero())
{
OutAim = TransformHistory[SafeN - 1].Aim;
}
}
// --- Simplified Kalman Filter ---
// Maintains state estimate [position, velocity, aim, angular_velocity]
// Only fed with SAFE (non-contaminated) measurements, predicts forward to current time
void UEBBarrel::UpdateKalmanFilter(double CurrentTime, const FVector& MeasuredLocation, const FVector& MeasuredAim)
{
if (!KalmanInitialized)
{
KalmanPosition = MeasuredLocation;
KalmanVelocity = FVector::ZeroVector;
KalmanAim = MeasuredAim;
KalmanAngularVelocity = FVector::ZeroVector;
KalmanPosVariance = 1.0f;
KalmanVelVariance = 1.0f;
KalmanAimVariance = 1.0f;
KalmanAngVelVariance = 1.0f;
KalmanInitialized = true;
KalmanLastTimestamp = CurrentTime;
return;
}
float dt = (float)(CurrentTime - KalmanLastTimestamp);
if (dt <= SMALL_NUMBER)
{
return;
}
KalmanLastTimestamp = CurrentTime;
float Q = KalmanProcessNoise;
float R = KalmanMeasurementNoise;
// --- Position / Velocity ---
// Predict step
FVector PredPos = KalmanPosition + KalmanVelocity * dt;
FVector PredVel = KalmanVelocity;
float PredPosVar = KalmanPosVariance + KalmanVelVariance * dt * dt + Q * dt;
float PredVelVar = KalmanVelVariance + Q * dt;
// Update step (measurement = MeasuredLocation)
float KGainPos = PredPosVar / (PredPosVar + R);
KalmanPosition = PredPos + (MeasuredLocation - PredPos) * KGainPos;
KalmanPosVariance = (1.0f - KGainPos) * PredPosVar;
// Update velocity estimate from innovation
FVector VelInnovation = (MeasuredLocation - PredPos) / dt;
float KGainVel = PredVelVar / (PredVelVar + R / (dt * dt));
KalmanVelocity = PredVel + VelInnovation * KGainVel;
KalmanVelVariance = (1.0f - KGainVel) * PredVelVar;
// --- Aim / Angular Velocity ---
FVector PredAim = KalmanAim + KalmanAngularVelocity * dt;
FVector PredAngVel = KalmanAngularVelocity;
float PredAimVar = KalmanAimVariance + KalmanAngVelVariance * dt * dt + Q * dt;
float PredAngVelVar = KalmanAngVelVariance + Q * dt;
float KGainAim = PredAimVar / (PredAimVar + R);
KalmanAim = PredAim + (MeasuredAim - PredAim) * KGainAim;
KalmanAimVariance = (1.0f - KGainAim) * PredAimVar;
FVector AngVelInnovation = (MeasuredAim - PredAim) / dt;
float KGainAngVel = PredAngVelVar / (PredAngVelVar + R / (dt * dt));
KalmanAngularVelocity = PredAngVel + AngVelInnovation * KGainAngVel;
KalmanAngVelVariance = (1.0f - KGainAngVel) * PredAngVelVar;
}
void UEBBarrel::PredictKalmanFilter(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
{
if (!KalmanInitialized)
{
OutLocation = GetComponentTransform().GetLocation();
OutAim = GetComponentTransform().GetUnitAxis(EAxis::X);
return;
}
// Extrapolate from Kalman state to current time
float dt = (float)(CurrentTime - KalmanLastTimestamp);
OutLocation = KalmanPosition + KalmanVelocity * dt;
FVector PredAim = KalmanAim + KalmanAngularVelocity * dt;
OutAim = PredAim.GetSafeNormal();
if (OutAim.IsNearlyZero())
{
OutAim = KalmanAim.GetSafeNormal();
}
}

View File

@ -1,5 +1,6 @@
// Copyright 2018 Mookie. All Rights Reserved.
#include "EBBarrel.h"
#include "DrawDebugHelpers.h"
UEBBarrel::UEBBarrel() {
PrimaryComponentTick.bCanEverTick = true;
@ -16,57 +17,22 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (AntiRecoil)
{
if (previousAim.Num() > AntiRecoilPrevCount)
{
previousAim.RemoveAt(0);
}
if (previousLocation.Num() > AntiRecoilPrevCount)
{
previousLocation.RemoveAt(0);
}
}
// Update anti-recoil transform history
UpdateTransformHistory();
if (ClientSideAim){
if (GetOwner()->GetRemoteRole()==ROLE_Authority){
TimeSinceAimUpdate += DeltaTime;
if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) {
if (AntiRecoil)
{
// ForAntiRecoil
previousAim.Add(GetComponentTransform().GetUnitAxis(EAxis::X));
previousLocation.Add(GetComponentTransform().GetLocation());
Aim = previousAim[0];
Location = previousLocation[0];
}
else
{
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation();
}
ComputeAntiRecoilTransform();
ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim);
TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency);
};
}else{
if (!RemoteAimReceived) {
if (AntiRecoil)
{
// ForAntiRecoil
previousAim.Add(GetComponentTransform().GetUnitAxis(EAxis::X));
previousLocation.Add(GetComponentTransform().GetLocation());
Aim = previousAim[0];
Location = previousLocation[0];
}
else
{
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation();
}
ComputeAntiRecoilTransform();
}
else {
FVector LocOffset = (Location - GetComponentLocation());
@ -78,19 +44,67 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
}
}
else {
if (AntiRecoil)
{
// ForAntiRecoil
previousAim.Add(GetComponentTransform().GetUnitAxis(EAxis::X));
previousLocation.Add(GetComponentTransform().GetLocation());
ComputeAntiRecoilTransform();
}
Aim = previousAim[0];
Location = previousLocation[0];
// Debug visualization: raw tracker (green) vs predicted aim (red)
if (DebugAntiRecoil)
{
// Use the latest buffer entry as "raw" data (includes simulated shock if active)
FVector RawLocation;
FVector RawAim;
if (TransformHistory.Num() > 0)
{
RawLocation = TransformHistory.Last().Location;
RawAim = TransformHistory.Last().Aim;
}
else
{
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
Location = GetComponentTransform().GetLocation();
RawLocation = GetComponentTransform().GetLocation();
RawAim = GetComponentTransform().GetUnitAxis(EAxis::X);
}
// Green line: raw tracker data (potentially shocked if IMU simulation is active)
DrawDebugLine(GetWorld(), RawLocation, RawLocation + RawAim * DebugAntiRecoilLineLength,
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, DebugAntiRecoilLineThickness);
// 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)
{
// Yellow line: uncorrected aim (where the bullet would have gone without anti-recoil)
DrawDebugLine(GetWorld(), DebugIMUShockCapturedLocation,
DebugIMUShockCapturedLocation + DebugIMUShockCapturedAim * DebugAntiRecoilLineLength,
FColor::Yellow, false, -1.0f, 0, DebugAntiRecoilLineThickness);
DrawDebugSphere(GetWorld(), DebugIMUShockCapturedLocation, 3.0f, 8, FColor::Yellow, false, -1.0f, 0, DebugAntiRecoilLineThickness);
}
else
{
DebugIMUShockLineCaptured = false;
}
}
}
@ -222,7 +236,7 @@ void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim, in
BeforeShotFired.Broadcast();
#ifdef WITH_EDITOR
if (shotTrace) {
DrawDebugLine(GetWorld(), OutLocation, Velocity, FColor(1, 0, 0, 1), false, 3, 0, 0);
DrawDebugLine(GetWorld(), OutLocation, OutLocation + Velocity, FColor::Red, false, 3, 0, 0);
};
#endif
if (ReplicateShotFiredEvents) {

View File

@ -22,6 +22,26 @@ enum class EFireMode : uint8
FM_Gatling UMETA(DisplayName = "Gatling")
};
UENUM(BlueprintType)
enum class EAntiRecoilMode : uint8
{
ARM_None UMETA(DisplayName = "Disabled", ToolTip = "No anti-recoil processing. Uses raw tracker data directly. Use this when no IMU shock compensation is needed."),
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_WeightedRegression UMETA(DisplayName = "Weighted Regression", ToolTip = "Fits a weighted least-squares regression line through all safe samples (recent safe samples weighted higher), then extrapolates to current time. More robust to individual noisy samples than linear extrapolation. Slightly heavier computation."),
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.")
};
USTRUCT()
struct FTimestampedTransform
{
GENERATED_BODY()
double Timestamp = 0.0;
FVector Location = FVector::ZeroVector;
FVector Aim = FVector::ForwardVector;
};
UCLASS(Blueprintable, ClassGroup = (Custom), hidecategories = (Object, LOD, Physics, Lighting, TextureStreaming, Collision, HLOD, Mobile, VirtualTexture, ComponentReplication), editinlinenew, meta = (BlueprintSpawnableComponent))
class EASYBALLISTICS_API UEBBarrel : public UPrimitiveComponent
{
@ -39,8 +59,41 @@ public:
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") bool shotTrace = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") float DebugArrowSize = 100.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil") bool AntiRecoil = true;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil") int AntiRecoilPrevCount = 5;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Draw real-time debug lines: Green = raw tracker, Red = anti-recoil predicted aim"))
bool DebugAntiRecoil = false;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Length of the debug aim lines (cm)", EditCondition = "DebugAntiRecoil"))
float DebugAntiRecoilLineLength = 200.0f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Thickness of the debug aim lines", EditCondition = "DebugAntiRecoil"))
float DebugAntiRecoilLineThickness = 2.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_WeightedRegression;
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.2f;
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.05f;
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Controls how the weight curve grows across safe samples in Weighted Regression mode. 1.0 = linear growth (default), >1.0 = recent samples weighted much more heavily (convex curve), <1.0 = more uniform weighting (concave curve), 0.0 = all samples weighted equally (unweighted regression). Formula: weight = pow(sampleIndex+1, exponent).", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_WeightedRegression", ClampMin = "0.0", ClampMax = "5.0"))
float RegressionWeightExponent = 1.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"))
float KalmanProcessNoise = 50.0f;
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.1f;
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;
@ -95,6 +148,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<FVector>& Trajectory, TSubclassOf<class AEBBullet> BulletClass, TArray<AActor*>IgnoredActors, 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<FVector>& Trajectory, TSubclassOf<class AEBBullet> BulletClass, FVector StartLocation, FVector AimDirection, TArray<AActor*>IgnoredActors, float MaxTime = 10.0f, float Step = 0.1f) const;
UFUNCTION(BlueprintCallable, Category = "Prediction") void CalculateAimDirection(TSubclassOf<class AEBBullet> 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;
@ -145,8 +201,38 @@ private:
FVector Aim;
FVector Location;
TArray<FVector> previousAim;
TArray<FVector> previousLocation;
TArray<FTimestampedTransform> 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 aim after shock)
bool DebugIMUShockLineCaptured = false;
double DebugIMUShockLineEndTime = 0.0;
FVector DebugIMUShockCapturedLocation = FVector::ZeroVector;
FVector DebugIMUShockCapturedAim = FVector::ForwardVector;
// Kalman filter state
FVector KalmanPosition = FVector::ZeroVector;
FVector KalmanVelocity = FVector::ZeroVector;
FVector KalmanAim = FVector::ForwardVector;
FVector KalmanAngularVelocity = FVector::ZeroVector;
float KalmanPosVariance = 1.0f;
float KalmanVelVariance = 1.0f;
float KalmanAimVariance = 1.0f;
float KalmanAngVelVariance = 1.0f;
bool KalmanInitialized = false;
double KalmanLastTimestamp = 0.0;
void UpdateTransformHistory();
void ComputeAntiRecoilTransform();
void PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
void PredictWeightedRegression(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;
bool RemoteAimReceived;
float TimeSinceAimUpdate;