Compare commits
21 Commits
c6fd71dc0b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b65de712db | |||
| f4d1f8f73b | |||
| 15e449a00c | |||
| 1e1a29baef | |||
| b98d89c609 | |||
| f6a0cdc1c4 | |||
| 97e8b709a0 | |||
| abd01c661a | |||
| 81bb610459 | |||
| 58df608550 | |||
| ba6b35b3d9 | |||
| 1a5b107b1f | |||
| cd097e4e55 | |||
| 4e9c33778c | |||
| 48737b60c9 | |||
| 83188b1fa1 | |||
| af723c944b | |||
| fa257fb87b | |||
| 9407d8a556 | |||
| 1b32c1eef1 | |||
| ed901a90d5 |
@@ -4,7 +4,35 @@
|
||||
"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 \":*)",
|
||||
"Bash(git -C C:/ASTERION/GIT/PS_Ballistics status --short)",
|
||||
"Bash(git -C C:/ASTERION/GIT/PS_Ballistics ls-files)",
|
||||
"Bash(grep -iE \"\\\\.\\(dll|pdb|exp|lib|exe|dylib|so|a|o\\)$\")",
|
||||
"Bash(git -C C:/ASTERION/GIT/PS_Ballistics add .gitignore)",
|
||||
"Bash(git -C C:/ASTERION/GIT/PS_Ballistics commit -m ' *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -3,4 +3,19 @@ Unreal/.vs/
|
||||
Unreal/Binaries/
|
||||
Unreal/Intermediate/
|
||||
Unreal/Plugins/EasyBallistics/Intermediate/
|
||||
Unreal/Plugins/EasyBallistics/Binaries/
|
||||
Unreal/Saved/
|
||||
Unreal/Plugins/PS_Ballistics/Intermediate/
|
||||
Unreal/Plugins/PS_Ballistics/Binaries/
|
||||
|
||||
# Binaires compilés
|
||||
*.dll
|
||||
*.exp
|
||||
*.pdb
|
||||
*.lib
|
||||
*.obj
|
||||
*.exe
|
||||
*.so
|
||||
*.dylib
|
||||
*.a
|
||||
*.o
|
||||
|
||||
1
Bind to PROSERVE.bat
Normal file
1
Bind to PROSERVE.bat
Normal file
@@ -0,0 +1 @@
|
||||
powershell -Command "New-Item -ItemType Junction -Path 'E:\ASTERION\SVN\DEV\PROSERVE_UE_5_5\Plugins\PS_Ballistics' -Target 'E:\ASTERION\GIT\PS_Ballistics\Unreal\Plugins\PS_Ballistics'"
|
||||
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.
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"BuildId": "37670630",
|
||||
"Modules":
|
||||
{
|
||||
"EasyBallistics": "UnrealEditor-EasyBallistics.dylib"
|
||||
}
|
||||
}
|
||||
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.
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.
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"BuildId": "37670630",
|
||||
"Modules":
|
||||
{
|
||||
"EasyBallistics": "UnrealEditor-EasyBallistics-Win64-DebugGame.dll"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"BuildId": "37670630",
|
||||
"Modules":
|
||||
{
|
||||
"EasyBallistics": "UnrealEditor-EasyBallistics.dll"
|
||||
}
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
// Anti-Recoil Prediction Methods for EBBarrel
|
||||
// Provides linear extrapolation, weighted regression, and Kalman filter prediction
|
||||
//
|
||||
// Key concept: The buffer stores samples within AntiRecoilBufferTime seconds.
|
||||
// Samples within the last AntiRecoilDiscardTime seconds may be contaminated by recoil shock.
|
||||
// All prediction algorithms work ONLY on the "safe" (oldest) portion of the buffer
|
||||
// and extrapolate forward to the current time.
|
||||
|
||||
#include "EBBarrel.h"
|
||||
|
||||
// Returns the number of safe (non-contaminated) samples based on a time threshold.
|
||||
// Samples whose timestamp >= (CurrentTime - DiscardTime) are considered potentially contaminated.
|
||||
static int32 GetSafeCount(const TArray<FTimestampedTransform>& History, double CurrentTime, float DiscardTime)
|
||||
{
|
||||
if (History.Num() == 0) return 0;
|
||||
double SafeCutoff = CurrentTime - FMath::Max(0.0f, DiscardTime);
|
||||
int32 SafeN = 0;
|
||||
for (int32 i = 0; i < History.Num(); i++)
|
||||
{
|
||||
if (History[i].Timestamp < SafeCutoff)
|
||||
{
|
||||
SafeN = i + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return SafeN;
|
||||
}
|
||||
|
||||
void UEBBarrel::TriggerDebugIMUShock()
|
||||
{
|
||||
if (!DebugSimulateIMUShock) return;
|
||||
|
||||
DebugIMUShockActive = true;
|
||||
DebugIMUShockLineCaptured = false; // Reset so the new shock replaces the previous yellow line
|
||||
DebugIMUShockStartTime = GetWorld()->GetTimeSeconds();
|
||||
|
||||
// Generate a random shock direction (sharp upward + random lateral, simulating recoil kick)
|
||||
FVector RandomDir = FMath::VRand();
|
||||
// Bias upward to simulate typical recoil pattern
|
||||
RandomDir.Z = FMath::Abs(RandomDir.Z) * 2.0f;
|
||||
RandomDir.Normalize();
|
||||
|
||||
DebugIMUShockAimOffset = RandomDir * FMath::DegreesToRadians(DebugIMUShockAngle);
|
||||
DebugIMUShockPosOffset = RandomDir * DebugIMUShockPosition;
|
||||
}
|
||||
|
||||
void UEBBarrel::UpdateTransformHistory()
|
||||
{
|
||||
if (AntiRecoilMode == EAntiRecoilMode::ARM_None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double CurrentTime = GetWorld()->GetTimeSeconds();
|
||||
|
||||
FTimestampedTransform Sample;
|
||||
Sample.Timestamp = CurrentTime;
|
||||
Sample.Location = GetComponentTransform().GetLocation();
|
||||
Sample.Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
|
||||
// Apply simulated IMU shock perturbation if active
|
||||
if (DebugSimulateIMUShock && DebugIMUShockActive)
|
||||
{
|
||||
float ElapsedShock = (float)(CurrentTime - DebugIMUShockStartTime);
|
||||
if (ElapsedShock < DebugIMUShockDuration)
|
||||
{
|
||||
// Decaying shock: intensity decreases over the shock duration
|
||||
float ShockAlpha = 1.0f - (ElapsedShock / DebugIMUShockDuration);
|
||||
// Add some high-frequency noise to simulate IMU vibration
|
||||
FVector FrameNoise = FMath::VRand() * 0.3f;
|
||||
|
||||
// Perturb aim direction
|
||||
FVector AimPerturbation = (DebugIMUShockAimOffset + FrameNoise * FMath::DegreesToRadians(DebugIMUShockAngle)) * ShockAlpha;
|
||||
Sample.Aim = (Sample.Aim + AimPerturbation).GetSafeNormal();
|
||||
|
||||
// Perturb position
|
||||
FVector PosPerturbation = (DebugIMUShockPosOffset + FrameNoise * DebugIMUShockPosition) * ShockAlpha;
|
||||
Sample.Location += PosPerturbation;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugIMUShockActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
TransformHistory.Add(Sample);
|
||||
|
||||
// Trim buffer: remove samples older than AntiRecoilBufferTime
|
||||
double OldestAllowed = CurrentTime - FMath::Max(0.05f, AntiRecoilBufferTime);
|
||||
while (TransformHistory.Num() > 0 && TransformHistory[0].Timestamp < OldestAllowed)
|
||||
{
|
||||
TransformHistory.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::ComputeAntiRecoilTransform()
|
||||
{
|
||||
switch (AntiRecoilMode)
|
||||
{
|
||||
case EAntiRecoilMode::ARM_None:
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_Buffer:
|
||||
if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_LinearExtrapolation:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN >= 2)
|
||||
{
|
||||
PredictLinearExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
}
|
||||
else if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_WeightedRegression:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN >= 2)
|
||||
{
|
||||
PredictWeightedRegression(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
}
|
||||
else if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_KalmanFilter:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN > 0)
|
||||
{
|
||||
// Feed only the latest SAFE sample to the Kalman filter
|
||||
const FTimestampedTransform& LatestSafe = TransformHistory[SafeN - 1];
|
||||
UpdateKalmanFilter(LatestSafe.Timestamp, LatestSafe.Location, LatestSafe.Aim);
|
||||
PredictKalmanFilter(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
}
|
||||
else if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Linear Extrapolation ---
|
||||
// Computes average velocity from ALL safe samples (consecutive differences),
|
||||
// then extrapolates from the last safe sample to current time.
|
||||
|
||||
void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN < 2)
|
||||
{
|
||||
OutLocation = TransformHistory[0].Location;
|
||||
OutAim = TransformHistory[0].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute average velocity from consecutive safe sample differences
|
||||
FVector AvgLinearVelocity = FVector::ZeroVector;
|
||||
FVector AvgAimDelta = FVector::ZeroVector;
|
||||
double TotalDt = 0.0;
|
||||
int32 ValidPairs = 0;
|
||||
|
||||
for (int32 i = 1; i < SafeN; i++)
|
||||
{
|
||||
double dt = TransformHistory[i].Timestamp - TransformHistory[i - 1].Timestamp;
|
||||
if (dt > SMALL_NUMBER)
|
||||
{
|
||||
AvgLinearVelocity += (TransformHistory[i].Location - TransformHistory[i - 1].Location) / dt;
|
||||
AvgAimDelta += (TransformHistory[i].Aim - TransformHistory[i - 1].Aim) / dt;
|
||||
TotalDt += dt;
|
||||
ValidPairs++;
|
||||
}
|
||||
}
|
||||
|
||||
if (ValidPairs == 0)
|
||||
{
|
||||
OutLocation = TransformHistory[SafeN - 1].Location;
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
AvgLinearVelocity /= (double)ValidPairs;
|
||||
AvgAimDelta /= (double)ValidPairs;
|
||||
|
||||
// Extrapolate from the last SAFE sample to current time
|
||||
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
|
||||
double ExtrapolationTime = CurrentTime - LastSafe.Timestamp;
|
||||
|
||||
OutLocation = LastSafe.Location + AvgLinearVelocity * ExtrapolationTime;
|
||||
|
||||
// Angular extrapolation using quaternion slerp
|
||||
// Use first and last safe samples for rotation direction
|
||||
const FTimestampedTransform& FirstSafe = TransformHistory[0];
|
||||
double SafeDeltaT = LastSafe.Timestamp - FirstSafe.Timestamp;
|
||||
|
||||
if (SafeDeltaT > SMALL_NUMBER)
|
||||
{
|
||||
FQuat FirstQuat = FRotationMatrix::MakeFromX(FirstSafe.Aim).ToQuat();
|
||||
FQuat LastQuat = FRotationMatrix::MakeFromX(LastSafe.Aim).ToQuat();
|
||||
|
||||
double TotalAlpha = ExtrapolationTime / SafeDeltaT;
|
||||
FQuat PredictedQuat = FQuat::Slerp(FirstQuat, LastQuat, 1.0 + TotalAlpha);
|
||||
|
||||
OutAim = PredictedQuat.GetForwardVector().GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
OutAim = LastSafe.Aim;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OutAim = LastSafe.Aim;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Weighted Linear Regression ---
|
||||
// Fits a weighted least-squares line through only the SAFE samples, extrapolates to current time.
|
||||
// More recent safe samples get higher weight.
|
||||
|
||||
void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN < 2)
|
||||
{
|
||||
OutLocation = TransformHistory[0].Location;
|
||||
OutAim = TransformHistory[0].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use timestamps relative to the first safe sample to avoid precision issues
|
||||
double T0 = TransformHistory[0].Timestamp;
|
||||
|
||||
// Weighted linear regression: y = a + b * t
|
||||
// Weights: linearly increasing (1, 2, 3, ..., SafeN)
|
||||
double SumW = 0.0;
|
||||
double SumWT = 0.0;
|
||||
double SumWTT = 0.0;
|
||||
FVector SumWY = FVector::ZeroVector;
|
||||
FVector SumWTY = FVector::ZeroVector;
|
||||
FVector SumWAim = FVector::ZeroVector;
|
||||
FVector SumWTAim = FVector::ZeroVector;
|
||||
|
||||
for (int32 i = 0; i < SafeN; i++)
|
||||
{
|
||||
double w = FMath::Pow((double)(i + 1), (double)RegressionWeightExponent); // Weight curve controlled by exponent
|
||||
double t = TransformHistory[i].Timestamp - T0;
|
||||
|
||||
SumW += w;
|
||||
SumWT += w * t;
|
||||
SumWTT += w * t * t;
|
||||
SumWY += TransformHistory[i].Location * w;
|
||||
SumWTY += TransformHistory[i].Location * (w * t);
|
||||
SumWAim += TransformHistory[i].Aim * w;
|
||||
SumWTAim += TransformHistory[i].Aim * (w * t);
|
||||
}
|
||||
|
||||
double Det = SumW * SumWTT - SumWT * SumWT;
|
||||
if (FMath::Abs(Det) <= SMALL_NUMBER)
|
||||
{
|
||||
OutLocation = TransformHistory[SafeN - 1].Location;
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
// Solve for position: intercept (a) and slope (b)
|
||||
FVector PosIntercept = (SumWY * SumWTT - SumWTY * SumWT) / Det;
|
||||
FVector PosSlope = (SumWTY * SumW - SumWY * SumWT) / Det;
|
||||
|
||||
// Solve for aim: intercept (a) and slope (b)
|
||||
FVector AimIntercept = (SumWAim * SumWTT - SumWTAim * SumWT) / Det;
|
||||
FVector AimSlope = (SumWTAim * SumW - SumWAim * SumWT) / Det;
|
||||
|
||||
// Extrapolate to current time
|
||||
double TPred = CurrentTime - T0;
|
||||
OutLocation = PosIntercept + PosSlope * TPred;
|
||||
|
||||
FVector PredAim = AimIntercept + AimSlope * TPred;
|
||||
OutAim = PredAim.GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Simplified Kalman Filter ---
|
||||
// Maintains state estimate [position, velocity, aim, angular_velocity]
|
||||
// Only fed with SAFE (non-contaminated) measurements, predicts forward to current time
|
||||
|
||||
void UEBBarrel::UpdateKalmanFilter(double CurrentTime, const FVector& MeasuredLocation, const FVector& MeasuredAim)
|
||||
{
|
||||
if (!KalmanInitialized)
|
||||
{
|
||||
KalmanPosition = MeasuredLocation;
|
||||
KalmanVelocity = FVector::ZeroVector;
|
||||
KalmanAim = MeasuredAim;
|
||||
KalmanAngularVelocity = FVector::ZeroVector;
|
||||
KalmanPosVariance = 1.0f;
|
||||
KalmanVelVariance = 1.0f;
|
||||
KalmanAimVariance = 1.0f;
|
||||
KalmanAngVelVariance = 1.0f;
|
||||
KalmanInitialized = true;
|
||||
KalmanLastTimestamp = CurrentTime;
|
||||
return;
|
||||
}
|
||||
|
||||
float dt = (float)(CurrentTime - KalmanLastTimestamp);
|
||||
if (dt <= SMALL_NUMBER)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KalmanLastTimestamp = CurrentTime;
|
||||
|
||||
float Q = KalmanProcessNoise;
|
||||
float R = KalmanMeasurementNoise;
|
||||
|
||||
// --- Position / Velocity ---
|
||||
// Predict step
|
||||
FVector PredPos = KalmanPosition + KalmanVelocity * dt;
|
||||
FVector PredVel = KalmanVelocity;
|
||||
float PredPosVar = KalmanPosVariance + KalmanVelVariance * dt * dt + Q * dt;
|
||||
float PredVelVar = KalmanVelVariance + Q * dt;
|
||||
|
||||
// Update step (measurement = MeasuredLocation)
|
||||
float KGainPos = PredPosVar / (PredPosVar + R);
|
||||
KalmanPosition = PredPos + (MeasuredLocation - PredPos) * KGainPos;
|
||||
KalmanPosVariance = (1.0f - KGainPos) * PredPosVar;
|
||||
|
||||
// Update velocity estimate from innovation
|
||||
FVector VelInnovation = (MeasuredLocation - PredPos) / dt;
|
||||
float KGainVel = PredVelVar / (PredVelVar + R / (dt * dt));
|
||||
KalmanVelocity = PredVel + VelInnovation * KGainVel;
|
||||
KalmanVelVariance = (1.0f - KGainVel) * PredVelVar;
|
||||
|
||||
// --- Aim / Angular Velocity ---
|
||||
FVector PredAim = KalmanAim + KalmanAngularVelocity * dt;
|
||||
FVector PredAngVel = KalmanAngularVelocity;
|
||||
float PredAimVar = KalmanAimVariance + KalmanAngVelVariance * dt * dt + Q * dt;
|
||||
float PredAngVelVar = KalmanAngVelVariance + Q * dt;
|
||||
|
||||
float KGainAim = PredAimVar / (PredAimVar + R);
|
||||
KalmanAim = PredAim + (MeasuredAim - PredAim) * KGainAim;
|
||||
KalmanAimVariance = (1.0f - KGainAim) * PredAimVar;
|
||||
|
||||
FVector AngVelInnovation = (MeasuredAim - PredAim) / dt;
|
||||
float KGainAngVel = PredAngVelVar / (PredAngVelVar + R / (dt * dt));
|
||||
KalmanAngularVelocity = PredAngVel + AngVelInnovation * KGainAngVel;
|
||||
KalmanAngVelVariance = (1.0f - KGainAngVel) * PredAngVelVar;
|
||||
}
|
||||
|
||||
void UEBBarrel::PredictKalmanFilter(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
if (!KalmanInitialized)
|
||||
{
|
||||
OutLocation = GetComponentTransform().GetLocation();
|
||||
OutAim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extrapolate from Kalman state to current time
|
||||
float dt = (float)(CurrentTime - KalmanLastTimestamp);
|
||||
|
||||
OutLocation = KalmanPosition + KalmanVelocity * dt;
|
||||
|
||||
FVector PredAim = KalmanAim + KalmanAngularVelocity * dt;
|
||||
OutAim = PredAim.GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
OutAim = KalmanAim.GetSafeNormal();
|
||||
}
|
||||
}
|
||||
@@ -1,317 +0,0 @@
|
||||
// Copyright 2018 Mookie. All Rights Reserved.
|
||||
#include "EBBarrel.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
|
||||
UEBBarrel::UEBBarrel() {
|
||||
PrimaryComponentTick.bCanEverTick = true;
|
||||
bHiddenInGame = true;
|
||||
bAutoActivate = true;
|
||||
SetIsReplicatedByDefault(ReplicateVariables);
|
||||
|
||||
RandomStream.GenerateNewSeed();
|
||||
|
||||
GatlingRPS = FireRateMin;
|
||||
}
|
||||
|
||||
void UEBBarrel::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
|
||||
{
|
||||
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
|
||||
|
||||
// Update anti-recoil transform history
|
||||
UpdateTransformHistory();
|
||||
|
||||
if (ClientSideAim){
|
||||
if (GetOwner()->GetRemoteRole()==ROLE_Authority){
|
||||
TimeSinceAimUpdate += DeltaTime;
|
||||
if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) {
|
||||
|
||||
ComputeAntiRecoilTransform();
|
||||
|
||||
ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim);
|
||||
TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency);
|
||||
};
|
||||
}else{
|
||||
if (!RemoteAimReceived) {
|
||||
ComputeAntiRecoilTransform();
|
||||
}
|
||||
else {
|
||||
FVector LocOffset = (Location - GetComponentLocation());
|
||||
if (LocOffset.Size() > ClientAimDistanceLimit) {
|
||||
//lag or cheater???
|
||||
Location = GetComponentLocation() + LocOffset.GetSafeNormal()*ClientAimDistanceLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
ComputeAntiRecoilTransform();
|
||||
}
|
||||
|
||||
// Debug visualization: raw tracker (green) vs predicted aim (red)
|
||||
if (DebugAntiRecoil)
|
||||
{
|
||||
// Use the latest buffer entry as "raw" data (includes simulated shock if active)
|
||||
FVector RawLocation;
|
||||
FVector RawAim;
|
||||
if (TransformHistory.Num() > 0)
|
||||
{
|
||||
RawLocation = TransformHistory.Last().Location;
|
||||
RawAim = TransformHistory.Last().Aim;
|
||||
}
|
||||
else
|
||||
{
|
||||
RawLocation = GetComponentTransform().GetLocation();
|
||||
RawAim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
}
|
||||
|
||||
// Green line: raw tracker data (potentially shocked if IMU simulation is active)
|
||||
DrawDebugLine(GetWorld(), RawLocation, RawLocation + RawAim * DebugAntiRecoilLineLength,
|
||||
FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
|
||||
// Red line: anti-recoil predicted aim (what would be used if shooting now)
|
||||
DrawDebugLine(GetWorld(), Location, Location + Aim * DebugAntiRecoilLineLength,
|
||||
FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
|
||||
// Small spheres at origins for clarity
|
||||
DrawDebugSphere(GetWorld(), RawLocation, 1.5f, 6, FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f);
|
||||
DrawDebugSphere(GetWorld(), Location, 1.5f, 6, FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f);
|
||||
|
||||
// Yellow line: shows where shot would land WITHOUT anti-recoil correction
|
||||
// Captures raw aim at shock onset and persists for DebugIMUShockDisplayTime seconds
|
||||
if (DebugIMUShockActive && !DebugIMUShockLineCaptured)
|
||||
{
|
||||
// Capture the raw (shocked) aim at the first frame of shock
|
||||
DebugIMUShockCapturedLocation = RawLocation;
|
||||
DebugIMUShockCapturedAim = RawAim;
|
||||
DebugIMUShockLineCaptured = true;
|
||||
DebugIMUShockLineEndTime = GetWorld()->GetTimeSeconds() + DebugIMUShockDisplayTime;
|
||||
}
|
||||
if (!DebugIMUShockActive && DebugIMUShockLineCaptured)
|
||||
{
|
||||
// Shock ended: keep displaying but update captured aim to worst-case (peak shock)
|
||||
// which was already captured at onset
|
||||
}
|
||||
if (DebugIMUShockLineCaptured)
|
||||
{
|
||||
if (GetWorld()->GetTimeSeconds() < DebugIMUShockLineEndTime)
|
||||
{
|
||||
// Yellow line: uncorrected aim (where the bullet would have gone without anti-recoil)
|
||||
DrawDebugLine(GetWorld(), DebugIMUShockCapturedLocation,
|
||||
DebugIMUShockCapturedLocation + DebugIMUShockCapturedAim * DebugAntiRecoilLineLength,
|
||||
FColor::Yellow, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
DrawDebugSphere(GetWorld(), DebugIMUShockCapturedLocation, 3.0f, 8, FColor::Yellow, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugIMUShockLineCaptured = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Only server can tick
|
||||
if (GetOwner()->GetLocalRole() == ROLE_Authority){
|
||||
|
||||
float RemainingDelta;
|
||||
|
||||
if (FireMode == EFireMode::FM_Gatling) {
|
||||
if (Spooling || (GatlingAutoSpool && Shooting)) {
|
||||
GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMax, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f));
|
||||
}
|
||||
else {
|
||||
GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMin, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f));
|
||||
}
|
||||
GatlingPhase += GatlingRPS*DeltaTime;
|
||||
for (int i = 1; i <= GatlingPhase; i++) {
|
||||
if (Cooldown <= 0.0f && LoadNext) {
|
||||
NextBullet();
|
||||
}
|
||||
|
||||
if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) {
|
||||
SpawnBullet(GetOwner(), Location, Aim, nextFireEventID);
|
||||
}
|
||||
}
|
||||
GatlingPhase = FMath::Fmod(GatlingPhase, 1.0f);
|
||||
|
||||
}
|
||||
else {
|
||||
RemainingDelta = DeltaTime;
|
||||
do {
|
||||
float step = FMath::Min(Cooldown, RemainingDelta);
|
||||
|
||||
Cooldown -= step;
|
||||
|
||||
RemainingDelta -= step;
|
||||
|
||||
if (Cooldown <= 0.0f && LoadNext) {
|
||||
NextBullet();
|
||||
}
|
||||
|
||||
//shoot when ready
|
||||
if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) {
|
||||
if (BurstRemaining > 0 || (FireMode != EFireMode::FM_Burst && FireMode != EFireMode::FM_InterBurst)) {
|
||||
SpawnBullet(GetOwner(), Location, Aim, nextFireEventID);
|
||||
}
|
||||
else {
|
||||
Shooting = false;
|
||||
}
|
||||
}
|
||||
} while (RemainingDelta > 0 && Cooldown > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::NextBullet() {
|
||||
if (ChamberedBullet == nullptr) {
|
||||
if (Ammo.Num() > 0 && (CycleAmmoCount > 0 || CycleAmmoUnlimited || (!CycleAmmo))) {
|
||||
|
||||
//cycle ammo
|
||||
if (CycleAmmo) {
|
||||
if (CycleAmmoPos >= Ammo.Num()) { CycleAmmoPos = 0; }
|
||||
ChamberedBullet = Ammo[CycleAmmoPos];
|
||||
CycleAmmoPos++;
|
||||
|
||||
if (!CycleAmmoUnlimited) {
|
||||
CycleAmmoCount--;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ChamberedBullet = Ammo[0];
|
||||
Ammo.RemoveAt(0, 1, EAllowShrinking::Yes);
|
||||
}
|
||||
|
||||
ReadyToShoot.Broadcast();
|
||||
}
|
||||
else {
|
||||
AmmoDepleted.Broadcast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim, int fireEventID) {
|
||||
TSubclassOf<class AEBBullet> BulletClass = ChamberedBullet;
|
||||
|
||||
if (BulletClass != nullptr) {
|
||||
FVector OutLocation;
|
||||
FVector OutAim;
|
||||
|
||||
InitialBulletTransform(InLocation, InAim, OutLocation, OutAim);
|
||||
|
||||
AEBBullet* Default = Cast<AEBBullet>(BulletClass->GetDefaultObject());
|
||||
|
||||
float BulletSpread = Default->Spread;
|
||||
if (Default->SpreadBias > 0.0f) {
|
||||
float SpreadMult = FMath::Pow(FMath::FRand(), Default->SpreadBias);
|
||||
BulletSpread *= SpreadMult;
|
||||
}
|
||||
float BarrelSpread = Spread;
|
||||
if (SpreadBias > 0.0f) {
|
||||
float SpreadMult = FMath::Pow(FMath::FRand(), SpreadBias);
|
||||
BarrelSpread *= SpreadMult;
|
||||
}
|
||||
|
||||
float TotalSpread = BulletSpread+BarrelSpread;
|
||||
|
||||
OutAim = RandomStream.VRandCone(OutAim,TotalSpread);
|
||||
float BulletVelocity = FMath::Lerp(MuzzleVelocityMultiplierMin* Default->MuzzleVelocityMin, MuzzleVelocityMultiplierMax*Default->MuzzleVelocityMax, RandomStream.FRand());
|
||||
FVector Velocity = OutAim*BulletVelocity;
|
||||
|
||||
//get parent physics body
|
||||
UPrimitiveComponent* parent = Cast<UPrimitiveComponent>(GetAttachParent());
|
||||
Velocity += AdditionalVelocity;
|
||||
|
||||
if (parent != nullptr) {
|
||||
|
||||
if (parent->IsSimulatingPhysics()) {
|
||||
Velocity += parent->GetPhysicsLinearVelocityAtPoint(OutLocation)*InheritVelocity;
|
||||
}
|
||||
|
||||
if (Default->Shotgun) {
|
||||
ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier*Default->ShotCount);
|
||||
}
|
||||
else{
|
||||
ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
BeforeShotFired.Broadcast();
|
||||
#ifdef WITH_EDITOR
|
||||
if (shotTrace) {
|
||||
DrawDebugLine(GetWorld(), OutLocation, OutLocation + Velocity, FColor::Red, false, 3, 0, 0);
|
||||
};
|
||||
#endif
|
||||
if (ReplicateShotFiredEvents) {
|
||||
SpawnBulletEventMulticast(OutLocation, Velocity);
|
||||
}
|
||||
else {
|
||||
SpawnBulletEvent.Broadcast(OutLocation, Velocity);
|
||||
}
|
||||
|
||||
AEBBullet::SpawnWithExactVelocity(BulletClass, Owner, Owner->GetInstigator(), OutLocation, Velocity, fireEventID);
|
||||
|
||||
//spend ammo
|
||||
ChamberedBullet = nullptr;
|
||||
if (FireMode != EFireMode::FM_Gatling) {
|
||||
Cooldown = 1.0f / FMath::Lerp(FireRateMin, FireRateMax, RandomStream.FRand());
|
||||
}
|
||||
|
||||
//fire modes
|
||||
switch (FireMode) {
|
||||
case EFireMode::FM_Auto:
|
||||
LoadNext = true;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Burst:
|
||||
LoadNext = true;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_InterBurst:
|
||||
LoadNext = true;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Semiauto:
|
||||
Shooting = false;
|
||||
LoadNext = true;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Manual:
|
||||
Shooting = false;
|
||||
LoadNext = false;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Slamfire:
|
||||
LoadNext = false;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Gatling:
|
||||
LoadNext = true;
|
||||
break;
|
||||
};
|
||||
|
||||
if (BurstRemaining > 0) {
|
||||
BurstRemaining--;
|
||||
}
|
||||
else {
|
||||
if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) {
|
||||
Cooldown = FMath::Max(Cooldown, BurstCooldown);
|
||||
}
|
||||
}
|
||||
|
||||
if (ReplicateShotFiredEvents) {
|
||||
ShotFiredMulticast();
|
||||
}
|
||||
else {
|
||||
ShotFired.Broadcast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::InitialBulletTransform_Implementation(FVector InLocation, FVector InDirection, FVector& OutLocation, FVector& OutDirection) {
|
||||
OutLocation = InLocation;
|
||||
OutDirection = InDirection;
|
||||
}
|
||||
|
||||
void UEBBarrel::ApplyRecoil_Implementation(UPrimitiveComponent* Component, FVector InLocation, FVector Impulse){
|
||||
if (Component->IsSimulatingPhysics()) {
|
||||
Component->AddImpulseAtLocation(Impulse, InLocation);
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
// Copyright 2016 Mookie. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Curves/CurveFloat.h"
|
||||
#include "Kismet/KismetMathLibrary.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Components/PrimitiveComponent.h"
|
||||
|
||||
#include "EBMaterialResponseMap.h"
|
||||
|
||||
#include "EBBullet.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EEBAtmosphereType : uint8
|
||||
{
|
||||
AT_Constant UMETA(DisplayName = "Constant"),
|
||||
AT_Curve UMETA(DisplayName = "Density Curve"),
|
||||
AT_Earth UMETA(DisplayName = "Earth/IGL")
|
||||
};
|
||||
|
||||
UCLASS(Blueprintable, BlueprintType)
|
||||
class EASYBALLISTICS_API AEBBullet : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Sets default values for this actor's properties
|
||||
AEBBullet();
|
||||
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "State") FVector Velocity;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "State") FRandomStream RandomStream;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "State") bool OwnerSafe=false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "PROSERVE") int fireEventID;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") bool DebugEnabled;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") float DebugTrailTime=1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") float DebugTrailWidth=0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") FLinearColor DebugTrailColorFast = FLinearColor(0, 1, 0, 1);
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") FLinearColor DebugTrailColorSlow = FLinearColor(1, 0, 0, 1);
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") bool DebugPooling;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World") FVector Wind;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Select atmosphere model")) EEBAtmosphereType AtmosphereType;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "Air Density at sea level - in KG/m^3", ClampMin = "0")) float SeaLevelAirDensity = 1.21;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "in cm/s", ClampMin = "0")) float SeaLevelSpeedOfSound = 34300;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "Used for Density Curve atmosphere model")) UCurveFloat* AirDensityCurve;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World") bool SpeedOfSoundVariesWithAltitude = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World") UCurveFloat* SpeedOfSoundCurve;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World") float WorldScale = 1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Atmosphere pressure at 0,0,0 - in millibars", ClampMin = "0")) float SeaLevelAirPressure = 1012.5f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Atmosphere Temperature at 0,0,0 - in degrees C")) float SeaLevelAirTemperature = 20.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Temperature Decrease With Altitude, degrees per meter")) float TemperatureLapseRate = 0.00649f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Altitude at which temperature stops decreasing, in meters")) float TropopauseAltitude = 11000.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Specific Gas Constant, dry air = 287.058", ClampMin = "0")) float SpecificGasConstant = 287.058;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "World Origin Location")) FVector WorldCenterLocation = FVector(0, 0, 0);
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Use spherical planet model to get altitude")) bool SphericalAltitude = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Planet radius, in Unreal units", EditCondition = "SphericalAltitude", ClampMin = "0")) float SeaLevelRadius = 637100000.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World") bool OverrideGravity = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World") FVector Gravity = FVector(0,0,-980);
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch") bool SafeLaunch = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", Meta = (EditCondition = "SafeLaunch")) bool SafeLaunchIgnoreAttachParent = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", Meta = (EditCondition = "SafeLaunchIgnoreAttachParent")) bool SafeLaunchIgnoreAllAttached = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", Meta = (EditCondition = "SafeLaunch", ClampMin = "0")) float SafeDelay = 1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", Meta = (EditCondition = "SafeLaunch")) TArray<AActor*> SafeLaunchIgnoredActors;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun") bool Shotgun=false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (EditCondition = "Shotgun")) int ShotCount=10;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (EditCondition = "Shotgun")) float ShotSpread=0.01;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (EditCondition = "Shotgun")) float ShotVelocitySpread = 0.01;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight") float MuzzleVelocityMin = 100000.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight") float MuzzleVelocityMax = 100000.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Maximum bullet spread, in radians", ClampMin = "0")) float Spread = 0.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Spread bias, higher is more accurate on average", ClampMin = "0")) float SpreadBias = 0.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight") float Mass = 0.005;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight") float Diameter = 0.556;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight") float FormFactor = 1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight") UCurveFloat* MachDragCurve;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float GrazingAngleExponent = 2.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float MinPenetration = 10.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float MaxPenetration = 20.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float PenetrationNormalization = 0.5;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float PenetrationNormalizationGrazing = 0.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float PenetrationEntryAngleSpread = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float PenetrationExitAngleSpread = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float RicochetProbability = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float RicochetProbabilityGrazing = 1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float RicochetRestitution = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float RicochetFriction = 0.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float RicochetSpread = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") bool SpeedControlsRicochetProbability = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") bool AddImpulse = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") float ImpulseMultiplier = 1.0;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") EPenTraceType DefaultPenTraceType = EPenTraceType::PT_BackTrace;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") UEBMaterialResponseMap* MaterialResponseMap;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") bool MaterialDensityControlsPenetrationDepth = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact") bool MaterialRestitutionControlsRicochet = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Replication") bool ReliableReplication = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Allow components to collide, intended for use with trigger volumes. Do not use for actual collisions.")) bool AllowComponentCollisions = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision") TEnumAsByte<ECollisionChannel> TraceChannel;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision") bool TraceComplex;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision") float CollisionMargin=1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Bullets with lower velocity will automatically despawn on impact, never despawn if set to zero or negative")) float DespawnVelocity=100.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision") TArray<AActor*> IgnoredActors;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (ToolTip = "Spawned bullet performs first trace immediately, instead of waiting for next simulation step")) bool DoFirstStepImmediately = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (EditCondition = "DoFirstStepImmediately")) bool RandomFirstStepDelta = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation") bool FixedStep = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (EditCondition = "FixedStep", ClampMin = "0")) float FixedStepSeconds = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation") int MaxTracesPerStep = 8;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Retrace") bool Retrace = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Retrace") bool RetraceOnAnotherChannel = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Retrace", meta=(EditCondition="RetraceOnAnotherChannel")) TEnumAsByte<ECollisionChannel> RetraceChannel;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Rotation") bool RotateActor = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Rotation") bool RotateRandomRoll = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Pooling") bool EnablePooling = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Pooling", meta = (EditCondition = "EnablePooling")) int MaxPoolSize = 50;
|
||||
|
||||
//rebase
|
||||
virtual void ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) override;
|
||||
|
||||
// Called when the game starts or when spawned
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
// Called every frame
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
virtual void LifeSpanExpired() override;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "EBBullet|Spawn")
|
||||
static void SpawnWithExactVelocity(TSubclassOf<class AEBBullet> BulletClass, AActor* BulletOwner, APawn* BulletInstigator, FVector BulletLocation, FVector BulletVelocity, int EventFireID);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "EBBullet|Spawn")
|
||||
static void Spawn(TSubclassOf<class AEBBullet> BulletClass, AActor* BulletOwner, APawn* BulletInstigator, FVector BulletLocation, FVector BulletVelocity, int EventFireID);
|
||||
|
||||
UFUNCTION(NetMulticast, Unreliable)
|
||||
void VelocityChangeBroadcast(FVector_NetQuantize NewLocation, FVector NewVelocity);
|
||||
UFUNCTION(NetMulticast, Reliable)
|
||||
void VelocityChangeBroadcastReliable(FVector_NetQuantize NewLocation, FVector NewVelocity);
|
||||
|
||||
UFUNCTION(BlueprintAuthorityOnly, BlueprintNativeEvent, Category = "EBBullet|Impact")
|
||||
void OnImpact(bool Ricochet, bool PassedThrough, FVector Location, FVector IncomingVelocity, FVector Normal, FVector ExitLocation, FVector ExitVelocity, FVector Impulse, float PenetrationDepth, AActor* Actor, USceneComponent* Component, FName BoneName, UPhysicalMaterial* PhysMaterial, FHitResult HitResult, int EventFireID);
|
||||
|
||||
UFUNCTION(BlueprintCosmetic, BlueprintNativeEvent, Category = "EBBullet|Impact")
|
||||
void OnNetPredictedImpact(bool Ricochet, bool PassedThrough, FVector Location, FVector IncomingVelocity, FVector Normal, FVector ExitLocation, FVector ExitVelocity, FVector Impulse, float PenetrationDepth, AActor* Actor, USceneComponent* Component, FName BoneName, UPhysicalMaterial* PhysMaterial, FHitResult HitResult, int EventFireID);
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "EBBullet|Impact")
|
||||
void OnTrace(FVector StartLocation, FVector EndLocation);
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "EBBullet|Remote")
|
||||
void OnTrajectoryUpdateReceived(FVector Location, FVector OldVelocity, FVector NewVelocity);
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|Activation")
|
||||
void OnDeactivated();
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|Flight")FVector UpdateVelocity(UWorld* World, FVector Location, FVector PreviousVelocity, float DeltaTime) const;
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|World") FVector GetWind(UWorld* World, FVector Location) const;
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|World") float GetAirDensity(UWorld* World, FVector Location) const;
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|World") float GetSpeedOfSound(UWorld* World, FVector Location) const;
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|World") bool CollisionFilter(FHitResult HitResult) const;
|
||||
|
||||
//pooling
|
||||
UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category = "EBBullet|Pooling")void Deactivate();
|
||||
|
||||
UFUNCTION(NetMulticast, Reliable)
|
||||
void ReactivationBroadcast(FVector_NetQuantize NewLocation, FVector NewVelocity, AActor* BulletOwner, APawn* BulletInstigator, int nextFireEventID);
|
||||
UFUNCTION(NetMulticast, Reliable)
|
||||
void DeactivationBroadcast();
|
||||
private:
|
||||
UPROPERTY() TArray<TWeakObjectPtr<AEBBullet>> Pooled;
|
||||
static AEBBullet* GetFromPool(UWorld* World, UClass* BulletClass);
|
||||
static AEBBullet* SpawnOrReactivate(UWorld* World, TSubclassOf<class AEBBullet> BulletClass, const FTransform& Transform, FVector BulletVelocity, AActor* BulletOwner, APawn* BulletInstigator, int nextFireEventID);
|
||||
void DeactivateToPool();
|
||||
|
||||
void FinishSpawning(FTransform Transform);
|
||||
|
||||
void Step(float DeltaTime);
|
||||
|
||||
float Trace(FVector start, FVector PreviousVelocity, float delta, TEnumAsByte<ECollisionChannel> channel);
|
||||
|
||||
TArray<AActor*> GetAttachedActorsRecursive(AActor* Actor, uint16 Depth = 0, TArray<AActor*> VisitedActors = TArray<AActor*>()) const;
|
||||
|
||||
float PenetrationTrace(FVector start, FVector end, TWeakObjectPtr<UPrimitiveComponent,FWeakObjectPtr> comp, EPenTraceType penType, TEnumAsByte<ECollisionChannel> channel, FVector &exitLoc, FVector &exitNormal);
|
||||
|
||||
float GetCurveValue(const UCurveFloat* curve, float in, float deflt) const;
|
||||
|
||||
float AccumulatedDelta;
|
||||
|
||||
bool CanRetrace = false;
|
||||
FVector LastTraceStart;
|
||||
float LastTraceDelta;
|
||||
FVector LastTraceVelocity;
|
||||
FVector LastTracePrevVelocity;
|
||||
bool LastTraceOwnerSafe = false;
|
||||
|
||||
bool IsRecycled;
|
||||
|
||||
FHitResult FilterHits(TArray<FHitResult> Results, bool &hit) const;
|
||||
TArray<AActor*>GetSafeLaunchIgnoredActors(AActor* Owner) const;
|
||||
|
||||
float GetAltitude(UWorld* World, FVector Location) const;
|
||||
float GetAltitudePressure(float AltitudeMeter) const;
|
||||
float GetAltitudeTemperature(float AltitudeMeter) const;
|
||||
float GetAltitudeDensity(float AltitudeMeter) const;
|
||||
|
||||
#ifdef WITH_EDITOR
|
||||
FLinearColor GetDebugColor(float In) const{
|
||||
return FMath::Lerp(DebugTrailColorSlow, DebugTrailColorFast, In);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
@@ -1,45 +0,0 @@
|
||||
// Copyright 2016 Mookie. All Rights Reserved.
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "PhysicalMaterials/PhysicalMaterial.h"
|
||||
#include "EBMaterialResponseMap.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EPenTraceType : uint8
|
||||
{
|
||||
PT_BackTrace UMETA(DisplayName = "Back Trace"),
|
||||
PT_ByComponent UMETA(DisplayName = "By Component"),
|
||||
PT_TwoSidedGeometry UMETA(DisplayName = "Double Sided Geometry"),
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FEBMaterialResponseMapEntry {
|
||||
GENERATED_USTRUCT_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Material") EPenTraceType PenTraceType = EPenTraceType::PT_BackTrace;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") bool NeverPenetrate = false;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float PenetrationDepthMultiplier = 1.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float PenetrationNormalization = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float PenetrationNormalizationGrazing = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float PenetrationEntryAngleSpread = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float PenetrationExitAngleSpread = 0.0;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") bool NeverRicochet = false;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float RicochetProbabilityMultiplier = 1.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float RicochetRestitution = 0.5f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float RicochetRestitutionInfluence = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float RicochetFriction = 0.5f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float RicochetFrictionInfluence = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material") float RicochetSpread = 0.0f;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class UEBMaterialResponseMap : public UDataAsset{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, Category = "Responses") TMap<UPhysicalMaterial*, FEBMaterialResponseMapEntry> Map;
|
||||
};
|
||||
@@ -10,9 +10,8 @@
|
||||
"DocsURL": "",
|
||||
"MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/bbecde0f66914263b57fd2af5a0c7ffe",
|
||||
"SupportURL": "",
|
||||
"EngineVersion": "5.5.0",
|
||||
"CanContainContent": false,
|
||||
"Installed": true,
|
||||
"Installed": false,
|
||||
"Modules": [
|
||||
{
|
||||
"Name": "EasyBallistics",
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
@@ -0,0 +1,840 @@
|
||||
// Anti-Recoil Prediction Methods for EBBarrel
|
||||
// Provides linear extrapolation, weighted regression, and Kalman filter prediction
|
||||
//
|
||||
// Key concept: The buffer stores samples within AntiRecoilBufferTime seconds.
|
||||
// Samples within the last AntiRecoilDiscardTime seconds may be contaminated by recoil shock.
|
||||
// All prediction algorithms work ONLY on the "safe" (oldest) portion of the buffer
|
||||
// and extrapolate forward to the current time.
|
||||
|
||||
#include "EBBarrel.h"
|
||||
|
||||
// Returns the number of safe (non-contaminated) samples based on a time threshold.
|
||||
// Samples whose timestamp >= (CurrentTime - DiscardTime) are considered potentially contaminated.
|
||||
static int32 GetSafeCount(const TArray<FTimestampedTransform>& History, double CurrentTime, float DiscardTime)
|
||||
{
|
||||
if (History.Num() == 0) return 0;
|
||||
double SafeCutoff = CurrentTime - FMath::Max(0.0f, DiscardTime);
|
||||
int32 SafeN = 0;
|
||||
for (int32 i = 0; i < History.Num(); i++)
|
||||
{
|
||||
if (History[i].Timestamp < SafeCutoff)
|
||||
{
|
||||
SafeN = i + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return SafeN;
|
||||
}
|
||||
|
||||
void UEBBarrel::TriggerDebugIMUShock()
|
||||
{
|
||||
if (!DebugSimulateIMUShock) return;
|
||||
|
||||
DebugIMUShockActive = true;
|
||||
DebugIMUShockLineCaptured = false; // Reset so the new shock replaces the previous yellow line
|
||||
DebugIMUShockStartTime = GetWorld()->GetTimeSeconds();
|
||||
|
||||
// Generate a random shock direction (sharp upward + random lateral, simulating recoil kick)
|
||||
FVector RandomDir = FMath::VRand();
|
||||
// Bias upward to simulate typical recoil pattern
|
||||
RandomDir.Z = FMath::Abs(RandomDir.Z) * 2.0f;
|
||||
RandomDir.Normalize();
|
||||
|
||||
DebugIMUShockAimOffset = RandomDir * FMath::DegreesToRadians(DebugIMUShockAngle);
|
||||
DebugIMUShockPosOffset = RandomDir * DebugIMUShockPosition;
|
||||
|
||||
// Recoil timing: for simulated shocks, the corruption will appear in the buffer
|
||||
// on subsequent ticks. No need to capture here — the continuous analysis in
|
||||
// TickComponent will detect it automatically.
|
||||
}
|
||||
|
||||
void UEBBarrel::UpdateTransformHistory()
|
||||
{
|
||||
if (AntiRecoilMode == EAntiRecoilMode::ARM_None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
double CurrentTime = GetWorld()->GetTimeSeconds();
|
||||
|
||||
FTimestampedTransform Sample;
|
||||
Sample.Timestamp = CurrentTime;
|
||||
Sample.Location = GetComponentTransform().GetLocation();
|
||||
Sample.Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
|
||||
// Apply simulated IMU shock perturbation if active
|
||||
if (DebugSimulateIMUShock && DebugIMUShockActive)
|
||||
{
|
||||
float ElapsedShock = (float)(CurrentTime - DebugIMUShockStartTime);
|
||||
if (ElapsedShock < DebugIMUShockDuration)
|
||||
{
|
||||
// Decaying shock: intensity decreases over the shock duration
|
||||
float ShockAlpha = 1.0f - (ElapsedShock / DebugIMUShockDuration);
|
||||
// Add some high-frequency noise to simulate IMU vibration
|
||||
FVector FrameNoise = FMath::VRand() * 0.3f;
|
||||
|
||||
// Perturb aim direction
|
||||
FVector AimPerturbation = (DebugIMUShockAimOffset + FrameNoise * FMath::DegreesToRadians(DebugIMUShockAngle)) * ShockAlpha;
|
||||
Sample.Aim = (Sample.Aim + AimPerturbation).GetSafeNormal();
|
||||
|
||||
// Perturb position
|
||||
FVector PosPerturbation = (DebugIMUShockPosOffset + FrameNoise * DebugIMUShockPosition) * ShockAlpha;
|
||||
Sample.Location += PosPerturbation;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugIMUShockActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
TransformHistory.Add(Sample);
|
||||
|
||||
// Trim buffer: remove samples older than AntiRecoilBufferTime
|
||||
// During calibration, keep a larger buffer (0.5s min) for reliable 3-sigma analysis
|
||||
float EffectiveBufferTime = CalibrateAntiRecoil
|
||||
? FMath::Max(AntiRecoilBufferTime, 0.5f)
|
||||
: AntiRecoilBufferTime;
|
||||
double OldestAllowed = CurrentTime - FMath::Max(0.05f, EffectiveBufferTime);
|
||||
while (TransformHistory.Num() > 0 && TransformHistory[0].Timestamp < OldestAllowed)
|
||||
{
|
||||
TransformHistory.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::ComputeAntiRecoilTransform()
|
||||
{
|
||||
switch (AntiRecoilMode)
|
||||
{
|
||||
case EAntiRecoilMode::ARM_None:
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_Buffer:
|
||||
if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_LinearExtrapolation:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN >= 2)
|
||||
{
|
||||
PredictLinearExtrapolation(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
}
|
||||
else if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_WeightedRegression:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN >= 2)
|
||||
{
|
||||
PredictWeightedRegression(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
}
|
||||
else if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_WeightedLinearRegression:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN >= 2)
|
||||
{
|
||||
PredictWeightedLinearRegression(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
}
|
||||
else if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_KalmanFilter:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN > 0)
|
||||
{
|
||||
// Feed only the latest SAFE sample to the Kalman filter
|
||||
const FTimestampedTransform& LatestSafe = TransformHistory[SafeN - 1];
|
||||
UpdateKalmanFilter(LatestSafe.Timestamp, LatestSafe.Location, LatestSafe.Aim);
|
||||
PredictKalmanFilter(GetWorld()->GetTimeSeconds(), Location, Aim);
|
||||
}
|
||||
else if (TransformHistory.Num() > 0)
|
||||
{
|
||||
Aim = TransformHistory[0].Aim;
|
||||
Location = TransformHistory[0].Location;
|
||||
}
|
||||
else
|
||||
{
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case EAntiRecoilMode::ARM_AdaptiveExtrapolation:
|
||||
{
|
||||
int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Linear Extrapolation ---
|
||||
// Computes average velocity from ALL safe samples (consecutive differences),
|
||||
// then extrapolates from the last safe sample to current time.
|
||||
|
||||
void UEBBarrel::PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN < 2)
|
||||
{
|
||||
OutLocation = TransformHistory[0].Location;
|
||||
OutAim = TransformHistory[0].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute average velocity from consecutive safe sample differences
|
||||
FVector AvgLinearVelocity = FVector::ZeroVector;
|
||||
FVector AvgAimDelta = FVector::ZeroVector;
|
||||
double TotalDt = 0.0;
|
||||
int32 ValidPairs = 0;
|
||||
|
||||
for (int32 i = 1; i < SafeN; i++)
|
||||
{
|
||||
double dt = TransformHistory[i].Timestamp - TransformHistory[i - 1].Timestamp;
|
||||
if (dt > SMALL_NUMBER)
|
||||
{
|
||||
AvgLinearVelocity += (TransformHistory[i].Location - TransformHistory[i - 1].Location) / dt;
|
||||
AvgAimDelta += (TransformHistory[i].Aim - TransformHistory[i - 1].Aim) / dt;
|
||||
TotalDt += dt;
|
||||
ValidPairs++;
|
||||
}
|
||||
}
|
||||
|
||||
if (ValidPairs == 0)
|
||||
{
|
||||
OutLocation = TransformHistory[SafeN - 1].Location;
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
AvgLinearVelocity /= (double)ValidPairs;
|
||||
AvgAimDelta /= (double)ValidPairs;
|
||||
|
||||
// Extrapolate from the last SAFE sample to current time
|
||||
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
|
||||
double ExtrapolationTime = CurrentTime - LastSafe.Timestamp;
|
||||
|
||||
// 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
|
||||
const FTimestampedTransform& FirstSafe = TransformHistory[0];
|
||||
double SafeDeltaT = LastSafe.Timestamp - FirstSafe.Timestamp;
|
||||
|
||||
if (SafeDeltaT > SMALL_NUMBER)
|
||||
{
|
||||
FQuat FirstQuat = FRotationMatrix::MakeFromX(FirstSafe.Aim).ToQuat();
|
||||
FQuat LastQuat = FRotationMatrix::MakeFromX(LastSafe.Aim).ToQuat();
|
||||
|
||||
double TotalAlpha = ExtrapolationTime / SafeDeltaT * DampingScale;
|
||||
FQuat PredictedQuat = FQuat::Slerp(FirstQuat, LastQuat, 1.0 + TotalAlpha);
|
||||
|
||||
OutAim = PredictedQuat.GetForwardVector().GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
OutAim = LastSafe.Aim;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OutAim = LastSafe.Aim;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Weighted Linear Regression ---
|
||||
// Fits a weighted least-squares line (y = a + bt) through SAFE samples, extrapolates to current time.
|
||||
// Simple and stable. More recent safe samples get higher weight.
|
||||
|
||||
void UEBBarrel::PredictWeightedLinearRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN < 2)
|
||||
{
|
||||
OutLocation = TransformHistory[0].Location;
|
||||
OutAim = TransformHistory[0].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
double T0 = TransformHistory[0].Timestamp;
|
||||
|
||||
double SumW = 0.0;
|
||||
double SumWT = 0.0;
|
||||
double SumWTT = 0.0;
|
||||
FVector SumWY = FVector::ZeroVector;
|
||||
FVector SumWTY = FVector::ZeroVector;
|
||||
FVector SumWAim = FVector::ZeroVector;
|
||||
FVector SumWTAim = FVector::ZeroVector;
|
||||
|
||||
for (int32 i = 0; i < SafeN; i++)
|
||||
{
|
||||
double w = FMath::Pow((double)(i + 1), (double)RegressionWeightExponent);
|
||||
double t = TransformHistory[i].Timestamp - T0;
|
||||
|
||||
SumW += w;
|
||||
SumWT += w * t;
|
||||
SumWTT += w * t * t;
|
||||
SumWY += TransformHistory[i].Location * w;
|
||||
SumWTY += TransformHistory[i].Location * (w * t);
|
||||
SumWAim += TransformHistory[i].Aim * w;
|
||||
SumWTAim += TransformHistory[i].Aim * (w * t);
|
||||
}
|
||||
|
||||
double Det = SumW * SumWTT - SumWT * SumWT;
|
||||
if (FMath::Abs(Det) <= SMALL_NUMBER)
|
||||
{
|
||||
OutLocation = TransformHistory[SafeN - 1].Location;
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
FVector PosIntercept = (SumWY * SumWTT - SumWTY * SumWT) / Det;
|
||||
FVector PosSlope = (SumWTY * SumW - SumWY * SumWT) / Det;
|
||||
FVector AimIntercept = (SumWAim * SumWTT - SumWTAim * SumWT) / Det;
|
||||
FVector AimSlope = (SumWTAim * SumW - SumWAim * SumWT) / Det;
|
||||
|
||||
double TPred = CurrentTime - T0;
|
||||
|
||||
// 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())
|
||||
{
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Weighted Quadratic Regression ---
|
||||
// Fits a weighted least-squares quadratic (y = a + bt + ct^2) through SAFE samples.
|
||||
// Captures deceleration naturally: if user stops before firing, c < 0 curves prediction toward stop.
|
||||
// Falls back to linear fit if < 3 samples or ill-conditioned 3x3 system.
|
||||
|
||||
void UEBBarrel::PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
const int32 SafeN = GetSafeCount(TransformHistory, GetWorld()->GetTimeSeconds(), AntiRecoilDiscardTime);
|
||||
if (SafeN < 2)
|
||||
{
|
||||
OutLocation = TransformHistory[0].Location;
|
||||
OutAim = TransformHistory[0].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
// Use timestamps relative to the first safe sample to avoid precision issues
|
||||
double T0 = TransformHistory[0].Timestamp;
|
||||
|
||||
// Accumulate weighted sums for quadratic regression: y = a + b*t + c*t^2
|
||||
double SumW = 0.0;
|
||||
double SumWT = 0.0;
|
||||
double SumWTT = 0.0;
|
||||
double SumWTTT = 0.0;
|
||||
double SumWTTTT = 0.0;
|
||||
FVector SumWY = FVector::ZeroVector;
|
||||
FVector SumWTY = FVector::ZeroVector;
|
||||
FVector SumWTTY = FVector::ZeroVector;
|
||||
FVector SumWAim = FVector::ZeroVector;
|
||||
FVector SumWTAim = FVector::ZeroVector;
|
||||
FVector SumWTTAim = FVector::ZeroVector;
|
||||
|
||||
for (int32 i = 0; i < SafeN; i++)
|
||||
{
|
||||
double w = FMath::Pow((double)(i + 1), (double)RegressionWeightExponent);
|
||||
double t = TransformHistory[i].Timestamp - T0;
|
||||
double tt = t * t;
|
||||
|
||||
SumW += w;
|
||||
SumWT += w * t;
|
||||
SumWTT += w * tt;
|
||||
SumWTTT += w * tt * t;
|
||||
SumWTTTT += w * tt * tt;
|
||||
SumWY += TransformHistory[i].Location * w;
|
||||
SumWTY += TransformHistory[i].Location * (w * t);
|
||||
SumWTTY += TransformHistory[i].Location * (w * tt);
|
||||
SumWAim += TransformHistory[i].Aim * w;
|
||||
SumWTAim += TransformHistory[i].Aim * (w * t);
|
||||
SumWTTAim += TransformHistory[i].Aim * (w * tt);
|
||||
}
|
||||
|
||||
double TPred = CurrentTime - T0;
|
||||
double TLastSafe = TransformHistory[SafeN - 1].Timestamp - T0;
|
||||
|
||||
// Try quadratic fit (3x3 system) if we have enough samples
|
||||
bool bUseQuadratic = false;
|
||||
FVector PosA, PosB, PosC;
|
||||
FVector AimA, AimB, AimC;
|
||||
|
||||
if (SafeN >= 3)
|
||||
{
|
||||
// Normal equations for weighted quadratic: M * [a,b,c]^T = R
|
||||
// M = | SumW SumWT SumWTT |
|
||||
// | SumWT SumWTT SumWTTT |
|
||||
// | SumWTT SumWTTT SumWTTTT |
|
||||
// Solve by Cramer's rule (3x3 determinant)
|
||||
double M00 = SumW, M01 = SumWT, M02 = SumWTT;
|
||||
double M10 = SumWT, M11 = SumWTT, M12 = SumWTTT;
|
||||
double M20 = SumWTT, M21 = SumWTTT, M22 = SumWTTTT;
|
||||
|
||||
double Det3 = M00 * (M11 * M22 - M12 * M21)
|
||||
- M01 * (M10 * M22 - M12 * M20)
|
||||
+ M02 * (M10 * M21 - M11 * M20);
|
||||
|
||||
if (FMath::Abs(Det3) > SMALL_NUMBER)
|
||||
{
|
||||
bUseQuadratic = true;
|
||||
double InvDet = 1.0 / Det3;
|
||||
|
||||
// Cofactors for Cramer's rule
|
||||
double C00 = M11 * M22 - M12 * M21;
|
||||
double C01 = -(M10 * M22 - M12 * M20);
|
||||
double C02 = M10 * M21 - M11 * M20;
|
||||
double C10 = -(M01 * M22 - M02 * M21);
|
||||
double C11 = M00 * M22 - M02 * M20;
|
||||
double C12 = -(M00 * M21 - M01 * M20);
|
||||
double C20 = M01 * M12 - M02 * M11;
|
||||
double C21 = -(M00 * M12 - M02 * M10);
|
||||
double C22 = M00 * M11 - M01 * M10;
|
||||
|
||||
// Solve for position coefficients: a, b, c
|
||||
PosA = (SumWY * C00 + SumWTY * C10 + SumWTTY * C20) * InvDet;
|
||||
PosB = (SumWY * C01 + SumWTY * C11 + SumWTTY * C21) * InvDet;
|
||||
PosC = (SumWY * C02 + SumWTY * C12 + SumWTTY * C22) * InvDet;
|
||||
|
||||
// Solve for aim coefficients: a, b, c
|
||||
AimA = (SumWAim * C00 + SumWTAim * C10 + SumWTTAim * C20) * InvDet;
|
||||
AimB = (SumWAim * C01 + SumWTAim * C11 + SumWTTAim * C21) * InvDet;
|
||||
AimC = (SumWAim * C02 + SumWTAim * C12 + SumWTTAim * C22) * InvDet;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Always compute linear result (2x2 system)
|
||||
FVector LinearLocation, LinearAim;
|
||||
{
|
||||
double Det = SumW * SumWTT - SumWT * SumWT;
|
||||
if (FMath::Abs(Det) <= SMALL_NUMBER)
|
||||
{
|
||||
OutLocation = TransformHistory[SafeN - 1].Location;
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
return;
|
||||
}
|
||||
|
||||
FVector PosIntercept = (SumWY * SumWTT - SumWTY * SumWT) / Det;
|
||||
FVector PosSlope = (SumWTY * SumW - SumWY * SumWT) / Det;
|
||||
FVector AimIntercept = (SumWAim * SumWTT - SumWTAim * SumWT) / Det;
|
||||
FVector AimSlope = (SumWTAim * SumW - SumWAim * SumWT) / Det;
|
||||
|
||||
LinearLocation = PosIntercept + PosSlope * TPred;
|
||||
FVector PredAim = AimIntercept + AimSlope * TPred;
|
||||
LinearAim = PredAim.GetSafeNormal();
|
||||
if (LinearAim.IsNearlyZero())
|
||||
{
|
||||
LinearAim = TransformHistory[SafeN - 1].Aim;
|
||||
}
|
||||
}
|
||||
|
||||
if (bUseQuadratic)
|
||||
{
|
||||
// Compute quadratic result with velocity-reversal clamping
|
||||
FVector QuadLocation;
|
||||
for (int32 Axis = 0; Axis < 3; Axis++)
|
||||
{
|
||||
double b = PosB[Axis];
|
||||
double c = PosC[Axis];
|
||||
double VelAtLastSafe = b + 2.0 * c * TLastSafe;
|
||||
double VelAtPred = b + 2.0 * c * TPred;
|
||||
double TUse = TPred;
|
||||
|
||||
if (VelAtLastSafe * VelAtPred < 0.0 && FMath::Abs(c) > SMALL_NUMBER)
|
||||
{
|
||||
double TStop = -b / (2.0 * c);
|
||||
if (TStop > TLastSafe && TStop < TPred)
|
||||
{
|
||||
TUse = TStop;
|
||||
}
|
||||
}
|
||||
|
||||
QuadLocation[Axis] = PosA[Axis] + b * TUse + c * TUse * TUse;
|
||||
}
|
||||
|
||||
FVector QuadAimVec;
|
||||
for (int32 Axis = 0; Axis < 3; Axis++)
|
||||
{
|
||||
double b = AimB[Axis];
|
||||
double c = AimC[Axis];
|
||||
double VelAtLastSafe = b + 2.0 * c * TLastSafe;
|
||||
double VelAtPred = b + 2.0 * c * TPred;
|
||||
double TUse = TPred;
|
||||
|
||||
if (VelAtLastSafe * VelAtPred < 0.0 && FMath::Abs(c) > SMALL_NUMBER)
|
||||
{
|
||||
double TStop = -b / (2.0 * c);
|
||||
if (TStop > TLastSafe && TStop < TPred)
|
||||
{
|
||||
TUse = TStop;
|
||||
}
|
||||
}
|
||||
|
||||
QuadAimVec[Axis] = AimA[Axis] + b * TUse + c * TUse * TUse;
|
||||
}
|
||||
|
||||
FVector QuadAim = QuadAimVec.GetSafeNormal();
|
||||
if (QuadAim.IsNearlyZero())
|
||||
{
|
||||
QuadAim = TransformHistory[SafeN - 1].Aim;
|
||||
}
|
||||
|
||||
// Smooth blend between linear and quadratic based on significance of c.
|
||||
// Alpha = 0 → pure linear, Alpha = 1 → pure quadratic.
|
||||
// Ramp from 0 to 1 as max(|c*t^2| / |b*t|) goes from 0.05 to 0.20.
|
||||
double PosCRatio = (FMath::Max(PosB.GetAbsMax() * TPred, SMALL_NUMBER) > SMALL_NUMBER)
|
||||
? (PosC.GetAbsMax() * TPred * TPred) / (PosB.GetAbsMax() * TPred) : 0.0;
|
||||
double AimCRatio = (FMath::Max(AimB.GetAbsMax() * TPred, SMALL_NUMBER) > SMALL_NUMBER)
|
||||
? (AimC.GetAbsMax() * TPred * TPred) / (AimB.GetAbsMax() * TPred) : 0.0;
|
||||
double MaxRatio = FMath::Max(PosCRatio, AimCRatio);
|
||||
float BlendAlpha = (float)FMath::Clamp((MaxRatio - 0.05) / (0.20 - 0.05), 0.0, 1.0);
|
||||
|
||||
OutLocation = FMath::Lerp(LinearLocation, QuadLocation, BlendAlpha);
|
||||
OutAim = FMath::Lerp(LinearAim, QuadAim, BlendAlpha).GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
OutAim = TransformHistory[SafeN - 1].Aim;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OutLocation = LinearLocation;
|
||||
OutAim = LinearAim;
|
||||
}
|
||||
|
||||
// Apply optional velocity damping on the final result
|
||||
// Blend toward the last safe sample position (i.e., reduce the extrapolation offset)
|
||||
if (ExtrapolationDamping > 0.0f)
|
||||
{
|
||||
float ExtrapolationTime = (float)(TPred - TLastSafe);
|
||||
float DampingScale = FMath::Exp(-ExtrapolationDamping * ExtrapolationTime);
|
||||
FVector LastSafeLocation = TransformHistory[SafeN - 1].Location;
|
||||
FVector LastSafeAim = TransformHistory[SafeN - 1].Aim;
|
||||
|
||||
// Damping blends from full extrapolation (DampingScale=1) toward last safe sample (DampingScale=0)
|
||||
OutLocation = LastSafeLocation + (OutLocation - LastSafeLocation) * DampingScale;
|
||||
OutAim = FMath::Lerp(LastSafeAim, OutAim, DampingScale).GetSafeNormal();
|
||||
if (OutAim.IsNearlyZero())
|
||||
{
|
||||
OutAim = LastSafeAim;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Simplified Kalman Filter ---
|
||||
// Maintains state estimate [position, velocity, aim, angular_velocity]
|
||||
// Only fed with SAFE (non-contaminated) measurements, predicts forward to current time
|
||||
|
||||
void UEBBarrel::UpdateKalmanFilter(double CurrentTime, const FVector& MeasuredLocation, const FVector& MeasuredAim)
|
||||
{
|
||||
if (!KalmanInitialized)
|
||||
{
|
||||
KalmanPosition = MeasuredLocation;
|
||||
KalmanVelocity = FVector::ZeroVector;
|
||||
KalmanAim = MeasuredAim;
|
||||
KalmanAngularVelocity = FVector::ZeroVector;
|
||||
KalmanPosVariance = 1.0f;
|
||||
KalmanVelVariance = 1.0f;
|
||||
KalmanAimVariance = 1.0f;
|
||||
KalmanAngVelVariance = 1.0f;
|
||||
KalmanInitialized = true;
|
||||
KalmanLastTimestamp = CurrentTime;
|
||||
return;
|
||||
}
|
||||
|
||||
float dt = (float)(CurrentTime - KalmanLastTimestamp);
|
||||
if (dt <= SMALL_NUMBER)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KalmanLastTimestamp = CurrentTime;
|
||||
|
||||
float Q = KalmanProcessNoise;
|
||||
float R = KalmanMeasurementNoise;
|
||||
|
||||
// --- Position / Velocity ---
|
||||
// Predict step
|
||||
FVector PredPos = KalmanPosition + KalmanVelocity * dt;
|
||||
FVector PredVel = KalmanVelocity;
|
||||
float PredPosVar = KalmanPosVariance + KalmanVelVariance * dt * dt + Q * dt;
|
||||
float PredVelVar = KalmanVelVariance + Q * dt;
|
||||
|
||||
// Update step (measurement = MeasuredLocation)
|
||||
float KGainPos = PredPosVar / (PredPosVar + R);
|
||||
KalmanPosition = PredPos + (MeasuredLocation - PredPos) * KGainPos;
|
||||
KalmanPosVariance = (1.0f - KGainPos) * PredPosVar;
|
||||
|
||||
// Update velocity estimate from innovation
|
||||
FVector VelInnovation = (MeasuredLocation - PredPos) / dt;
|
||||
float KGainVel = PredVelVar / (PredVelVar + R / (dt * dt));
|
||||
KalmanVelocity = PredVel + VelInnovation * KGainVel;
|
||||
KalmanVelVariance = (1.0f - KGainVel) * PredVelVar;
|
||||
|
||||
// --- Aim / Angular Velocity ---
|
||||
FVector PredAim = KalmanAim + KalmanAngularVelocity * dt;
|
||||
FVector PredAngVel = KalmanAngularVelocity;
|
||||
float PredAimVar = KalmanAimVariance + KalmanAngVelVariance * dt * dt + Q * dt;
|
||||
float PredAngVelVar = KalmanAngVelVariance + Q * dt;
|
||||
|
||||
float KGainAim = PredAimVar / (PredAimVar + R);
|
||||
KalmanAim = PredAim + (MeasuredAim - PredAim) * KGainAim;
|
||||
KalmanAimVariance = (1.0f - KGainAim) * PredAimVar;
|
||||
|
||||
FVector AngVelInnovation = (MeasuredAim - PredAim) / dt;
|
||||
float KGainAngVel = PredAngVelVar / (PredAngVelVar + R / (dt * dt));
|
||||
KalmanAngularVelocity = PredAngVel + AngVelInnovation * KGainAngVel;
|
||||
KalmanAngVelVariance = (1.0f - KGainAngVel) * PredAngVelVar;
|
||||
}
|
||||
|
||||
void UEBBarrel::PredictKalmanFilter(double CurrentTime, FVector& OutLocation, FVector& OutAim) const
|
||||
{
|
||||
if (!KalmanInitialized)
|
||||
{
|
||||
OutLocation = GetComponentTransform().GetLocation();
|
||||
OutAim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extrapolate from Kalman state to current time
|
||||
float dt = (float)(CurrentTime - KalmanLastTimestamp);
|
||||
|
||||
// Apply optional velocity damping
|
||||
float DampingScale = 1.0f;
|
||||
if (ExtrapolationDamping > 0.0f)
|
||||
{
|
||||
DampingScale = FMath::Exp(-ExtrapolationDamping * 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(), AntiRecoilDiscardTime);
|
||||
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 (normal fluctuations).
|
||||
// Remapped ratio: (ratio - deadzone) / (1 - deadzone), clamped to [0, 1].
|
||||
// AdaptiveSensitivity is the power exponent on the remapped ratio.
|
||||
float PosRatio = 1.0f;
|
||||
float PosConfidence = 1.0f;
|
||||
if (AvgPosSpeed > SMALL_NUMBER)
|
||||
{
|
||||
PosRatio = FMath::Clamp(RecentPosSpeed / AvgPosSpeed, 0.0f, 1.0f);
|
||||
float PosRemapped = (AdaptiveDeadZone < 1.0f)
|
||||
? FMath::Clamp((PosRatio - AdaptiveDeadZone) / (1.0f - AdaptiveDeadZone), 0.0f, 1.0f)
|
||||
: (PosRatio >= 1.0f ? 1.0f : 0.0f);
|
||||
PosConfidence = FMath::Pow(PosRemapped, AdaptiveSensitivity);
|
||||
}
|
||||
|
||||
float AimRatio = 1.0f;
|
||||
float AimConfidence = 1.0f;
|
||||
if (AvgAimSpeed > SMALL_NUMBER)
|
||||
{
|
||||
AimRatio = FMath::Clamp(RecentAimSpeed / AvgAimSpeed, 0.0f, 1.0f);
|
||||
float AimRemapped = (AdaptiveDeadZone < 1.0f)
|
||||
? FMath::Clamp((AimRatio - AdaptiveDeadZone) / (1.0f - AdaptiveDeadZone), 0.0f, 1.0f)
|
||||
: (AimRatio >= 1.0f ? 1.0f : 0.0f);
|
||||
AimConfidence = FMath::Pow(AimRemapped, AdaptiveSensitivity);
|
||||
}
|
||||
|
||||
// Extrapolate from last safe sample
|
||||
const FTimestampedTransform& LastSafe = TransformHistory[SafeN - 1];
|
||||
double ExtrapolationTime = CurrentTime - LastSafe.Timestamp;
|
||||
|
||||
// Write debug values for HUD display
|
||||
DbgPosRatio = PosRatio;
|
||||
DbgAimRatio = AimRatio;
|
||||
DbgPosConfidence = PosConfidence;
|
||||
DbgAimConfidence = AimConfidence;
|
||||
DbgAvgPosSpeed = AvgPosSpeed;
|
||||
DbgAvgAimSpeed = AvgAimSpeed;
|
||||
DbgRecentPosSpeed = RecentPosSpeed;
|
||||
DbgRecentAimSpeed = RecentAimSpeed;
|
||||
DbgExtrapolationTime = (float)ExtrapolationTime;
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ void UEBBarrel::SpawnBulletEventMulticast_Implementation(FVector Start, FVector
|
||||
}
|
||||
|
||||
void UEBBarrel::Shoot(bool Trigger, int nextFireID) {
|
||||
if (ClientSideAim && GetOwner()->GetRemoteRole() == ROLE_Authority && Trigger) {
|
||||
if (ClientSideAim && GetOwner()->GetLocalRole() == ROLE_AutonomousProxy && Trigger) {
|
||||
Aim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
Location = GetComponentTransform().GetLocation();
|
||||
nextFireEventID = nextFireID;
|
||||
@@ -0,0 +1,837 @@
|
||||
// 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);
|
||||
|
||||
RandomStream.GenerateNewSeed();
|
||||
|
||||
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);
|
||||
|
||||
// Update anti-recoil transform history
|
||||
UpdateTransformHistory();
|
||||
|
||||
if (ClientSideAim){
|
||||
const ENetRole LocalRole = GetOwner()->GetLocalRole();
|
||||
if (LocalRole == ROLE_AutonomousProxy){
|
||||
TimeSinceAimUpdate += DeltaTime;
|
||||
if (TimeSinceAimUpdate >= 1.0f / ClientAimUpdateFrequency) {
|
||||
|
||||
ComputeAntiRecoilTransform();
|
||||
|
||||
ClientAim(UGameplayStatics::RebaseLocalOriginOntoZero(GetWorld(),Location), Aim);
|
||||
TimeSinceAimUpdate = FMath::Fmod(TimeSinceAimUpdate, 1.0f / ClientAimUpdateFrequency);
|
||||
};
|
||||
}else if (LocalRole == ROLE_Authority){
|
||||
if (!RemoteAimReceived) {
|
||||
ComputeAntiRecoilTransform();
|
||||
}
|
||||
else {
|
||||
FVector LocOffset = (Location - GetComponentLocation());
|
||||
if (LocOffset.Size() > ClientAimDistanceLimit) {
|
||||
//lag or cheater???
|
||||
Location = GetComponentLocation() + LocOffset.GetSafeNormal()*ClientAimDistanceLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
// ROLE_SimulatedProxy: observer, aim comes from replicated transform — nothing to do
|
||||
}
|
||||
else {
|
||||
ComputeAntiRecoilTransform();
|
||||
}
|
||||
|
||||
// Debug visualization: raw tracker (green) vs predicted aim (red)
|
||||
if (DebugAntiRecoil)
|
||||
{
|
||||
// Use the latest buffer entry as "raw" data (includes simulated shock if active)
|
||||
FVector RawLocation;
|
||||
FVector RawAim;
|
||||
if (TransformHistory.Num() > 0)
|
||||
{
|
||||
RawLocation = TransformHistory.Last().Location;
|
||||
RawAim = TransformHistory.Last().Aim;
|
||||
}
|
||||
else
|
||||
{
|
||||
RawLocation = GetComponentTransform().GetLocation();
|
||||
RawAim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
}
|
||||
|
||||
// Green line: raw tracker data (potentially shocked if IMU simulation is active)
|
||||
DrawDebugLine(GetWorld(), RawLocation, RawLocation + RawAim * DebugAntiRecoilLineLength,
|
||||
FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
|
||||
// Red line: anti-recoil predicted aim (what would be used if shooting now)
|
||||
DrawDebugLine(GetWorld(), Location, Location + Aim * DebugAntiRecoilLineLength,
|
||||
FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
|
||||
// Small spheres at origins for clarity
|
||||
DrawDebugSphere(GetWorld(), RawLocation, 1.5f, 6, FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f);
|
||||
DrawDebugSphere(GetWorld(), Location, 1.5f, 6, FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness * 0.5f);
|
||||
|
||||
// Yellow line: shows where shot would land WITHOUT anti-recoil correction
|
||||
// Captures raw aim at shock onset and persists for DebugIMUShockDisplayTime seconds
|
||||
if (DebugIMUShockActive && !DebugIMUShockLineCaptured)
|
||||
{
|
||||
// Capture the raw (shocked) aim at the first frame of shock
|
||||
DebugIMUShockCapturedLocation = RawLocation;
|
||||
DebugIMUShockCapturedAim = RawAim;
|
||||
DebugIMUShockLineCaptured = true;
|
||||
DebugIMUShockLineEndTime = GetWorld()->GetTimeSeconds() + DebugIMUShockDisplayTime;
|
||||
}
|
||||
if (!DebugIMUShockActive && DebugIMUShockLineCaptured)
|
||||
{
|
||||
// Shock ended: keep displaying but update captured aim to worst-case (peak shock)
|
||||
// which was already captured at onset
|
||||
}
|
||||
if (DebugIMUShockLineCaptured)
|
||||
{
|
||||
if (GetWorld()->GetTimeSeconds() < DebugIMUShockLineEndTime)
|
||||
{
|
||||
// Green persistent line: raw tracker aim at moment of shot (same color as real-time green)
|
||||
DrawDebugLine(GetWorld(), DebugIMUShockCapturedLocation,
|
||||
DebugIMUShockCapturedLocation + DebugIMUShockCapturedAim * DebugAntiRecoilLineLength,
|
||||
FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
DrawDebugSphere(GetWorld(), DebugIMUShockCapturedLocation, 3.0f, 8, FColor::Green, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugIMUShockLineCaptured = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Red persistent line: corrected aim retained for the shot (same color as real-time red)
|
||||
if (DebugCorrectedShotLineCaptured)
|
||||
{
|
||||
if (GetWorld()->GetTimeSeconds() < DebugCorrectedShotLineEndTime)
|
||||
{
|
||||
DrawDebugLine(GetWorld(), DebugCorrectedShotCapturedLocation,
|
||||
DebugCorrectedShotCapturedLocation + DebugCorrectedShotCapturedAim * DebugAntiRecoilLineLength,
|
||||
FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
DrawDebugSphere(GetWorld(), DebugCorrectedShotCapturedLocation, 3.0f, 8, FColor::Red, false, -1.0f, 0, DebugAntiRecoilLineThickness);
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugCorrectedShotLineCaptured = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CSV Prediction Recording
|
||||
if (RecordPredictionCSV && AntiRecoilMode != EAntiRecoilMode::ARM_None)
|
||||
{
|
||||
if (!bCSVFileOpen)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
bCSVFileOpen = true;
|
||||
FString Header = TEXT("Timestamp,RealPosX,RealPosY,RealPosZ,RealAimX,RealAimY,RealAimZ,PredPosX,PredPosY,PredPosZ,PredAimX,PredAimY,PredAimZ,SafeCount,BufferCount,ExtrapolationTime\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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() - AntiRecoilDiscardTime;
|
||||
int32 SafeN = 0;
|
||||
for (int32 si = 0; si < TransformHistory.Num(); si++)
|
||||
{
|
||||
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\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);
|
||||
auto LineUtf8 = StringCast<ANSICHAR>(*Line);
|
||||
CSVFileHandle->Write((const uint8*)LineUtf8.Get(), LineUtf8.Length());
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
// Anti-Recoil Debug HUD
|
||||
if (DebugAntiRecoilHUD && GEngine && AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation)
|
||||
{
|
||||
// Compute live errors
|
||||
FVector RealPos = GetComponentTransform().GetLocation();
|
||||
FVector RealAim = GetComponentTransform().GetUnitAxis(EAxis::X);
|
||||
DbgPosError = FVector::Dist(Location, RealPos);
|
||||
float AimDot = FMath::Clamp(FVector::DotProduct(Aim, RealAim), -1.0f, 1.0f);
|
||||
DbgAimError = FMath::RadiansToDegrees(FMath::Acos(AimDot));
|
||||
|
||||
int32 HudKey = -500;
|
||||
FColor HudTitle = FColor::Yellow;
|
||||
FColor HudVal = FColor::White;
|
||||
FColor HudGood = FColor::Green;
|
||||
FColor HudWarn = FColor::Orange;
|
||||
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudTitle,
|
||||
TEXT("====== ADAPTIVE EXTRAPOLATION ======"));
|
||||
|
||||
// Speed ratio + confidence (position)
|
||||
FColor PosColor = (DbgPosConfidence > 0.9f) ? HudGood : HudWarn;
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, PosColor,
|
||||
FString::Printf(TEXT(" Pos: ratio=%.2f conf=%.2f speed=%.1f/%.1f cm/s"),
|
||||
DbgPosRatio, DbgPosConfidence, DbgRecentPosSpeed, DbgAvgPosSpeed));
|
||||
|
||||
// Speed ratio + confidence (aim)
|
||||
FColor AimColor = (DbgAimConfidence > 0.9f) ? HudGood : HudWarn;
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, AimColor,
|
||||
FString::Printf(TEXT(" Aim: ratio=%.2f conf=%.2f speed=%.4f/%.4f /s"),
|
||||
DbgAimRatio, DbgAimConfidence, DbgRecentAimSpeed, DbgAvgAimSpeed));
|
||||
|
||||
// Extrapolation time
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, HudVal,
|
||||
FString::Printf(TEXT(" Extrap: %.0f ms"), DbgExtrapolationTime * 1000.0f));
|
||||
|
||||
// Errors
|
||||
FColor PosErrColor = (DbgPosError < 2.0f) ? HudGood : (DbgPosError < 5.0f) ? HudVal : HudWarn;
|
||||
FColor AimErrColor = (DbgAimError < 2.0f) ? HudGood : (DbgAimError < 5.0f) ? HudVal : HudWarn;
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, PosErrColor,
|
||||
FString::Printf(TEXT(" Pos error: %.2f cm"), DbgPosError));
|
||||
GEngine->AddOnScreenDebugMessage(HudKey--, 0.0f, AimErrColor,
|
||||
FString::Printf(TEXT(" Aim error: %.2f deg"), DbgAimError));
|
||||
}
|
||||
|
||||
// Calibration HUD
|
||||
if (CalibrateAntiRecoil && GEngine)
|
||||
{
|
||||
int32 CalKey = -600;
|
||||
FColor CalTitle = FColor::Magenta;
|
||||
FColor CalValue = FColor::White;
|
||||
FColor CalGood = FColor::Green;
|
||||
FColor CalWarn = FColor::Yellow;
|
||||
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalTitle,
|
||||
TEXT("====== ANTI-RECOIL CALIBRATION ======"));
|
||||
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalValue,
|
||||
FString::Printf(TEXT(" Collecting: %d / %d shots"),
|
||||
CalibrationShotsCollected, CalibrationShotCount));
|
||||
|
||||
// Running stats from current sequence
|
||||
if (CalibrationShots.Num() > 0)
|
||||
{
|
||||
// Quick re-analysis of already captured shots for running display
|
||||
float RunMax = 0.0f, RunSum = 0.0f;
|
||||
int32 RunCount = 0;
|
||||
for (const FCalibrationShotData& S : CalibrationShots)
|
||||
{
|
||||
// Simplified: use buffer size as rough proxy until full analysis
|
||||
// We'll show "pending analysis" for individual shots
|
||||
RunCount++;
|
||||
}
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalValue,
|
||||
FString::Printf(TEXT(" %d shots captured, awaiting sequence completion..."), RunCount));
|
||||
}
|
||||
else
|
||||
{
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalValue,
|
||||
TEXT(" Fire weapon to collect data..."));
|
||||
}
|
||||
|
||||
// Show last completed sequence results
|
||||
if (LastCalibrationResult.bValid)
|
||||
{
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalTitle,
|
||||
TEXT(" --- Last Sequence Results ---"));
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalGood,
|
||||
FString::Printf(TEXT(" Shots: %d (outliers removed: %d)"),
|
||||
LastCalibrationResult.TotalShots, LastCalibrationResult.OutliersRemoved));
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalGood,
|
||||
FString::Printf(TEXT(" Corruption: Med %.3fs | P95 %.3fs | Max %.3fs"),
|
||||
LastCalibrationResult.MedianCorruptionDuration,
|
||||
LastCalibrationResult.P95CorruptionDuration,
|
||||
LastCalibrationResult.MaxCorruptionDuration));
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalGood,
|
||||
FString::Printf(TEXT(" Peak deviations: %.2f deg | %.2f cm"),
|
||||
LastCalibrationResult.AvgPeakAngleDeviation,
|
||||
LastCalibrationResult.AvgPeakPositionDeviation));
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn,
|
||||
FString::Printf(TEXT(" >> DiscardTime: %.4fs"),
|
||||
LastCalibrationResult.RecommendedDiscardTime));
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn,
|
||||
FString::Printf(TEXT(" >> BufferTime: %.4fs"),
|
||||
LastCalibrationResult.RecommendedBufferTime));
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn,
|
||||
FString::Printf(TEXT(" >> KalmanProcessNoise: %.3f"),
|
||||
LastCalibrationResult.RecommendedKalmanProcessNoise));
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalWarn,
|
||||
FString::Printf(TEXT(" >> KalmanMeasurementNoise: %.4f"),
|
||||
LastCalibrationResult.RecommendedKalmanMeasurementNoise));
|
||||
}
|
||||
|
||||
GEngine->AddOnScreenDebugMessage(CalKey--, 0.0f, CalTitle,
|
||||
TEXT("================================="));
|
||||
}
|
||||
|
||||
//Only server can tick
|
||||
if (GetOwner()->GetLocalRole() == ROLE_Authority){
|
||||
|
||||
float RemainingDelta;
|
||||
|
||||
if (FireMode == EFireMode::FM_Gatling) {
|
||||
if (Spooling || (GatlingAutoSpool && Shooting)) {
|
||||
GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMax, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f));
|
||||
}
|
||||
else {
|
||||
GatlingRPS = FMath::Lerp(GatlingRPS, FireRateMin, FMath::Min(GatlingSpoolUpTime*DeltaTime, 1.0f));
|
||||
}
|
||||
GatlingPhase += GatlingRPS*DeltaTime;
|
||||
for (int i = 1; i <= GatlingPhase; i++) {
|
||||
if (Cooldown <= 0.0f && LoadNext) {
|
||||
NextBullet();
|
||||
}
|
||||
|
||||
if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) {
|
||||
SpawnBullet(GetOwner(), Location, Aim, nextFireEventID);
|
||||
}
|
||||
}
|
||||
GatlingPhase = FMath::Fmod(GatlingPhase, 1.0f);
|
||||
|
||||
}
|
||||
else {
|
||||
RemainingDelta = DeltaTime;
|
||||
do {
|
||||
float step = FMath::Min(Cooldown, RemainingDelta);
|
||||
|
||||
Cooldown -= step;
|
||||
|
||||
RemainingDelta -= step;
|
||||
|
||||
if (Cooldown <= 0.0f && LoadNext) {
|
||||
NextBullet();
|
||||
}
|
||||
|
||||
//shoot when ready
|
||||
if (Shooting && ChamberedBullet != nullptr && (!ShootingBlocked)) {
|
||||
if (BurstRemaining > 0 || (FireMode != EFireMode::FM_Burst && FireMode != EFireMode::FM_InterBurst)) {
|
||||
SpawnBullet(GetOwner(), Location, Aim, nextFireEventID);
|
||||
}
|
||||
else {
|
||||
Shooting = false;
|
||||
}
|
||||
}
|
||||
} while (RemainingDelta > 0 && Cooldown > 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::NextBullet() {
|
||||
if (ChamberedBullet == nullptr) {
|
||||
if (Ammo.Num() > 0 && (CycleAmmoCount > 0 || CycleAmmoUnlimited || (!CycleAmmo))) {
|
||||
|
||||
//cycle ammo
|
||||
if (CycleAmmo) {
|
||||
if (CycleAmmoPos >= Ammo.Num()) { CycleAmmoPos = 0; }
|
||||
ChamberedBullet = Ammo[CycleAmmoPos];
|
||||
CycleAmmoPos++;
|
||||
|
||||
if (!CycleAmmoUnlimited) {
|
||||
CycleAmmoCount--;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ChamberedBullet = Ammo[0];
|
||||
Ammo.RemoveAt(0, 1, EAllowShrinking::Yes);
|
||||
}
|
||||
|
||||
ReadyToShoot.Broadcast();
|
||||
}
|
||||
else {
|
||||
AmmoDepleted.Broadcast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::SpawnBullet(AActor* Owner, FVector InLocation, FVector InAim, int fireEventID) {
|
||||
TSubclassOf<class AEBBullet> BulletClass = ChamberedBullet;
|
||||
|
||||
if (BulletClass != nullptr) {
|
||||
FVector OutLocation;
|
||||
FVector OutAim;
|
||||
|
||||
InitialBulletTransform(InLocation, InAim, OutLocation, OutAim);
|
||||
|
||||
AEBBullet* Default = Cast<AEBBullet>(BulletClass->GetDefaultObject());
|
||||
|
||||
float BulletSpread = Default->Spread;
|
||||
if (Default->SpreadBias > 0.0f) {
|
||||
float SpreadMult = FMath::Pow(FMath::FRand(), Default->SpreadBias);
|
||||
BulletSpread *= SpreadMult;
|
||||
}
|
||||
float BarrelSpread = Spread;
|
||||
if (SpreadBias > 0.0f) {
|
||||
float SpreadMult = FMath::Pow(FMath::FRand(), SpreadBias);
|
||||
BarrelSpread *= SpreadMult;
|
||||
}
|
||||
|
||||
float TotalSpread = BulletSpread+BarrelSpread;
|
||||
|
||||
OutAim = RandomStream.VRandCone(OutAim,TotalSpread);
|
||||
float BulletVelocity = FMath::Lerp(MuzzleVelocityMultiplierMin* Default->MuzzleVelocityMin, MuzzleVelocityMultiplierMax*Default->MuzzleVelocityMax, RandomStream.FRand());
|
||||
FVector Velocity = OutAim*BulletVelocity;
|
||||
|
||||
//get parent physics body
|
||||
UPrimitiveComponent* parent = Cast<UPrimitiveComponent>(GetAttachParent());
|
||||
Velocity += AdditionalVelocity;
|
||||
|
||||
if (parent != nullptr) {
|
||||
|
||||
if (parent->IsSimulatingPhysics()) {
|
||||
Velocity += parent->GetPhysicsLinearVelocityAtPoint(OutLocation)*InheritVelocity;
|
||||
}
|
||||
|
||||
if (Default->Shotgun) {
|
||||
ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier*Default->ShotCount);
|
||||
}
|
||||
else{
|
||||
ApplyRecoil(parent, OutLocation, -Velocity*Default->Mass*RecoilMultiplier);
|
||||
}
|
||||
}
|
||||
|
||||
// Capture yellow debug line on real shot: shows raw tracker aim at firing moment
|
||||
// (where the bullet would go WITHOUT anti-recoil correction)
|
||||
if (DebugAntiRecoil && TransformHistory.Num() > 0)
|
||||
{
|
||||
DebugIMUShockCapturedLocation = TransformHistory.Last().Location;
|
||||
DebugIMUShockCapturedAim = TransformHistory.Last().Aim;
|
||||
DebugIMUShockLineCaptured = true;
|
||||
DebugIMUShockLineEndTime = GetWorld()->GetTimeSeconds() + DebugIMUShockDisplayTime;
|
||||
|
||||
// Blue line: corrected aim direction retained for this shot (after anti-recoil filtering)
|
||||
DebugCorrectedShotCapturedLocation = OutLocation;
|
||||
DebugCorrectedShotCapturedAim = OutAim.GetSafeNormal();
|
||||
DebugCorrectedShotLineCaptured = true;
|
||||
DebugCorrectedShotLineEndTime = GetWorld()->GetTimeSeconds() + DebugIMUShockDisplayTime;
|
||||
}
|
||||
|
||||
// Calibration: snapshot the entire buffer for offline analysis
|
||||
if (CalibrateAntiRecoil && TransformHistory.Num() > 0)
|
||||
{
|
||||
FCalibrationShotData ShotData;
|
||||
ShotData.BufferSnapshot = TransformHistory;
|
||||
ShotData.ShotTime = GetWorld()->GetTimeSeconds();
|
||||
CalibrationShots.Add(MoveTemp(ShotData));
|
||||
CalibrationShotsCollected++;
|
||||
|
||||
if (CalibrationShotsCollected >= CalibrationShotCount)
|
||||
{
|
||||
ComputeCalibrationResult();
|
||||
CalibrationShots.Empty();
|
||||
CalibrationShotsCollected = 0;
|
||||
}
|
||||
}
|
||||
|
||||
BeforeShotFired.Broadcast();
|
||||
#ifdef WITH_EDITOR
|
||||
if (shotTrace) {
|
||||
DrawDebugLine(GetWorld(), OutLocation, OutLocation + Velocity, FColor::Red, false, 3, 0, 0);
|
||||
};
|
||||
#endif
|
||||
if (ReplicateShotFiredEvents) {
|
||||
SpawnBulletEventMulticast(OutLocation, Velocity);
|
||||
}
|
||||
else {
|
||||
SpawnBulletEvent.Broadcast(OutLocation, Velocity);
|
||||
}
|
||||
|
||||
AEBBullet::SpawnWithExactVelocity(BulletClass, Owner, Owner->GetInstigator(), OutLocation, Velocity, fireEventID);
|
||||
|
||||
//spend ammo
|
||||
ChamberedBullet = nullptr;
|
||||
if (FireMode != EFireMode::FM_Gatling) {
|
||||
Cooldown = 1.0f / FMath::Lerp(FireRateMin, FireRateMax, RandomStream.FRand());
|
||||
}
|
||||
|
||||
//fire modes
|
||||
switch (FireMode) {
|
||||
case EFireMode::FM_Auto:
|
||||
LoadNext = true;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Burst:
|
||||
LoadNext = true;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_InterBurst:
|
||||
LoadNext = true;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Semiauto:
|
||||
Shooting = false;
|
||||
LoadNext = true;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Manual:
|
||||
Shooting = false;
|
||||
LoadNext = false;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Slamfire:
|
||||
LoadNext = false;
|
||||
break;
|
||||
|
||||
case EFireMode::FM_Gatling:
|
||||
LoadNext = true;
|
||||
break;
|
||||
};
|
||||
|
||||
if (BurstRemaining > 0) {
|
||||
BurstRemaining--;
|
||||
}
|
||||
else {
|
||||
if (FireMode == EFireMode::FM_Burst || FireMode == EFireMode::FM_InterBurst) {
|
||||
Cooldown = FMath::Max(Cooldown, BurstCooldown);
|
||||
}
|
||||
}
|
||||
|
||||
if (ReplicateShotFiredEvents) {
|
||||
ShotFiredMulticast();
|
||||
}
|
||||
else {
|
||||
ShotFired.Broadcast();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::InitialBulletTransform_Implementation(FVector InLocation, FVector InDirection, FVector& OutLocation, FVector& OutDirection) {
|
||||
OutLocation = InLocation;
|
||||
OutDirection = InDirection;
|
||||
}
|
||||
|
||||
void UEBBarrel::ApplyRecoil_Implementation(UPrimitiveComponent* Component, FVector InLocation, FVector Impulse){
|
||||
if (Component->IsSimulatingPhysics()) {
|
||||
Component->AddImpulseAtLocation(Impulse, InLocation);
|
||||
}
|
||||
}
|
||||
|
||||
void UEBBarrel::ComputeCalibrationResult()
|
||||
{
|
||||
LastCalibrationResult = FCalibrationResult();
|
||||
LastCalibrationResult.TotalShots = CalibrationShots.Num();
|
||||
|
||||
if (CalibrationShots.Num() < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TArray<float> CorruptionDurations;
|
||||
TArray<float> PeakAngles;
|
||||
TArray<float> PeakPositions;
|
||||
|
||||
// Collect all clean-sample acceleration variances and residuals for Kalman estimation
|
||||
TArray<float> AllAccelerationVariances;
|
||||
TArray<float> AllResidualVariances;
|
||||
|
||||
for (const FCalibrationShotData& Shot : CalibrationShots)
|
||||
{
|
||||
const TArray<FTimestampedTransform>& Buffer = Shot.BufferSnapshot;
|
||||
if (Buffer.Num() < 5)
|
||||
{
|
||||
CorruptionDurations.Add(0.0f);
|
||||
PeakAngles.Add(0.0f);
|
||||
PeakPositions.Add(0.0f);
|
||||
continue;
|
||||
}
|
||||
|
||||
// --- Auto-seuil 3-sigma corruption detection ---
|
||||
// Baseline: 30% oldest samples (min 3)
|
||||
int32 BaselineCount = FMath::Max(3, Buffer.Num() * 3 / 10);
|
||||
|
||||
// Fit linear trend on baseline (position and aim vs time)
|
||||
double T0 = Buffer[0].Timestamp;
|
||||
|
||||
// Weighted means for linear regression on baseline
|
||||
double SumT = 0.0, SumTT = 0.0;
|
||||
FVector SumPos = FVector::ZeroVector, SumTPos = FVector::ZeroVector;
|
||||
FVector SumAim = FVector::ZeroVector, SumTAim = FVector::ZeroVector;
|
||||
|
||||
for (int32 i = 0; i < BaselineCount; i++)
|
||||
{
|
||||
double t = Buffer[i].Timestamp - T0;
|
||||
SumT += t;
|
||||
SumTT += t * t;
|
||||
SumPos += Buffer[i].Location;
|
||||
SumTPos += Buffer[i].Location * t;
|
||||
SumAim += Buffer[i].Aim;
|
||||
SumTAim += Buffer[i].Aim * t;
|
||||
}
|
||||
|
||||
double N = (double)BaselineCount;
|
||||
double Det = N * SumTT - SumT * SumT;
|
||||
|
||||
FVector PosIntercept, PosSlope, AimIntercept, AimSlope;
|
||||
if (FMath::Abs(Det) > SMALL_NUMBER)
|
||||
{
|
||||
PosIntercept = (SumPos * SumTT - SumTPos * SumT) / Det;
|
||||
PosSlope = (SumTPos * N - SumPos * SumT) / Det;
|
||||
AimIntercept = (SumAim * SumTT - SumTAim * SumT) / Det;
|
||||
AimSlope = (SumTAim * N - SumAim * SumT) / Det;
|
||||
}
|
||||
else
|
||||
{
|
||||
PosIntercept = SumPos / N;
|
||||
PosSlope = FVector::ZeroVector;
|
||||
AimIntercept = SumAim / N;
|
||||
AimSlope = FVector::ZeroVector;
|
||||
}
|
||||
|
||||
// Compute residuals on baseline to get sigma
|
||||
float SumAngleResidSq = 0.0f;
|
||||
float SumPosResidSq = 0.0f;
|
||||
|
||||
for (int32 i = 0; i < BaselineCount; i++)
|
||||
{
|
||||
double t = Buffer[i].Timestamp - T0;
|
||||
FVector PredAim = (AimIntercept + AimSlope * t).GetSafeNormal();
|
||||
FVector PredPos = PosIntercept + PosSlope * t;
|
||||
|
||||
float Dot = FVector::DotProduct(Buffer[i].Aim.GetSafeNormal(), PredAim);
|
||||
float AngleResid = FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(Dot, -1.0f, 1.0f)));
|
||||
float PosResid = FVector::Dist(Buffer[i].Location, PredPos);
|
||||
|
||||
SumAngleResidSq += AngleResid * AngleResid;
|
||||
SumPosResidSq += PosResid * PosResid;
|
||||
}
|
||||
|
||||
float SigmaAngle = FMath::Sqrt(SumAngleResidSq / FMath::Max(1.0f, N - 2.0f));
|
||||
float SigmaPos = FMath::Sqrt(SumPosResidSq / FMath::Max(1.0f, N - 2.0f));
|
||||
|
||||
// Minimum sigma to avoid zero-threshold (perfectly still tracker)
|
||||
SigmaAngle = FMath::Max(SigmaAngle, 0.05f);
|
||||
SigmaPos = FMath::Max(SigmaPos, 0.01f);
|
||||
|
||||
float AngleThreshold = SigmaAngle * 3.0f;
|
||||
float PosThreshold = SigmaPos * 3.0f;
|
||||
|
||||
// Detect corruption in all samples after baseline
|
||||
float ShotCorruptionDuration = 0.0f;
|
||||
float ShotPeakAngle = 0.0f;
|
||||
float ShotPeakPos = 0.0f;
|
||||
double FirstCorruptedTime = 0.0;
|
||||
|
||||
for (int32 i = BaselineCount; i < Buffer.Num(); i++)
|
||||
{
|
||||
double t = Buffer[i].Timestamp - T0;
|
||||
FVector PredAim = (AimIntercept + AimSlope * t).GetSafeNormal();
|
||||
FVector PredPos = PosIntercept + PosSlope * t;
|
||||
|
||||
float Dot = FVector::DotProduct(Buffer[i].Aim.GetSafeNormal(), PredAim);
|
||||
float AngleDev = FMath::RadiansToDegrees(FMath::Acos(FMath::Clamp(Dot, -1.0f, 1.0f)));
|
||||
float PosDev = FVector::Dist(Buffer[i].Location, PredPos);
|
||||
|
||||
if (AngleDev > AngleThreshold || PosDev > PosThreshold)
|
||||
{
|
||||
if (FirstCorruptedTime == 0.0)
|
||||
{
|
||||
FirstCorruptedTime = Buffer[i].Timestamp;
|
||||
}
|
||||
if (AngleDev > ShotPeakAngle) ShotPeakAngle = AngleDev;
|
||||
if (PosDev > ShotPeakPos) ShotPeakPos = PosDev;
|
||||
}
|
||||
}
|
||||
|
||||
if (FirstCorruptedTime > 0.0)
|
||||
{
|
||||
ShotCorruptionDuration = (float)(Shot.ShotTime - FirstCorruptedTime);
|
||||
}
|
||||
|
||||
CorruptionDurations.Add(ShotCorruptionDuration);
|
||||
PeakAngles.Add(ShotPeakAngle);
|
||||
PeakPositions.Add(ShotPeakPos);
|
||||
|
||||
// --- Kalman parameter estimation from clean baseline samples ---
|
||||
// ProcessNoise: variance of acceleration (velocity changes between consecutive samples)
|
||||
if (BaselineCount >= 3)
|
||||
{
|
||||
float SumAccelSq = 0.0f;
|
||||
int32 AccelCount = 0;
|
||||
FVector PrevVel = FVector::ZeroVector;
|
||||
bool bHasPrevVel = false;
|
||||
|
||||
for (int32 i = 1; i < BaselineCount; i++)
|
||||
{
|
||||
double dt = Buffer[i].Timestamp - Buffer[i - 1].Timestamp;
|
||||
if (dt > SMALL_NUMBER)
|
||||
{
|
||||
FVector Vel = (Buffer[i].Location - Buffer[i - 1].Location) / dt;
|
||||
if (bHasPrevVel)
|
||||
{
|
||||
FVector Accel = (Vel - PrevVel) / dt;
|
||||
SumAccelSq += Accel.SizeSquared();
|
||||
AccelCount++;
|
||||
}
|
||||
PrevVel = Vel;
|
||||
bHasPrevVel = true;
|
||||
}
|
||||
}
|
||||
if (AccelCount > 0)
|
||||
{
|
||||
AllAccelerationVariances.Add(SumAccelSq / AccelCount);
|
||||
}
|
||||
}
|
||||
|
||||
// MeasurementNoise: variance of residuals from linear trend
|
||||
if (SumPosResidSq > 0.0f)
|
||||
{
|
||||
AllResidualVariances.Add(SumPosResidSq / FMath::Max(1.0f, N - 2.0f));
|
||||
}
|
||||
}
|
||||
|
||||
// --- Aggregate timing statistics ---
|
||||
CorruptionDurations.Sort();
|
||||
int32 Num = CorruptionDurations.Num();
|
||||
|
||||
// IQR outlier removal
|
||||
float Q1 = CorruptionDurations[Num / 4];
|
||||
float Q3 = CorruptionDurations[(3 * Num) / 4];
|
||||
float IQR = Q3 - Q1;
|
||||
float UpperFence = Q3 + 1.5f * IQR;
|
||||
|
||||
TArray<float> CleanDurations;
|
||||
int32 OutlierCount = 0;
|
||||
for (float D : CorruptionDurations)
|
||||
{
|
||||
if (D <= UpperFence)
|
||||
CleanDurations.Add(D);
|
||||
else
|
||||
OutlierCount++;
|
||||
}
|
||||
|
||||
if (CleanDurations.Num() < 3)
|
||||
{
|
||||
CleanDurations = CorruptionDurations;
|
||||
OutlierCount = 0;
|
||||
}
|
||||
|
||||
CleanDurations.Sort();
|
||||
int32 CN = CleanDurations.Num();
|
||||
|
||||
float Median = CleanDurations[CN / 2];
|
||||
int32 P95Index = FMath::Min((int32)(CN * 0.95f), CN - 1);
|
||||
float P95 = CleanDurations[P95Index];
|
||||
float Max = CleanDurations.Last();
|
||||
|
||||
float RecommendedDiscard = P95 * 1.3f;
|
||||
RecommendedDiscard = FMath::Clamp(RecommendedDiscard, 0.011f, 0.2f);
|
||||
|
||||
float SafeWindow = FMath::Max(0.05f, RecommendedDiscard * 0.5f);
|
||||
float RecommendedBuffer = RecommendedDiscard + SafeWindow;
|
||||
|
||||
// Average peak deviations
|
||||
float SumAngle = 0.0f, SumPos = 0.0f;
|
||||
for (int32 i = 0; i < PeakAngles.Num(); i++)
|
||||
{
|
||||
SumAngle += PeakAngles[i];
|
||||
SumPos += PeakPositions[i];
|
||||
}
|
||||
|
||||
// --- Kalman parameter recommendations ---
|
||||
float RecommendedProcessNoise = 200.0f; // default fallback
|
||||
float RecommendedMeasurementNoise = 0.01f; // default fallback
|
||||
|
||||
if (AllAccelerationVariances.Num() > 0)
|
||||
{
|
||||
float SumAccelVar = 0.0f;
|
||||
for (float V : AllAccelerationVariances) SumAccelVar += V;
|
||||
RecommendedProcessNoise = SumAccelVar / AllAccelerationVariances.Num();
|
||||
// Clamp to sane range
|
||||
RecommendedProcessNoise = FMath::Clamp(RecommendedProcessNoise, 0.1f, 10000.0f);
|
||||
}
|
||||
|
||||
if (AllResidualVariances.Num() > 0)
|
||||
{
|
||||
float SumResidVar = 0.0f;
|
||||
for (float V : AllResidualVariances) SumResidVar += V;
|
||||
RecommendedMeasurementNoise = SumResidVar / AllResidualVariances.Num();
|
||||
RecommendedMeasurementNoise = FMath::Clamp(RecommendedMeasurementNoise, 0.001f, 100.0f);
|
||||
}
|
||||
|
||||
// Populate result
|
||||
LastCalibrationResult.RecommendedDiscardTime = RecommendedDiscard;
|
||||
LastCalibrationResult.RecommendedBufferTime = RecommendedBuffer;
|
||||
LastCalibrationResult.RecommendedKalmanProcessNoise = RecommendedProcessNoise;
|
||||
LastCalibrationResult.RecommendedKalmanMeasurementNoise = RecommendedMeasurementNoise;
|
||||
LastCalibrationResult.MedianCorruptionDuration = Median;
|
||||
LastCalibrationResult.P95CorruptionDuration = P95;
|
||||
LastCalibrationResult.MaxCorruptionDuration = Max;
|
||||
LastCalibrationResult.AvgPeakAngleDeviation = SumAngle / PeakAngles.Num();
|
||||
LastCalibrationResult.AvgPeakPositionDeviation = SumPos / PeakPositions.Num();
|
||||
LastCalibrationResult.OutliersRemoved = OutlierCount;
|
||||
LastCalibrationResult.bValid = true;
|
||||
}
|
||||
@@ -28,8 +28,10 @@ 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_WeightedRegression UMETA(DisplayName = "Weighted Quadratic Regression", ToolTip = "Fits a weighted quadratic curve (y=a+bt+ct^2) through safe samples, capturing deceleration naturally. Blends smoothly between linear and quadratic based on acceleration significance. Falls back to linear with < 3 samples. Includes velocity-reversal clamping."),
|
||||
ARM_KalmanFilter UMETA(DisplayName = "Kalman Filter", ToolTip = "Maintains an internal state model (position + velocity, aim + angular velocity) updated only with safe samples. Predicts forward using the estimated dynamics. Best for smooth continuous tracking with optimal noise rejection. Requires tuning ProcessNoise and MeasurementNoise for best results."),
|
||||
ARM_AdaptiveExtrapolation UMETA(DisplayName = "Adaptive Extrapolation", ToolTip = "Deceleration-aware linear extrapolation. Compares recent speed (last 25% of safe window) to average speed. During steady movement: full extrapolation (zero lag). During deceleration/stop: extrapolation is reduced proportionally. Prevents overshoot on fast draw-aim-fire sequences without adding lag during normal tracking. Tuning: AdaptiveSensitivity controls the power curve (1=linear, 2=aggressive, 0.5=gentle).")
|
||||
};
|
||||
|
||||
USTRUCT()
|
||||
@@ -42,6 +44,33 @@ struct FTimestampedTransform
|
||||
FVector Aim = FVector::ForwardVector;
|
||||
};
|
||||
|
||||
// Stores calibration measurement results from a sequence of shots
|
||||
USTRUCT()
|
||||
struct FCalibrationResult
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
float RecommendedDiscardTime = 0.0f;
|
||||
float RecommendedBufferTime = 0.0f;
|
||||
float RecommendedKalmanProcessNoise = 0.0f;
|
||||
float RecommendedKalmanMeasurementNoise = 0.0f;
|
||||
float MedianCorruptionDuration = 0.0f;
|
||||
float P95CorruptionDuration = 0.0f;
|
||||
float MaxCorruptionDuration = 0.0f;
|
||||
float AvgPeakAngleDeviation = 0.0f;
|
||||
float AvgPeakPositionDeviation = 0.0f;
|
||||
int32 TotalShots = 0;
|
||||
int32 OutliersRemoved = 0;
|
||||
bool bValid = false;
|
||||
};
|
||||
|
||||
// Raw buffer snapshot captured at shot time for offline analysis
|
||||
struct FCalibrationShotData
|
||||
{
|
||||
TArray<FTimestampedTransform> BufferSnapshot;
|
||||
double ShotTime = 0.0;
|
||||
};
|
||||
|
||||
UCLASS(Blueprintable, ClassGroup = (Custom), hidecategories = (Object, LOD, Physics, Lighting, TextureStreaming, Collision, HLOD, Mobile, VirtualTexture, ComponentReplication), editinlinenew, meta = (BlueprintSpawnableComponent))
|
||||
class EASYBALLISTICS_API UEBBarrel : public UPrimitiveComponent
|
||||
{
|
||||
@@ -52,20 +81,55 @@ 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;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "PROSERVE") int nextFireEventID = 100.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") bool shotTrace = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug") float DebugArrowSize = 100.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "PROSERVE", meta = (ToolTip = "Next fire event ID to assign to spawned bullets. Auto-incremented after each shot. Used to correlate fire events with impact callbacks. Starting value should be unique per barrel to avoid ID collisions in multiplayer."))
|
||||
int nextFireEventID = 100.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "When true, draws a red debug line from the barrel muzzle along the aim direction each time a bullet is spawned. Useful for verifying barrel alignment and aim direction. Persists for 3 seconds."))
|
||||
bool shotTrace = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Length (cm) of the editor-only debug arrow drawn at the barrel's location in the viewport. Shows the barrel's forward direction. Only visible in editor, not in-game. Default: 100 cm.", EditCondition = "shotTrace"))
|
||||
float DebugArrowSize = 100.0f;
|
||||
|
||||
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;
|
||||
float DebugAntiRecoilLineLength = 400.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Thickness of the debug aim lines", EditCondition = "DebugAntiRecoil"))
|
||||
float DebugAntiRecoilLineThickness = 0.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Calibration", meta = (ToolTip = "When enabled, automatically measures recoil corruption over sequences of CalibrationShotCount shots. After each sequence, displays recommended DiscardTime, BufferTime, and Kalman parameters on HUD. Loops automatically until disabled. No thresholds needed — uses statistical 3-sigma auto-detection."))
|
||||
bool CalibrateAntiRecoil = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|Calibration", meta = (ToolTip = "Number of shots per calibration sequence.", EditCondition = "CalibrateAntiRecoil", ClampMin = "3", ClampMax = "50"))
|
||||
int32 CalibrationShotCount = 10;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Show real-time anti-recoil prediction HUD: speed ratio, confidence, position/aim errors, extrapolation time."))
|
||||
bool DebugAntiRecoilHUD = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|CSV Recording", meta = (ToolTip = "Record per-frame prediction data to CSV for offline analysis. File saved to project Saved/Logs/ folder. Toggle off to stop and close the file."))
|
||||
bool RecordPredictionCSV = false;
|
||||
|
||||
// CSV recording state (not exposed)
|
||||
bool bCSVFileOpen = false;
|
||||
FString CSVFilePath;
|
||||
IFileHandle* CSVFileHandle = nullptr;
|
||||
|
||||
// Debug HUD state (written by const prediction functions, read by TickComponent)
|
||||
mutable float DbgPosRatio = 0.0f;
|
||||
mutable float DbgAimRatio = 0.0f;
|
||||
mutable float DbgPosConfidence = 0.0f;
|
||||
mutable float DbgAimConfidence = 0.0f;
|
||||
mutable float DbgAvgPosSpeed = 0.0f;
|
||||
mutable float DbgAvgAimSpeed = 0.0f;
|
||||
mutable float DbgRecentPosSpeed = 0.0f;
|
||||
mutable float DbgRecentAimSpeed = 0.0f;
|
||||
mutable float DbgExtrapolationTime = 0.0f;
|
||||
float DbgPosError = 0.0f;
|
||||
float DbgAimError = 0.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Enable IMU shock simulation for testing anti-recoil prediction without firing"))
|
||||
bool DebugSimulateIMUShock = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug|IMU Shock Simulation", meta = (ToolTip = "Angular perturbation intensity in degrees", EditCondition = "DebugSimulateIMUShock", ClampMin = "0.0"))
|
||||
@@ -86,8 +150,8 @@ public:
|
||||
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 = "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_WeightedRegression || 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;
|
||||
@@ -95,6 +159,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 = 1.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Dead zone for deceleration detection. Speed ratios (recent/avg) above this value are treated as 1.0 (no correction). Only ratios below trigger extrapolation reduction. Higher = more tolerant to natural speed fluctuations (less false positives). Lower = more sensitive to deceleration. 0.8 = ignore normal jitter, only react to real braking.", EditCondition = "AntiRecoilMode == EAntiRecoilMode::ARM_AdaptiveExtrapolation", ClampMin = "0.0", ClampMax = "0.95"))
|
||||
float AdaptiveDeadZone = 0.8f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "AntiRecoil", meta = (ToolTip = "Velocity damping during extrapolation. 0 = disabled (default). Higher values cause extrapolated velocity to decay exponentially toward zero over the discard window. Reduces overshoot on fast draw-aim-fire sequences where the user stops moving before firing. Applies to all prediction modes except Buffer. Typical range: 5-15.", ClampMin = "0.0", ClampMax = "50.0"))
|
||||
float ExtrapolationDamping = 0.0f;
|
||||
|
||||
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);
|
||||
@@ -105,36 +178,58 @@ public:
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Maximum of random multiplier applied to bullet muzzle velocity")) float MuzzleVelocityMultiplierMax = 1.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum fire rate, rounds per second")) float FireRateMin = 1.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Maximum fire rate, rounds per second, set to same number as FireRateMin to disable randomization")) float FireRateMax = 1.0f;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Weapon") EFireMode FireMode = EFireMode::FM_Auto;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Weapon") bool ShootingBlocked;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Current fire mode. Full Auto: fires continuously while trigger held. Semiauto: one shot per trigger pull. Burst: fires BurstCount rounds per trigger pull. Interruptible Burst: burst that stops if trigger is released. Manual: requires explicit Charge() call between shots. Slam Fire: fires on trigger release. Gatling: requires spool-up before firing, rate controlled by GatlingRPS."))
|
||||
EFireMode FireMode = EFireMode::FM_Auto;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "When true, the barrel cannot fire. Use to temporarily disable shooting (e.g. during reload animation, weapon switching, or game state lock). Replicated so the server can block client firing."))
|
||||
bool ShootingBlocked;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Number of rounds auto fired in burst mode")) int BurstCount = 3;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Minimum time between bursts")) float BurstCooldown = 0.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Automatically spin up gatling when trigger is being held down")) bool GatlingAutoSpool = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon") float GatlingSpoolUpTime = 1.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon") float GatlingSpoolDownTime = 1.0f;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Weapon") float GatlingPhase = 0.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Time (seconds) for the gatling barrel to spool from idle to full firing speed. During spool-up, the fire rate ramps up linearly. Only used in FM_Gatling fire mode. Default: 1.0s.", ClampMin = "0.01", EditCondition = "FireMode == EFireMode::FM_Gatling"))
|
||||
float GatlingSpoolUpTime = 1.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Weapon", meta = (ToolTip = "Time (seconds) for the gatling barrel to spool down from full speed to idle after trigger release. During spool-down, the fire rate decreases linearly and the weapon continues firing. Default: 1.0s.", ClampMin = "0.01", EditCondition = "FireMode == EFireMode::FM_Gatling"))
|
||||
float GatlingSpoolDownTime = 1.0f;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Weapon", meta = (ToolTip = "Current gatling spool phase (0.0-1.0). 0 = idle, 1 = full speed. Automatically updated during spool up/down. Read this to drive barrel spin animations or sound pitch. Do not set manually unless you want to force a specific spool state."))
|
||||
float GatlingPhase = 0.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Ammo") bool CycleAmmo = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Ammo", meta = (EditCondition = "CycleAmmo")) bool CycleAmmoUnlimited = true;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Ammo") TArray<TSubclassOf<class AEBBullet>> Ammo;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Ammo", meta = (EditCondition = "CycleAmmo")) int CycleAmmoCount;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Ammo", meta = (EditCondition = "CycleAmmo")) int CycleAmmoPos;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Ammo", meta = (ToolTip = "When true, the barrel automatically cycles through the Ammo array, chambering the next bullet class after each shot. When false, you must manually set ChamberedBullet. Enables magazine/belt-fed behavior with mixed ammo types."))
|
||||
bool CycleAmmo = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Ammo", meta = (ToolTip = "When true, ammo is never depleted — the barrel loops through the Ammo array indefinitely. When false, ammo count decreases with each shot and the AmmoDepleted event fires when empty. Disable for realistic magazine management.", EditCondition = "CycleAmmo"))
|
||||
bool CycleAmmoUnlimited = true;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Ammo", meta = (ToolTip = "Array of bullet classes defining the ammo belt/magazine. Each entry is a bullet class (AEBBullet subclass). The barrel cycles through this list in order. Multiple entries of the same class simulate a uniform magazine. Different classes allow mixed ammo (e.g. tracer every 5th round)."))
|
||||
TArray<TSubclassOf<class AEBBullet>> Ammo;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Ammo", meta = (ToolTip = "Number of rounds remaining before ammo depletion. Only used when CycleAmmoUnlimited is false. Decremented after each shot. When it reaches 0, the AmmoDepleted event fires and the barrel stops firing. Set via SetAmmo() or directly.", EditCondition = "CycleAmmo"))
|
||||
int CycleAmmoCount;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, EditAnywhere, Category = "Ammo", meta = (ToolTip = "Current index in the Ammo array. Determines which bullet class is chambered next. Wraps around to 0 when reaching the end of the array. Replicated so server and clients agree on ammo type.", EditCondition = "CycleAmmo"))
|
||||
int CycleAmmoPos;
|
||||
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") TSubclassOf<class AEBBullet> ChamberedBullet;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") bool Shooting;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState") bool Spooling = false;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Weapon") float GatlingRPS = 0.0f;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState", meta = (ToolTip = "The bullet class currently chambered and ready to fire. Set automatically by CycleAmmo or manually via Charge(). If null, the barrel cannot fire. Replicated so server and clients agree on the next round type."))
|
||||
TSubclassOf<class AEBBullet> ChamberedBullet;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState", meta = (ToolTip = "True while the trigger is held down (for auto/gatling modes) or while a burst is in progress. Read this to drive muzzle flash, sound loops, or animation states. Replicated."))
|
||||
bool Shooting;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "WeaponState", meta = (ToolTip = "True while the gatling barrel is spooling (up or down). Read this to drive barrel spin-up animations and sound. Only relevant in FM_Gatling fire mode. Replicated."))
|
||||
bool Spooling = false;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "Weapon", meta = (ToolTip = "Current gatling rounds per second, computed from GatlingPhase and fire rate. Read-only runtime value. Use to drive audio or visual feedback proportional to actual firing speed."))
|
||||
float GatlingRPS = 0.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "WeaponState") bool LoadNext=true;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "WeaponState") float Cooldown;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "WeaponState") int BurstRemaining;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "WeaponState", meta = (ToolTip = "When true, the barrel automatically chambers the next round after firing. Set to false to require manual Charge() calls between shots (e.g. bolt-action, pump-action). Works with CycleAmmo to advance the ammo position."))
|
||||
bool LoadNext=true;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "WeaponState", meta = (ToolTip = "Remaining cooldown time (seconds) before the barrel can fire again. Determined by fire rate (1/FireRate). Counts down each tick. The barrel fires when this reaches 0 and the trigger is held. Read-only at runtime."))
|
||||
float Cooldown;
|
||||
UPROPERTY(BlueprintReadWrite, Category = "WeaponState", meta = (ToolTip = "Number of rounds remaining in the current burst. Only used in FM_Burst and FM_InterBurst fire modes. Set to BurstCount when a burst starts, decremented after each shot. Burst ends when this reaches 0."))
|
||||
int BurstRemaining;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ReplicateVariables=true;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ReplicateShotFiredEvents = true;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") bool ClientSideAim=false;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") float ClientAimUpdateFrequency = 15.0f;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication") float ClientAimDistanceLimit = 200.0f;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication", meta = (ToolTip = "When true, weapon state variables (Ammo, ChamberedBullet, FireMode, etc.) are replicated from server to clients. Disable only if you handle replication manually or in a single-player context."))
|
||||
bool ReplicateVariables=true;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication", meta = (ToolTip = "When true, the ShotFired event is broadcast to all clients via multicast RPC. Clients receive the event to play muzzle flash, sound, etc. Disable if you handle shot feedback through other means."))
|
||||
bool ReplicateShotFiredEvents = true;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication", meta = (ToolTip = "When true, the owning client computes the barrel aim position/direction locally and sends it to the server via RPC. The server uses the client's aim to spawn the bullet. Essential for VR/tracked controllers where the server cannot know the exact barrel orientation. When false, the server uses the barrel component's replicated transform."))
|
||||
bool ClientSideAim=false;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication", meta = (ToolTip = "How often (Hz) the owning client sends aim updates to the server when ClientSideAim is true. Higher values = more accurate server-side aim at the cost of bandwidth. 60 Hz matches typical VR tracking rates. Only relevant when ClientSideAim is enabled. Only the owning client sends these updates (observers skip the RPC).", EditCondition = "ClientSideAim", ClampMin = "1"))
|
||||
float ClientAimUpdateFrequency = 60.0f;
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Replication", meta = (ToolTip = "Maximum allowed distance (UU/cm) between the client-reported aim position and the server's barrel position. Acts as an anti-cheat clamp: if the client aim is further than this, the server clamps it. 200 = 2 meters. Set higher if the tracker has large offsets from the replicated barrel position.", EditCondition = "ClientSideAim", ClampMin = "0"))
|
||||
float ClientAimDistanceLimit = 200.0f;
|
||||
|
||||
FRandomStream RandomStream;
|
||||
|
||||
@@ -209,12 +304,24 @@ private:
|
||||
FVector DebugIMUShockAimOffset = FVector::ZeroVector;
|
||||
FVector DebugIMUShockPosOffset = FVector::ZeroVector;
|
||||
|
||||
// Debug yellow line persistence (shows uncorrected aim after shock)
|
||||
// Debug yellow line persistence (shows uncorrected raw aim after shock)
|
||||
bool DebugIMUShockLineCaptured = false;
|
||||
double DebugIMUShockLineEndTime = 0.0;
|
||||
FVector DebugIMUShockCapturedLocation = FVector::ZeroVector;
|
||||
FVector DebugIMUShockCapturedAim = FVector::ForwardVector;
|
||||
|
||||
// Debug blue line persistence (shows corrected aim retained for the shot)
|
||||
bool DebugCorrectedShotLineCaptured = false;
|
||||
double DebugCorrectedShotLineEndTime = 0.0;
|
||||
FVector DebugCorrectedShotCapturedLocation = FVector::ZeroVector;
|
||||
FVector DebugCorrectedShotCapturedAim = FVector::ForwardVector;
|
||||
|
||||
// Calibration state
|
||||
int32 CalibrationShotsCollected = 0;
|
||||
TArray<FCalibrationShotData> CalibrationShots;
|
||||
FCalibrationResult LastCalibrationResult;
|
||||
void ComputeCalibrationResult();
|
||||
|
||||
// Kalman filter state
|
||||
FVector KalmanPosition = FVector::ZeroVector;
|
||||
FVector KalmanVelocity = FVector::ZeroVector;
|
||||
@@ -230,7 +337,9 @@ private:
|
||||
void UpdateTransformHistory();
|
||||
void ComputeAntiRecoilTransform();
|
||||
void PredictLinearExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
|
||||
void PredictWeightedLinearRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
|
||||
void PredictWeightedRegression(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
|
||||
void PredictAdaptiveExtrapolation(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
|
||||
void UpdateKalmanFilter(double CurrentTime, const FVector& MeasuredLocation, const FVector& MeasuredAim);
|
||||
void PredictKalmanFilter(double CurrentTime, FVector& OutLocation, FVector& OutAim) const;
|
||||
|
||||
@@ -0,0 +1,293 @@
|
||||
// Copyright 2016 Mookie. All Rights Reserved.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "Engine/Engine.h"
|
||||
#include "Engine/World.h"
|
||||
#include "Curves/CurveFloat.h"
|
||||
#include "Kismet/KismetMathLibrary.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "DrawDebugHelpers.h"
|
||||
#include "Components/PrimitiveComponent.h"
|
||||
|
||||
#include "EBMaterialResponseMap.h"
|
||||
|
||||
#include "EBBullet.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EEBAtmosphereType : uint8
|
||||
{
|
||||
AT_Constant UMETA(DisplayName = "Constant"),
|
||||
AT_Curve UMETA(DisplayName = "Density Curve"),
|
||||
AT_Earth UMETA(DisplayName = "Earth/IGL")
|
||||
};
|
||||
|
||||
UCLASS(Blueprintable, BlueprintType)
|
||||
class EASYBALLISTICS_API AEBBullet : public AActor
|
||||
{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
// Sets default values for this actor's properties
|
||||
AEBBullet();
|
||||
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "State", meta = (ToolTip = "Current bullet velocity vector (cm/s). Set at spawn from muzzle velocity and direction. Updated each tick by gravity, drag, and wind. Also modified on ricochet/penetration. Replicated to clients."))
|
||||
FVector Velocity;
|
||||
UPROPERTY(Replicated, BlueprintReadWrite, Category = "State", meta = (ToolTip = "Random stream used for deterministic randomization (spread, first step delta). Seed is generated at spawn so each bullet has unique but reproducible random values. Replicated to clients."))
|
||||
FRandomStream RandomStream;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, Category = "State", meta = (ToolTip = "When true, the bullet ignores the owner and attached actors during collision traces. Automatically set to true at spawn if SafeLaunch is enabled, then cleared after SafeDelay seconds. Prevents the shooter from hitting themselves."))
|
||||
bool OwnerSafe=false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "PROSERVE", meta = (ToolTip = "Unique identifier for this fire event, used to correlate bullet spawn with impact callbacks. Set by the barrel when firing and passed through to OnImpact events."))
|
||||
int fireEventID;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Enable debug trail visualization. When true, draws colored lines along the bullet trajectory using DrawDebugLine. The color interpolates between DebugTrailColorSlow and DebugTrailColorFast based on current speed relative to muzzle velocity. Editor-only (WITH_EDITOR)."))
|
||||
bool DebugEnabled;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Duration (seconds) the debug trail lines persist on screen before fading. Higher values let you see the full trajectory longer. Default: 1.0s.", EditCondition = "DebugEnabled"))
|
||||
float DebugTrailTime=1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Width of the debug trail lines in world units. 0 = thin hairline (1px). Higher values make trails more visible at distance. The no-hit trace path always uses width 0 regardless of this setting.", EditCondition = "DebugEnabled"))
|
||||
float DebugTrailWidth=0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Trail color when the bullet is at or above muzzle velocity. The debug trail lerps between ColorSlow and ColorFast based on speed ratio. Default: green (0,1,0).", EditCondition = "DebugEnabled"))
|
||||
FLinearColor DebugTrailColorFast = FLinearColor(0, 1, 0, 1);
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Trail color when the bullet has lost most of its velocity. The debug trail lerps between ColorSlow and ColorFast based on speed ratio. Default: red (1,0,0).", EditCondition = "DebugEnabled"))
|
||||
FLinearColor DebugTrailColorSlow = FLinearColor(1, 0, 0, 1);
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Debug", meta = (ToolTip = "Enable on-screen debug messages for bullet pooling operations (spawn, recycle, pool size). Only active in editor builds (WITH_EDITOR). Useful for verifying that pooling is working correctly."))
|
||||
bool DebugPooling;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "Global wind velocity vector (cm/s). Applied as a constant offset to air-relative velocity for drag calculation. X/Y/Z components define wind direction and strength. Example: FVector(500,0,0) = 5 m/s wind along X axis. Can be overridden per-bullet via the GetWind Blueprint event."))
|
||||
FVector Wind;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Select atmosphere model")) EEBAtmosphereType AtmosphereType;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "Air Density at sea level - in KG/m^3", ClampMin = "0")) float SeaLevelAirDensity = 1.21;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "in cm/s", ClampMin = "0")) float SeaLevelSpeedOfSound = 34300;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "Used for Density Curve atmosphere model")) UCurveFloat* AirDensityCurve;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "When true, the speed of sound varies with altitude using SpeedOfSoundCurve. When false, SeaLevelSpeedOfSound is used everywhere. Affects Mach number calculation for drag. Only relevant if MachDragCurve is set."))
|
||||
bool SpeedOfSoundVariesWithAltitude = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "Float curve mapping altitude (cm, on X axis) to speed of sound (cm/s, on Y axis). Only used when SpeedOfSoundVariesWithAltitude is true. If null while the option is enabled, falls back to SeaLevelSpeedOfSound.", EditCondition = "SpeedOfSoundVariesWithAltitude"))
|
||||
UCurveFloat* SpeedOfSoundCurve;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "Scale factor applied to the world for drag calculation. 1.0 = real world scale (1 UU = 1 cm). Values >1 mean the world is larger than real scale. Affects drag deceleration: higher WorldScale = less drag effect per UU traveled. Useful for scaled-down environments."))
|
||||
float WorldScale = 1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Atmosphere pressure at 0,0,0 - in millibars", ClampMin = "0")) float SeaLevelAirPressure = 1012.5f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Atmosphere Temperature at 0,0,0 - in degrees C")) float SeaLevelAirTemperature = 20.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Temperature Decrease With Altitude, degrees per meter")) float TemperatureLapseRate = 0.00649f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Altitude at which temperature stops decreasing, in meters")) float TropopauseAltitude = 11000.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Specific Gas Constant, dry air = 287.058", ClampMin = "0")) float SpecificGasConstant = 287.058;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "World Origin Location")) FVector WorldCenterLocation = FVector(0, 0, 0);
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Use spherical planet model to get altitude")) bool SphericalAltitude = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, SaveGame, Category = "World", meta = (ToolTip = "Planet radius, in Unreal units", EditCondition = "SphericalAltitude", ClampMin = "0")) float SeaLevelRadius = 637100000.0f;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "When true, uses the custom Gravity vector below instead of the world's default gravity (Project Settings > Physics > Gravity). Enable this to simulate different gravitational conditions per bullet class."))
|
||||
bool OverrideGravity = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "World", meta = (ToolTip = "Custom gravity acceleration vector (cm/s^2). Only used when OverrideGravity is true. Default: (0,0,-980) = Earth gravity. Example: (0,0,-162) for Moon gravity, (0,0,0) for zero-G.", EditCondition = "OverrideGravity"))
|
||||
FVector Gravity = FVector(0,0,-980);
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", meta = (ToolTip = "When true, the bullet ignores its owner (and optionally attached actors) during the initial SafeDelay period. Prevents the shooter from hitting themselves or their own equipment (e.g. weapon mesh, ejected brass). Essential for first-person shooters."))
|
||||
bool SafeLaunch = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", meta = (ToolTip = "When true, the bullet also ignores the owner's attachment parent during SafeDelay. Useful when the weapon is attached to a character — the bullet won't hit the character mesh.", EditCondition = "SafeLaunch"))
|
||||
bool SafeLaunchIgnoreAttachParent = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", meta = (ToolTip = "When true, the bullet ignores ALL actors in the owner's attachment hierarchy (parent, siblings, children, recursively). Ensures the bullet passes through the entire character rig and all attached props.", EditCondition = "SafeLaunchIgnoreAttachParent"))
|
||||
bool SafeLaunchIgnoreAllAttached = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", meta = (ToolTip = "Duration (seconds) the bullet remains owner-safe after spawn. After this delay, the bullet can hit the owner. Default: 1.0s. Set higher for slow projectiles that stay near the shooter longer.", EditCondition = "SafeLaunch", ClampMin = "0"))
|
||||
float SafeDelay = 1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Safe launch", meta = (ToolTip = "Additional actors to always ignore during the SafeDelay period, beyond the owner and attachment hierarchy. Useful for ignoring specific actors like vehicles or deployable shields that the shooter is near.", EditCondition = "SafeLaunch"))
|
||||
TArray<AActor*> SafeLaunchIgnoredActors;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (ToolTip = "When true, spawning this bullet class produces multiple pellets per shot (ShotCount). Each pellet is an independent bullet with its own spread and velocity variation. The barrel fires one logical shot but multiple physical bullets."))
|
||||
bool Shotgun=false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (ToolTip = "Number of pellets spawned per shot. Each pellet is a full bullet with independent trajectory. Typical values: 8-9 for buckshot, 200+ for birdshot. Performance cost scales linearly with count.", EditCondition = "Shotgun"))
|
||||
int ShotCount=10;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (ToolTip = "Angular spread cone (radians) for pellet direction randomization. Each pellet direction is offset randomly within this cone. 0.01 rad ≈ 0.57°. Typical buckshot: 0.02-0.05 rad. This is in addition to the bullet's own Spread value.", EditCondition = "Shotgun"))
|
||||
float ShotSpread=0.01;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Shotgun", meta = (ToolTip = "Fractional velocity variation between pellets. Each pellet velocity is randomized by ±(ShotVelocitySpread * MuzzleVelocity). 0.01 = ±1% variation. Simulates real-world pellet velocity inconsistency.", EditCondition = "Shotgun"))
|
||||
float ShotVelocitySpread = 0.01;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Minimum muzzle velocity (cm/s). The actual muzzle velocity is randomized between Min and Max. Set both equal to disable randomization. Example: 9mm = ~35000 cm/s, 5.56mm = ~94000 cm/s, airsoft 6mm = ~9000 cm/s. Also used as reference for debug trail color scaling.", ClampMin = "0"))
|
||||
float MuzzleVelocityMin = 100000.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Maximum muzzle velocity (cm/s). The actual muzzle velocity is randomized between Min and Max. Set both equal to disable randomization. Also used as the upper bound for debug trail color interpolation (ColorFast).", ClampMin = "0"))
|
||||
float MuzzleVelocityMax = 100000.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Maximum bullet spread, in radians", ClampMin = "0")) float Spread = 0.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Spread bias, higher is more accurate on average", ClampMin = "0")) float SpreadBias = 0.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Bullet mass (kg). Affects drag deceleration (lighter = decelerates faster) and impulse on hit. Example: 9mm = 0.008 kg, 5.56mm = 0.004 kg, airsoft 0.2g BB = 0.0002 kg.", ClampMin = "0.0001"))
|
||||
float Mass = 0.005;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Bullet diameter (cm). Used to compute the bullet's cross-sectional area for drag calculation (pi * (D/2)^2). Example: 9mm = 0.9 cm, 5.56mm = 0.556 cm, airsoft 6mm = 0.6 cm.", ClampMin = "0.01"))
|
||||
float Diameter = 0.556;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Aerodynamic form factor multiplier for drag. 1.0 = standard sphere drag. Lower values = more streamlined (less drag), higher = blunter (more drag). Typical rifle bullet: 0.5-0.8. Combined with MachDragCurve to compute total drag coefficient."))
|
||||
float FormFactor = 1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Flight", meta = (ToolTip = "Float curve mapping Mach number (X axis) to drag coefficient multiplier (Y axis). Defines how drag varies with speed relative to the speed of sound. If null, a constant drag coefficient of 0.25 is used. Typical curve peaks near Mach 1.0 (transonic drag rise)."))
|
||||
UCurveFloat* MachDragCurve;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Exponent controlling how impact angle affects penetration and ricochet. At grazing angles, the probability/depth is interpolated using pow(angle, exponent). Higher values = sharper transition between head-on and grazing behavior. 1.0 = linear, 2.0 = quadratic (default). Affects both penetration normalization and ricochet probability blending."))
|
||||
float GrazingAngleExponent = 2.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Minimum penetration depth (cm) at head-on impact angle. The bullet traces this far through the surface to find an exit point. If the surface is thinner than this, the bullet passes through. Example: 10 cm for pistol rounds.", ClampMin = "0"))
|
||||
float MinPenetration = 10.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Maximum penetration depth (cm) at the most favorable angle (after normalization). Used with MinPenetration to define the penetration range. Thicker surfaces than this value will stop the bullet completely.", ClampMin = "0"))
|
||||
float MaxPenetration = 20.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "How much the bullet path straightens (normalizes) toward the surface normal on entry at head-on angles. 0.0 = bullet continues straight, 1.0 = bullet aligns fully with surface normal. Simulates bullet yaw/tumble on impact. 0.5 = halfway normalization."))
|
||||
float PenetrationNormalization = 0.5;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Same as PenetrationNormalization but applied at extreme grazing angles. The actual normalization is interpolated between the head-on and grazing values using GrazingAngleExponent. 0.0 = no normalization at grazing angles."))
|
||||
float PenetrationNormalizationGrazing = 0.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Random angular spread (radians) applied to bullet direction on entry into a surface. Simulates unpredictable deflection when entering material. 0.1 rad ≈ 5.7°. Set to 0 for perfectly predictable entry."))
|
||||
float PenetrationEntryAngleSpread = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Random angular spread (radians) applied to bullet direction on exit from a surface. Simulates unpredictable deflection when exiting material. 0.1 rad ≈ 5.7°. Set to 0 for predictable exit direction."))
|
||||
float PenetrationExitAngleSpread = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Base probability (0.0-1.0) of ricochet at head-on impact angle. The actual probability is interpolated between this and RicochetProbabilityGrazing based on impact angle. 0.1 = 10% chance at perpendicular impact. If the random check fails, the bullet attempts penetration instead.", ClampMin = "0", ClampMax = "1"))
|
||||
float RicochetProbability = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Probability (0.0-1.0) of ricochet at extreme grazing angles. Typically higher than RicochetProbability since shallow angles favor bouncing. 1.0 = always ricochet at grazing. The actual probability is interpolated using GrazingAngleExponent.", ClampMin = "0", ClampMax = "1"))
|
||||
float RicochetProbabilityGrazing = 1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Coefficient of restitution for ricochet (0.0-1.0). Controls how much velocity is preserved in the normal (bounce) direction. 0.0 = slides along surface, 1.0 = perfect elastic bounce. 0.1 = mostly absorbed, slight bounce. Can be overridden per-material via MaterialResponseMap."))
|
||||
float RicochetRestitution = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Friction coefficient for ricochet (0.0-1.0). Controls how much tangential (along-surface) velocity is lost on bounce. 0.0 = no friction (full slide), 1.0 = full friction (no slide). Can be overridden per-material via MaterialResponseMap."))
|
||||
float RicochetFriction = 0.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Random angular spread (radians) applied to the bullet direction after ricochet. Simulates surface roughness causing unpredictable bounce direction. 0.1 rad ≈ 5.7°. Set to 0 for perfectly predictable ricochets."))
|
||||
float RicochetSpread = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "When true, bullet speed affects ricochet probability. Slower bullets are more likely to ricochet (probability increases as velocity decreases). Simulates the fact that high-energy impacts tend to penetrate while low-energy impacts tend to deflect."))
|
||||
bool SpeedControlsRicochetProbability = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "When true, applies a physics impulse to hit actors with simulating physics components. The impulse direction follows the bullet velocity and magnitude is based on bullet mass, velocity, and ImpulseMultiplier."))
|
||||
bool AddImpulse = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Multiplier applied to the physics impulse on hit. 1.0 = realistic momentum transfer (mass * velocity). Higher values exaggerate the knockback effect. Only used when AddImpulse is true.", EditCondition = "AddImpulse"))
|
||||
float ImpulseMultiplier = 1.0;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Method used to find the bullet exit point during penetration. BackTrace: traces backward from the far side to find the exit (works for convex shapes). ByComponent: traces forward against the same component. TwoSidedGeometry: uses double-sided collision (requires two-sided mesh collision). Can be overridden per-material via MaterialResponseMap."))
|
||||
EPenTraceType DefaultPenTraceType = EPenTraceType::PT_BackTrace;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "Data asset mapping Physical Materials to per-material impact behavior overrides (penetration depth, ricochet probability, restitution, friction, spread, etc.). If null, all surfaces use the default bullet impact settings. Assign a UEBMaterialResponseMap asset to differentiate between wood, metal, concrete, flesh, etc."))
|
||||
UEBMaterialResponseMap* MaterialResponseMap;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "When true, the physical material's density property scales the penetration depth. Denser materials reduce penetration depth proportionally. When false, penetration depth is the same regardless of material density (only MaterialResponseMap overrides apply)."))
|
||||
bool MaterialDensityControlsPenetrationDepth = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Impact", meta = (ToolTip = "When true, the physical material's restitution property influences ricochet behavior. Higher material restitution = bouncier ricochets. When false, only the bullet's own RicochetRestitution and MaterialResponseMap values are used."))
|
||||
bool MaterialRestitutionControlsRicochet = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Replication", meta = (ToolTip = "When true, velocity change broadcasts after ricochet/penetration use Reliable RPC (guaranteed delivery, ordered). When false, uses Unreliable RPC (faster, may be dropped). Enable for critical gameplay bullets where trajectory changes must be seen by all clients. Disable for high-volume fire where occasional missed updates are acceptable."))
|
||||
bool ReliableReplication = false;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Allow components to collide, intended for use with trigger volumes. Do not use for actual collisions.")) bool AllowComponentCollisions = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Collision channel used for the bullet's line trace each tick. Determines which objects the bullet can hit based on collision channel responses. Typical setup: create a dedicated 'Bullet' channel in Project Settings > Collision, and configure per-object responses. Default: Visibility channel."))
|
||||
TEnumAsByte<ECollisionChannel> TraceChannel;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "When true, traces against complex (per-polygon) collision instead of simple collision shapes. More accurate for detailed meshes (e.g. hitting between bars of a fence) but significantly more expensive. Recommended false for most use cases."))
|
||||
bool TraceComplex;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Extra distance (cm) added to the trace length beyond the calculated travel distance per step. Acts as a safety margin to prevent fast bullets from tunneling through thin surfaces due to floating-point precision. Default: 1.0 cm. Increase for very fast bullets or very thin obstacles.", ClampMin = "0"))
|
||||
float CollisionMargin=1.0;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "Bullets with lower velocity will automatically despawn on impact, never despawn if set to zero or negative")) float DespawnVelocity=100.0f;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Collision", meta = (ToolTip = "List of actors permanently ignored by this bullet's traces (not time-limited like SafeLaunch). These actors are never hit regardless of SafeDelay. Useful for ignoring specific world actors like invisible blocking volumes or the weapon mesh itself."))
|
||||
TArray<AActor*> IgnoredActors;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (ToolTip = "Spawned bullet performs first trace immediately, instead of waiting for next simulation step")) bool DoFirstStepImmediately = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (EditCondition = "DoFirstStepImmediately")) bool RandomFirstStepDelta = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (ToolTip = "When true, the simulation uses a fixed timestep (FixedStepSeconds) instead of the frame delta time. Accumulated time is consumed in fixed increments. Ensures deterministic trajectories regardless of framerate. Recommended for multiplayer or replay consistency."))
|
||||
bool FixedStep = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (ToolTip = "Fixed timestep duration (seconds) when FixedStep is enabled. Smaller values = more accurate simulation but more traces per frame. 0.01 = 100 sub-steps/second. Too small may cause multiple traces per frame at low FPS.", EditCondition = "FixedStep", ClampMin = "0"))
|
||||
float FixedStepSeconds = 0.1;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Simulation", meta = (ToolTip = "Maximum number of collision traces per simulation step. Limits how many ricochets/penetrations can occur in a single step to prevent infinite bounce loops. 8 = up to 8 consecutive ricochets per tick. If the bullet still has remaining travel time after this many traces, it stops for this tick."))
|
||||
int MaxTracesPerStep = 8;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Retrace", meta = (ToolTip = "Enable lag compensation retrace. Each tick, the bullet replays the previous tick's trace before performing the current one. This 'time travel' mechanism allows the bullet to detect actors that moved into the bullet's previous path between ticks (e.g. fast-moving targets, newly spawned objects). Essential for accurate hit detection in networked games."))
|
||||
bool Retrace = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Retrace", meta = (ToolTip = "When true, the retrace uses a different collision channel (RetraceChannel) than the normal trace (TraceChannel). Useful if you want the retrace to hit different object types than the normal trace, e.g. retrace on a channel that includes lag-compensated hitboxes.", EditCondition = "Retrace"))
|
||||
bool RetraceOnAnotherChannel = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Retrace", meta = (ToolTip = "Collision channel used for the retrace when RetraceOnAnotherChannel is true. Configure this to a channel with different collision responses than TraceChannel for selective lag compensation.", EditCondition = "RetraceOnAnotherChannel"))
|
||||
TEnumAsByte<ECollisionChannel> RetraceChannel;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Rotation", meta = (ToolTip = "When true, the bullet actor's rotation is updated each tick to face its velocity direction. The roll angle is preserved (not affected by velocity). Disable if bullet rotation is irrelevant (e.g. invisible projectiles) for a minor performance gain."))
|
||||
bool RotateActor = true;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Rotation", meta = (ToolTip = "When true, the bullet starts with a random roll rotation (0-360°). Adds visual variety for visible projectiles with non-symmetric meshes or trails. Only applied at spawn, not updated during flight.", EditCondition = "RotateActor"))
|
||||
bool RotateRandomRoll = true;
|
||||
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Pooling", meta = (ToolTip = "When true, deactivated bullets are recycled into a pool instead of being destroyed. Subsequent spawns reuse pooled bullets, avoiding the cost of actor creation/destruction. Significantly improves performance for high fire rates. Pool is stored on the CDO (Class Default Object) per bullet class."))
|
||||
bool EnablePooling = false;
|
||||
UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Pooling", meta = (ToolTip = "Maximum number of inactive bullets kept in the pool per class. When the pool exceeds this size, the oldest bullet is destroyed. Higher values use more memory but reduce the chance of needing to spawn a new actor. 50 is suitable for most scenarios.", EditCondition = "EnablePooling", ClampMin = "1"))
|
||||
int MaxPoolSize = 50;
|
||||
|
||||
//rebase
|
||||
virtual void ApplyWorldOffset(const FVector& InOffset, bool bWorldShift) override;
|
||||
|
||||
// Called when the game starts or when spawned
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
// Called every frame
|
||||
virtual void Tick(float DeltaSeconds) override;
|
||||
|
||||
virtual void LifeSpanExpired() override;
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "EBBullet|Spawn")
|
||||
static void SpawnWithExactVelocity(TSubclassOf<class AEBBullet> BulletClass, AActor* BulletOwner, APawn* BulletInstigator, FVector BulletLocation, FVector BulletVelocity, int EventFireID);
|
||||
|
||||
UFUNCTION(BlueprintCallable, Category = "EBBullet|Spawn")
|
||||
static void Spawn(TSubclassOf<class AEBBullet> BulletClass, AActor* BulletOwner, APawn* BulletInstigator, FVector BulletLocation, FVector BulletVelocity, int EventFireID);
|
||||
|
||||
UFUNCTION(NetMulticast, Unreliable)
|
||||
void VelocityChangeBroadcast(FVector_NetQuantize NewLocation, FVector NewVelocity);
|
||||
UFUNCTION(NetMulticast, Reliable)
|
||||
void VelocityChangeBroadcastReliable(FVector_NetQuantize NewLocation, FVector NewVelocity);
|
||||
|
||||
UFUNCTION(BlueprintAuthorityOnly, BlueprintNativeEvent, Category = "EBBullet|Impact")
|
||||
void OnImpact(bool Ricochet, bool PassedThrough, FVector Location, FVector IncomingVelocity, FVector Normal, FVector ExitLocation, FVector ExitVelocity, FVector Impulse, float PenetrationDepth, AActor* Actor, USceneComponent* Component, FName BoneName, UPhysicalMaterial* PhysMaterial, FHitResult HitResult, int EventFireID);
|
||||
|
||||
UFUNCTION(BlueprintCosmetic, BlueprintNativeEvent, Category = "EBBullet|Impact")
|
||||
void OnNetPredictedImpact(bool Ricochet, bool PassedThrough, FVector Location, FVector IncomingVelocity, FVector Normal, FVector ExitLocation, FVector ExitVelocity, FVector Impulse, float PenetrationDepth, AActor* Actor, USceneComponent* Component, FName BoneName, UPhysicalMaterial* PhysMaterial, FHitResult HitResult, int EventFireID);
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "EBBullet|Impact")
|
||||
void OnTrace(FVector StartLocation, FVector EndLocation);
|
||||
|
||||
UFUNCTION(BlueprintImplementableEvent, Category = "EBBullet|Remote")
|
||||
void OnTrajectoryUpdateReceived(FVector Location, FVector OldVelocity, FVector NewVelocity);
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|Activation")
|
||||
void OnDeactivated();
|
||||
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|Flight")FVector UpdateVelocity(UWorld* World, FVector Location, FVector PreviousVelocity, float DeltaTime) const;
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|World") FVector GetWind(UWorld* World, FVector Location) const;
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|World") float GetAirDensity(UWorld* World, FVector Location) const;
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|World") float GetSpeedOfSound(UWorld* World, FVector Location) const;
|
||||
UFUNCTION(BlueprintNativeEvent, Category = "EBBullet|World") bool CollisionFilter(FHitResult HitResult) const;
|
||||
|
||||
//pooling
|
||||
UFUNCTION(BlueprintAuthorityOnly, BlueprintCallable, Category = "EBBullet|Pooling")void Deactivate();
|
||||
|
||||
UFUNCTION(NetMulticast, Reliable)
|
||||
void ReactivationBroadcast(FVector_NetQuantize NewLocation, FVector NewVelocity, AActor* BulletOwner, APawn* BulletInstigator, int nextFireEventID);
|
||||
UFUNCTION(NetMulticast, Reliable)
|
||||
void DeactivationBroadcast();
|
||||
private:
|
||||
UPROPERTY() TArray<TWeakObjectPtr<AEBBullet>> Pooled;
|
||||
static AEBBullet* GetFromPool(UWorld* World, UClass* BulletClass);
|
||||
static AEBBullet* SpawnOrReactivate(UWorld* World, TSubclassOf<class AEBBullet> BulletClass, const FTransform& Transform, FVector BulletVelocity, AActor* BulletOwner, APawn* BulletInstigator, int nextFireEventID);
|
||||
void DeactivateToPool();
|
||||
|
||||
void FinishSpawning(FTransform Transform);
|
||||
|
||||
void Step(float DeltaTime);
|
||||
|
||||
float Trace(FVector start, FVector PreviousVelocity, float delta, TEnumAsByte<ECollisionChannel> channel);
|
||||
|
||||
TArray<AActor*> GetAttachedActorsRecursive(AActor* Actor, uint16 Depth = 0, TArray<AActor*> VisitedActors = TArray<AActor*>()) const;
|
||||
|
||||
float PenetrationTrace(FVector start, FVector end, TWeakObjectPtr<UPrimitiveComponent,FWeakObjectPtr> comp, EPenTraceType penType, TEnumAsByte<ECollisionChannel> channel, FVector &exitLoc, FVector &exitNormal);
|
||||
|
||||
float GetCurveValue(const UCurveFloat* curve, float in, float deflt) const;
|
||||
|
||||
float AccumulatedDelta;
|
||||
|
||||
bool CanRetrace = false;
|
||||
FVector LastTraceStart;
|
||||
float LastTraceDelta;
|
||||
FVector LastTraceVelocity;
|
||||
FVector LastTracePrevVelocity;
|
||||
bool LastTraceOwnerSafe = false;
|
||||
|
||||
bool IsRecycled;
|
||||
|
||||
FHitResult FilterHits(TArray<FHitResult> Results, bool &hit) const;
|
||||
TArray<AActor*>GetSafeLaunchIgnoredActors(AActor* Owner) const;
|
||||
|
||||
float GetAltitude(UWorld* World, FVector Location) const;
|
||||
float GetAltitudePressure(float AltitudeMeter) const;
|
||||
float GetAltitudeTemperature(float AltitudeMeter) const;
|
||||
float GetAltitudeDensity(float AltitudeMeter) const;
|
||||
|
||||
#ifdef WITH_EDITOR
|
||||
FLinearColor GetDebugColor(float In) const{
|
||||
return FMath::Lerp(DebugTrailColorSlow, DebugTrailColorFast, In);
|
||||
}
|
||||
#endif
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2016 Mookie. All Rights Reserved.
|
||||
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
#include "Engine/DataAsset.h"
|
||||
#include "PhysicalMaterials/PhysicalMaterial.h"
|
||||
#include "EBMaterialResponseMap.generated.h"
|
||||
|
||||
UENUM(BlueprintType)
|
||||
enum class EPenTraceType : uint8
|
||||
{
|
||||
PT_BackTrace UMETA(DisplayName = "Back Trace"),
|
||||
PT_ByComponent UMETA(DisplayName = "By Component"),
|
||||
PT_TwoSidedGeometry UMETA(DisplayName = "Double Sided Geometry"),
|
||||
};
|
||||
|
||||
USTRUCT(BlueprintType)
|
||||
struct FEBMaterialResponseMapEntry {
|
||||
GENERATED_USTRUCT_BODY()
|
||||
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Method used to find the bullet exit point for this material. BackTrace: traces backward from max penetration depth. ByComponent: traces forward against the same component. TwoSidedGeometry: uses double-sided collision. Overrides the bullet's DefaultPenTraceType when this material is hit."))
|
||||
EPenTraceType PenTraceType = EPenTraceType::PT_BackTrace;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "When true, bullets never penetrate this material regardless of thickness or bullet energy. The bullet either ricochets or stops. Use for impenetrable surfaces like thick steel or armored walls."))
|
||||
bool NeverPenetrate = false;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Multiplier applied to the bullet's penetration depth when hitting this material. 1.0 = use bullet defaults. <1.0 = harder to penetrate (e.g. 0.5 for steel = half penetration depth). >1.0 = easier to penetrate (e.g. 2.0 for drywall). Only used when NeverPenetrate is false.", ClampMin = "0"))
|
||||
float PenetrationDepthMultiplier = 1.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Per-material override for penetration normalization at head-on angles. How much the bullet path straightens toward surface normal inside this material. 0.0 = no normalization (bullet goes straight through). If non-zero, overrides the bullet's PenetrationNormalization for this material."))
|
||||
float PenetrationNormalization = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Per-material override for penetration normalization at grazing angles. If non-zero, overrides the bullet's PenetrationNormalizationGrazing for this material. Interpolated with the head-on value using GrazingAngleExponent."))
|
||||
float PenetrationNormalizationGrazing = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Per-material random angular spread (radians) applied to bullet direction on entry. If non-zero, overrides the bullet's PenetrationEntryAngleSpread for this material. Simulates material-specific deflection on entry (e.g. higher for wood grain, lower for glass)."))
|
||||
float PenetrationEntryAngleSpread = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Per-material random angular spread (radians) applied to bullet direction on exit. If non-zero, overrides the bullet's PenetrationExitAngleSpread for this material. Simulates material-specific deflection on exit."))
|
||||
float PenetrationExitAngleSpread = 0.0;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "When true, bullets never ricochet off this material. They either penetrate or stop. Use for soft materials like flesh, sand, or foam that absorb impacts without bouncing."))
|
||||
bool NeverRicochet = false;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Multiplier applied to the bullet's ricochet probability when hitting this material. 1.0 = use bullet defaults. >1.0 = more likely to ricochet (e.g. 2.0 for polished metal). <1.0 = less likely (e.g. 0.3 for earth). Only used when NeverRicochet is false.", ClampMin = "0"))
|
||||
float RicochetProbabilityMultiplier = 1.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Per-material coefficient of restitution for ricochet (0.0-1.0). Controls bounce energy in the normal direction. 0.0 = no bounce, 1.0 = perfect elastic bounce. How much this value is used depends on RicochetRestitutionInfluence."))
|
||||
float RicochetRestitution = 0.5f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Blend factor (0.0-1.0) controlling how much this material's RicochetRestitution overrides the bullet's own restitution. 0.0 = use bullet's value entirely. 1.0 = use this material's value entirely. 0.5 = average of both.", ClampMin = "0", ClampMax = "1"))
|
||||
float RicochetRestitutionInfluence = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Per-material friction coefficient for ricochet (0.0-1.0). Controls tangential velocity loss on bounce. 0.0 = no friction (full slide), 1.0 = full friction. How much this value is used depends on RicochetFrictionInfluence."))
|
||||
float RicochetFriction = 0.5f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Blend factor (0.0-1.0) controlling how much this material's RicochetFriction overrides the bullet's own friction. 0.0 = use bullet's value entirely. 1.0 = use this material's value entirely.", ClampMin = "0", ClampMax = "1"))
|
||||
float RicochetFrictionInfluence = 0.0f;
|
||||
UPROPERTY(EditAnywhere, Category = "Material", meta = (ToolTip = "Per-material random angular spread (radians) applied after ricochet. If non-zero, overrides the bullet's RicochetSpread for this material. Higher values simulate rougher surfaces causing more unpredictable bounces."))
|
||||
float RicochetSpread = 0.0f;
|
||||
};
|
||||
|
||||
UCLASS(BlueprintType)
|
||||
class UEBMaterialResponseMap : public UDataAsset{
|
||||
GENERATED_BODY()
|
||||
|
||||
public:
|
||||
UPROPERTY(EditAnywhere, Category = "Responses", meta = (ToolTip = "Maps Unreal Physical Materials to per-material impact behavior overrides. Each entry defines how bullets interact with a specific surface type (penetration, ricochet, spread). Physical Materials are assigned to mesh surfaces in material instances. Materials not in this map use the bullet's default impact settings."))
|
||||
TMap<UPhysicalMaterial*, FEBMaterialResponseMapEntry> Map;
|
||||
};
|
||||
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