omath::projectile_prediction::ProjPredEngineLegacy — Legacy trait-based aim solver
Header:
omath/projectile_prediction/proj_pred_engine_legacy.hppNamespace:omath::projectile_predictionInherits:ProjPredEngineInterfaceTemplate param (default):EngineTrait = source_engine::PredEngineTraitPurpose: compute a world-space aim point to hit a (possibly moving) target using a discrete time scan and a closed-form ballistic pitch under constant gravity.
Overview
ProjPredEngineLegacy is a portable, trait-driven projectile lead solver. At each simulation time step t it:
- Predicts target position with
EngineTrait::predict_target_position(target, t, g). - Computes launch pitch via a gravity-aware closed form (or a direct angle if gravity is zero).
- Validates that a projectile fired with that pitch (and direct yaw) actually reaches the predicted target within a distance tolerance at time
t. - On success, returns an aim point computed by
EngineTrait::calc_viewpoint_from_angles(...).
If no time step yields a feasible solution up to maximum_simulation_time, returns std::nullopt.
API
template<class EngineTrait = source_engine::PredEngineTrait>
requires PredEngineConcept<EngineTrait>
class ProjPredEngineLegacy final : public ProjPredEngineInterface {
public:
ProjPredEngineLegacy(float gravity_constant,
float simulation_time_step,
float maximum_simulation_time,
float distance_tolerance);
[[nodiscard]]
std::optional<Vector3<float>>
maybe_calculate_aim_point(const Projectile& projectile,
const Target& target) const override;
private:
// Closed-form ballistic pitch solver (internal)
std::optional<float>
maybe_calculate_projectile_launch_pitch_angle(const Projectile& projectile,
const Vector3<float>& target_position) const noexcept;
bool is_projectile_reached_target(const Vector3<float>& target_position,
const Projectile& projectile,
float pitch, float time) const noexcept;
const float m_gravity_constant;
const float m_simulation_time_step;
const float m_maximum_simulation_time;
const float m_distance_tolerance;
};
Constructor parameters
gravity_constant— magnitude of gravity (e.g.,9.81f), world units/s².simulation_time_step— Δt for the scan (e.g.,1/240.f).maximum_simulation_time— search horizon in seconds.distance_tolerance— max allowed miss distance at timetto accept a solution.
Trait requirements (PredEngineConcept)
Your EngineTrait must expose noexcept static methods with these signatures:
Vector3<float> predict_projectile_position(const Projectile&, float pitch_deg, float yaw_deg,
float time, float gravity) noexcept;
Vector3<float> predict_target_position(const Target&, float time, float gravity) noexcept;
float calc_vector_2d_distance(const Vector3<float>& v) noexcept; // typically length in XZ plane
float get_vector_height_coordinate(const Vector3<float>& v) noexcept; // typically Y
Vector3<float> calc_viewpoint_from_angles(const Projectile&, Vector3<float> target,
std::optional<float> maybe_pitch_deg) noexcept;
float calc_direct_pitch_angle(const Vector3<float>& from, const Vector3<float>& to) noexcept;
float calc_direct_yaw_angle (const Vector3<float>& from, const Vector3<float>& to) noexcept;
This design lets you adapt different game/physics conventions (axes, units, handedness) without changing the solver.
Algorithm details
Time scan
For t = 0 .. maximum_simulation_time in steps of simulation_time_step:
T = EngineTrait::predict_target_position(target, t, g)-
pitch = maybe_calculate_projectile_launch_pitch_angle(projectile, T)- If
std::nullopt: continue yaw = EngineTrait::calc_direct_yaw_angle(projectile.m_origin, T)P = EngineTrait::predict_projectile_position(projectile, pitch, yaw, t, g)- Accept if
|P - T| <= distance_tolerance - Return
EngineTrait::calc_viewpoint_from_angles(projectile, T, pitch)
- If
Closed-form pitch (gravity on)
Implements the classic ballistic formula (low-arc branch), where:
v= muzzle speed,g=gravity_constant * projectile.m_gravity_scale,x= horizontal (2D) distance to target,y= vertical offset to target.
[ \theta ;=; \arctan!\left(\frac{v^{2} ;-; \sqrt{v^{4}-g!\left(gx^{2}+2yv^{2}\right)}}{gx}\right) ]
- If the discriminant ( v^{4}-g(gx^{2}+2yv^{2}) < 0 ) ⇒ no real solution.
- If
g == 0, falls back toEngineTrait::calc_direct_pitch_angle(...). - Returns degrees (internally converts from radians).
Usage example
using namespace omath::projectile_prediction;
ProjPredEngineLegacy solver(
/*gravity*/ 9.81f,
/*dt*/ 1.f / 240.f,
/*Tmax*/ 3.0f,
/*tol*/ 0.05f
);
Projectile proj; // fill: m_origin, m_launch_speed, m_gravity_scale, etc.
Target tgt; // fill: position/velocity as required by your trait
if (auto aim = solver.maybe_calculate_aim_point(proj, tgt)) {
// Drive your turret/reticle toward *aim
} else {
// No feasible intercept in the given horizon
}
Behavior & edge cases
- Zero gravity or zero distance: uses direct pitch toward the target.
- Negative discriminant in the pitch formula: returns
std::nulloptfor that time step. - Very small
x(horizontal distance): the formula’s denominatorgxapproaches zero; your trait’s direct pitch helper provides a stable fallback. - Tolerance:
distance_tolerancecontrols acceptance; tighten for accuracy, loosen for robustness.
Complexity & tuning
- Time: O(T) where ( T \approx \frac{\text{maximum_simulation_time}}{\text{simulation_time_step}} ) plus trait costs for prediction and angle math per step.
- Smaller
simulation_time_stepimproves precision but increases runtime. - If needed, do a coarse-to-fine search: coarse Δt scan, then refine around the best hit time.
Testing checklist
- Stationary, level target → pitch ≈ 0 for short ranges; accepted within tolerance.
- Elevated/depressed targets → pitch positive/negative as expected.
- Receding fast target → unsolved within horizon ⇒
nullopt. - Gravity scale = 0 → identical to straight-line solution.
- Near-horizon shots (large range, small arc) → discriminant near zero; verify stability.
Notes
- All angles produced/consumed by the trait in this implementation are degrees.
calc_viewpoint_from_anglesdefines what “aim point” means in your engine (e.g., a point along the initial ray or the predicted impact point). Keep this consistent with your HUD/reticle.
Last updated: 1 Nov 2025