Compare commits
3 Commits
4e9c33778c
...
ba6b35b3d9
| Author | SHA1 | Date | |
|---|---|---|---|
| ba6b35b3d9 | |||
| 1a5b107b1f | |||
| cd097e4e55 |
@ -17,7 +17,17 @@
|
||||
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_170543.csv\")",
|
||||
"Bash(find C:ASTERIONSVNDEVPROSERVE_UE_5_5Plugins -type f \\\\\\(-name *.cpp -o -name *.h \\\\\\))",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)"
|
||||
"Bash(git commit:*)",
|
||||
"Bash(find E:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved -name AntiRecoil* -type f)",
|
||||
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_140946.csv\" --grid)",
|
||||
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_140946.csv\" \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_141329.csv\")",
|
||||
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_140946.csv\" \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_141329.csv\" --grid)",
|
||||
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_140946.csv\" \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_141329.csv\" --grid --strategy worst_case)",
|
||||
"Bash(python analyze_antirecoil.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_162726.csv\" \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_162234.csv\" --grid --strategy worst_case)",
|
||||
"Bash(python analyze_shots.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_162726.csv\")",
|
||||
"Bash(python analyze_shots.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_163533.csv\")",
|
||||
"Bash(python analyze_shots.py \"C:/ASTERION/SVN/DEV/PROSERVE_UE_5_5/Saved/Logs/AntiRecoil_20260318_181404.csv\")",
|
||||
"Bash(python -c \":*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Tools/__pycache__/analyze_antirecoil.cpython-312.pyc
Normal file
BIN
Tools/__pycache__/analyze_antirecoil.cpython-312.pyc
Normal file
Binary file not shown.
BIN
Tools/__pycache__/analyze_shots.cpython-312.pyc
Normal file
BIN
Tools/__pycache__/analyze_shots.cpython-312.pyc
Normal file
Binary file not shown.
@ -1,27 +1,32 @@
|
||||
"""
|
||||
Anti-Recoil Prediction Analyzer
|
||||
Anti-Recoil Parameter Optimizer
|
||||
================================
|
||||
Reads CSV files recorded by the EBBarrel CSV recording feature and finds
|
||||
optimal AdaptiveSensitivity and scaling factor for the Adaptive Extrapolation mode.
|
||||
optimal parameters for the Adaptive Extrapolation mode.
|
||||
|
||||
Usage:
|
||||
python analyze_antirecoil.py <path_to_csv>
|
||||
python analyze_antirecoil.py <path_to_csv> --plot (requires matplotlib)
|
||||
python analyze_antirecoil.py <csv_file> [csv_file2 ...] [options]
|
||||
|
||||
Options:
|
||||
--plot Generate comparison plots (requires matplotlib)
|
||||
--grid Use grid search instead of differential evolution
|
||||
--strategy <s> Multi-file aggregation: mean (default), worst_case
|
||||
--max-iter <n> Max optimizer iterations (default: 200)
|
||||
|
||||
The script:
|
||||
1. Loads per-frame data (real position/aim vs predicted position/aim)
|
||||
2. Computes prediction error at each frame
|
||||
3. Simulates different AdaptiveSensitivity values offline
|
||||
4. Finds the value that minimizes total prediction error
|
||||
5. Reports recommended parameters
|
||||
2. Simulates adaptive extrapolation offline (matching C++ exactly)
|
||||
3. Optimizes all 4 parameters: Sensitivity, DeadZone, MinSpeed, Damping
|
||||
4. Reports recommended parameters with per-file breakdown
|
||||
"""
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import math
|
||||
import os
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -34,13 +39,38 @@ class Frame:
|
||||
safe_count: int
|
||||
buffer_count: int
|
||||
extrap_time: float
|
||||
shot_fired: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class AdaptiveParams:
|
||||
sensitivity: float = 3.0
|
||||
dead_zone: float = 0.95
|
||||
min_speed: float = 0.0
|
||||
damping: float = 5.0
|
||||
buffer_time_ms: float = 200.0
|
||||
discard_time_ms: float = 30.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScoreResult:
|
||||
pos_mean: float
|
||||
pos_p95: float
|
||||
aim_mean: float
|
||||
aim_p95: float
|
||||
jitter: float
|
||||
overshoot: float
|
||||
score: float
|
||||
|
||||
|
||||
def load_csv(path: str) -> List[Frame]:
|
||||
frames = []
|
||||
with open(path, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
has_shot_col = False
|
||||
for row in reader:
|
||||
if not has_shot_col and 'ShotFired' in row:
|
||||
has_shot_col = True
|
||||
frames.append(Frame(
|
||||
timestamp=float(row['Timestamp']),
|
||||
real_pos=(float(row['RealPosX']), float(row['RealPosY']), float(row['RealPosZ'])),
|
||||
@ -50,10 +80,13 @@ def load_csv(path: str) -> List[Frame]:
|
||||
safe_count=int(row['SafeCount']),
|
||||
buffer_count=int(row['BufferCount']),
|
||||
extrap_time=float(row['ExtrapolationTime']),
|
||||
shot_fired=int(row.get('ShotFired', 0)) == 1,
|
||||
))
|
||||
return frames
|
||||
|
||||
|
||||
# --- Vector math helpers ---
|
||||
|
||||
def vec_dist(a, b):
|
||||
return math.sqrt(sum((ai - bi) ** 2 for ai, bi in zip(a, b)))
|
||||
|
||||
@ -88,6 +121,8 @@ def angle_between(a, b):
|
||||
return math.degrees(math.acos(dot))
|
||||
|
||||
|
||||
# --- Prediction error from recorded data ---
|
||||
|
||||
def compute_prediction_error(frames: List[Frame]) -> dict:
|
||||
"""Compute error between predicted and actual (real) positions/aims."""
|
||||
pos_errors = []
|
||||
@ -122,231 +157,614 @@ def compute_prediction_error(frames: List[Frame]) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def simulate_adaptive(frames: List[Frame], sensitivity: float) -> List[Tuple[float, float]]:
|
||||
# --- Shot contamination analysis ---
|
||||
|
||||
def analyze_shot_contamination(frames: List[Frame], analysis_window_ms: float = 200.0):
|
||||
"""
|
||||
Simulate the adaptive extrapolation offline with given sensitivity.
|
||||
Uses separate pos/aim confidences with relative variance (matching C++ code).
|
||||
Returns list of (position_error, aim_error) per frame.
|
||||
Analyze how shots contaminate the tracking data.
|
||||
For each shot, measure the velocity/acceleration spike and how long it takes
|
||||
to return to baseline. This tells us the minimum discard_time needed.
|
||||
|
||||
Returns a dict with analysis results, or None if no shots found.
|
||||
"""
|
||||
shot_indices = [i for i, f in enumerate(frames) if f.shot_fired]
|
||||
if not shot_indices:
|
||||
return None
|
||||
|
||||
analysis_window_s = analysis_window_ms / 1000.0
|
||||
|
||||
# Compute per-frame speeds
|
||||
speeds = [0.0]
|
||||
for i in range(1, len(frames)):
|
||||
dt = frames[i].timestamp - frames[i - 1].timestamp
|
||||
if dt > 1e-6:
|
||||
d = vec_dist(frames[i].real_pos, frames[i - 1].real_pos)
|
||||
speeds.append(d / dt)
|
||||
else:
|
||||
speeds.append(speeds[-1] if speeds else 0.0)
|
||||
|
||||
# For each shot, measure the speed profile before and after
|
||||
contamination_durations = []
|
||||
speed_spikes = []
|
||||
|
||||
for si in shot_indices:
|
||||
# Baseline speed: average speed in 100ms BEFORE the shot
|
||||
baseline_speeds = []
|
||||
for j in range(si - 1, -1, -1):
|
||||
if frames[si].timestamp - frames[j].timestamp > 0.1:
|
||||
break
|
||||
baseline_speeds.append(speeds[j])
|
||||
|
||||
if not baseline_speeds:
|
||||
continue
|
||||
|
||||
baseline_mean = sum(baseline_speeds) / len(baseline_speeds)
|
||||
baseline_std = math.sqrt(sum((s - baseline_mean) ** 2 for s in baseline_speeds) / len(baseline_speeds)) if len(baseline_speeds) > 1 else baseline_mean * 0.1
|
||||
|
||||
# Threshold: speed is "contaminated" if it deviates by more than 3 sigma from baseline
|
||||
threshold = baseline_mean + max(3.0 * baseline_std, 10.0) # at least 10 cm/s spike
|
||||
|
||||
# Find how long after the shot the speed stays above threshold
|
||||
max_speed = 0.0
|
||||
last_contaminated_time = 0.0
|
||||
for j in range(si, len(frames)):
|
||||
dt_from_shot = frames[j].timestamp - frames[si].timestamp
|
||||
if dt_from_shot > analysis_window_s:
|
||||
break
|
||||
if speeds[j] > threshold:
|
||||
last_contaminated_time = dt_from_shot
|
||||
if speeds[j] > max_speed:
|
||||
max_speed = speeds[j]
|
||||
|
||||
contamination_durations.append(last_contaminated_time * 1000.0) # in ms
|
||||
speed_spikes.append(max_speed - baseline_mean)
|
||||
|
||||
if not contamination_durations:
|
||||
return None
|
||||
|
||||
contamination_durations.sort()
|
||||
return {
|
||||
'num_shots': len(shot_indices),
|
||||
'contamination_mean_ms': sum(contamination_durations) / len(contamination_durations),
|
||||
'contamination_p95_ms': contamination_durations[int(len(contamination_durations) * 0.95)],
|
||||
'contamination_max_ms': contamination_durations[-1],
|
||||
'speed_spike_mean': sum(speed_spikes) / len(speed_spikes) if speed_spikes else 0,
|
||||
'speed_spike_max': max(speed_spikes) if speed_spikes else 0,
|
||||
'recommended_discard_ms': math.ceil(contamination_durations[int(len(contamination_durations) * 0.95)] / 5.0) * 5.0, # round up to 5ms
|
||||
}
|
||||
|
||||
|
||||
# --- Offline adaptive extrapolation simulation (matches C++ exactly) ---
|
||||
|
||||
def simulate_adaptive(frames: List[Frame], params: AdaptiveParams) -> Tuple[List[float], List[float]]:
|
||||
"""
|
||||
Simulate the adaptive extrapolation offline with given parameters.
|
||||
Matches the C++ PredictAdaptiveExtrapolation algorithm exactly.
|
||||
Optimized for speed: pre-extracts arrays, inlines math, avoids allocations.
|
||||
"""
|
||||
pos_errors = []
|
||||
aim_errors = []
|
||||
|
||||
# Sliding window for velocity estimation (matches C++ safe window ~18 samples)
|
||||
window_size = 12
|
||||
n_frames = len(frames)
|
||||
if n_frames < 4:
|
||||
return pos_errors, aim_errors
|
||||
|
||||
# Pre-extract into flat arrays for speed
|
||||
ts = [f.timestamp for f in frames]
|
||||
px = [f.real_pos[0] for f in frames]
|
||||
py = [f.real_pos[1] for f in frames]
|
||||
pz = [f.real_pos[2] for f in frames]
|
||||
ax = [f.real_aim[0] for f in frames]
|
||||
ay = [f.real_aim[1] for f in frames]
|
||||
az = [f.real_aim[2] for f in frames]
|
||||
|
||||
buffer_s = params.buffer_time_ms / 1000.0
|
||||
discard_s = params.discard_time_ms / 1000.0
|
||||
sensitivity = params.sensitivity
|
||||
dead_zone = params.dead_zone
|
||||
min_speed = params.min_speed
|
||||
damping = params.damping
|
||||
SMALL = 1e-10
|
||||
_sqrt = math.sqrt
|
||||
_exp = math.exp
|
||||
_acos = math.acos
|
||||
_degrees = math.degrees
|
||||
_pow = pow
|
||||
|
||||
for i in range(window_size + 1, len(frames) - 1):
|
||||
# Build velocity pairs from recent real positions and aims
|
||||
pos_vels = []
|
||||
aim_vels = []
|
||||
for j in range(1, min(window_size, i)):
|
||||
dt = frames[i - j].timestamp - frames[i - j - 1].timestamp
|
||||
if dt > 1e-6:
|
||||
pos_vel = vec_scale(vec_sub(frames[i - j].real_pos, frames[i - j - 1].real_pos), 1.0 / dt)
|
||||
aim_vel = vec_scale(vec_sub(frames[i - j].real_aim, frames[i - j - 1].real_aim), 1.0 / dt)
|
||||
pos_vels.append(pos_vel)
|
||||
aim_vels.append(aim_vel)
|
||||
for i in range(2, n_frames - 1):
|
||||
ct = ts[i]
|
||||
safe_cutoff = ct - discard_s
|
||||
oldest_allowed = ct - buffer_s
|
||||
|
||||
if len(pos_vels) < 4:
|
||||
# Collect safe sample indices (backward scan, then reverse)
|
||||
safe = []
|
||||
for j in range(i, -1, -1):
|
||||
t = ts[j]
|
||||
if t < oldest_allowed:
|
||||
break
|
||||
if t <= safe_cutoff:
|
||||
safe.append(j)
|
||||
safe.reverse()
|
||||
|
||||
ns = len(safe)
|
||||
if ns < 2:
|
||||
continue
|
||||
|
||||
n = len(pos_vels)
|
||||
# Build velocity pairs inline
|
||||
vpx = []; vpy = []; vpz = []
|
||||
vax = []; vay = []; vaz = []
|
||||
for k in range(1, ns):
|
||||
p, c = safe[k - 1], safe[k]
|
||||
dt = ts[c] - ts[p]
|
||||
if dt > 1e-6:
|
||||
inv_dt = 1.0 / dt
|
||||
vpx.append((px[c] - px[p]) * inv_dt)
|
||||
vpy.append((py[c] - py[p]) * inv_dt)
|
||||
vpz.append((pz[c] - pz[p]) * inv_dt)
|
||||
vax.append((ax[c] - ax[p]) * inv_dt)
|
||||
vay.append((ay[c] - ay[p]) * inv_dt)
|
||||
vaz.append((az[c] - az[p]) * inv_dt)
|
||||
|
||||
# Weighted average velocity (recent samples weighted more, matching C++)
|
||||
total_w = 0.0
|
||||
avg_pos_vel = (0.0, 0.0, 0.0)
|
||||
avg_aim_vel = (0.0, 0.0, 0.0)
|
||||
for k in range(n):
|
||||
w = (k + 1) ** 2
|
||||
avg_pos_vel = vec_add(avg_pos_vel, vec_scale(pos_vels[k], w))
|
||||
avg_aim_vel = vec_add(avg_aim_vel, vec_scale(aim_vels[k], w))
|
||||
total_w += w
|
||||
avg_pos_vel = vec_scale(avg_pos_vel, 1.0 / total_w)
|
||||
avg_aim_vel = vec_scale(avg_aim_vel, 1.0 / total_w)
|
||||
nv = len(vpx)
|
||||
if nv < 2:
|
||||
continue
|
||||
|
||||
# Deceleration detection: recent speed (last 25%) vs average speed
|
||||
recent_start = max(0, n - max(1, n // 4))
|
||||
recent_count = n - recent_start
|
||||
recent_pos_vel = (0.0, 0.0, 0.0)
|
||||
recent_aim_vel = (0.0, 0.0, 0.0)
|
||||
for k in range(recent_start, n):
|
||||
recent_pos_vel = vec_add(recent_pos_vel, pos_vels[k])
|
||||
recent_aim_vel = vec_add(recent_aim_vel, aim_vels[k])
|
||||
recent_pos_vel = vec_scale(recent_pos_vel, 1.0 / recent_count)
|
||||
recent_aim_vel = vec_scale(recent_aim_vel, 1.0 / recent_count)
|
||||
# Weighted average velocity (quadratic weights, oldest=index 0)
|
||||
tw = 0.0
|
||||
apx = apy = apz = 0.0
|
||||
aax = aay = aaz = 0.0
|
||||
for k in range(nv):
|
||||
w = (k + 1) * (k + 1)
|
||||
apx += vpx[k] * w; apy += vpy[k] * w; apz += vpz[k] * w
|
||||
aax += vax[k] * w; aay += vay[k] * w; aaz += vaz[k] * w
|
||||
tw += w
|
||||
inv_tw = 1.0 / tw
|
||||
apx *= inv_tw; apy *= inv_tw; apz *= inv_tw
|
||||
aax *= inv_tw; aay *= inv_tw; aaz *= inv_tw
|
||||
|
||||
avg_pos_speed = vec_len(avg_pos_vel)
|
||||
avg_aim_speed = vec_len(avg_aim_vel)
|
||||
recent_pos_speed = vec_len(recent_pos_vel)
|
||||
recent_aim_speed = vec_len(recent_aim_vel)
|
||||
# Recent velocity (last 25%, unweighted)
|
||||
rs = max(0, nv - max(1, nv // 4))
|
||||
rc = nv - rs
|
||||
rpx = rpy = rpz = 0.0
|
||||
rax = ray = raz = 0.0
|
||||
for k in range(rs, nv):
|
||||
rpx += vpx[k]; rpy += vpy[k]; rpz += vpz[k]
|
||||
rax += vax[k]; ray += vay[k]; raz += vaz[k]
|
||||
inv_rc = 1.0 / rc
|
||||
rpx *= inv_rc; rpy *= inv_rc; rpz *= inv_rc
|
||||
rax *= inv_rc; ray *= inv_rc; raz *= inv_rc
|
||||
|
||||
# Confidence = (recentSpeed / avgSpeed) ^ sensitivity
|
||||
pos_confidence = 1.0
|
||||
if avg_pos_speed > SMALL:
|
||||
pos_ratio = min(recent_pos_speed / avg_pos_speed, 1.0)
|
||||
pos_confidence = pos_ratio ** sensitivity
|
||||
avg_ps = _sqrt(apx*apx + apy*apy + apz*apz)
|
||||
avg_as = _sqrt(aax*aax + aay*aay + aaz*aaz)
|
||||
rec_ps = _sqrt(rpx*rpx + rpy*rpy + rpz*rpz)
|
||||
rec_as = _sqrt(rax*rax + ray*ray + raz*raz)
|
||||
|
||||
aim_confidence = 1.0
|
||||
if avg_aim_speed > SMALL:
|
||||
aim_ratio = min(recent_aim_speed / avg_aim_speed, 1.0)
|
||||
aim_confidence = aim_ratio ** sensitivity
|
||||
# Position confidence
|
||||
pc = 1.0
|
||||
if avg_ps > min_speed:
|
||||
ratio = rec_ps / avg_ps
|
||||
if ratio > 1.0: ratio = 1.0
|
||||
if ratio < dead_zone:
|
||||
rm = ratio / dead_zone if dead_zone > SMALL else 0.0
|
||||
if rm > 1.0: rm = 1.0
|
||||
pc = _pow(rm, sensitivity)
|
||||
|
||||
# Aim confidence
|
||||
ac = 1.0
|
||||
if avg_as > min_speed:
|
||||
ratio = rec_as / avg_as
|
||||
if ratio > 1.0: ratio = 1.0
|
||||
if ratio < dead_zone:
|
||||
rm = ratio / dead_zone if dead_zone > SMALL else 0.0
|
||||
if rm > 1.0: rm = 1.0
|
||||
ac = _pow(rm, sensitivity)
|
||||
|
||||
# Extrapolation time
|
||||
extrap_dt = frames[i].extrap_time if frames[i].extrap_time > 0 else 0.011
|
||||
lsi = safe[-1]
|
||||
edt = ct - ts[lsi]
|
||||
if edt <= 0: edt = 0.011
|
||||
|
||||
# Predict from last safe position
|
||||
pred_pos = vec_add(frames[i - 1].real_pos, vec_scale(avg_pos_vel, extrap_dt * pos_confidence))
|
||||
pred_aim_raw = vec_add(frames[i - 1].real_aim, vec_scale(avg_aim_vel, extrap_dt * aim_confidence))
|
||||
pred_aim = vec_normalize(pred_aim_raw)
|
||||
# Damping
|
||||
ds = _exp(-damping * edt) if damping > 0.0 else 1.0
|
||||
|
||||
# Errors
|
||||
pos_errors.append(vec_dist(pred_pos, frames[i].real_pos))
|
||||
real_aim_n = vec_normalize(frames[i].real_aim)
|
||||
if vec_len(pred_aim) > 0.5 and vec_len(real_aim_n) > 0.5:
|
||||
aim_errors.append(angle_between(pred_aim, real_aim_n))
|
||||
# Predict
|
||||
m = edt * pc * ds
|
||||
ppx = px[lsi] + apx * m
|
||||
ppy = py[lsi] + apy * m
|
||||
ppz = pz[lsi] + apz * m
|
||||
|
||||
ma = edt * ac * ds
|
||||
pax_r = ax[lsi] + aax * ma
|
||||
pay_r = ay[lsi] + aay * ma
|
||||
paz_r = az[lsi] + aaz * ma
|
||||
pa_len = _sqrt(pax_r*pax_r + pay_r*pay_r + paz_r*paz_r)
|
||||
|
||||
# Position error
|
||||
dx = ppx - px[i]; dy = ppy - py[i]; dz = ppz - pz[i]
|
||||
pos_errors.append(_sqrt(dx*dx + dy*dy + dz*dz))
|
||||
|
||||
# Aim error
|
||||
if pa_len > 0.5:
|
||||
inv_pa = 1.0 / pa_len
|
||||
pax_n = pax_r * inv_pa; pay_n = pay_r * inv_pa; paz_n = paz_r * inv_pa
|
||||
ra_len = _sqrt(ax[i]*ax[i] + ay[i]*ay[i] + az[i]*az[i])
|
||||
if ra_len > 0.5:
|
||||
inv_ra = 1.0 / ra_len
|
||||
dot = pax_n * ax[i] * inv_ra + pay_n * ay[i] * inv_ra + paz_n * az[i] * inv_ra
|
||||
if dot > 1.0: dot = 1.0
|
||||
if dot < -1.0: dot = -1.0
|
||||
aim_errors.append(_degrees(_acos(dot)))
|
||||
|
||||
return pos_errors, aim_errors
|
||||
|
||||
|
||||
def find_optimal_parameters(frames: List[Frame]) -> dict:
|
||||
"""Search for optimal AdaptiveSensitivity (power curve exponent for deceleration detection)."""
|
||||
print("\nSearching for optimal AdaptiveSensitivity (power exponent)...")
|
||||
print("-" * 60)
|
||||
# --- Scoring ---
|
||||
|
||||
best_score = float('inf')
|
||||
best_sensitivity = 1.0
|
||||
|
||||
# Search grid — exponent range 0.1 to 5.0
|
||||
sensitivities = [0.1, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]
|
||||
|
||||
results = []
|
||||
|
||||
for sens in sensitivities:
|
||||
pos_errors, aim_errors = simulate_adaptive(frames, sens)
|
||||
def compute_score(pos_errors: List[float], aim_errors: List[float]) -> ScoreResult:
|
||||
"""Compute a combined score from position and aim errors, including stability metrics."""
|
||||
if not pos_errors:
|
||||
continue
|
||||
pos_errors_s = sorted(pos_errors)
|
||||
aim_errors_s = sorted(aim_errors) if aim_errors else [0]
|
||||
return ScoreResult(0, 0, 0, 0, 0, 0, float('inf'))
|
||||
|
||||
pos_sorted = sorted(pos_errors)
|
||||
aim_sorted = sorted(aim_errors) if aim_errors else [0]
|
||||
|
||||
pos_mean = sum(pos_errors) / len(pos_errors)
|
||||
pos_p95 = pos_errors_s[int(len(pos_errors_s) * 0.95)]
|
||||
pos_p95 = pos_sorted[int(len(pos_sorted) * 0.95)]
|
||||
aim_mean = sum(aim_errors) / len(aim_errors) if aim_errors else 0
|
||||
aim_p95 = aim_errors_s[int(len(aim_errors_s) * 0.95)] if aim_errors else 0
|
||||
aim_p95 = aim_sorted[int(len(aim_sorted) * 0.95)] if aim_errors else 0
|
||||
|
||||
# Score: combine position and aim errors (aim weighted more since it's the main issue)
|
||||
score = pos_mean * 0.3 + pos_p95 * 0.2 + aim_mean * 0.3 + aim_p95 * 0.2
|
||||
# Jitter: standard deviation of frame-to-frame error change
|
||||
jitter = 0.0
|
||||
if len(pos_errors) > 1:
|
||||
deltas = [abs(pos_errors[i] - pos_errors[i - 1]) for i in range(1, len(pos_errors))]
|
||||
delta_mean = sum(deltas) / len(deltas)
|
||||
jitter = math.sqrt(sum((d - delta_mean) ** 2 for d in deltas) / len(deltas))
|
||||
|
||||
results.append((sens, pos_mean, pos_p95, aim_mean, aim_p95, score))
|
||||
# Overshoot: percentage of frames where error spikes above 2x mean
|
||||
overshoot = 0.0
|
||||
if pos_mean > 0:
|
||||
overshoot_count = sum(1 for e in pos_errors if e > 2.0 * pos_mean)
|
||||
overshoot = overshoot_count / len(pos_errors)
|
||||
|
||||
# Combined score
|
||||
score = (pos_mean * 0.25 + pos_p95 * 0.15 +
|
||||
aim_mean * 0.25 + aim_p95 * 0.15 +
|
||||
jitter * 0.10 + overshoot * 0.10)
|
||||
|
||||
return ScoreResult(pos_mean, pos_p95, aim_mean, aim_p95, jitter, overshoot, score)
|
||||
|
||||
|
||||
def aggregate_scores(per_file_scores: List[Tuple[str, ScoreResult]],
|
||||
strategy: str = "mean") -> float:
|
||||
"""Aggregate scores across multiple files."""
|
||||
scores = [s.score for _, s in per_file_scores]
|
||||
if not scores:
|
||||
return float('inf')
|
||||
if strategy == "worst_case":
|
||||
return max(scores)
|
||||
else: # mean
|
||||
return sum(scores) / len(scores)
|
||||
|
||||
|
||||
# --- Optimizer ---
|
||||
|
||||
def objective(x, all_frames, strategy):
|
||||
"""Objective function for the optimizer."""
|
||||
params = AdaptiveParams(
|
||||
sensitivity=x[0],
|
||||
dead_zone=x[1],
|
||||
min_speed=x[2],
|
||||
damping=x[3],
|
||||
buffer_time_ms=x[4],
|
||||
discard_time_ms=x[5]
|
||||
)
|
||||
per_file_scores = []
|
||||
for name, frames in all_frames:
|
||||
pos_errors, aim_errors = simulate_adaptive(frames, params)
|
||||
score_result = compute_score(pos_errors, aim_errors)
|
||||
per_file_scores.append((name, score_result))
|
||||
return aggregate_scores(per_file_scores, strategy)
|
||||
|
||||
|
||||
def optimize_differential_evolution(all_frames, strategy="mean", max_iter=200, min_discard_ms=10.0):
|
||||
"""Find optimal parameters using scipy differential evolution."""
|
||||
try:
|
||||
from scipy.optimize import differential_evolution
|
||||
except ImportError:
|
||||
print("ERROR: scipy is required for optimization.")
|
||||
print("Install with: pip install scipy")
|
||||
sys.exit(1)
|
||||
|
||||
bounds = [
|
||||
(0.1, 5.0), # sensitivity
|
||||
(0.0, 0.95), # dead_zone
|
||||
(0.0, 200.0), # min_speed
|
||||
(0.0, 50.0), # damping
|
||||
(100.0, 500.0), # buffer_time_ms
|
||||
(max(10.0, min_discard_ms), 100.0), # discard_time_ms (floor from contamination analysis)
|
||||
]
|
||||
|
||||
print(f"\nRunning differential evolution (maxiter={max_iter}, popsize=25, min_discard={min_discard_ms:.0f}ms)...")
|
||||
print("This may take a few minutes...\n")
|
||||
|
||||
result = differential_evolution(
|
||||
objective,
|
||||
bounds,
|
||||
args=(all_frames, strategy),
|
||||
maxiter=max_iter,
|
||||
seed=42,
|
||||
tol=1e-4,
|
||||
popsize=25,
|
||||
disp=True,
|
||||
workers=1
|
||||
)
|
||||
|
||||
best_params = AdaptiveParams(
|
||||
sensitivity=round(result.x[0], 2),
|
||||
dead_zone=round(result.x[1], 3),
|
||||
min_speed=round(result.x[2], 1),
|
||||
damping=round(result.x[3], 1),
|
||||
buffer_time_ms=round(result.x[4], 0),
|
||||
discard_time_ms=round(result.x[5], 0)
|
||||
)
|
||||
return best_params, result.fun
|
||||
|
||||
|
||||
def optimize_grid_search(all_frames, strategy="mean", min_discard_ms=10.0):
|
||||
"""Find optimal parameters using grid search (slower but no scipy needed)."""
|
||||
print(f"\nRunning grid search over 6 parameters (min_discard={min_discard_ms:.0f}ms)...")
|
||||
|
||||
sensitivities = [1.0, 2.0, 3.0, 4.0]
|
||||
dead_zones = [0.7, 0.8, 0.9]
|
||||
min_speeds = [0.0, 30.0]
|
||||
dampings = [5.0, 10.0, 15.0]
|
||||
buffer_times = [300.0, 400.0, 500.0, 600.0, 800.0]
|
||||
discard_times = [d for d in [20.0, 40.0, 60.0, 100.0, 150.0, 200.0] if d >= min_discard_ms]
|
||||
if not discard_times:
|
||||
discard_times = [min_discard_ms]
|
||||
|
||||
total = (len(sensitivities) * len(dead_zones) * len(min_speeds) *
|
||||
len(dampings) * len(buffer_times) * len(discard_times))
|
||||
print(f"Total combinations: {total}")
|
||||
|
||||
best_score = float('inf')
|
||||
best_params = AdaptiveParams()
|
||||
count = 0
|
||||
|
||||
for sens in sensitivities:
|
||||
for dz in dead_zones:
|
||||
for ms in min_speeds:
|
||||
for damp in dampings:
|
||||
for bt in buffer_times:
|
||||
for dt in discard_times:
|
||||
count += 1
|
||||
if count % 500 == 0:
|
||||
print(f" Progress: {count}/{total} ({100 * count / total:.0f}%) best={best_score:.4f}")
|
||||
|
||||
params = AdaptiveParams(sens, dz, ms, damp, bt, dt)
|
||||
per_file_scores = []
|
||||
for name, frames in all_frames:
|
||||
pos_errors, aim_errors = simulate_adaptive(frames, params)
|
||||
score_result = compute_score(pos_errors, aim_errors)
|
||||
per_file_scores.append((name, score_result))
|
||||
|
||||
score = aggregate_scores(per_file_scores, strategy)
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best_sensitivity = sens
|
||||
best_params = params
|
||||
|
||||
# Print results
|
||||
results.sort(key=lambda x: x[5])
|
||||
print(f"\n{'Sensitivity':>12} {'Pos Mean':>10} {'Pos P95':>10} {'Aim Mean':>10} {'Aim P95':>10} {'Score':>10}")
|
||||
print("-" * 65)
|
||||
for sens, pm, pp, am, ap, score in results[:10]:
|
||||
marker = " <-- BEST" if sens == best_sensitivity else ""
|
||||
print(f"{sens:>12.2f} {pm:>10.3f} {pp:>10.3f} {am:>10.3f} {ap:>10.3f} {score:>10.3f}{marker}")
|
||||
return best_params, best_score
|
||||
|
||||
return {
|
||||
'sensitivity': best_sensitivity,
|
||||
'score': best_score,
|
||||
}
|
||||
|
||||
# --- Main ---
|
||||
|
||||
def print_file_stats(name: str, frames: List[Frame]):
|
||||
"""Print basic stats for a CSV file."""
|
||||
duration = frames[-1].timestamp - frames[0].timestamp
|
||||
avg_fps = len(frames) / duration if duration > 0 else 0
|
||||
avg_safe = sum(f.safe_count for f in frames) / len(frames)
|
||||
avg_buffer = sum(f.buffer_count for f in frames) / len(frames)
|
||||
avg_extrap = sum(f.extrap_time for f in frames) / len(frames) * 1000
|
||||
num_shots = sum(1 for f in frames if f.shot_fired)
|
||||
print(f" {os.path.basename(name)}: {len(frames)} frames, {avg_fps:.0f}fps, "
|
||||
f"{duration:.1f}s, safe={avg_safe:.1f}, extrap={avg_extrap:.1f}ms, shots={num_shots}")
|
||||
|
||||
|
||||
def print_score_detail(name: str, score: ScoreResult):
|
||||
"""Print detailed score for a file."""
|
||||
print(f" {os.path.basename(name):30s} Pos: mean={score.pos_mean:.3f}cm p95={score.pos_p95:.3f}cm | "
|
||||
f"Aim: mean={score.aim_mean:.3f}deg p95={score.aim_p95:.3f}deg | "
|
||||
f"jitter={score.jitter:.3f} overshoot={score.overshoot:.1%} | "
|
||||
f"score={score.score:.4f}")
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python analyze_antirecoil.py <csv_file> [--plot]")
|
||||
print("\nCSV files are saved by the EBBarrel component in:")
|
||||
print(" <Project>/Saved/Logs/AntiRecoil_*.csv")
|
||||
sys.exit(1)
|
||||
|
||||
csv_path = sys.argv[1]
|
||||
do_plot = '--plot' in sys.argv
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Anti-Recoil Parameter Optimizer - finds optimal AdaptiveExtrapolation parameters"
|
||||
)
|
||||
parser.add_argument("csv_files", nargs="+", help="One or more CSV recording files")
|
||||
parser.add_argument("--plot", action="store_true", help="Generate comparison plots (requires matplotlib)")
|
||||
parser.add_argument("--grid", action="store_true", help="Use grid search instead of differential evolution")
|
||||
parser.add_argument("--strategy", choices=["mean", "worst_case"], default="mean",
|
||||
help="Multi-file score aggregation strategy (default: mean)")
|
||||
parser.add_argument("--max-iter", type=int, default=200, help="Max optimizer iterations (default: 200)")
|
||||
args = parser.parse_args()
|
||||
|
||||
# Load all CSV files
|
||||
all_frames = []
|
||||
for csv_path in args.csv_files:
|
||||
if not os.path.exists(csv_path):
|
||||
print(f"Error: File not found: {csv_path}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Loading: {csv_path}")
|
||||
frames = load_csv(csv_path)
|
||||
print(f"Loaded {len(frames)} frames")
|
||||
|
||||
if len(frames) < 50:
|
||||
print("Warning: very few frames. Record at least a few seconds of movement for good results.")
|
||||
print(f"Warning: {csv_path} has only {len(frames)} frames (need at least 50 for good results)")
|
||||
all_frames.append((csv_path, frames))
|
||||
|
||||
# Basic stats
|
||||
duration = frames[-1].timestamp - frames[0].timestamp
|
||||
avg_fps = len(frames) / duration if duration > 0 else 0
|
||||
print(f"Duration: {duration:.1f}s | Avg FPS: {avg_fps:.1f}")
|
||||
print(f"Avg safe samples: {sum(f.safe_count for f in frames) / len(frames):.1f}")
|
||||
print(f"Avg buffer samples: {sum(f.buffer_count for f in frames) / len(frames):.1f}")
|
||||
print(f"Avg extrapolation time: {sum(f.extrap_time for f in frames) / len(frames) * 1000:.1f}ms")
|
||||
print(f"\nLoaded {len(all_frames)} file(s)")
|
||||
print("=" * 70)
|
||||
|
||||
# Current prediction error (as recorded)
|
||||
print("\n=== CURRENT PREDICTION ERROR (as recorded) ===")
|
||||
current_err = compute_prediction_error(frames)
|
||||
print(f"Position error - Mean: {current_err['pos_mean']:.3f}cm | P95: {current_err['pos_p95']:.3f}cm | Max: {current_err['pos_max']:.3f}cm")
|
||||
print(f"Aim error - Mean: {current_err['aim_mean']:.3f}deg | P95: {current_err['aim_p95']:.3f}deg | Max: {current_err['aim_max']:.3f}deg")
|
||||
# Per-file stats
|
||||
print("\n=== FILE STATISTICS ===")
|
||||
for name, frames in all_frames:
|
||||
print_file_stats(name, frames)
|
||||
|
||||
# Find optimal parameters
|
||||
print("\n=== PARAMETER OPTIMIZATION ===")
|
||||
optimal = find_optimal_parameters(frames)
|
||||
# Shot contamination analysis
|
||||
has_shots = any(any(f.shot_fired for f in frames) for _, frames in all_frames)
|
||||
if has_shots:
|
||||
print("\n=== SHOT CONTAMINATION ANALYSIS ===")
|
||||
max_recommended_discard = 0.0
|
||||
for name, frames in all_frames:
|
||||
result = analyze_shot_contamination(frames)
|
||||
if result:
|
||||
print(f" {os.path.basename(name)}:")
|
||||
print(f" Shots detected: {result['num_shots']}")
|
||||
print(f" Speed spike: mean={result['speed_spike_mean']:.1f} cm/s, max={result['speed_spike_max']:.1f} cm/s")
|
||||
print(f" Contamination duration: mean={result['contamination_mean_ms']:.1f}ms, "
|
||||
f"p95={result['contamination_p95_ms']:.1f}ms, max={result['contamination_max_ms']:.1f}ms")
|
||||
print(f" Recommended discard_time: >= {result['recommended_discard_ms']:.0f}ms")
|
||||
max_recommended_discard = max(max_recommended_discard, result['recommended_discard_ms'])
|
||||
else:
|
||||
print(f" {os.path.basename(name)}: no shots detected")
|
||||
|
||||
print(f"\n{'=' * 50}")
|
||||
print(f" RECOMMENDED SETTINGS:")
|
||||
print(f" AdaptiveSensitivity = {optimal['sensitivity']:.2f}")
|
||||
print(f"{'=' * 50}")
|
||||
if max_recommended_discard > 0:
|
||||
print(f"\n >>> MINIMUM SAFE DiscardTime across all files: {max_recommended_discard:.0f}ms <<<")
|
||||
else:
|
||||
print("\n (No ShotFired data in CSV - record with updated plugin to get contamination analysis)")
|
||||
|
||||
# Baseline: current default parameters
|
||||
default_params = AdaptiveParams()
|
||||
print(f"\n=== BASELINE (defaults: sens={default_params.sensitivity}, dz={default_params.dead_zone}, "
|
||||
f"minspd={default_params.min_speed}, damp={default_params.damping}, "
|
||||
f"buf={default_params.buffer_time_ms}ms, disc={default_params.discard_time_ms}ms) ===")
|
||||
|
||||
baseline_scores = []
|
||||
for name, frames in all_frames:
|
||||
pos_errors, aim_errors = simulate_adaptive(frames, default_params)
|
||||
score = compute_score(pos_errors, aim_errors)
|
||||
baseline_scores.append((name, score))
|
||||
print_score_detail(name, score)
|
||||
|
||||
baseline_agg = aggregate_scores(baseline_scores, args.strategy)
|
||||
print(f"\n Aggregate score ({args.strategy}): {baseline_agg:.4f}")
|
||||
|
||||
# Also show recorded prediction error (as-is from the engine)
|
||||
print(f"\n=== RECORDED PREDICTION ERROR (as captured in-engine) ===")
|
||||
for name, frames in all_frames:
|
||||
err = compute_prediction_error(frames)
|
||||
print(f" {os.path.basename(name):30s} Pos: mean={err['pos_mean']:.3f}cm p95={err['pos_p95']:.3f}cm | "
|
||||
f"Aim: mean={err['aim_mean']:.3f}deg p95={err['aim_p95']:.3f}deg")
|
||||
|
||||
# Compute minimum safe discard time from shot contamination analysis
|
||||
min_discard_ms = 10.0 # absolute minimum
|
||||
if has_shots:
|
||||
for name, frames in all_frames:
|
||||
result = analyze_shot_contamination(frames)
|
||||
if result and result['recommended_discard_ms'] > min_discard_ms:
|
||||
min_discard_ms = result['recommended_discard_ms']
|
||||
|
||||
# Optimize
|
||||
print(f"\n=== OPTIMIZATION ({args.strategy}) ===")
|
||||
|
||||
if args.grid:
|
||||
best_params, best_score = optimize_grid_search(all_frames, args.strategy, min_discard_ms)
|
||||
else:
|
||||
best_params, best_score = optimize_differential_evolution(all_frames, args.strategy, args.max_iter, min_discard_ms)
|
||||
|
||||
# Results
|
||||
print(f"\n{'=' * 70}")
|
||||
print(f" BEST PARAMETERS FOUND:")
|
||||
print(f" AdaptiveSensitivity = {best_params.sensitivity}")
|
||||
print(f" AdaptiveDeadZone = {best_params.dead_zone}")
|
||||
print(f" AdaptiveMinSpeed = {best_params.min_speed}")
|
||||
print(f" ExtrapolationDamping = {best_params.damping}")
|
||||
print(f" AntiRecoilBufferTimeMs = {best_params.buffer_time_ms}")
|
||||
print(f" AntiRecoilDiscardTimeMs= {best_params.discard_time_ms}")
|
||||
print(f"{'=' * 70}")
|
||||
|
||||
# Per-file breakdown with optimized params
|
||||
print(f"\n=== OPTIMIZED RESULTS ===")
|
||||
opt_scores = []
|
||||
for name, frames in all_frames:
|
||||
pos_errors, aim_errors = simulate_adaptive(frames, best_params)
|
||||
score = compute_score(pos_errors, aim_errors)
|
||||
opt_scores.append((name, score))
|
||||
print_score_detail(name, score)
|
||||
|
||||
opt_agg = aggregate_scores(opt_scores, args.strategy)
|
||||
print(f"\n Aggregate score ({args.strategy}): {opt_agg:.4f}")
|
||||
|
||||
# Improvement
|
||||
print(f"\n=== IMPROVEMENT vs BASELINE ===")
|
||||
for (name, baseline), (_, optimized) in zip(baseline_scores, opt_scores):
|
||||
pos_pct = ((baseline.pos_mean - optimized.pos_mean) / baseline.pos_mean * 100) if baseline.pos_mean > 0 else 0
|
||||
aim_pct = ((baseline.aim_mean - optimized.aim_mean) / baseline.aim_mean * 100) if baseline.aim_mean > 0 else 0
|
||||
score_pct = ((baseline.score - optimized.score) / baseline.score * 100) if baseline.score > 0 else 0
|
||||
print(f" {os.path.basename(name):30s} Pos: {pos_pct:+.1f}% | Aim: {aim_pct:+.1f}% | Score: {score_pct:+.1f}%")
|
||||
|
||||
total_pct = ((baseline_agg - opt_agg) / baseline_agg * 100) if baseline_agg > 0 else 0
|
||||
print(f" {'TOTAL':30s} Score: {total_pct:+.1f}%")
|
||||
|
||||
# Plotting
|
||||
if do_plot:
|
||||
if args.plot:
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
fig, axes = plt.subplots(3, 1, figsize=(14, 10), sharex=True)
|
||||
n_files = len(all_frames)
|
||||
fig, axes = plt.subplots(n_files, 3, figsize=(18, 5 * n_files), squeeze=False)
|
||||
|
||||
for row, (name, frames) in enumerate(all_frames):
|
||||
timestamps = [f.timestamp - frames[0].timestamp for f in frames]
|
||||
short_name = os.path.basename(name)
|
||||
|
||||
# Baseline errors
|
||||
bl_pos, bl_aim = simulate_adaptive(frames, default_params)
|
||||
# Optimized errors
|
||||
op_pos, op_aim = simulate_adaptive(frames, best_params)
|
||||
|
||||
# Time axis for simulated errors (offset by window_size)
|
||||
t_start = window_size = 12
|
||||
sim_timestamps = [frames[i].timestamp - frames[0].timestamp
|
||||
for i in range(t_start + 1, t_start + 1 + len(bl_pos))]
|
||||
|
||||
# Position error
|
||||
pos_errors = [vec_dist(f.pred_pos, f.real_pos) for f in frames]
|
||||
axes[0].plot(timestamps, pos_errors, 'r-', alpha=0.5, linewidth=0.5)
|
||||
axes[0].set_ylabel('Position Error (cm)')
|
||||
axes[0].set_title('Prediction Error Over Time')
|
||||
axes[0].axhline(y=current_err['pos_mean'], color='r', linestyle='--', alpha=0.5, label=f"Mean: {current_err['pos_mean']:.2f}cm")
|
||||
axes[0].legend()
|
||||
ax = axes[row][0]
|
||||
if len(sim_timestamps) == len(bl_pos):
|
||||
ax.plot(sim_timestamps, bl_pos, 'r-', alpha=0.4, linewidth=0.5, label='Baseline')
|
||||
ax.plot(sim_timestamps, op_pos, 'g-', alpha=0.4, linewidth=0.5, label='Optimized')
|
||||
ax.set_ylabel('Position Error (cm)')
|
||||
ax.set_title(f'{short_name} - Position Error')
|
||||
ax.legend()
|
||||
|
||||
# Aim error
|
||||
aim_errors = []
|
||||
for f in frames:
|
||||
a = vec_normalize(f.pred_aim)
|
||||
b = vec_normalize(f.real_aim)
|
||||
if vec_len(a) > 0.5 and vec_len(b) > 0.5:
|
||||
aim_errors.append(angle_between(a, b))
|
||||
else:
|
||||
aim_errors.append(0)
|
||||
axes[1].plot(timestamps, aim_errors, 'b-', alpha=0.5, linewidth=0.5)
|
||||
axes[1].set_ylabel('Aim Error (degrees)')
|
||||
axes[1].axhline(y=current_err['aim_mean'], color='b', linestyle='--', alpha=0.5, label=f"Mean: {current_err['aim_mean']:.2f}deg")
|
||||
axes[1].legend()
|
||||
ax = axes[row][1]
|
||||
if len(sim_timestamps) >= len(bl_aim):
|
||||
t_aim = sim_timestamps[:len(bl_aim)]
|
||||
ax.plot(t_aim, bl_aim, 'r-', alpha=0.4, linewidth=0.5, label='Baseline')
|
||||
if len(sim_timestamps) >= len(op_aim):
|
||||
t_aim = sim_timestamps[:len(op_aim)]
|
||||
ax.plot(t_aim, op_aim, 'g-', alpha=0.4, linewidth=0.5, label='Optimized')
|
||||
ax.set_ylabel('Aim Error (deg)')
|
||||
ax.set_title(f'{short_name} - Aim Error')
|
||||
ax.legend()
|
||||
|
||||
# Speed (from real positions)
|
||||
# Speed profile
|
||||
ax = axes[row][2]
|
||||
speeds = [0]
|
||||
for i in range(1, len(frames)):
|
||||
dt = frames[i].timestamp - frames[i-1].timestamp
|
||||
dt = frames[i].timestamp - frames[i - 1].timestamp
|
||||
if dt > 1e-6:
|
||||
d = vec_dist(frames[i].real_pos, frames[i-1].real_pos)
|
||||
d = vec_dist(frames[i].real_pos, frames[i - 1].real_pos)
|
||||
speeds.append(d / dt)
|
||||
else:
|
||||
speeds.append(speeds[-1])
|
||||
axes[2].plot(timestamps, speeds, 'g-', alpha=0.7, linewidth=0.5)
|
||||
axes[2].set_ylabel('Speed (cm/s)')
|
||||
axes[2].set_xlabel('Time (s)')
|
||||
ax.plot(timestamps, speeds, 'b-', alpha=0.7, linewidth=0.5)
|
||||
ax.set_ylabel('Speed (cm/s)')
|
||||
ax.set_xlabel('Time (s)')
|
||||
ax.set_title(f'{short_name} - Speed Profile')
|
||||
|
||||
plt.tight_layout()
|
||||
plot_path = csv_path.replace('.csv', '_analysis.png')
|
||||
plot_path = args.csv_files[0].replace('.csv', '_optimizer.png')
|
||||
plt.savefig(plot_path, dpi=150)
|
||||
print(f"\nPlot saved: {plot_path}")
|
||||
plt.show()
|
||||
|
||||
366
Tools/analyze_shots.py
Normal file
366
Tools/analyze_shots.py
Normal file
@ -0,0 +1,366 @@
|
||||
"""
|
||||
Shot Contamination Analyzer
|
||||
============================
|
||||
Analyzes the precise contamination zone around each shot event.
|
||||
Shows speed/acceleration profiles before and after each shot to identify
|
||||
the exact duration of IMU perturbation vs voluntary movement.
|
||||
|
||||
Usage:
|
||||
python analyze_shots.py <csv_file> [--plot] [--window 100]
|
||||
|
||||
Protocol for best results:
|
||||
1. Stay stable (no movement) for 2-3 seconds
|
||||
2. Fire a single shot
|
||||
3. Stay stable again for 2-3 seconds
|
||||
4. Repeat 10+ times
|
||||
This isolates the IMU shock from voluntary movement.
|
||||
"""
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import math
|
||||
import os
|
||||
import argparse
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
@dataclass
|
||||
class Frame:
|
||||
timestamp: float
|
||||
real_pos: Tuple[float, float, float]
|
||||
real_aim: Tuple[float, float, float]
|
||||
pred_pos: Tuple[float, float, float]
|
||||
pred_aim: Tuple[float, float, float]
|
||||
safe_count: int
|
||||
buffer_count: int
|
||||
extrap_time: float
|
||||
shot_fired: bool = False
|
||||
|
||||
|
||||
def load_csv(path: str) -> List[Frame]:
|
||||
frames = []
|
||||
with open(path, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
frames.append(Frame(
|
||||
timestamp=float(row['Timestamp']),
|
||||
real_pos=(float(row['RealPosX']), float(row['RealPosY']), float(row['RealPosZ'])),
|
||||
real_aim=(float(row['RealAimX']), float(row['RealAimY']), float(row['RealAimZ'])),
|
||||
pred_pos=(float(row['PredPosX']), float(row['PredPosY']), float(row['PredPosZ'])),
|
||||
pred_aim=(float(row['PredAimX']), float(row['PredAimY']), float(row['PredAimZ'])),
|
||||
safe_count=int(row['SafeCount']),
|
||||
buffer_count=int(row['BufferCount']),
|
||||
extrap_time=float(row['ExtrapolationTime']),
|
||||
shot_fired=int(row.get('ShotFired', 0)) == 1,
|
||||
))
|
||||
return frames
|
||||
|
||||
|
||||
def vec_dist(a, b):
|
||||
return math.sqrt(sum((ai - bi) ** 2 for ai, bi in zip(a, b)))
|
||||
|
||||
|
||||
def vec_sub(a, b):
|
||||
return tuple(ai - bi for ai, bi in zip(a, b))
|
||||
|
||||
|
||||
def vec_len(a):
|
||||
return math.sqrt(sum(ai * ai for ai in a))
|
||||
|
||||
|
||||
def vec_normalize(a):
|
||||
l = vec_len(a)
|
||||
if l < 1e-10:
|
||||
return (0, 0, 0)
|
||||
return tuple(ai / l for ai in a)
|
||||
|
||||
|
||||
def angle_between(a, b):
|
||||
dot = sum(ai * bi for ai, bi in zip(a, b))
|
||||
dot = max(-1.0, min(1.0, dot))
|
||||
return math.degrees(math.acos(dot))
|
||||
|
||||
|
||||
def compute_per_frame_metrics(frames):
|
||||
"""Compute speed, acceleration, and aim angular speed per frame."""
|
||||
n = len(frames)
|
||||
pos_speed = [0.0] * n
|
||||
aim_speed = [0.0] * n
|
||||
pos_accel = [0.0] * n
|
||||
|
||||
for i in range(1, n):
|
||||
dt = frames[i].timestamp - frames[i - 1].timestamp
|
||||
if dt > 1e-6:
|
||||
pos_speed[i] = vec_dist(frames[i].real_pos, frames[i - 1].real_pos) / dt
|
||||
|
||||
aim_a = vec_normalize(frames[i].real_aim)
|
||||
aim_b = vec_normalize(frames[i - 1].real_aim)
|
||||
if vec_len(aim_a) > 0.5 and vec_len(aim_b) > 0.5:
|
||||
aim_speed[i] = angle_between(aim_a, aim_b) / dt # deg/s
|
||||
|
||||
for i in range(1, n):
|
||||
dt = frames[i].timestamp - frames[i - 1].timestamp
|
||||
if dt > 1e-6:
|
||||
pos_accel[i] = (pos_speed[i] - pos_speed[i - 1]) / dt
|
||||
|
||||
return pos_speed, aim_speed, pos_accel
|
||||
|
||||
|
||||
def analyze_single_shot(frames, shot_idx, pos_speed, aim_speed, pos_accel, window_ms=200.0):
|
||||
"""Analyze contamination around a single shot event."""
|
||||
window_s = window_ms / 1000.0
|
||||
shot_time = frames[shot_idx].timestamp
|
||||
|
||||
# Collect frames in window before and after shot
|
||||
before = [] # (time_relative_ms, pos_speed, aim_speed, pos_accel)
|
||||
after = []
|
||||
|
||||
for i in range(max(0, shot_idx - 100), min(len(frames), shot_idx + 100)):
|
||||
dt_ms = (frames[i].timestamp - shot_time) * 1000.0
|
||||
if -window_ms <= dt_ms < 0:
|
||||
before.append((dt_ms, pos_speed[i], aim_speed[i], pos_accel[i]))
|
||||
elif dt_ms >= 0 and dt_ms <= window_ms:
|
||||
after.append((dt_ms, pos_speed[i], aim_speed[i], pos_accel[i]))
|
||||
|
||||
if not before:
|
||||
return None
|
||||
|
||||
# Baseline: average speed in the window before the shot
|
||||
baseline_pos_speed = sum(s for _, s, _, _ in before) / len(before)
|
||||
baseline_aim_speed = sum(s for _, _, s, _ in before) / len(before)
|
||||
baseline_pos_std = math.sqrt(sum((s - baseline_pos_speed) ** 2 for _, s, _, _ in before) / len(before)) if len(before) > 1 else 0.0
|
||||
baseline_aim_std = math.sqrt(sum((s - baseline_aim_speed) ** 2 for _, _, s, _ in before) / len(before)) if len(before) > 1 else 0.0
|
||||
|
||||
# Find contamination end: when speed returns to within 2 sigma of baseline
|
||||
pos_threshold = baseline_pos_speed + max(2.0 * baseline_pos_std, 5.0) # at least 5 cm/s
|
||||
aim_threshold = baseline_aim_speed + max(2.0 * baseline_aim_std, 5.0) # at least 5 deg/s
|
||||
|
||||
pos_contamination_end_ms = 0.0
|
||||
aim_contamination_end_ms = 0.0
|
||||
max_pos_spike = 0.0
|
||||
max_aim_spike = 0.0
|
||||
|
||||
for dt_ms, ps, ais, _ in after:
|
||||
if ps > pos_threshold:
|
||||
pos_contamination_end_ms = dt_ms
|
||||
if ais > aim_threshold:
|
||||
aim_contamination_end_ms = dt_ms
|
||||
max_pos_spike = max(max_pos_spike, ps - baseline_pos_speed)
|
||||
max_aim_spike = max(max_aim_spike, ais - baseline_aim_speed)
|
||||
|
||||
return {
|
||||
'shot_time': shot_time,
|
||||
'baseline_pos_speed': baseline_pos_speed,
|
||||
'baseline_aim_speed': baseline_aim_speed,
|
||||
'baseline_pos_std': baseline_pos_std,
|
||||
'baseline_aim_std': baseline_aim_std,
|
||||
'pos_contamination_ms': pos_contamination_end_ms,
|
||||
'aim_contamination_ms': aim_contamination_end_ms,
|
||||
'max_contamination_ms': max(pos_contamination_end_ms, aim_contamination_end_ms),
|
||||
'max_pos_spike': max_pos_spike,
|
||||
'max_aim_spike': max_aim_spike,
|
||||
'pos_threshold': pos_threshold,
|
||||
'aim_threshold': aim_threshold,
|
||||
'before': before,
|
||||
'after': after,
|
||||
'is_stable': baseline_pos_speed < 30.0 and baseline_aim_speed < 200.0,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Shot Contamination Analyzer")
|
||||
parser.add_argument("csv_file", help="CSV recording file with ShotFired column")
|
||||
parser.add_argument("--plot", action="store_true", help="Generate per-shot plots (requires matplotlib)")
|
||||
parser.add_argument("--window", type=float, default=200.0, help="Analysis window in ms before/after shot (default: 200)")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.exists(args.csv_file):
|
||||
print(f"Error: File not found: {args.csv_file}")
|
||||
sys.exit(1)
|
||||
|
||||
frames = load_csv(args.csv_file)
|
||||
print(f"Loaded {len(frames)} frames from {os.path.basename(args.csv_file)}")
|
||||
|
||||
duration = frames[-1].timestamp - frames[0].timestamp
|
||||
fps = len(frames) / duration if duration > 0 else 0
|
||||
print(f"Duration: {duration:.1f}s | FPS: {fps:.0f}")
|
||||
|
||||
shot_indices = [i for i, f in enumerate(frames) if f.shot_fired]
|
||||
print(f"Shots detected: {len(shot_indices)}")
|
||||
|
||||
if not shot_indices:
|
||||
print("No shots found! Make sure the CSV has a ShotFired column.")
|
||||
sys.exit(1)
|
||||
|
||||
pos_speed, aim_speed, pos_accel = compute_per_frame_metrics(frames)
|
||||
|
||||
# Analyze each shot
|
||||
results = []
|
||||
print(f"\n{'=' * 90}")
|
||||
print(f"{'Shot':>4} {'Time':>8} {'Stable':>7} {'PosSpike':>10} {'AimSpike':>10} "
|
||||
f"{'PosContam':>10} {'AimContam':>10} {'MaxContam':>10}")
|
||||
print(f"{'':>4} {'(s)':>8} {'':>7} {'(cm/s)':>10} {'(deg/s)':>10} "
|
||||
f"{'(ms)':>10} {'(ms)':>10} {'(ms)':>10}")
|
||||
print(f"{'-' * 90}")
|
||||
|
||||
for idx, si in enumerate(shot_indices):
|
||||
result = analyze_single_shot(frames, si, pos_speed, aim_speed, pos_accel, args.window)
|
||||
if result is None:
|
||||
continue
|
||||
results.append(result)
|
||||
|
||||
stable_str = "YES" if result['is_stable'] else "no"
|
||||
print(f"{idx + 1:>4} {result['shot_time']:>8.2f} {stable_str:>7} "
|
||||
f"{result['max_pos_spike']:>10.1f} {result['max_aim_spike']:>10.1f} "
|
||||
f"{result['pos_contamination_ms']:>10.1f} {result['aim_contamination_ms']:>10.1f} "
|
||||
f"{result['max_contamination_ms']:>10.1f}")
|
||||
|
||||
# Summary: only stable shots (user was not moving)
|
||||
stable_results = [r for r in results if r['is_stable']]
|
||||
all_results = results
|
||||
|
||||
print(f"\n{'=' * 90}")
|
||||
print(f"SUMMARY - ALL SHOTS ({len(all_results)} shots)")
|
||||
if all_results:
|
||||
contam_all = sorted([r['max_contamination_ms'] for r in all_results])
|
||||
pos_spikes = sorted([r['max_pos_spike'] for r in all_results])
|
||||
aim_spikes = sorted([r['max_aim_spike'] for r in all_results])
|
||||
p95_idx = int(len(contam_all) * 0.95)
|
||||
print(f" Contamination: mean={sum(contam_all)/len(contam_all):.1f}ms, "
|
||||
f"median={contam_all[len(contam_all)//2]:.1f}ms, "
|
||||
f"p95={contam_all[min(p95_idx, len(contam_all)-1)]:.1f}ms, "
|
||||
f"max={contam_all[-1]:.1f}ms")
|
||||
print(f" Pos spike: mean={sum(pos_spikes)/len(pos_spikes):.1f}cm/s, "
|
||||
f"max={pos_spikes[-1]:.1f}cm/s")
|
||||
print(f" Aim spike: mean={sum(aim_spikes)/len(aim_spikes):.1f}deg/s, "
|
||||
f"max={aim_spikes[-1]:.1f}deg/s")
|
||||
|
||||
print(f"\nSUMMARY - STABLE SHOTS ONLY ({len(stable_results)} shots, baseline speed < 20cm/s)")
|
||||
if stable_results:
|
||||
contam_stable = sorted([r['max_contamination_ms'] for r in stable_results])
|
||||
pos_spikes_s = sorted([r['max_pos_spike'] for r in stable_results])
|
||||
aim_spikes_s = sorted([r['max_aim_spike'] for r in stable_results])
|
||||
p95_idx = int(len(contam_stable) * 0.95)
|
||||
print(f" Contamination: mean={sum(contam_stable)/len(contam_stable):.1f}ms, "
|
||||
f"median={contam_stable[len(contam_stable)//2]:.1f}ms, "
|
||||
f"p95={contam_stable[min(p95_idx, len(contam_stable)-1)]:.1f}ms, "
|
||||
f"max={contam_stable[-1]:.1f}ms")
|
||||
print(f" Pos spike: mean={sum(pos_spikes_s)/len(pos_spikes_s):.1f}cm/s, "
|
||||
f"max={pos_spikes_s[-1]:.1f}cm/s")
|
||||
print(f" Aim spike: mean={sum(aim_spikes_s)/len(aim_spikes_s):.1f}deg/s, "
|
||||
f"max={aim_spikes_s[-1]:.1f}deg/s")
|
||||
|
||||
recommended = math.ceil(contam_stable[min(p95_idx, len(contam_stable)-1)] / 5.0) * 5.0
|
||||
print(f"\n >>> RECOMMENDED DiscardTime (from stable shots P95): {recommended:.0f}ms <<<")
|
||||
else:
|
||||
print(" No stable shots found! Make sure you stay still before firing.")
|
||||
print(" Shots where baseline speed > 20cm/s are excluded as 'not stable'.")
|
||||
|
||||
# Plotting
|
||||
if args.plot:
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Plot each shot individually
|
||||
n_shots = len(results)
|
||||
cols = min(4, n_shots)
|
||||
rows = math.ceil(n_shots / cols)
|
||||
fig, axes = plt.subplots(rows, cols, figsize=(5 * cols, 4 * rows), squeeze=False)
|
||||
fig.suptitle(f'Per-Shot Speed Profile ({os.path.basename(args.csv_file)})', fontsize=14)
|
||||
|
||||
for idx, result in enumerate(results):
|
||||
r, c = divmod(idx, cols)
|
||||
ax = axes[r][c]
|
||||
|
||||
# Before shot
|
||||
if result['before']:
|
||||
t_before = [b[0] for b in result['before']]
|
||||
s_before = [b[1] for b in result['before']]
|
||||
ax.plot(t_before, s_before, 'b-', linewidth=1, label='Before')
|
||||
|
||||
# After shot
|
||||
if result['after']:
|
||||
t_after = [a[0] for a in result['after']]
|
||||
s_after = [a[1] for a in result['after']]
|
||||
ax.plot(t_after, s_after, 'r-', linewidth=1, label='After')
|
||||
|
||||
# Shot line
|
||||
ax.axvline(x=0, color='red', linestyle='--', alpha=0.7, label='Shot')
|
||||
|
||||
# Threshold
|
||||
ax.axhline(y=result['pos_threshold'], color='orange', linestyle=':', alpha=0.5, label='Threshold')
|
||||
|
||||
# Contamination zone
|
||||
if result['pos_contamination_ms'] > 0:
|
||||
ax.axvspan(0, result['pos_contamination_ms'], alpha=0.15, color='red')
|
||||
|
||||
stable_str = "STABLE" if result['is_stable'] else "MOVING"
|
||||
ax.set_title(f"Shot {idx+1} ({stable_str}) - {result['max_contamination_ms']:.0f}ms",
|
||||
fontsize=9, color='green' if result['is_stable'] else 'orange')
|
||||
ax.set_xlabel('Time from shot (ms)', fontsize=8)
|
||||
ax.set_ylabel('Pos Speed (cm/s)', fontsize=8)
|
||||
ax.tick_params(labelsize=7)
|
||||
if idx == 0:
|
||||
ax.legend(fontsize=6)
|
||||
|
||||
# Hide unused subplots
|
||||
for idx in range(n_shots, rows * cols):
|
||||
r, c = divmod(idx, cols)
|
||||
axes[r][c].set_visible(False)
|
||||
|
||||
plt.tight_layout()
|
||||
plot_path = args.csv_file.replace('.csv', '_shots.png')
|
||||
plt.savefig(plot_path, dpi=150)
|
||||
print(f"\nPlot saved: {plot_path}")
|
||||
plt.show()
|
||||
|
||||
# Also plot aim speed
|
||||
fig2, axes2 = plt.subplots(rows, cols, figsize=(5 * cols, 4 * rows), squeeze=False)
|
||||
fig2.suptitle(f'Per-Shot Aim Angular Speed ({os.path.basename(args.csv_file)})', fontsize=14)
|
||||
|
||||
for idx, result in enumerate(results):
|
||||
r, c = divmod(idx, cols)
|
||||
ax = axes2[r][c]
|
||||
|
||||
if result['before']:
|
||||
t_before = [b[0] for b in result['before']]
|
||||
a_before = [b[2] for b in result['before']] # aim_speed
|
||||
ax.plot(t_before, a_before, 'b-', linewidth=1)
|
||||
|
||||
if result['after']:
|
||||
t_after = [a[0] for a in result['after']]
|
||||
a_after = [a[2] for a in result['after']] # aim_speed
|
||||
ax.plot(t_after, a_after, 'r-', linewidth=1)
|
||||
|
||||
ax.axvline(x=0, color='red', linestyle='--', alpha=0.7)
|
||||
ax.axhline(y=result['aim_threshold'], color='orange', linestyle=':', alpha=0.5)
|
||||
|
||||
if result['aim_contamination_ms'] > 0:
|
||||
ax.axvspan(0, result['aim_contamination_ms'], alpha=0.15, color='red')
|
||||
|
||||
stable_str = "STABLE" if result['is_stable'] else "MOVING"
|
||||
ax.set_title(f"Shot {idx+1} ({stable_str}) - Aim {result['aim_contamination_ms']:.0f}ms",
|
||||
fontsize=9, color='green' if result['is_stable'] else 'orange')
|
||||
ax.set_xlabel('Time from shot (ms)', fontsize=8)
|
||||
ax.set_ylabel('Aim Speed (deg/s)', fontsize=8)
|
||||
ax.tick_params(labelsize=7)
|
||||
|
||||
for idx in range(n_shots, rows * cols):
|
||||
r, c = divmod(idx, cols)
|
||||
axes2[r][c].set_visible(False)
|
||||
|
||||
plt.tight_layout()
|
||||
plot_path2 = args.csv_file.replace('.csv', '_shots_aim.png')
|
||||
plt.savefig(plot_path2, dpi=150)
|
||||
print(f"Plot saved: {plot_path2}")
|
||||
plt.show()
|
||||
|
||||
except ImportError:
|
||||
print("\nmatplotlib not installed. Install with: pip install matplotlib")
|
||||
|
||||
print("\nDone.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -29,28 +29,6 @@ static int32 GetSafeCount(const TArray<FTimestampedTransform>& 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)
|
||||
@ -65,40 +43,11 @@ 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
|
||||
// During calibration, keep a larger buffer (0.5s min) for reliable 3-sigma analysis
|
||||
const float AntiRecoilBufferTime = AntiRecoilBufferTimeMs / 1000.0f;
|
||||
float EffectiveBufferTime = CalibrateAntiRecoil
|
||||
? FMath::Max(AntiRecoilBufferTime, 0.5f)
|
||||
: AntiRecoilBufferTime;
|
||||
double OldestAllowed = CurrentTime - FMath::Max(0.05f, EffectiveBufferTime);
|
||||
double OldestAllowed = CurrentTime - FMath::Max(0.05f, AntiRecoilBufferTime);
|
||||
while (TransformHistory.Num() > 0 && TransformHistory[0].Timestamp < OldestAllowed)
|
||||
{
|
||||
TransformHistory.RemoveAt(0);
|
||||
@ -147,26 +96,6 @@ void UEBBarrel::ComputeAntiRecoilTransform()
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_WeightedRegression:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
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);
|
||||
@ -384,226 +313,6 @@ 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(), AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
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
|
||||
@ -788,7 +497,7 @@ void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLoc
|
||||
// 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;
|
||||
const float MinSpeedThreshold = SMALL_NUMBER;
|
||||
|
||||
float PosRatio = 1.0f;
|
||||
float PosConfidence = 1.0f;
|
||||
@ -838,31 +547,10 @@ void UEBBarrel::PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLoc
|
||||
}
|
||||
// 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
|
||||
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
|
||||
double ExtrapolationTime = CurrentTime - LastSafe.Timestamp;
|
||||
|
||||
// Write debug values for HUD display
|
||||
DbgPosRatio = PosRatio;
|
||||
DbgAimRatio = AimRatio;
|
||||
DbgPosRemapped = PosRemapped;
|
||||
DbgAimRemapped = AimRemapped;
|
||||
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)
|
||||
|
||||
@ -17,7 +17,6 @@ void UEBBarrel::Shoot(bool Trigger, int nextFireID) {
|
||||
if (ClientSideAim && GetOwner()->GetRemoteRole() == ROLE_Authority && Trigger) {
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
ApplyAimStabilization();
|
||||
nextFireEventID = nextFireID;
|
||||
ShootRepCSA(Trigger, UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(), Location), Aim, nextFireID);
|
||||
}
|
||||
|
||||
@ -45,41 +45,6 @@ void UEBBarrel::EndPlay(const EEndPlayReason::Type 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)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
@ -93,7 +58,6 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
|
||||
if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) {
|
||||
|
||||
ComputeAntiRecoilTransform();
|
||||
ApplyAimStabilization();
|
||||
|
||||
ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim);
|
||||
TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency);
|
||||
@ -101,7 +65,6 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
|
||||
}else{
|
||||
if (!RemoteAimReceived) {
|
||||
ComputeAntiRecoilTransform();
|
||||
ApplyAimStabilization();
|
||||
}
|
||||
else {
|
||||
FVector LocOffset = (Location - GetComponentLocation());
|
||||
@ -114,7 +77,6 @@ void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorCompon
|
||||
}
|
||||
else {
|
||||
ComputeAntiRecoilTransform();
|
||||
ApplyAimStabilization();
|
||||
}
|
||||
|
||||
// Debug visualization: raw tracker (green) vs predicted aim (red)
|
||||
@ -136,62 +98,15 @@ 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, DebugAntiRecoilLineThickness);
|
||||
FColor::Green, false, -1.0f, 0, 0.0f);
|
||||
|
||||
// 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);
|
||||
FColor::Red, false, -1.0f, 0, 0.0f);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
DrawDebugPoint(GetWorld(), DebugIMUShockCapturedLocation, 3.0f, FColor::Green, false, -1.0f, 0);
|
||||
}
|
||||
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);
|
||||
DrawDebugPoint(GetWorld(), DebugCorrectedShotCapturedLocation, 3.0f, FColor::Red, false, -1.0f, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugCorrectedShotLineCaptured = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSV Prediction Recording
|
||||
@ -206,7 +121,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\n");
|
||||
FString Header = TEXT("Timestamp,RealPosX,RealPosY,RealPosZ,RealAimX,RealAimY,RealAimZ,PredPosX,PredPosY,PredPosZ,PredAimX,PredAimY,PredAimZ,SafeCount,BufferCount,ExtrapolationTime,ShotFired\n");
|
||||
auto HeaderUtf8 = StringCast<ANSICHAR>(*Header);
|
||||
CSVFileHandle->Write((const uint8*)HeaderUtf8.Get(), HeaderUtf8.Length());
|
||||
|
||||
@ -231,15 +146,17 @@ 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\n"),
|
||||
FString Line = FString::Printf(TEXT("%.6f,%.4f,%.4f,%.4f,%.6f,%.6f,%.6f,%.4f,%.4f,%.4f,%.6f,%.6f,%.6f,%d,%d,%.6f,%d\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);
|
||||
SafeN, TransformHistory.Num(), ExtrapTime,
|
||||
bShotFiredThisFrame ? 1 : 0);
|
||||
auto LineUtf8 = StringCast<ANSICHAR>(*Line);
|
||||
CSVFileHandle->Write((const uint8*)LineUtf8.Get(), LineUtf8.Length());
|
||||
bShotFiredThisFrame = false;
|
||||
}
|
||||
}
|
||||
else if (bCSVFileOpen)
|
||||
@ -259,131 +176,6 @@ 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 ======"));
|
||||
|
||||
// Dead zone info
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudVal,
|
||||
FString::Printf(TEXT(" DeadZone=%.2f Sensitivity=%.1f"),
|
||||
AdaptiveDeadZone, AdaptiveSensitivity));
|
||||
|
||||
// Speed ratio + confidence (position)
|
||||
bool bPosInDeadZone = (DbgPosRatio >= AdaptiveDeadZone);
|
||||
FColor PosColor = bPosInDeadZone ? HudGood : HudWarn;
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, PosColor,
|
||||
FString::Printf(TEXT(" Pos: ratio=%.3f %s remap=%.3f conf=%.3f spd=%.1f/%.1f"),
|
||||
DbgPosRatio, bPosInDeadZone ? TEXT("[DZ]") : TEXT("[!!]"),
|
||||
DbgPosRemapped, DbgPosConfidence, DbgRecentPosSpeed, DbgAvgPosSpeed));
|
||||
|
||||
// Speed ratio + confidence (aim)
|
||||
bool bAimInDeadZone = (DbgAimRatio >= AdaptiveDeadZone);
|
||||
FColor AimColor = bAimInDeadZone ? HudGood : HudWarn;
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, AimColor,
|
||||
FString::Printf(TEXT(" Aim: ratio=%.3f %s remap=%.3f conf=%.3f spd=%.4f/%.4f"),
|
||||
DbgAimRatio, bAimInDeadZone ? TEXT("[DZ]") : TEXT("[!!]"),
|
||||
DbgAimRemapped, 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: %.1f ms"),
|
||||
LastCalibrationResult.RecommendedDiscardTime * 1000.0f));
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn,
|
||||
FString::Printf(TEXT(" >> BufferTime: %.1f ms"),
|
||||
LastCalibrationResult.RecommendedBufferTime * 1000.0f));
|
||||
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){
|
||||
|
||||
@ -509,39 +301,6 @@ 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) {
|
||||
@ -605,6 +364,8 @@ void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim, in
|
||||
}
|
||||
}
|
||||
|
||||
bShotFiredThisFrame = true;
|
||||
|
||||
if (ReplicateShotFiredEvents) {
|
||||
ShotFiredMulticast();
|
||||
}
|
||||
@ -625,258 +386,3 @@ void UEBBarrel::ApplyRecoil_Implementation(UPrimitiveComponent* Component, FVect
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::ComputeCalibrationResult()
|
||||
{
|
||||
LastCalibrationResult = FCalibrationResult();
|
||||
LastCalibrationResult.TotalShots = CalibrationShots.Num();
|
||||
|
||||
if (CalibrationShots.Num() < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<float> CorruptionDurations;
|
||||
TArray<float> PeakAngles;
|
||||
TArray<float> PeakPositions;
|
||||
|
||||
// Collect all clean-sample acceleration variances and residuals for Kalman estimation
|
||||
TArray<float> AllAccelerationVariances;
|
||||
TArray<float> AllResidualVariances;
|
||||
|
||||
for (const FCalibrationShotData& Shot : CalibrationShots)
|
||||
{
|
||||
const TArray<FTimestampedTransform>& 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<float> 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;
|
||||
}
|
||||
@ -29,7 +29,6 @@ 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).")
|
||||
};
|
||||
@ -44,33 +43,6 @@ 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<FTimestampedTransform> 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
|
||||
{
|
||||
@ -98,16 +70,6 @@ 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;
|
||||
@ -116,43 +78,18 @@ public:
|
||||
bool bCSVFileOpen = false;
|
||||
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 DbgPosRemapped = 0.0f;
|
||||
mutable float DbgAimRemapped = 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;
|
||||
bool bShotFiredThisFrame = false;
|
||||
|
||||
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;
|
||||
|
||||
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 = 300.0f;
|
||||
float AntiRecoilBufferTimeMs = 200.0f;
|
||||
|
||||
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 = 40.0f;
|
||||
float AntiRecoilDiscardTimeMs = 30.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_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"))
|
||||
@ -162,19 +99,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 = 1.5f;
|
||||
float AdaptiveSensitivity = 3.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 = "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;
|
||||
float AdaptiveDeadZone = 0.95f;
|
||||
|
||||
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 = 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;
|
||||
float ExtrapolationDamping = 5.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;
|
||||
@ -251,9 +182,6 @@ 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;
|
||||
@ -304,36 +232,8 @@ private:
|
||||
FVector Aim;
|
||||
FVector Location;
|
||||
|
||||
// Aim stabilization: last accepted aim direction (outside dead zone)
|
||||
FVector StabilizedAim = FVector::ForwardVector;
|
||||
bool bStabilizedAimInitialized = false;
|
||||
|
||||
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 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<FCalibrationShotData> CalibrationShots;
|
||||
FCalibrationResult LastCalibrationResult;
|
||||
void ComputeCalibrationResult();
|
||||
|
||||
// Kalman filter state
|
||||
FVector KalmanPosition = FVector::ZeroVector;
|
||||
FVector KalmanVelocity = FVector::ZeroVector;
|
||||
@ -348,10 +248,8 @@ private:
|
||||
|
||||
void UpdateTransformHistory();
|
||||
void ComputeAntiRecoilTransform();
|
||||
void ApplyAimStabilization();
|
||||
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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user