Skip to content

Best Practices

Guidelines for using OMath effectively and avoiding common pitfalls.


Code Organization

Use Type Aliases

Define clear type aliases for commonly used types:

// Good: Clear and concise
using Vec3f = omath::Vector3<float>;
using Vec2f = omath::Vector2<float>;
using Mat4 = omath::Mat4X4;

Vec3f position{1.0f, 2.0f, 3.0f};
// Avoid: Verbose and repetitive
omath::Vector3<float> position{1.0f, 2.0f, 3.0f};
omath::Vector3<float> velocity{0.0f, 0.0f, 0.0f};

Namespace Usage

Be selective with using namespace:

// Good: Specific namespace for your engine
using namespace omath::source_engine;

// Good: Import specific types
using omath::Vector3;
using omath::Vector2;

// Avoid: Too broad
using namespace omath;  // Imports everything

Include What You Use

// Good: Include specific headers
#include <omath/linear_algebra/vector3.hpp>
#include <omath/projection/camera.hpp>

// Okay for development
#include <omath/omath.hpp>

// Production: Include only what you need
// to reduce compile times

Error Handling

Always Check Optional Results

// Good: Check before using
if (auto screen = camera.world_to_screen(world_pos)) {
    draw_at(screen->x, screen->y);
} else {
    // Handle point not visible
}

// Bad: Unchecked access can crash
auto screen = camera.world_to_screen(world_pos);
draw_at(screen->x, screen->y);  // Undefined behavior if nullopt!

Handle Expected Errors

// Good: Handle error case
if (auto angle = v1.angle_between(v2)) {
    use_angle(*angle);
} else {
    switch (angle.error()) {
        case Vector3Error::IMPOSSIBLE_BETWEEN_ANGLE:
            // Handle zero-length vector
            break;
    }
}

// Bad: Assume success
auto angle = v1.angle_between(v2);
use_angle(*angle);  // Throws if error!

Validate Inputs

// Good: Validate before expensive operations
bool is_valid_projectile(const Projectile& proj) {
    return proj.speed > 0.0f &&
           std::isfinite(proj.speed) &&
           std::isfinite(proj.origin.length());
}

if (is_valid_projectile(proj) && is_valid_target(target)) {
    auto aim = engine.maybe_calculate_aim_point(proj, target);
}

Performance

Use constexpr When Possible

// Good: Computed at compile time
constexpr Vector3<float> gravity{0.0f, 0.0f, -9.81f};
constexpr float max_range = 1000.0f;
constexpr float max_range_sq = max_range * max_range;

// Use in runtime calculations
if (position.length_sqr() < max_range_sq) {
    // ...
}

Prefer Squared Distance

// Good: Avoids expensive sqrt
constexpr float max_dist_sq = 100.0f * 100.0f;
for (const auto& entity : entities) {
    if (entity.pos.distance_to_sqr(player_pos) < max_dist_sq) {
        // Process nearby entity
    }
}

// Avoid: Unnecessary sqrt calls
constexpr float max_dist = 100.0f;
for (const auto& entity : entities) {
    if (entity.pos.distance_to(player_pos) < max_dist) {
        // More expensive
    }
}

Cache Expensive Calculations

// Good: Update camera once per frame
void update_frame() {
    camera.update(current_position, current_angles);

    // All projections use cached matrices
    for (const auto& entity : entities) {
        if (auto screen = camera.world_to_screen(entity.pos)) {
            draw_entity(screen->x, screen->y);
        }
    }
}

// Bad: Camera recreated each call
for (const auto& entity : entities) {
    Camera cam(pos, angles, viewport, fov, near, far);  // Expensive!
    auto screen = cam.world_to_screen(entity.pos);
}

Choose the Right Engine

// Good: Use AVX2 when available
#ifdef __AVX2__
    using Engine = ProjPredEngineAVX2;
#else
    using Engine = ProjPredEngineLegacy;
#endif

Engine prediction_engine;

// Or runtime detection
Engine* create_best_engine() {
    if (cpu_supports_avx2()) {
        return new ProjPredEngineAVX2();
    }
    return new ProjPredEngineLegacy();
}

Minimize Allocations

// Good: Reuse vectors
std::vector<Vector3<float>> positions;
positions.reserve(expected_count);

// In loop
positions.clear();  // Doesn't deallocate
for (...) {
    positions.push_back(compute_position());
}

// Bad: Allocate every time
for (...) {
    std::vector<Vector3<float>> positions;  // Allocates each iteration
    // ...
}

Type Safety

Use Strong Angle Types

// Good: Type-safe angles
PitchAngle pitch = PitchAngle::from_degrees(45.0f);
YawAngle yaw = YawAngle::from_degrees(90.0f);
ViewAngles angles{pitch, yaw, RollAngle::from_degrees(0.0f)};

// Bad: Raw floats lose safety
float pitch = 45.0f;  // No range checking
float yaw = 90.0f;    // Can go out of bounds

Match Engine Types

// Good: Use matching types from same engine
using namespace omath::source_engine;
Camera camera = /* ... */;
ViewAngles angles = /* ... */;

// Bad: Mixing engine types
using UnityCamera = omath::unity_engine::Camera;
using SourceAngles = omath::source_engine::ViewAngles;
UnityCamera camera{pos, SourceAngles{}, ...};  // May cause issues!

Template Type Parameters

// Good: Explicit and clear
Vector3<float> position;
Vector3<double> high_precision_pos;

// Okay: Use default float
Vector3<> position;  // Defaults to float

// Avoid: Mixing types unintentionally
Vector3<float> a;
Vector3<double> b;
auto result = a + b;  // Type mismatch!

Testing & Validation

Test Edge Cases

void test_projection() {
    Camera camera = setup_camera();

    // Test normal case
    assert(camera.world_to_screen({100, 100, 100}).has_value());

    // Test edge cases
    assert(!camera.world_to_screen({0, 0, -100}).has_value());  // Behind
    assert(!camera.world_to_screen({1e10, 0, 0}).has_value());  // Too far

    // Test boundaries
    Vector3<float> at_near{0, 0, camera.near_plane() + 0.1f};
    assert(camera.world_to_screen(at_near).has_value());
}

Validate Assumptions

void validate_game_data() {
    // Validate FOV
    float fov = read_game_fov();
    assert(fov > 1.0f && fov < 179.0f);

    // Validate positions
    Vector3<float> pos = read_player_position();
    assert(std::isfinite(pos.x));
    assert(std::isfinite(pos.y));
    assert(std::isfinite(pos.z));

    // Validate viewport
    ViewPort vp = read_viewport();
    assert(vp.width > 0 && vp.height > 0);
}

Use Assertions

// Good: Catch errors early in development
void shoot_projectile(const Projectile& proj) {
    assert(proj.speed > 0.0f && "Projectile speed must be positive");
    assert(std::isfinite(proj.origin.length()) && "Invalid projectile origin");

    // Continue with logic
}

// Add debug-only checks
#ifndef NDEBUG
    if (!is_valid_input(data)) {
        std::cerr << "Warning: Invalid input detected\n";
    }
#endif

Memory & Resources

RAII for Resources

// Good: Automatic cleanup
class GameOverlay {
    Camera camera_;
    std::vector<Entity> entities_;

public:
    GameOverlay(/* ... */) : camera_(/* ... */) {
        entities_.reserve(1000);
    }

    // Resources cleaned up automatically
    ~GameOverlay() = default;
};

Avoid Unnecessary Copies

// Good: Pass by const reference
void draw_entities(const std::vector<Vector3<float>>& positions) {
    for (const auto& pos : positions) {
        // Process position
    }
}

// Bad: Copies entire vector
void draw_entities(std::vector<Vector3<float>> positions) {
    // Expensive copy!
}

// Good: Move when transferring ownership
std::vector<Vector3<float>> compute_positions();
auto positions = compute_positions();  // Move, not copy

Use Structured Bindings

// Good: Clear and concise
if (auto [success, screen_pos] = try_project(world_pos); success) {
    draw_at(screen_pos.x, screen_pos.y);
}

// Good: Decompose results
auto [x, y, z] = position.as_tuple();

Documentation

Document Assumptions

// Good: Clear documentation
/**
 * Projects world position to screen space.
 * 
 * @param world_pos Position in world coordinates (meters)
 * @return Screen position if visible, nullopt if behind camera or out of view
 * 
 * @note Assumes camera.update() was called this frame
 * @note Screen coordinates are in viewport space [0, width] x [0, height]
 */
std::optional<Vector2<float>> project(const Vector3<float>& world_pos);

Explain Non-Obvious Code

// Good: Explain the math
// Use squared distance to avoid expensive sqrt
// max_range = 100.0 → max_range_sq = 10000.0
constexpr float max_range_sq = 100.0f * 100.0f;
if (dist_sq < max_range_sq) {
    // Entity is in range
}

// Explain engine-specific quirks
// Source Engine uses Z-up coordinates, but angles are in degrees
// Pitch: [-89, 89], Yaw: [-180, 180], Roll: [-180, 180]
ViewAngles angles{pitch, yaw, roll};

Debugging

Add Debug Visualization

#ifndef NDEBUG
void debug_draw_projection() {
    // Draw camera frustum
    draw_frustum(camera);

    // Draw world axes
    draw_line({0,0,0}, {100,0,0}, Color::Red);    // X
    draw_line({0,0,0}, {0,100,0}, Color::Green);  // Y
    draw_line({0,0,0}, {0,0,100}, Color::Blue);   // Z

    // Draw projected points
    for (const auto& entity : entities) {
        if (auto screen = camera.world_to_screen(entity.pos)) {
            draw_cross(screen->x, screen->y);
        }
    }
}
#endif

Log Important Values

void debug_projection_failure(const Vector3<float>& pos) {
    std::cerr << "Projection failed for position: "
              << pos.x << ", " << pos.y << ", " << pos.z << "\n";

    auto view_matrix = camera.get_view_matrix();
    std::cerr << "View matrix:\n";
    // Print matrix...

    std::cerr << "Camera position: "
              << camera.position().x << ", "
              << camera.position().y << ", "
              << camera.position().z << "\n";
}

Use Debug Builds

# CMakeLists.txt
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
    target_compile_definitions(your_target PRIVATE
        DEBUG_PROJECTION=1
        VALIDATE_INPUTS=1
    )
endif()
#ifdef DEBUG_PROJECTION
    std::cout << "Projecting: " << world_pos << "\n";
#endif

#ifdef VALIDATE_INPUTS
    assert(std::isfinite(world_pos.length()));
#endif

Platform Considerations

Cross-Platform Code

// Good: Platform-agnostic
constexpr float PI = 3.14159265359f;

// Avoid: Platform-specific
#ifdef _WIN32
    // Windows-only code
#endif

Handle Different Compilers

// Good: Compiler-agnostic
#if defined(_MSC_VER)
    // MSVC-specific
#elif defined(__GNUC__)
    // GCC/Clang-specific
#endif

// Use OMath's built-in compatibility
// It handles compiler differences automatically

Summary

Key principles: 1. Safety first: Always check optional/expected results 2. Performance matters: Use constexpr, avoid allocations, cache results 3. Type safety: Use strong types, match engine types 4. Clear code: Use aliases, document assumptions, explain non-obvious logic 5. Test thoroughly: Validate inputs, test edge cases, add assertions 6. Debug effectively: Add visualization, log values, use debug builds


See Also


Last updated: 1 Nov 2025