Compare commits
11 Commits
1b32c1eef1
...
claude/vib
| Author | SHA1 | Date | |
|---|---|---|---|
| 81bb610459 | |||
| 58df608550 | |||
| ba6b35b3d9 | |||
| 1a5b107b1f | |||
| cd097e4e55 | |||
| 4e9c33778c | |||
| 48737b60c9 | |||
| 83188b1fa1 | |||
| af723c944b | |||
| fa257fb87b | |||
| 9407d8a556 |
@@ -4,7 +4,30 @@
|
||||
"Bash(find /c/ASTERION/GIT/PS_Ballistics/Unreal -name *.bat -o -name Generate*.sh -o -name *Generate*)",
|
||||
"Bash(\"C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" PS_BallisticsEditor Win64 Development -Project=\"C:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject\" -WaitMutex -FromMsBuild)",
|
||||
"Bash(\"C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" PS_BallisticsEditor Win64 Development -Project=\"C:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject\" -WaitMutex -FromMsBuild -NoLiveCoding)",
|
||||
"Bash(\"C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" PS_BallisticsEditor Win64 Development -Project=\"C:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject\" -NoLiveCoding)"
|
||||
"Bash(\"C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\Build.bat\" PS_BallisticsEditor Win64 Development -Project=\"C:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject\" -NoLiveCoding)",
|
||||
"Bash(grep -l \"Shoot\\\\|ClientAim\\\\|ShootRep\" \"E:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\Plugins\\\\PS_Ballistics\\\\Source\\\\EasyBallistics\\\\Private\"/*.cpp)",
|
||||
"Bash(xargs grep:*)",
|
||||
"Bash(ls Source/EasyBallistics/Private/*.cpp Source/EasyBallistics/Public/*.h)",
|
||||
"Bash(powershell.exe -Command \"& ''''C:\\\\Program Files\\\\Epic Games\\\\UE_5.5\\\\Engine\\\\Build\\\\BatchFiles\\\\RunUAT.bat'''' BuildEditor -project=''''E:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Unreal\\\\PS_Ballistics.uproject'''' -notools -noP4 2>&1\")",
|
||||
"Bash(python \"E:\\\\ASTERION\\\\GIT\\\\PS_Ballistics\\\\Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_150326.csv\")",
|
||||
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_150326.csv\")",
|
||||
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_153607.csv\")",
|
||||
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_160323.csv\")",
|
||||
"Bash(python \"Tools\\\\analyze_antirecoil.py\" \"E:\\\\ASTERION\\\\SVN\\\\DEV\\\\PROSERVE_UE_5_5\\\\Saved\\\\Logs\\\\AntiRecoil_20260316_164341.csv\")",
|
||||
"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(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 \":*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ Unreal/Binaries/
|
||||
Unreal/Intermediate/
|
||||
Unreal/Plugins/EasyBallistics/Intermediate/
|
||||
Unreal/Saved/
|
||||
Unreal/Plugins/PS_Ballistics/Intermediate/
|
||||
|
||||
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.
779
Tools/analyze_antirecoil.py
Normal file
779
Tools/analyze_antirecoil.py
Normal file
@@ -0,0 +1,779 @@
|
||||
"""
|
||||
Anti-Recoil Parameter Optimizer
|
||||
================================
|
||||
Reads CSV files recorded by the EBBarrel CSV recording feature and finds
|
||||
optimal parameters for the Adaptive Extrapolation mode.
|
||||
|
||||
Usage:
|
||||
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. 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, Optional
|
||||
|
||||
|
||||
@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
|
||||
|
||||
|
||||
@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'])),
|
||||
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
|
||||
|
||||
|
||||
# --- Vector math helpers ---
|
||||
|
||||
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_add(a, b):
|
||||
return tuple(ai + bi for ai, bi in zip(a, b))
|
||||
|
||||
|
||||
def vec_scale(a, s):
|
||||
return tuple(ai * s for ai in a)
|
||||
|
||||
|
||||
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):
|
||||
"""Angle in degrees between two direction vectors."""
|
||||
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))
|
||||
|
||||
|
||||
# --- Prediction error from recorded data ---
|
||||
|
||||
def compute_prediction_error(frames: List[Frame]) -> dict:
|
||||
"""Compute error between predicted and actual (real) positions/aims."""
|
||||
pos_errors = []
|
||||
aim_errors = []
|
||||
|
||||
for f in frames:
|
||||
pos_err = vec_dist(f.pred_pos, f.real_pos)
|
||||
pos_errors.append(pos_err)
|
||||
|
||||
aim_a = vec_normalize(f.pred_aim)
|
||||
aim_b = vec_normalize(f.real_aim)
|
||||
if vec_len(aim_a) > 0.5 and vec_len(aim_b) > 0.5:
|
||||
aim_err = angle_between(aim_a, aim_b)
|
||||
aim_errors.append(aim_err)
|
||||
|
||||
if not pos_errors:
|
||||
return {'pos_mean': 0, 'pos_p95': 0, 'pos_max': 0, 'aim_mean': 0, 'aim_p95': 0, 'aim_max': 0}
|
||||
|
||||
pos_errors.sort()
|
||||
aim_errors.sort()
|
||||
|
||||
p95_idx_pos = int(len(pos_errors) * 0.95)
|
||||
p95_idx_aim = int(len(aim_errors) * 0.95) if aim_errors else 0
|
||||
|
||||
return {
|
||||
'pos_mean': sum(pos_errors) / len(pos_errors),
|
||||
'pos_p95': pos_errors[min(p95_idx_pos, len(pos_errors) - 1)],
|
||||
'pos_max': pos_errors[-1],
|
||||
'aim_mean': sum(aim_errors) / len(aim_errors) if aim_errors else 0,
|
||||
'aim_p95': aim_errors[min(p95_idx_aim, len(aim_errors) - 1)] if aim_errors else 0,
|
||||
'aim_max': aim_errors[-1] if aim_errors else 0,
|
||||
}
|
||||
|
||||
|
||||
# --- Shot contamination analysis ---
|
||||
|
||||
def analyze_shot_contamination(frames: List[Frame], analysis_window_ms: float = 200.0):
|
||||
"""
|
||||
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 = []
|
||||
|
||||
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(2, n_frames - 1):
|
||||
ct = ts[i]
|
||||
safe_cutoff = ct - discard_s
|
||||
oldest_allowed = ct - buffer_s
|
||||
|
||||
# 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
|
||||
|
||||
# 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)
|
||||
|
||||
nv = len(vpx)
|
||||
if nv < 2:
|
||||
continue
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
lsi = safe[-1]
|
||||
edt = ct - ts[lsi]
|
||||
if edt <= 0: edt = 0.011
|
||||
|
||||
# Damping
|
||||
ds = _exp(-damping * edt) if damping > 0.0 else 1.0
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
# --- Scoring ---
|
||||
|
||||
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:
|
||||
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_sorted[int(len(pos_sorted) * 0.95)]
|
||||
aim_mean = sum(aim_errors) / len(aim_errors) if aim_errors else 0
|
||||
aim_p95 = aim_sorted[int(len(aim_sorted) * 0.95)] if aim_errors else 0
|
||||
|
||||
# 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))
|
||||
|
||||
# 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_params = params
|
||||
|
||||
return best_params, 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():
|
||||
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)
|
||||
frames = load_csv(csv_path)
|
||||
if len(frames) < 50:
|
||||
print(f"Warning: {csv_path} has only {len(frames)} frames (need at least 50 for good results)")
|
||||
all_frames.append((csv_path, frames))
|
||||
|
||||
print(f"\nLoaded {len(all_frames)} file(s)")
|
||||
print("=" * 70)
|
||||
|
||||
# Per-file stats
|
||||
print("\n=== FILE STATISTICS ===")
|
||||
for name, frames in all_frames:
|
||||
print_file_stats(name, 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")
|
||||
|
||||
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 args.plot:
|
||||
try:
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
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
|
||||
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
|
||||
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 profile
|
||||
ax = axes[row][2]
|
||||
speeds = [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])
|
||||
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 = args.csv_files[0].replace('.csv', '_optimizer.png')
|
||||
plt.savefig(plot_path, dpi=150)
|
||||
print(f"\nPlot saved: {plot_path}")
|
||||
plt.show()
|
||||
|
||||
except ImportError:
|
||||
print("\nmatplotlib not installed. Install with: pip install matplotlib")
|
||||
|
||||
print("\nDone.")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
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.
Binary file not shown.
Binary file not shown.
@@ -29,24 +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;
|
||||
}
|
||||
|
||||
void UEBBarrel::UpdateTransformHistory()
|
||||
{
|
||||
if (AntiRecoilMode == EAntiRecoilMode::ARM_None)
|
||||
@@ -61,34 +43,10 @@ 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 AntiRecoilBufferTime
|
||||
// Trim buffer: remove samples older than AntiRecoilBufferTimeMs
|
||||
const float AntiRecoilBufferTime = AntiRecoilBufferTimeMs / 1000.0f;
|
||||
double OldestAllowed = CurrentTime - FMath::Max(0.05f, AntiRecoilBufferTime);
|
||||
while (TransformHistory.Num() > 0 && TransformHistory[0].Timestamp < OldestAllowed)
|
||||
{
|
||||
@@ -120,7 +78,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
|
||||
|
||||
case EAntiRecoilMode::ARM_LinearExtrapolation:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
if (SafeN >= 2)
|
||||
{
|
||||
PredictLinearExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
@@ -138,12 +96,12 @@ void UEBBarrel::ComputeAntiRecoilTransform()
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_WeightedRegression:
|
||||
case EAntiRecoilMode::ARM_WeightedLinearRegression:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
if (SafeN >= 2)
|
||||
{
|
||||
PredictWeightedRegression(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
PredictWeightedLinearRegression(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
}
|
||||
else if (TransformHistory.Num() > 0)
|
||||
{
|
||||
@@ -160,7 +118,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
|
||||
|
||||
case EAntiRecoilMode::ARM_KalmanFilter:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
if (SafeN > 0)
|
||||
{
|
||||
// Feed only the latest SAFE sample to the Kalman filter
|
||||
@@ -180,6 +138,26 @@ void UEBBarrel::ComputeAntiRecoilTransform()
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_AdaptiveExtrapolation:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
if (SafeN >= 2)
|
||||
{
|
||||
PredictAdaptiveExtrapolation(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +167,7 @@ void UEBBarrel::ComputeAntiRecoilTransform()
|
||||
|
||||
void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
if (SafeN < 2)
|
||||
{
|
||||
OutLocation = TransformHistory[0].Location;
|
||||
@@ -229,7 +207,14 @@ void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocat
|
||||
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
|
||||
double ExtrapolationTime = CurrentTime - LastSafe.Timestamp;
|
||||
|
||||
OutLocation = LastSafe.Location + AvgLinearVelocity * ExtrapolationTime;
|
||||
// Apply optional velocity damping: exponential decay toward zero
|
||||
float DampingScale = 1.0f;
|
||||
if (ExtrapolationDamping > 0.0f)
|
||||
{
|
||||
DampingScale = FMath::Exp(-ExtrapolationDamping * (float)ExtrapolationTime);
|
||||
}
|
||||
|
||||
OutLocation = LastSafe.Location + AvgLinearVelocity * ExtrapolationTime * DampingScale;
|
||||
|
||||
// Angular extrapolation using quaternion slerp
|
||||
// Use first and last safe samples for rotation direction
|
||||
@@ -241,7 +226,7 @@ void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocat
|
||||
FQuat FirstQuat = FRotationMatrix::MakeFromX(FirstSafe.Aim).ToQuat();
|
||||
FQuat LastQuat = FRotationMatrix::MakeFromX(LastSafe.Aim).ToQuat();
|
||||
|
||||
double TotalAlpha = ExtrapolationTime / SafeDeltaT;
|
||||
double TotalAlpha = ExtrapolationTime / SafeDeltaT * DampingScale;
|
||||
FQuat PredictedQuat = FQuat::Slerp(FirstQuat, LastQuat, 1.0 + TotalAlpha);
|
||||
|
||||
OutAim = PredictedQuat.GetForwardVector().GetSafeNormal();
|
||||
@@ -257,12 +242,12 @@ void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocat
|
||||
}
|
||||
|
||||
// --- Weighted Linear Regression ---
|
||||
// Fits a weighted least-squares line through only the SAFE samples, extrapolates to current time.
|
||||
// More recent safe samples get higher weight.
|
||||
// Fits a weighted least-squares line (y = a + bt) through SAFE samples, extrapolates to current time.
|
||||
// Simple and stable. More recent safe samples get higher weight.
|
||||
|
||||
void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
void UEBBarrel::PredictWeightedLinearRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
if (SafeN < 2)
|
||||
{
|
||||
OutLocation = TransformHistory[0].Location;
|
||||
@@ -270,11 +255,8 @@ void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocati
|
||||
return;
|
||||
}
|
||||
|
||||
// Use timestamps relative to the first safe sample to avoid precision issues
|
||||
double T0 = TransformHistory[0].Timestamp;
|
||||
|
||||
// Weighted linear regression: y = a + b * t
|
||||
// Weights: linearly increasing (1, 2, 3, ..., SafeN)
|
||||
double SumW = 0.0;
|
||||
double SumWT = 0.0;
|
||||
double SumWTT = 0.0;
|
||||
@@ -285,7 +267,7 @@ void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocati
|
||||
|
||||
for (int32 i = 0; i < SafeN; i++)
|
||||
{
|
||||
double w = FMath::Pow((double)(i + 1), (double)RegressionWeightExponent); // Weight curve controlled by exponent
|
||||
double w = FMath::Pow((double)(i + 1), (double)RegressionWeightExponent);
|
||||
double t = TransformHistory[i].Timestamp - T0;
|
||||
|
||||
SumW += w;
|
||||
@@ -305,19 +287,25 @@ void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocati
|
||||
return;
|
||||
}
|
||||
|
||||
// Solve for position: intercept (a) and slope (b)
|
||||
FVector PosIntercept = (SumWY * SumWTT - SumWTY * SumWT) / Det;
|
||||
FVector PosSlope = (SumWTY * SumW - SumWY * SumWT) / Det;
|
||||
|
||||
// Solve for aim: intercept (a) and slope (b)
|
||||
FVector AimIntercept = (SumWAim * SumWTT - SumWTAim * SumWT) / Det;
|
||||
FVector AimSlope = (SumWTAim * SumW - SumWAim * SumWT) / Det;
|
||||
|
||||
// Extrapolate to current time
|
||||
double TPred = CurrentTime - T0;
|
||||
OutLocation = PosIntercept + PosSlope * TPred;
|
||||
|
||||
FVector PredAim = AimIntercept + AimSlope * TPred;
|
||||
// Apply optional velocity damping
|
||||
float DampingScale = 1.0f;
|
||||
if (ExtrapolationDamping > 0.0f)
|
||||
{
|
||||
double TLastSafe = TransformHistory[SafeN - 1].Timestamp - T0;
|
||||
float ExtrapolationTime = (float)(TPred - TLastSafe);
|
||||
DampingScale = FMath::Exp(-ExtrapolationDamping * ExtrapolationTime);
|
||||
}
|
||||
|
||||
OutLocation = PosIntercept + PosSlope * TPred * DampingScale;
|
||||
|
||||
FVector PredAim = AimIntercept + AimSlope * TPred * DampingScale;
|
||||
OutAim = PredAim.GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
@@ -403,12 +391,179 @@ void UEBBarrel::PredictKalmanFilter(double CurrentTime, FVector& OutLocation, FV
|
||||
// Extrapolate from Kalman state to current time
|
||||
float dt = (float)(CurrentTime - KalmanLastTimestamp);
|
||||
|
||||
OutLocation = KalmanPosition + KalmanVelocity * dt;
|
||||
// Apply optional velocity damping
|
||||
float DampingScale = 1.0f;
|
||||
if (ExtrapolationDamping > 0.0f)
|
||||
{
|
||||
DampingScale = FMath::Exp(-ExtrapolationDamping * dt);
|
||||
}
|
||||
|
||||
FVector PredAim = KalmanAim + KalmanAngularVelocity * dt;
|
||||
OutLocation = KalmanPosition + KalmanVelocity * dt * DampingScale;
|
||||
|
||||
FVector PredAim = KalmanAim + KalmanAngularVelocity * dt * DampingScale;
|
||||
OutAim = PredAim.GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
OutAim = KalmanAim.GetSafeNormal();
|
||||
}
|
||||
}
|
||||
|
||||
// --- Adaptive Extrapolation ---
|
||||
// Computes weighted average velocity from safe samples, then scales extrapolation
|
||||
// by a confidence factor based on velocity consistency.
|
||||
// Low velocity variance → full extrapolation (steady movement).
|
||||
// High velocity variance → reduced extrapolation (deceleration/direction change).
|
||||
|
||||
void UEBBarrel::PredictAdaptiveExtrapolation(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;
|
||||
}
|
||||
|
||||
// Compute weighted velocities from consecutive safe sample pairs
|
||||
// Weight recent pairs more heavily
|
||||
TArray<FVector> PosVelocities;
|
||||
// Compute per-pair velocities
|
||||
TArray<FVector> PosVels;
|
||||
TArray<FVector> AimVels;
|
||||
PosVels.Reserve(SafeN - 1);
|
||||
AimVels.Reserve(SafeN - 1);
|
||||
|
||||
for (int32 i = 1; i < SafeN; i++)
|
||||
{
|
||||
double dt = TransformHistory[i].Timestamp - TransformHistory[i - 1].Timestamp;
|
||||
if (dt > SMALL_NUMBER)
|
||||
{
|
||||
PosVels.Add((TransformHistory[i].Location - TransformHistory[i - 1].Location) / dt);
|
||||
AimVels.Add((TransformHistory[i].Aim - TransformHistory[i - 1].Aim) / dt);
|
||||
}
|
||||
}
|
||||
|
||||
if (PosVels.Num() == 0)
|
||||
{
|
||||
OutLocation = TransformHistory[SafeN - 1].Location;
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute overall weighted average velocity (recent samples weighted more)
|
||||
double TotalWeight = 0.0;
|
||||
FVector AvgPosVel = FVector::ZeroVector;
|
||||
FVector AvgAimVel = FVector::ZeroVector;
|
||||
|
||||
for (int32 i = 0; i < PosVels.Num(); i++)
|
||||
{
|
||||
double w = FMath::Pow((double)(i + 1), 2.0);
|
||||
AvgPosVel += PosVels[i] * w;
|
||||
AvgAimVel += AimVels[i] * w;
|
||||
TotalWeight += w;
|
||||
}
|
||||
|
||||
AvgPosVel /= TotalWeight;
|
||||
AvgAimVel /= TotalWeight;
|
||||
|
||||
// Deceleration detection: compare recent speed (last 25% of pairs) vs overall speed.
|
||||
// If the user is stopping, recent speed will drop toward 0 while avg is still high.
|
||||
// Confidence = recentSpeed / avgSpeed, clamped to [0, 1].
|
||||
// During steady movement: ratio ≈ 1 → full extrapolation, zero lag.
|
||||
// During deceleration: ratio < 1 → reduced extrapolation, prevents overshoot.
|
||||
int32 RecentStart = FMath::Max(0, PosVels.Num() - FMath::Max(1, PosVels.Num() / 4));
|
||||
int32 RecentCount = PosVels.Num() - RecentStart;
|
||||
|
||||
// Recent average velocity (unweighted, just the latest samples)
|
||||
FVector RecentPosVel = FVector::ZeroVector;
|
||||
FVector RecentAimVel = FVector::ZeroVector;
|
||||
for (int32 i = RecentStart; i < PosVels.Num(); i++)
|
||||
{
|
||||
RecentPosVel += PosVels[i];
|
||||
RecentAimVel += AimVels[i];
|
||||
}
|
||||
RecentPosVel /= (double)RecentCount;
|
||||
RecentAimVel /= (double)RecentCount;
|
||||
|
||||
// Compute speed ratio: recent / average
|
||||
float AvgPosSpeed = AvgPosVel.Size();
|
||||
float AvgAimSpeed = AvgAimVel.Size();
|
||||
float RecentPosSpeed = RecentPosVel.Size();
|
||||
float RecentAimSpeed = RecentAimVel.Size();
|
||||
|
||||
// Confidence: ratio of recent speed to average speed.
|
||||
// Dead zone: ratios above AdaptiveDeadZone are treated as 1.0 (no correction).
|
||||
// Only ratios BELOW AdaptiveDeadZone trigger extrapolation reduction.
|
||||
// Below dead zone: remap [0, deadzone] → [0, 1] then apply sensitivity exponent.
|
||||
// Minimum speed threshold: below this, speed ratios are unreliable (noise dominates),
|
||||
// so we keep full confidence to avoid false deceleration detection.
|
||||
const float MinSpeedThreshold = SMALL_NUMBER;
|
||||
|
||||
float PosRatio = 1.0f;
|
||||
float PosConfidence = 1.0f;
|
||||
float PosRemapped = 1.0f;
|
||||
if (AvgPosSpeed > MinSpeedThreshold)
|
||||
{
|
||||
PosRatio = FMath::Clamp(RecentPosSpeed / AvgPosSpeed, 0.0f, 1.0f);
|
||||
if (PosRatio >= AdaptiveDeadZone)
|
||||
{
|
||||
// Inside dead zone: no correction, full confidence
|
||||
PosRemapped = 1.0f;
|
||||
PosConfidence = 1.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Below dead zone: real deceleration detected
|
||||
// Remap [0, deadzone] → [0, 1]
|
||||
PosRemapped = (AdaptiveDeadZone > SMALL_NUMBER)
|
||||
? FMath::Clamp(PosRatio / AdaptiveDeadZone, 0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
PosConfidence = FMath::Pow(PosRemapped, AdaptiveSensitivity);
|
||||
}
|
||||
}
|
||||
// else: AvgPosSpeed <= MinSpeedThreshold → keep defaults (Ratio=1, Conf=1)
|
||||
|
||||
float AimRatio = 1.0f;
|
||||
float AimConfidence = 1.0f;
|
||||
float AimRemapped = 1.0f;
|
||||
if (AvgAimSpeed > MinSpeedThreshold)
|
||||
{
|
||||
AimRatio = FMath::Clamp(RecentAimSpeed / AvgAimSpeed, 0.0f, 1.0f);
|
||||
if (AimRatio >= AdaptiveDeadZone)
|
||||
{
|
||||
// Inside dead zone: no correction, full confidence
|
||||
AimRemapped = 1.0f;
|
||||
AimConfidence = 1.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Below dead zone: real deceleration detected
|
||||
// Remap [0, deadzone] → [0, 1]
|
||||
AimRemapped = (AdaptiveDeadZone > SMALL_NUMBER)
|
||||
? FMath::Clamp(AimRatio / AdaptiveDeadZone, 0.0f, 1.0f)
|
||||
: 0.0f;
|
||||
AimConfidence = FMath::Pow(AimRemapped, AdaptiveSensitivity);
|
||||
}
|
||||
}
|
||||
// else: AvgAimSpeed <= MinSpeedThreshold → keep defaults (Ratio=1, Conf=1)
|
||||
|
||||
// Extrapolate from last safe sample
|
||||
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
|
||||
double ExtrapolationTime = CurrentTime - LastSafe.Timestamp;
|
||||
|
||||
// Apply optional damping
|
||||
float DampingScale = 1.0f;
|
||||
if (ExtrapolationDamping > 0.0f)
|
||||
{
|
||||
DampingScale = FMath::Exp(-ExtrapolationDamping * (float)ExtrapolationTime);
|
||||
}
|
||||
|
||||
OutLocation = LastSafe.Location + AvgPosVel * ExtrapolationTime * (PosConfidence * DampingScale);
|
||||
|
||||
FVector PredAim = LastSafe.Aim + AvgAimVel * ExtrapolationTime * (AimConfidence * DampingScale);
|
||||
OutAim = PredAim.GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
OutAim = LastSafe.Aim;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ FPrimitiveSceneProxy* UEBBarrel::CreateSceneProxy() {
|
||||
const FLinearColor DrawColor = GetViewSelectionColor(FColor::Green, *View, IsSelected(), IsHovered(), true, IsIndividuallySelected());
|
||||
|
||||
FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex);
|
||||
DrawDirectionalArrow(PDI, Transform, DrawColor, Component->DebugArrowSize, Component->DebugArrowSize*0.1f, 16, Component->DebugArrowSize*0.01f);
|
||||
DrawDirectionalArrow(PDI, Transform, DrawColor, Component->DebugArrowSize, Component->DebugArrowSize*0.1f, 16, 0.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
// Copyright 2018 Mookie. All Rights Reserved.
|
||||
#include "EBBarrel.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "HAL/PlatformFileManager.h"
|
||||
#include "Misc/Paths.h"
|
||||
#include "Misc/DateTime.h"
|
||||
|
||||
UEBBarrel::UEBBarrel() {
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
PrimaryComponentTick.TickGroup = TG_PostPhysics;
|
||||
bHiddenInGame = true;
|
||||
bAutoActivate = true;
|
||||
SetIsReplicatedByDefault(ReplicateVariables);
|
||||
@@ -13,6 +18,33 @@ UEBBarrel::UEBBarrel() {
|
||||
GatlingRPS = FireRateMin;
|
||||
}
|
||||
|
||||
void UEBBarrel::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Add tick prerequisite on the attach parent so this barrel ticks after
|
||||
// its parent's transform is updated. UE propagates transforms down the
|
||||
// attach chain, so this is sufficient even with deep hierarchies
|
||||
// (Pawn -> MotionController -> ChildActor -> Weapon -> EBBarrel).
|
||||
if (USceneComponent* Parent = GetAttachParent())
|
||||
{
|
||||
PrimaryComponentTick.AddPrerequisite(Parent, Parent->PrimaryComponentTick);
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::EndPlay(const EEndPlayReason::Type EndPlayReason)
|
||||
{
|
||||
// Close CSV file on stop/exit so the file isn't left locked
|
||||
if (bCSVFileOpen && CSVFileHandle)
|
||||
{
|
||||
delete CSVFileHandle;
|
||||
CSVFileHandle = nullptr;
|
||||
bCSVFileOpen = false;
|
||||
}
|
||||
|
||||
Super::EndPlay(EndPlayReason);
|
||||
}
|
||||
|
||||
void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
@@ -66,45 +98,81 @@ 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 spheres at origins for clarity
|
||||
DrawDebugSphere(GetWorld(), RawLocation, 1.5f, 6, FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f);
|
||||
DrawDebugSphere(GetWorld(), Location, 1.5f, 6, FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f);
|
||||
// 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)
|
||||
// CSV Prediction Recording
|
||||
if (RecordPredictionCSV && AntiRecoilMode != EAntiRecoilMode::ARM_None)
|
||||
{
|
||||
if (!bCSVFileOpen)
|
||||
{
|
||||
// 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)
|
||||
// Open new CSV file
|
||||
FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
|
||||
CSVFilePath = FPaths::ProjectSavedDir() / TEXT("Logs") / FString::Printf(TEXT("AntiRecoil_%s.csv"), *Timestamp);
|
||||
CSVFileHandle = FPlatformFileManager::Get().GetPlatformFile().OpenWrite(*CSVFilePath);
|
||||
if (CSVFileHandle)
|
||||
{
|
||||
// Yellow line: uncorrected aim (where the bullet would have gone without anti-recoil)
|
||||
DrawDebugLine(GetWorld(), DebugIMUShockCapturedLocation,
|
||||
DebugIMUShockCapturedLocation + DebugIMUShockCapturedAim * DebugAntiRecoilLineLength,
|
||||
FColor::Yellow, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
DrawDebugSphere(GetWorld(), DebugIMUShockCapturedLocation, 3.0f, 8, FColor::Yellow, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
bCSVFileOpen = true;
|
||||
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());
|
||||
|
||||
if (GEngine)
|
||||
{
|
||||
GEngine->AddOnScreenDebugMessage(-700, 5.0f, FColor::Cyan,
|
||||
FString::Printf(TEXT("CSV Recording started: %s"), *CSVFilePath));
|
||||
}
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
if (bCSVFileOpen && CSVFileHandle)
|
||||
{
|
||||
FVector RealPos = GetComponentTransform().GetLocation();
|
||||
FVector RealAim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
// Count safe samples (same logic as GetSafeCount in AntiRecoilPredict.cpp)
|
||||
double SafeCutoff = GetWorld()->GetTimeSeconds() - (AntiRecoilDiscardTimeMs / 1000.0f);
|
||||
int32 SafeN = 0;
|
||||
for (int32 si = 0; si < TransformHistory.Num(); si++)
|
||||
{
|
||||
DebugIMUShockLineCaptured = false;
|
||||
if (TransformHistory[si].Timestamp < SafeCutoff) SafeN++;
|
||||
}
|
||||
double ExtrapTime = (SafeN > 0) ? (GetWorld()->GetTimeSeconds() - TransformHistory[SafeN - 1].Timestamp) : 0.0;
|
||||
|
||||
FString Line = FString::Printf(TEXT("%.6f,%.4f,%.4f,%.4f,%.6f,%.6f,%.6f,%.4f,%.4f,%.4f,%.6f,%.6f,%.6f,%d,%d,%.6f,%d\n"),
|
||||
GetWorld()->GetTimeSeconds(),
|
||||
RealPos.X, RealPos.Y, RealPos.Z,
|
||||
RealAim.X, RealAim.Y, RealAim.Z,
|
||||
Location.X, Location.Y, Location.Z,
|
||||
Aim.X, Aim.Y, Aim.Z,
|
||||
SafeN, TransformHistory.Num(), ExtrapTime,
|
||||
bShotFiredThisFrame ? 1 : 0);
|
||||
auto LineUtf8 = StringCast<ANSICHAR>(*Line);
|
||||
CSVFileHandle->Write((const uint8*)LineUtf8.Get(), LineUtf8.Length());
|
||||
bShotFiredThisFrame = false;
|
||||
}
|
||||
}
|
||||
else if (bCSVFileOpen)
|
||||
{
|
||||
// Close CSV file
|
||||
if (CSVFileHandle)
|
||||
{
|
||||
delete CSVFileHandle;
|
||||
CSVFileHandle = nullptr;
|
||||
}
|
||||
bCSVFileOpen = false;
|
||||
|
||||
if (GEngine)
|
||||
{
|
||||
GEngine->AddOnScreenDebugMessage(-700, 5.0f, FColor::Cyan,
|
||||
FString::Printf(TEXT("CSV Recording stopped: %s"), *CSVFilePath));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,6 +364,8 @@ void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim, in
|
||||
}
|
||||
}
|
||||
|
||||
bShotFiredThisFrame = true;
|
||||
|
||||
if (ReplicateShotFiredEvents) {
|
||||
ShotFiredMulticast();
|
||||
}
|
||||
@@ -315,3 +385,4 @@ void UEBBarrel::ApplyRecoil_Implementation(UPrimitiveComponent* Component, FVect
|
||||
Component->AddImpulseAtLocation(Impulse, InLocation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,22 +80,58 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
|
||||
}
|
||||
|
||||
if (MaterialDensityControlsPenetrationDepth) {
|
||||
penDepthMultiplier /= PhysMaterial->Density;
|
||||
float SafeDensity = FMath::Max(PhysMaterial->Density, 0.001f);
|
||||
penDepthMultiplier /= SafeDensity;
|
||||
}
|
||||
|
||||
if (MaterialRestitutionControlsRicochet) {
|
||||
RicochetRestitution *= PhysMaterial->Restitution;
|
||||
RicochetRestitution *= FMath::Clamp(PhysMaterial->Restitution, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Guard: if bullet has near-zero velocity, stop it immediately
|
||||
if (Velocity.SizeSquared() < SMALL_NUMBER)
|
||||
{
|
||||
SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin);
|
||||
FVector Impulse = Velocity * Mass * ImpulseMultiplier;
|
||||
if (AddImpulse && HitResult.Component->IsSimulatingPhysics()) {
|
||||
HitResult.Component->AddImpulseAtLocation(Impulse, HitResult.Location, HitResult.BoneName);
|
||||
}
|
||||
if (HasAuthority()) {
|
||||
OnImpact(false, false, HitResult.Location, Velocity, HitResult.Normal, GetActorLocation(), FVector::ZeroVector, Impulse, 0.0f, HitResult.GetActor(), HitResult.Component.Get(), HitResult.BoneName, PhysMaterial, HitResult, fireEventID);
|
||||
} else {
|
||||
OnNetPredictedImpact(false, false, HitResult.Location, Velocity, HitResult.Normal, GetActorLocation(), FVector::ZeroVector, Impulse, 0.0f, HitResult.GetActor(), HitResult.Component.Get(), HitResult.BoneName, PhysMaterial, HitResult, fireEventID);
|
||||
}
|
||||
Velocity = FVector::ZeroVector;
|
||||
Deactivate();
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float dot = FVector::DotProduct(Velocity.GetSafeNormal(), HitResult.Normal) + 1.0f;
|
||||
FVector cross = FVector::CrossProduct(Velocity.GetSafeNormal(), HitResult.Normal);
|
||||
FVector flat = HitResult.Normal.RotateAngleAxis(-90.0f, cross);
|
||||
|
||||
// Guard: near-zero cross product at very shallow grazing angles
|
||||
FVector flat;
|
||||
if (cross.SizeSquared() < SMALL_NUMBER)
|
||||
{
|
||||
// Bullet nearly parallel to surface: project velocity onto surface plane
|
||||
flat = FVector::VectorPlaneProject(Velocity.GetSafeNormal(), HitResult.Normal).GetSafeNormal();
|
||||
if (flat.IsNearlyZero())
|
||||
{
|
||||
flat = FMath::Abs(HitResult.Normal.Z) < 0.9f
|
||||
? FVector::CrossProduct(HitResult.Normal, FVector::UpVector).GetSafeNormal()
|
||||
: FVector::CrossProduct(HitResult.Normal, FVector::RightVector).GetSafeNormal();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
flat = HitResult.Normal.RotateAngleAxis(-90.0f, cross);
|
||||
}
|
||||
|
||||
#ifdef WITH_EDITOR
|
||||
if (DebugEnabled) {
|
||||
FColor DebugColor = FColor::MakeRedToGreenColorFromScalar(Velocity.Size() / MuzzleVelocityMax);
|
||||
FColor DebugColor = FColor::MakeRedToGreenColorFromScalar(Velocity.Size() / FMath::Max(MuzzleVelocityMax, 1.0f));
|
||||
DrawDebugLine(GetWorld(), start, HitResult.Location, DebugColor, false, DebugTrailTime, 0, DebugTrailWidth);
|
||||
};
|
||||
#endif
|
||||
@@ -103,32 +139,43 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
|
||||
float GrazingAngle = FMath::Pow(dot, GrazingAngleExponent);
|
||||
FVector PenetrationVector = RandomStream.VRandCone(Velocity, penEnterSpread);
|
||||
PenetrationVector = FMath::Lerp(PenetrationVector, -HitResult.Normal, FMath::Lerp(penNormalization, penNormalizationGrazing, GrazingAngle));
|
||||
float PenetrationDistance = FMath::Lerp(MinPenetration, MaxPenetration, RandomStream.FRand()) * FMath::Pow((Velocity.Size() / ((MuzzleVelocityMin + MuzzleVelocityMax) * 0.5f)), 2.0f) * penDepthMultiplier;
|
||||
float AvgMuzzleVelocity = FMath::Max((MuzzleVelocityMin + MuzzleVelocityMax) * 0.5f, 1.0f);
|
||||
// Incidence angle factor: head-on (dot=2) -> full depth, grazing (dot=0) -> minimal depth
|
||||
float IncidenceFactor = FMath::Clamp(dot * 0.5f, 0.05f, 1.0f);
|
||||
float PenetrationDistance = FMath::Lerp(MinPenetration, MaxPenetration, RandomStream.FRand()) * FMath::Pow((Velocity.Size() / AvgMuzzleVelocity), 2.0f) * penDepthMultiplier * IncidenceFactor;
|
||||
float PenetrationDepth = -FVector::DotProduct(PenetrationVector, HitResult.Normal) * PenetrationDistance;
|
||||
|
||||
float BlockTIme = 1.0f;
|
||||
float BlockTime = 1.0f;
|
||||
|
||||
if (PenetrationDistance > 0.0f) {
|
||||
if (!neverPenetrate) {
|
||||
BlockTIme = PenetrationTrace(HitResult.Location - (HitResult.Normal * CollisionMargin), HitResult.Location + PenetrationVector * PenetrationDistance, HitResult.Component, PenTraceType, CollisionChannel, exitLoc, exitNormal);
|
||||
BlockTime = PenetrationTrace(HitResult.Location - (HitResult.Normal * CollisionMargin), HitResult.Location + PenetrationVector * PenetrationDistance, HitResult.Component, PenTraceType, CollisionChannel, exitLoc, exitNormal);
|
||||
}
|
||||
}
|
||||
|
||||
if (BlockTIme >= 0.999999f) {
|
||||
if (BlockTime >= 0.999999f) {
|
||||
|
||||
//no pen
|
||||
SetActorLocation(HitResult.Location + HitResult.Normal * CollisionMargin);
|
||||
|
||||
float ricThreshold = 1.0f;
|
||||
if (SpeedControlsRicochetProbability) { ricThreshold *= Velocity.Size() / MuzzleVelocityMax; };
|
||||
if (SpeedControlsRicochetProbability) { ricThreshold *= Velocity.Size() / FMath::Max(MuzzleVelocityMax, 1.0f); };
|
||||
|
||||
if (!neverRicochet && RandomStream.FRand() * ricThreshold < FMath::Lerp(RicochetProbability * ricProbMultiplier, RicochetProbabilityGrazing * ricProbMultiplier, GrazingAngle)) {
|
||||
//bounce
|
||||
FVector bounceAngle = flat * dot * (1.0f - ricFriction);
|
||||
bounceAngle += HitResult.Normal * (1.0f - dot) * ricRestitution;
|
||||
bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceAngle.Size();
|
||||
|
||||
NewVelocity = bounceAngle * Velocity.Size();
|
||||
float bounceSize = bounceAngle.Size();
|
||||
if (bounceSize > SMALL_NUMBER)
|
||||
{
|
||||
bounceAngle = RandomStream.VRandCone(bounceAngle, ricSpread) * bounceSize;
|
||||
NewVelocity = bounceAngle * Velocity.Size();
|
||||
}
|
||||
else
|
||||
{
|
||||
// bounceAngle is zero (head-on + high friction + low restitution): reflect off normal
|
||||
NewVelocity = RandomStream.VRandCone(HitResult.Normal, ricSpread) * Velocity.Size() * ricRestitution;
|
||||
}
|
||||
Ricochet = true;
|
||||
OwnerSafe = false;
|
||||
}
|
||||
@@ -139,7 +186,7 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
|
||||
}
|
||||
else {
|
||||
//penetration
|
||||
float RemainingEnergy = FMath::Pow(1.0f - BlockTIme, 2.0f);
|
||||
float RemainingEnergy = FMath::Pow(1.0f - BlockTime, 2.0f);
|
||||
SetActorLocation(exitLoc + exitNormal * CollisionMargin);
|
||||
NewVelocity = RandomStream.VRandCone(PenetrationVector, penExitSpread * (1.0f - RemainingEnergy));
|
||||
NewVelocity = FMath::Lerp(NewVelocity, Velocity.GetSafeNormal(), RemainingEnergy);
|
||||
@@ -189,7 +236,7 @@ float AEBBullet::Trace(FVector start, FVector PreviousVelocity, float delta, TEn
|
||||
|
||||
#ifdef WITH_EDITOR
|
||||
if (DebugEnabled) {
|
||||
FLinearColor Color = GetDebugColor(Velocity.Size() / ((MuzzleVelocityMin + MuzzleVelocityMax)*0.5f));
|
||||
FLinearColor Color = GetDebugColor(Velocity.Size() / FMath::Max((MuzzleVelocityMin + MuzzleVelocityMax)*0.5f, 1.0f));
|
||||
DrawDebugLine(GetWorld(), start, start + TraceDistance, Color.ToFColor(true), false, DebugTrailTime, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ enum class EAntiRecoilMode : uint8
|
||||
ARM_None UMETA(DisplayName = "Disabled", ToolTip = "No anti-recoil processing. Uses raw tracker data directly. Use this when no IMU shock compensation is needed."),
|
||||
ARM_Buffer UMETA(DisplayName = "Buffer (No Prediction)", ToolTip = "Legacy mode. Returns the oldest sample in the buffer, guaranteed to be pre-shock. Simple and reliable but introduces a fixed time delay equal to BufferTime. Best for static or slow-moving aiming."),
|
||||
ARM_LinearExtrapolation UMETA(DisplayName = "Linear Extrapolation", ToolTip = "Computes average linear and angular velocity from consecutive safe (pre-shock) samples, then extrapolates forward to the current time. Good balance of simplicity and accuracy for steady movements. May overshoot on sudden direction changes."),
|
||||
ARM_WeightedRegression UMETA(DisplayName = "Weighted Regression", ToolTip = "Fits a weighted least-squares regression line through all safe samples (recent safe samples weighted higher), then extrapolates to current time. More robust to individual noisy samples than linear extrapolation. Slightly heavier computation."),
|
||||
ARM_KalmanFilter UMETA(DisplayName = "Kalman Filter", ToolTip = "Maintains an internal state model (position + velocity, aim + angular velocity) updated only with safe samples. Predicts forward using the estimated dynamics. Best for smooth continuous tracking with optimal noise rejection. Requires tuning ProcessNoise and MeasurementNoise for best results.")
|
||||
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_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).")
|
||||
};
|
||||
|
||||
USTRUCT()
|
||||
@@ -52,6 +53,9 @@ public:
|
||||
// Sets default values for this component's properties
|
||||
UEBBarrel();
|
||||
|
||||
virtual void BeginPlay() override;
|
||||
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
|
||||
|
||||
// Called every frame
|
||||
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
|
||||
|
||||
@@ -65,32 +69,28 @@ public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Draw real-time debug lines: Green = raw tracker, Red = anti-recoil predicted aim"))
|
||||
bool DebugAntiRecoil = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Length of the debug aim lines (cm)", EditCondition = "DebugAntiRecoil"))
|
||||
float DebugAntiRecoilLineLength = 200.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Thickness of the debug aim lines", EditCondition = "DebugAntiRecoil"))
|
||||
float DebugAntiRecoilLineThickness = 0.0f;
|
||||
float DebugAntiRecoilLineLength = 400.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Enable IMU shock simulation for testing anti-recoil prediction without firing"))
|
||||
bool DebugSimulateIMUShock = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Angular perturbation intensity in degrees", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.0"))
|
||||
float DebugIMUShockAngle = 15.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Position perturbation intensity in cm", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.0"))
|
||||
float DebugIMUShockPosition = 2.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Duration of the simulated shock in seconds", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.01"))
|
||||
float DebugIMUShockDuration = 0.08f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "How long (seconds) the yellow debug line persists after a shock, showing where the shot would have gone without anti-recoil correction", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.5"))
|
||||
float DebugIMUShockDisplayTime = 3.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "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;
|
||||
|
||||
// CSV recording state (not exposed)
|
||||
bool bCSVFileOpen = false;
|
||||
FString CSVFilePath;
|
||||
IFileHandle* CSVFileHandle = nullptr;
|
||||
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_KalmanFilter;
|
||||
EAntiRecoilMode AntiRecoilMode = EAntiRecoilMode::ARM_AdaptiveExtrapolation;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Total time window (seconds) of tracker history to keep. Determines how far back in time samples are stored. Must be greater than DiscardTime. Example: 0.2s at 60fps stores ~12 samples.", ClampMin = "0.05"))
|
||||
float AntiRecoilBufferTime = 0.15f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Total time window (ms) of tracker history to keep. Determines how far back in time samples are stored. Must be greater than DiscardTime. Example: 200ms at 60fps stores ~12 samples.", ClampMin = "5"))
|
||||
float AntiRecoilBufferTimeMs = 200.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Time window (seconds) of most recent samples to exclude as potentially contaminated by IMU recoil shock. The prediction algorithms only use samples older than this. Increase if the shock lasts longer. Safe window = BufferTime - DiscardTime.", ClampMin = "0.0"))
|
||||
float AntiRecoilDiscardTime = 0.03f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Time window (ms) of most recent samples to exclude as potentially contaminated by IMU recoil shock. The prediction algorithms only use samples older than this. Increase if the shock lasts longer. Safe window = BufferTime - DiscardTime.", ClampMin = "0.0"))
|
||||
float AntiRecoilDiscardTimeMs = 30.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Controls how the weight curve grows across safe samples in Weighted Regression mode. 1.0 = linear growth (default), >1.0 = recent samples weighted much more heavily (convex curve), <1.0 = more uniform weighting (concave curve), 0.0 = all samples weighted equally (unweighted regression). Formula: weight = pow(sampleIndex+1, exponent).", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_WeightedRegression", ClampMin = "0.0", ClampMax = "5.0"))
|
||||
float RegressionWeightExponent = 3.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_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"))
|
||||
float KalmanProcessNoise = 200.0f;
|
||||
@@ -98,6 +98,15 @@ public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Kalman filter measurement noise (higher = trusts model over measurements, lower = trusts measurements). Should be low since safe samples are clean.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_KalmanFilter", ClampMin = "0.001"))
|
||||
float KalmanMeasurementNoise = 0.01f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Power curve exponent for deceleration detection. Controls how aggressively slowing down reduces extrapolation. confidence = (remappedRatio)^sensitivity. 1.0 = linear (gentle). 2.0 = quadratic (aggressive). 0.5 = square root (very gentle). During steady movement, ratio is ~1 so confidence is always 1 regardless of this value.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation", ClampMin = "0.1", ClampMax = "5.0"))
|
||||
float AdaptiveSensitivity = 3.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Dead zone for deceleration detection. Speed ratios (recent/avg) above this value are treated as 1.0 (no correction). Only ratios below trigger extrapolation reduction. Higher = more tolerant to natural speed fluctuations (less false positives). Lower = more sensitive to deceleration. 0.8 = ignore normal jitter, only react to real braking.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation", ClampMin = "0.0", ClampMax = "0.95"))
|
||||
float AdaptiveDeadZone = 0.95f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Velocity damping during extrapolation. 0 = disabled (default). Higher values cause extrapolated velocity to decay exponentially toward zero over the discard window. Reduces overshoot on fast draw-aim-fire sequences where the user stops moving before firing. Applies to all prediction modes except Buffer. Typical range: 5-15.", ClampMin = "0.0", ClampMax = "50.0"))
|
||||
float ExtrapolationDamping = 5.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Velocity", meta = (ToolTip = "Bullet inherits barrel velocity, only works with physics enabled or with additional velocity set")) float InheritVelocity = 1.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Velocity", meta = (ToolTip = "Amount of recoil applied to the barrel, only works with physics enabled")) float RecoilMultiplier = 1.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Velocity", meta = (ToolTip = "Additional velocity, for use with InheritVelocity")) FVector AdditionalVelocity = FVector(0,0,0);
|
||||
@@ -173,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;
|
||||
@@ -228,18 +234,6 @@ private:
|
||||
|
||||
TArray<FTimestampedTransform> TransformHistory;
|
||||
|
||||
// Debug IMU shock simulation state
|
||||
double DebugIMUShockStartTime = 0.0;
|
||||
bool DebugIMUShockActive = false;
|
||||
FVector DebugIMUShockAimOffset = FVector::ZeroVector;
|
||||
FVector DebugIMUShockPosOffset = FVector::ZeroVector;
|
||||
|
||||
// Debug yellow line persistence (shows uncorrected aim after shock)
|
||||
bool DebugIMUShockLineCaptured = false;
|
||||
double DebugIMUShockLineEndTime = 0.0;
|
||||
FVector DebugIMUShockCapturedLocation = FVector::ZeroVector;
|
||||
FVector DebugIMUShockCapturedAim = FVector::ForwardVector;
|
||||
|
||||
// Kalman filter state
|
||||
FVector KalmanPosition = FVector::ZeroVector;
|
||||
FVector KalmanVelocity = FVector::ZeroVector;
|
||||
@@ -255,7 +249,8 @@ private:
|
||||
void UpdateTransformHistory();
|
||||
void ComputeAntiRecoilTransform();
|
||||
void PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
|
||||
void PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
|
||||
void PredictWeightedLinearRegression(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;
|
||||
|
||||
|
||||
35
Unreal/build Lancelot.bat
Normal file
35
Unreal/build Lancelot.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@echo off
|
||||
chcp 65001 >nul
|
||||
title Build PS_AI_Agent
|
||||
|
||||
echo ============================================================
|
||||
echo PS_AI_Agent - Compilation plugin ElevenLabs (UE 5.5)
|
||||
echo ============================================================
|
||||
echo.
|
||||
echo ATTENTION : Ferme l'Unreal Editor avant de continuer !
|
||||
echo (Les DLL seraient verrouillees et la compilation echouerait)
|
||||
echo.
|
||||
pause
|
||||
|
||||
echo.
|
||||
echo Compilation en cours...
|
||||
echo (Seuls les .cpp modifies sont recompiles, ~16s)
|
||||
echo.
|
||||
|
||||
powershell.exe -Command "& 'C:\Program Files\Epic Games\UE_5.5\Engine\Build\BatchFiles\RunUAT.bat' BuildEditor -project='E:\ASTERION\GIT\PS_Ballistics\Unreal\PS_Ballistics.uproject' -notools -noP4 2>&1"
|
||||
|
||||
echo.
|
||||
if %ERRORLEVEL% == 0 (
|
||||
echo ============================================================
|
||||
echo SUCCES - Compilation terminee sans erreur.
|
||||
echo Tu peux relancer l'Unreal Editor.
|
||||
echo ============================================================
|
||||
) else (
|
||||
echo ============================================================
|
||||
echo ECHEC - Erreur de compilation (code %ERRORLEVEL%)
|
||||
echo Consulte le log ci-dessus pour le detail.
|
||||
echo ============================================================
|
||||
)
|
||||
|
||||
echo.
|
||||
pause
|
||||
Reference in New Issue
Block a user