Files
millimeters-of-aluminum/scripts/singletons/orbital_mechanics.gd
olof.pettersson c662714997 WIP Wiring
2025-10-20 17:21:16 +02:00

242 lines
8.9 KiB
GDScript

# OrbitalMechanics.gd
extends Node
# This singleton holds all universal physics constants and functions.
# The scaled gravitational constant for the entire simulation.
const G = 1.0 # Adjust this to control the "speed" of your simulation
const MIN_INFLUENCE_THRESHOLD = 0.00001
const ROCHE_LIMIT_MASS_MULTIPLIER = 0.5
class BodyTuple:
var body_a: OrbitalBody2D
var body_b: OrbitalBody2D
var cached_forces: Dictionary[BodyTuple, Vector2] = {
}
# --- Centralized Physics Process ---
func _physics_process(_delta: float) -> void:
var star_system: StarSystem = GameManager.current_star_system
if not star_system:
return
var star = star_system.get_star()
var planetary_systems = star_system.get_planetary_systems()
# TODO: Would this be true in case we are working with a system that is just a rouge planet or a brown dwarf?
if not star:
return
# 1: Calculate star system pull
# a: Get the star and top level Barycenters
var top_level_bodies: Array[OrbitalBody2D] = [star]
top_level_bodies.append_array(planetary_systems)
# b: calculate and apply pull between these
apply_n_body_forces(top_level_bodies)
# 2: Calculate Barycenters local pull
for system in planetary_systems:
# a: Get each Planetary Barycenters OrbitalBody2Ds (including Ships, Satelites, and Stations fully within the Barycenter)
var system_attractors = system.get_internal_attractors()
# b: Calculate and apply pull within each Barycenter
apply_n_body_forces(system_attractors)
# 3: Calculate top level Ships, Satelites, and Stations pull
# a: Get top level OrbitalBody2Ds of non-celestial classes
for star_orbiter in star_system.get_orbital_bodies():
# b: Split into Star Orbiting and On-Approach using mass/distance ratios to Barycenters
# TODO: Check for distance to Barycenter
# c: For Star Orbiting objects -> Calculate and apply pull to star and Barycenter
star_orbiter.apply_force(calculate_n_body_force(star_orbiter, top_level_bodies))
# d: For On Approach -> Calculate and apply pull to star and distant Barycenters
# as well as individual bodies within approaching Barycenter
func calculate_gravitational_force(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> Vector2:
if not is_instance_valid(orbiter) or not is_instance_valid(primary):
return Vector2.ZERO
var distance_sq = orbiter.global_position.distance_squared_to(primary.global_position)
if distance_sq < 1.0:
return Vector2.ZERO
# --- Influence Pruning (Culling) ---
# We check both directions of influence
var influence_a = primary.mass / distance_sq
var influence_b = orbiter.mass / distance_sq
if influence_a < MIN_INFLUENCE_THRESHOLD and influence_b < MIN_INFLUENCE_THRESHOLD:
return Vector2.ZERO
var force_magnitude = (G * primary.mass * orbiter.mass) / distance_sq
var direction = orbiter.global_position.direction_to(primary.global_position)
return direction * force_magnitude
# Calculates the pull between a set number of bodies
# Use carefully to simulate each level of the simulation
# Iterate through every unique pair of bodies (i, j) where j > i
func apply_n_body_forces(attractors: Array[OrbitalBody2D]):
# Iterate through every unique pair of bodies (i, j) where j > i
for i in range(attractors.size()):
var body_a: OrbitalBody2D = attractors[i]
if not is_instance_valid(body_a): continue
for j in range(i + 1, attractors.size()):
var body_b: OrbitalBody2D = attractors[j]
if not is_instance_valid(body_b): continue
# Calculate the force vector ONCE
var force_vector = calculate_gravitational_force(body_a, body_b)
# Apply the force symmetrically
if force_vector != Vector2.ZERO:
body_a.apply_force(force_vector)
body_b.apply_force(-force_vector)
func calculate_n_body_force(body: OrbitalBody2D, attractors: Array[OrbitalBody2D]) -> Vector2:
var total_pull: Vector2 = Vector2.ZERO
for attractor in attractors:
total_pull += calculate_gravitational_force(body, attractor)
return total_pull
func calculate_n_body_gravity_forces(body_to_affect: Node2D) -> Vector2:
var total_force = Vector2.ZERO
if not is_instance_valid(body_to_affect):
return total_force
# Get the list of all major gravitational bodies from the GameManager.
var system_data = GameManager.get_system_data()
if not system_data:
return total_force
# We only consider planets and the star as major attractors for performance.
var attractors = system_data.all_bodies()
for attractor in attractors:
if is_instance_valid(attractor) and attractor != body_to_affect:
total_force += calculate_gravitational_force(body_to_affect, attractor)
return total_force
# Calculates the perfect initial velocity for a stable circular orbit.
func calculate_circular_orbit_velocity(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> Vector2:
if not is_instance_valid(primary):
return Vector2.ZERO
var distance = orbiter.global_position.distance_to(primary.global_position)
if distance == 0:
return Vector2.ZERO
# v = sqrt(G * M / r)
var speed_magnitude = sqrt(G * primary.mass / distance)
var direction_to_orbiter = primary.global_position.direction_to(orbiter.global_position)
var perpendicular_direction = Vector2(direction_to_orbiter.y, -direction_to_orbiter.x)
return perpendicular_direction * speed_magnitude
func _calculate_n_body_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVector2Array:
var num_steps = 10
var time_step = 60
var ghost_position = body_to_trace.global_position
var ghost_velocity = body_to_trace.linear_velocity
var path_points = PackedVector2Array()
for i in range(num_steps):
# Create a temporary "ghost" body to calculate forces on.
var ghost_body = OrbitalBody2D.new()
ghost_body.global_position = ghost_position
ghost_body.mass = body_to_trace.mass
# Use our library to get the total gravitational force at the ghost's position.
var total_force = calculate_n_body_gravity_forces(ghost_body)
var acceleration = total_force / ghost_body.mass
ghost_velocity += acceleration * time_step
ghost_position += ghost_velocity * time_step
path_points.append(ghost_position)
ghost_body.free() # Clean up the temporary node
return path_points
# Calculates an array of points for the orbit RELATIVE to the primary body.
func _calculate_relative_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVector2Array:
if not is_instance_valid(body_to_trace) or not body_to_trace.has_method("get_primary") or not is_instance_valid(body_to_trace.get_primary()):
return PackedVector2Array()
var primary = body_to_trace.get_primary()
var primary_mass = primary.mass
var body_mass = body_to_trace.mass
var ghost_relative_pos = body_to_trace.global_position - primary.global_position
var ghost_relative_vel = body_to_trace.linear_velocity - primary.linear_velocity
var r_magnitude = ghost_relative_pos.length()
if r_magnitude == 0:
return PackedVector2Array()
var v_sq = ghost_relative_vel.length_squared()
var mu = G * primary_mass
var specific_energy = v_sq / 2.0 - mu / r_magnitude
var num_steps = 200
var time_step: float
if specific_energy >= 0:
time_step = 0.1
else:
var semi_major_axis = -mu / (2.0 * specific_energy)
var orbital_period = 2.0 * PI * sqrt(pow(semi_major_axis, 3) / mu)
time_step = orbital_period / float(num_steps)
var path_points = PackedVector2Array()
for i in range(num_steps):
var distance_sq = ghost_relative_pos.length_squared()
if distance_sq < 1.0:
break
var direction = -ghost_relative_pos.normalized()
var force_magnitude = (G * primary_mass * body_mass) / distance_sq
var force_vector = direction * force_magnitude
var acceleration = force_vector / body_mass
ghost_relative_vel += acceleration * time_step
ghost_relative_pos += ghost_relative_vel * time_step
path_points.append(ghost_relative_pos)
return path_points
# Calculates the Hill Sphere radius for a satellite.
# This is the region where the satellite's gravity is dominant over its primary's.
func calculate_hill_sphere(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> float:
if not is_instance_valid(orbiter) or not is_instance_valid(primary) or primary.mass <= 0:
return 0.0
var distance = orbiter.global_position.distance_to(primary.global_position)
# The formula is: a * (m / 3M)^(1/3)
var mass_ratio = orbiter.mass / (3.0 * primary.mass)
if mass_ratio < 0: return 0.0
return distance * pow(mass_ratio, 1.0/3.0)
# Calculates a simplified Roche Limit, or minimum safe orbital distance.
func calculate_simplified_roche_limit(primary: OrbitalBody2D) -> float:
if not is_instance_valid(primary) or primary.mass <= 0:
return 100.0 # Return a small default if primary is invalid
# We approximate a "radius" from the square root of the mass, then apply a multiplier.
# This ensures more massive stars and planets have larger "keep-out" zones.
return sqrt(primary.mass) * ROCHE_LIMIT_MASS_MULTIPLIER
func get_orbital_time_in_seconds(orbiter: OrbitalBody2D, primary: OrbitalBody2D) -> float:
var mu = OrbitalMechanics.G * primary.mass
var r = orbiter.global_position.distance_to(primary.global_position)
return TAU * sqrt(pow(r, 3) / mu)