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