fixed projectile prediction for double

This commit is contained in:
2026-04-25 21:05:00 +03:00
parent 4c65781c6f
commit 9234704010
17 changed files with 260 additions and 236 deletions

View File

@@ -44,46 +44,46 @@ static void expect_matrix_near(const MatT& a, const MatT& b, float eps = 1e-5f)
#include <omath/engines/cry_engine/traits/pred_engine_trait.hpp>
// Helper: verify that zero offset matches default-initialized offset behavior
template<typename Trait>
static void verify_launch_offset_at_time_zero(const Vector3<float>& origin, const Vector3<float>& offset)
template<typename Trait, typename AT = float>
static void verify_launch_offset_at_time_zero(const Vector3<AT>& origin, const Vector3<AT>& offset)
{
projectile_prediction::Projectile p;
projectile_prediction::Projectile<AT> p;
p.m_origin = origin;
p.m_launch_offset = offset;
p.m_launch_speed = 100.f;
p.m_gravity_scale = 1.f;
p.m_launch_speed = static_cast<AT>(100);
p.m_gravity_scale = static_cast<AT>(1);
const auto pos = Trait::predict_projectile_position(p, 0.f, 0.f, 0.f, 9.81f);
const auto pos = Trait::predict_projectile_position(p, AT{0}, AT{0}, AT{0}, static_cast<AT>(9.81));
const auto expected = origin + offset;
EXPECT_NEAR(pos.x, expected.x, 1e-4f);
EXPECT_NEAR(pos.y, expected.y, 1e-4f);
EXPECT_NEAR(pos.z, expected.z, 1e-4f);
EXPECT_NEAR(static_cast<double>(pos.x), static_cast<double>(expected.x), 1e-4);
EXPECT_NEAR(static_cast<double>(pos.y), static_cast<double>(expected.y), 1e-4);
EXPECT_NEAR(static_cast<double>(pos.z), static_cast<double>(expected.z), 1e-4);
}
template<typename Trait>
template<typename Trait, typename AT = float>
static void verify_zero_offset_matches_default()
{
projectile_prediction::Projectile p;
p.m_origin = {10.f, 20.f, 30.f};
p.m_launch_offset = {0.f, 0.f, 0.f};
p.m_launch_speed = 50.f;
p.m_gravity_scale = 1.f;
projectile_prediction::Projectile<AT> p;
p.m_origin = {static_cast<AT>(10), static_cast<AT>(20), static_cast<AT>(30)};
p.m_launch_offset = {};
p.m_launch_speed = static_cast<AT>(50);
p.m_gravity_scale = static_cast<AT>(1);
projectile_prediction::Projectile p2;
p2.m_origin = {10.f, 20.f, 30.f};
p2.m_launch_speed = 50.f;
p2.m_gravity_scale = 1.f;
projectile_prediction::Projectile<AT> p2;
p2.m_origin = {static_cast<AT>(10), static_cast<AT>(20), static_cast<AT>(30)};
p2.m_launch_speed = static_cast<AT>(50);
p2.m_gravity_scale = static_cast<AT>(1);
const auto pos1 = Trait::predict_projectile_position(p, 15.f, 30.f, 1.f, 9.81f);
const auto pos2 = Trait::predict_projectile_position(p2, 15.f, 30.f, 1.f, 9.81f);
const auto pos1 = Trait::predict_projectile_position(p, static_cast<AT>(15), static_cast<AT>(30), static_cast<AT>(1), static_cast<AT>(9.81));
const auto pos2 = Trait::predict_projectile_position(p2, static_cast<AT>(15), static_cast<AT>(30), static_cast<AT>(1), static_cast<AT>(9.81));
#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64)
constexpr float tol = 1e-6f;
constexpr double tol = 1e-6;
#else
constexpr float tol = 1e-4f;
constexpr double tol = 1e-4;
#endif
EXPECT_NEAR(pos1.x, pos2.x, tol);
EXPECT_NEAR(pos1.y, pos2.y, tol);
EXPECT_NEAR(pos1.z, pos2.z, tol);
EXPECT_NEAR(static_cast<double>(pos1.x), static_cast<double>(pos2.x), tol);
EXPECT_NEAR(static_cast<double>(pos1.y), static_cast<double>(pos2.y), tol);
EXPECT_NEAR(static_cast<double>(pos1.z), static_cast<double>(pos2.z), tol);
}
TEST(LaunchOffsetTests, Source_OffsetAtTimeZero)
@@ -128,11 +128,11 @@ TEST(LaunchOffsetTests, Unity_ZeroOffsetMatchesDefault)
}
TEST(LaunchOffsetTests, Unreal_OffsetAtTimeZero)
{
verify_launch_offset_at_time_zero<unreal_engine::PredEngineTrait>({0, 0, 0}, {5, 3, -2});
verify_launch_offset_at_time_zero<unreal_engine::PredEngineTrait, double>({0, 0, 0}, {5, 3, -2});
}
TEST(LaunchOffsetTests, Unreal_ZeroOffsetMatchesDefault)
{
verify_zero_offset_matches_default<unreal_engine::PredEngineTrait>();
verify_zero_offset_matches_default<unreal_engine::PredEngineTrait, double>();
}
TEST(LaunchOffsetTests, CryEngine_OffsetAtTimeZero)
{
@@ -401,38 +401,38 @@ TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
{
namespace e = omath::unreal_engine;
projectile_prediction::Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
projectile_prediction::Projectile<double> p;
p.m_origin = {0.0, 0.0, 0.0};
p.m_launch_speed = 10.0;
p.m_gravity_scale = 1.0;
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.f, 0.f, 1.f, 9.81f);
EXPECT_NEAR(pos.x, 10.f, 1e-4f);
EXPECT_NEAR(pos.y, -9.81f * 0.5f, 1e-4f);
const auto pos = e::PredEngineTrait::predict_projectile_position(p, 0.0, 0.0, 1.0, 9.81);
EXPECT_NEAR(pos.x, 10.0, 1e-4);
EXPECT_NEAR(pos.y, -9.81 * 0.5, 1e-4);
projectile_prediction::Target t;
t.m_origin = {0.f, 5.f, 0.f};
t.m_velocity = {2.f, 0.f, 0.f};
projectile_prediction::Target<double> t;
t.m_origin = {0.0, 5.0, 0.0};
t.m_velocity = {2.0, 0.0, 0.0};
t.m_is_airborne = true;
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred.x, 4.f, 1e-6f);
EXPECT_NEAR(pred.y, 5.f - 9.81f * (2.f * 2.f) * 0.5f, 1e-6f);
const auto pred = e::PredEngineTrait::predict_target_position(t, 2.0, 9.81);
EXPECT_NEAR(pred.x, 4.0, 1e-6);
EXPECT_NEAR(pred.y, 5.0 - 9.81 * (2.0 * 2.0) * 0.5, 1e-6);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.f, 0.f, 4.f}), 5.f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.f, 2.5f, 3.f}), 2.5f, 1e-6f);
EXPECT_NEAR(e::PredEngineTrait::calc_vector_2d_distance({3.0, 0.0, 4.0}), 5.0, 1e-6);
EXPECT_NEAR(e::PredEngineTrait::get_vector_height_coordinate({1.0, 2.5, 3.0}), 2.5, 1e-6);
std::optional<float> pitch = 45.f;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, {10.f, 0.f, 0.f}, pitch);
EXPECT_NEAR(vp.z, 0.f + 10.f * std::tan(angles::degrees_to_radians(45.f)), 1e-6f);
std::optional<double> pitch = 45.0;
auto vp = e::PredEngineTrait::calc_viewpoint_from_angles(p, Vector3<double>{10.0, 0.0, 0.0}, pitch);
EXPECT_NEAR(vp.z, 0.0 + 10.0 * std::tan(angles::degrees_to_radians(45.0)), 1e-6);
Vector3<float> origin{0.f, 0.f, 0.f};
Vector3<float> view_to{1.f, 1.f, 1.f};
Vector3<double> origin{0.0, 0.0, 0.0};
Vector3<double> view_to{1.0, 1.0, 1.0};
const auto pitch_calc = e::PredEngineTrait::calc_direct_pitch_angle(origin, view_to);
const auto dir = (view_to - origin).normalized();
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3f);
EXPECT_NEAR(pitch_calc, angles::radians_to_degrees(std::asin(dir.z)), 1e-3);
const auto yaw_calc = e::PredEngineTrait::calc_direct_yaw_angle(origin, view_to);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3f);
EXPECT_NEAR(yaw_calc, angles::radians_to_degrees(std::atan2(dir.y, dir.x)), 1e-3);
e::ViewAngles va;
expect_matrix_near(e::MeshTrait::rotation_matrix(va), e::rotation_matrix(va));
@@ -448,8 +448,8 @@ TEST(TraitTests, Unreal_Pred_And_Mesh_And_Camera)
// non-airborne
t.m_is_airborne = false;
const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.f, 9.81f);
EXPECT_NEAR(pred_ground_unreal.x, 4.f, 1e-6f);
const auto pred_ground_unreal = e::PredEngineTrait::predict_target_position(t, 2.0, 9.81);
EXPECT_NEAR(pred_ground_unreal.x, 4.0, 1e-6);
}
// ── NDC Depth Range tests for Source and CryEngine camera traits ────────────

View File

@@ -7,9 +7,12 @@
using namespace omath;
using namespace omath::source_engine;
using Projectile = projectile_prediction::Projectile<float>;
using Target = projectile_prediction::Target<float>;
TEST(PredEngineTrait, PredictProjectilePositionBasic)
{
projectile_prediction::Projectile p;
Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
p.m_gravity_scale = 1.f;
@@ -23,7 +26,7 @@ TEST(PredEngineTrait, PredictProjectilePositionBasic)
TEST(PredEngineTrait, PredictTargetPositionAirborne)
{
projectile_prediction::Target t;
Target t;
t.m_origin = {0.f, 0.f, 10.f};
t.m_velocity = {1.f, 0.f, 0.f};
t.m_is_airborne = true;
@@ -42,7 +45,7 @@ TEST(PredEngineTrait, CalcVector2dDistance)
TEST(PredEngineTrait, CalcViewpointFromAngles)
{
projectile_prediction::Projectile p;
Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_speed = 10.f;
@@ -55,7 +58,7 @@ TEST(PredEngineTrait, CalcViewpointFromAngles)
TEST(PredEngineTrait, PredictProjectilePositionWithLaunchOffset)
{
projectile_prediction::Projectile p;
Projectile p;
p.m_origin = {0.f, 0.f, 0.f};
p.m_launch_offset = {5.f, 3.f, -2.f};
p.m_launch_speed = 10.f;
@@ -76,13 +79,13 @@ TEST(PredEngineTrait, PredictProjectilePositionWithLaunchOffset)
TEST(PredEngineTrait, ZeroLaunchOffsetMatchesOriginalBehavior)
{
projectile_prediction::Projectile p;
Projectile p;
p.m_origin = {10.f, 20.f, 30.f};
p.m_launch_offset = {0.f, 0.f, 0.f};
p.m_launch_speed = 15.f;
p.m_gravity_scale = 0.5f;
projectile_prediction::Projectile p_no_offset;
Projectile p_no_offset;
p_no_offset.m_origin = {10.f, 20.f, 30.f};
p_no_offset.m_launch_speed = 15.f;
p_no_offset.m_gravity_scale = 0.5f;

View File

@@ -1,14 +1,19 @@
#include <gtest/gtest.h>
#include <omath/projectile_prediction/proj_pred_engine_legacy.hpp>
#include <omath/engines/source_engine/traits/camera_trait.hpp>
using Projectile = omath::projectile_prediction::Projectile<float>;
using Target = omath::projectile_prediction::Target<float>;
using Engine = omath::projectile_prediction::ProjPredEngineLegacy<>;
TEST(UnitTestPrediction, PredictionTest)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
constexpr Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
const auto viewPoint =
omath::projectile_prediction::ProjPredEngineLegacy(400, 1.f / 1000.f, 50, 5.f).maybe_calculate_aim_point(proj, target);
Engine(400.f, 1.f / 1000.f, 50.f, 5.f).maybe_calculate_aim_point(proj, target);
const auto [pitch, yaw, _] =omath::source_engine::CameraTrait::calc_look_at_angle(proj.m_origin, viewPoint.value());
@@ -18,12 +23,12 @@ TEST(UnitTestPrediction, PredictionTest)
}
// Helper: verify aim_angles match angles derived from aim_point via CameraTrait
static void expect_angles_match_aim_point(const omath::projectile_prediction::Projectile& proj,
const omath::projectile_prediction::Target& target,
static void expect_angles_match_aim_point(const Projectile& proj,
const Target& target,
float gravity, float step, float max_time, float tolerance,
float angle_eps = 0.01f)
{
const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, step, max_time, tolerance);
const Engine engine(gravity, step, max_time, tolerance);
const auto aim_point = engine.maybe_calculate_aim_point(proj, target);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);
@@ -45,30 +50,30 @@ static void expect_angles_match_aim_point(const omath::projectile_prediction::Pr
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_StaticTarget)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
constexpr Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_MovingTarget)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 3000.f, .m_gravity_scale = 1.0f};
expect_angles_match_aim_point(proj, target, 800, 1.f / 500.f, 30, 10.f);
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_AirborneTarget)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 4000.f, .m_gravity_scale = 0.5f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 10.f);
}
@@ -76,10 +81,10 @@ TEST(UnitTestPrediction, AimAnglesMatchAimPoint_AirborneTarget)
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_HighArc)
{
// Target nearly directly above — high pitch angle
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.3f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
}
@@ -87,20 +92,20 @@ TEST(UnitTestPrediction, AimAnglesMatchAimPoint_HighArc)
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_NegativeYaw)
{
// Target behind and to the left — negative yaw quadrant
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
}
TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_angles_match_aim_point(proj, target, 400, 1.f / 1000.f, 50, 5.f);
}
@@ -108,13 +113,13 @@ TEST(UnitTestPrediction, AimAnglesMatchAimPoint_WithLaunchOffset)
// Helper: simulate projectile flight using aim_angles and verify it reaches the target.
// Steps the projectile forward in small increments, simultaneously predicts target position,
// and checks that the minimum distance is within hit_tolerance.
static void expect_projectile_hits_target(const omath::projectile_prediction::Projectile& proj,
const omath::projectile_prediction::Target& target,
static void expect_projectile_hits_target(const Projectile& proj,
const Target& target,
float gravity, float engine_step, float max_time, float engine_tolerance,
float hit_tolerance, float sim_step = 1.f / 2000.f)
{
using Trait = omath::source_engine::PredEngineTrait;
const omath::projectile_prediction::ProjPredEngineLegacy engine(gravity, engine_step, max_time, engine_tolerance);
const Engine engine(gravity, engine_step, max_time, engine_tolerance);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);
ASSERT_TRUE(aim_angles.has_value()) << "engine must find a solution";
@@ -148,50 +153,50 @@ static void expect_projectile_hits_target(const omath::projectile_prediction::Pr
TEST(ProjectileSimulation, HitsStaticTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {100, 0, 90}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
constexpr Projectile proj = {
.m_origin = {3, 2, 1}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsMovingTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {500, 100, 0}, .m_velocity = {-50, 20, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 3000, .m_gravity_scale = 1.0};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 3000.f, .m_gravity_scale = 1.0f};
expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f);
}
TEST(ProjectileSimulation, HitsAirborneTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {200, 50, 300}, .m_velocity = {10, -5, -20}, .m_is_airborne = true};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 4000, .m_gravity_scale = 0.5};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 4000.f, .m_gravity_scale = 0.5f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f);
}
TEST(ProjectileSimulation, HitsHighTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {10, 0, 500}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.3};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.3f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsNegativeYawTarget_NoOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
@@ -200,92 +205,92 @@ TEST(ProjectileSimulation, HitsNegativeYawTarget_NoOffset)
TEST(ProjectileSimulation, HitsStaticTarget_SmallOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {200, 0, 50}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {5, 0, -3}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsStaticTarget_LargeXOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {300, 100, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {20, 0, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {20, 0, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsStaticTarget_LargeYOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {150, -200, 30}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {0, 15, 0}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {0, 15, 0}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsStaticTarget_LargeZOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {100, 0, 200}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {0, 0, -10}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {0, 0, -10}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsStaticTarget_AllAxesOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {250, 80, 60}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {10, 5, 20}, .m_launch_offset = {8, -4, -6}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
const Projectile proj = {
.m_origin = {10, 5, 20}, .m_launch_offset = {8, -4, -6}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(ProjectileSimulation, HitsMovingTarget_WithOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {400, 0, 50}, .m_velocity = {-30, 10, 5}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {10, -5, 2}, .m_launch_speed = 3000, .m_gravity_scale = 0.8};
const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {10, -5, 2}, .m_launch_speed = 3000.f, .m_gravity_scale = 0.8f};
expect_projectile_hits_target(proj, target, 800, 1.f / 500.f, 30, 10.f, 15.f);
}
TEST(ProjectileSimulation, HitsAirborneTarget_WithOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {150, 80, 250}, .m_velocity = {5, -10, -30}, .m_is_airborne = true};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 50}, .m_launch_offset = {3, 7, -5}, .m_launch_speed = 4000, .m_gravity_scale = 0.5};
const Projectile proj = {
.m_origin = {0, 0, 50}, .m_launch_offset = {3, 7, -5}, .m_launch_speed = 4000.f, .m_gravity_scale = 0.5f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 10.f, 15.f);
}
TEST(ProjectileSimulation, HitsNegativeYawTarget_WithOffset)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {-200, -150, 10}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
const omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {-5, 3, 2}, .m_launch_speed = 5000, .m_gravity_scale = 0.4};
const Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_offset = {-5, 3, 2}, .m_launch_speed = 5000.f, .m_gravity_scale = 0.4f};
expect_projectile_hits_target(proj, target, 400, 1.f / 1000.f, 50, 5.f, 10.f);
}
TEST(UnitTestPrediction, AimAnglesReturnsNulloptWhenNoSolution)
{
constexpr omath::projectile_prediction::Target target{
constexpr Target target{
.m_origin = {100000, 0, 0}, .m_velocity = {0, 0, 0}, .m_is_airborne = false};
constexpr omath::projectile_prediction::Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 1, .m_gravity_scale = 1};
constexpr Projectile proj = {
.m_origin = {0, 0, 0}, .m_launch_speed = 1.f, .m_gravity_scale = 1.f};
const omath::projectile_prediction::ProjPredEngineLegacy engine(9.81f, 0.1f, 2.f, 5.f);
const Engine engine(9.81f, 0.1f, 2.f, 5.f);
const auto aim_point = engine.maybe_calculate_aim_point(proj, target);
const auto aim_angles = engine.maybe_calculate_aim_angles(proj, target);

View File

@@ -4,8 +4,8 @@
#include <omath/projectile_prediction/target.hpp>
#include <omath/linear_algebra/vector3.hpp>
using omath::projectile_prediction::Projectile;
using omath::projectile_prediction::Target;
using Projectile = omath::projectile_prediction::Projectile<float>;
using Target = omath::projectile_prediction::Target<float>;
using omath::Vector3;
// Fake engine trait where gravity is effectively zero and projectile prediction always hits the target