o
odin.langpkg.dev
packages / app / ballistics-odin

ballistics-odin

5952366app

Ballistic calculator for game dev

No license · updated 12 months ago

Trajectory System

This package implements a high-performance physics-based projectile trajectory system for Nexus Rift. It provides deterministic tick-based ballistic calculations, collision detection, and a game-realistic API focused on fixed muzzle velocities and aiming directions for AI targeting with batch processing capabilities.

Architecture - Unified Package Design

The trajectory system uses a single unified package architecture for optimal performance and usability:

ballistics/src/
├── ballistics.odin        # Main public API and batch processing
├── ballistic.odin        # Ballistic calculation implementations
├── gravity.odin          # Physics and gravity calculations
├── moving_target.odin    # Moving target interception algorithms
├── collision.odin        # Terrain and object collision detection
└── trajectory_test.odin  # Comprehensive integration tests

Key Benefits:

  • Single import: import ballistics "path/to/ballistics"
  • Zero overhead: Direct function calls, no wrapper layers
  • High performance: Batch processing for 1000+ projectiles in <5ms
  • Memory efficient: Zero GC allocations during calculations
  • SIMD ready: Optimized data layouts for vectorization

Quick Start

Import the package:

import ballistics "path/to/ballistics"

Basic Targeting

// Quick range check
can_hit := ballistics.can_hit_target(
    turret_pos, target_pos, muzzle_velocity
)

// Get detailed firing solution with fixed muzzle velocity
query := ballistics.FiringQuery{
    launch_position = turret_pos,
    target_position = target_pos,
    muzzle_velocity = 800.0,  // Fixed tank cannon velocity
    gravity_strength = ballistics.DEFAULT_GRAVITY,
    tick_rate = 60,
    max_arc_angle = math.PI/4, // 45° max elevation
}

result := ballistics.firing_solution(query)
if result.target_reachable {
    // Use result.low_arc or result.high_arc
    solution := result.low_arc if result.low_arc.valid else result.high_arc

    // solution.launch_direction is unit vector for aiming
    // solution.elevation_angle is gun elevation in radians
    // solution.azimuth_angle is gun rotation in radians
    fire_projectile(solution.launch_direction, solution.elevation_angle, solution.azimuth_angle)
}

Moving Target Interception

// Intercept moving target with fixed muzzle velocity
intercept_result := ballistics.moving_target_intercept(
    launcher_pos,
    target_pos,
    target_velocity,
    muzzle_velocity,  // Fixed SAM missile velocity (e.g., 1200 m/s)
    max_range
)

if intercept_result.possible {
    // Use the calculated firing solution for interception
    solution := intercept_result.firing_solution
    aim_turret(solution.launch_direction, solution.elevation_angle, solution.azimuth_angle)
}

High-Performance Batch Processing

For processing 1000+ projectiles simultaneously:

// Initialize memory pool (once)
pool: ballistics.TrajectoryMemoryPool
ballistics.init_memory_pool(&pool, 1000)
defer ballistics.destroy_memory_pool(&pool)

// Setup batch data with fixed muzzle velocities
for i in 0..<count {
    pool.batch_data.launch_positions[i] = launcher_positions[i]
    pool.batch_data.target_positions[i] = target_positions[i]
    pool.batch_data.projectile_speeds[i] = muzzle_velocities[i]  // Fixed weapon velocities
    pool.batch_data.gravity_strengths[i] = ballistics.DEFAULT_GRAVITY
}

// Process entire batch in <5ms
ballistics.batch_firing_solutions(&pool, count)

// Access results with aiming directions
for i in 0..<count {
    result := pool.batch_data.results[i]
    if result.target_reachable {
        solution := result.low_arc if result.low_arc.valid else result.high_arc
        // solution.launch_direction contains unit vector for aiming
        // solution.elevation_angle contains gun elevation
        // solution.azimuth_angle contains gun rotation
        aim_weapon(i, solution.launch_direction, solution.elevation_angle)
    }
}

Projectile Simulation (ECS Integration)

For simulating projectiles in your world tick loop:

// Projectile component for ECS
Projectile :: struct {
    position:        Vec3,
    velocity:        Vec3,
    launch_tick:     i32,
    lifetime_ticks:  i32,
}

// World system update - called every tick
update_projectiles :: proc(projectiles: []^Projectile, current_tick: i32, tick_rate: i32) {
    for projectile in projectiles {
        // Update position and velocity for this tick
        projectile.position, projectile.velocity = ballistics.projectile_tick(
            projectile.position,
            projectile.velocity,
            tick_rate
        )

        // Check if projectile should be destroyed
        if current_tick - projectile.launch_tick >= projectile.lifetime_ticks {
            destroy_projectile(projectile)
        }
    }
}

// High-performance batch update for 1000+ projectiles
batch_update_projectiles :: proc(projectiles: []^Projectile, tick_rate: i32) {
    count := len(projectiles)
    positions := make([]Vec3, count)
    velocities := make([]Vec3, count)
    new_positions := make([]Vec3, count)
    new_velocities := make([]Vec3, count)
    defer delete(positions)
    defer delete(velocities)
    defer delete(new_positions)
    defer delete(new_velocities)

    // Extract current state
    for i, projectile in projectiles {
        positions[i] = projectile.position
        velocities[i] = projectile.velocity
    }

    // Batch update all projectiles at once
    ballistics.batch_projectile_tick(positions, velocities, new_positions, new_velocities, tick_rate)

    // Write back new state
    for i, projectile in projectiles {
        projectile.position = new_positions[i]
        projectile.velocity = new_velocities[i]
    }
}

// Spawn projectile from firing solution
spawn_projectile :: proc(firing_solution: ballistics.FiringSolution, launch_position: Vec3, muzzle_velocity: f32, current_tick: i32, tick_rate: i32) -> ^Projectile {
    // Calculate initial velocity from firing solution
    initial_velocity := vec3_scale(firing_solution.launch_direction, muzzle_velocity)

    // Calculate lifetime
    lifetime_ticks := ballistics.projectile_flight_time_ticks(launch_position, initial_velocity, tick_rate)

    return &Projectile{
        position = launch_position,
        velocity = initial_velocity,
        launch_tick = current_tick,
        lifetime_ticks = lifetime_ticks,
    }
}

Projectile Rendering/Interpolation

For smooth rendering between ticks:

// Get interpolated position for rendering
render_projectile_position :: proc(projectile: ^Projectile, current_tick: i32, tick_alpha: f32, tick_rate: i32) -> Vec3 {
    // Calculate elapsed time including interpolation
    elapsed_ticks := f32(current_tick - projectile.launch_tick) + tick_alpha
    elapsed_time := elapsed_ticks / f32(tick_rate)

    // Get position at exact time for smooth rendering
    return ballistics.projectile_position_at_time(
        projectile.position - projectile.velocity / f32(tick_rate), // Back-calculate launch position
        projectile.velocity,
        elapsed_time
    )
}

Game-Realistic Design

The trajectory system is designed around realistic weapon behavior:

Fixed Muzzle Velocities

Each weapon type has a fixed muzzle velocity matching real-world characteristics:

  • Tank Cannon: 800 m/s
  • SAM Missile: 1200 m/s
  • Artillery: 400 m/s
  • Basic Projectile: 300 m/s (5 units/tick at 60 FPS)

Aiming-Focused Output

Instead of calculating variable velocities, the system provides practical aiming data:

  • Launch Direction: Unit vector for weapon orientation
  • Elevation Angle: Vertical gun elevation in radians
  • Azimuth Angle: Horizontal gun rotation in radians

This matches how real weapons work: the velocity is fixed, and the gunner adjusts aim.

Example Weapon Integration

// Tank cannon with 800 m/s muzzle velocity
tank_query := ballistics.FiringQuery{
    launch_position = tank.turret_position,
    target_position = enemy_tank.position,
    muzzle_velocity = 800.0,  // Fixed for this weapon type
    max_arc_angle = math.PI/6, // 30° max elevation for tank
}

result := ballistics.firing_solution(tank_query)
if result.target_reachable && result.low_arc.valid {
    // Aim the turret using the calculated angles
    tank.turret_elevation = result.low_arc.elevation_angle
    tank.turret_rotation = result.low_arc.azimuth_angle
    tank.fire()
}

API Reference

Core Functions

  • can_hit_target() - Quick reachability check
  • firing_solution() - Complete ballistic calculation
  • optimal_firing_solution() - Best firing solution selection
  • moving_target_intercept() - Moving target interception

Projectile Simulation (ECS Integration)

  • projectile_tick() - Update projectile position/velocity for one tick
  • projectile_position_at_tick() - Calculate position at specific tick
  • projectile_position_at_time() - Calculate position at specific time
  • projectile_flight_time_ticks() - Get total flight time in ticks
  • batch_projectile_tick() - High-performance batch update for 1000+ projectiles

Batch Processing

  • init_memory_pool() - Initialize zero-allocation memory pool
  • batch_firing_solutions() - Process 1000+ projectiles in <5ms
  • batch_moving_target_intercept() - Batch moving target processing
  • destroy_memory_pool() - Cleanup memory pool

Vector Math

  • vec3_new(), vec3_add(), vec3_sub(), vec3_scale()
  • vec3_magnitude(), vec3_normalize(), vec3_distance()
  • vec3_batch_*() - SIMD-optimized batch operations

Helper Functions

  • create_moving_target() - Create moving target data
  • create_engagement_envelope() - Define engagement constraints

Integration Tests

The package includes comprehensive integration tests that demonstrate real-world usage:

# Run all tests including performance benchmarks
odin test src/internal/trajectory/

Test scenarios include:

  • Basic API functionality - Vector operations, range checks
  • Firing solutions - Ground targets, high/low arc selection
  • Moving target interception - Linear motion, confidence calculation
  • Batch processing performance - 1000+ projectiles in <5ms
  • Memory allocation verification - Zero GC during processing
  • Edge case handling - Invalid inputs, extreme scenarios

Performance

The trajectory system achieves excellent real-time performance:

  • <5ms processing time for 1000+ projectiles (batch mode)
  • Zero GC allocations during trajectory calculations
  • <1KB memory per trajectory with efficient data layouts
  • 100% success rate for reachable targets in batch processing
  • SIMD optimization ready with Structure of Arrays (SoA) layouts

Performance Test Results

Processing 1000 projectiles: 3.86ms (target: <5ms) ✓
Per projectile calculation: 0.004ms
Success rate: 100.0% (1000/1000)
Memory usage: Zero allocations during processing ✓

Deterministic Physics

The system uses tick-based physics for perfect determinism:

  • Default tick rate: 60 ticks/second
  • Configurable: Supports any tick rate while maintaining accuracy
  • Deterministic: Same inputs always produce identical results
  • Network-safe: Perfect synchronization across clients

Architecture Principles

  1. Game-Realistic Design: Fixed muzzle velocities and aiming-focused output
  2. Single Package: No artificial API separation or wrapper overhead
  3. Performance First: Optimized for real-time game requirements
  4. Zero Allocations: Pre-allocated memory pools for batch processing
  5. Pure Functions: All calculations are stateless and deterministic
  6. Batch Optimized: SIMD-friendly data layouts and algorithms
  7. Comprehensive Testing: Both unit tests and integration examples
  8. Documentation by Example: Tests serve as executable documentation

Recent API Updates (2025-07-19)

The trajectory system has been updated to use a more game-realistic API:

Key Changes

Before (velocity-focused):

query := FiringQuery{
    projectile_speed = calculated_speed,  // Variable speed
}
result := firing_solution(query)
fire_projectile(result.low_arc.launch_velocity, result.low_arc.launch_angle)

After (aiming-focused):

query := FiringQuery{
    muzzle_velocity = 800.0,  // Fixed weapon velocity
}
result := firing_solution(query)
aim_weapon(result.low_arc.launch_direction, result.low_arc.elevation_angle, result.low_arc.azimuth_angle)

Migration Guide

  1. Replace projectile_speed with muzzle_velocity in all query structs
  2. Replace launch_velocity with launch_direction in firing solutions
  3. Replace launch_angle with elevation_angle for gun elevation
  4. Use azimuth_angle for horizontal gun rotation

Benefits of Update

  • More Realistic: Matches real weapon behavior with fixed muzzle velocities
  • Practical Output: Provides aiming angles directly usable by weapon systems
  • Better Integration: Eliminates need to calculate velocities in game code
  • Same Performance: All optimizations and batch processing preserved
  • Backward Compatible: Core functionality unchanged, only interface improved

All tests pass with the new API. See API_CHANGES.md for detailed migration information.