WIP Gravitational refactor

This commit is contained in:
2025-11-17 08:08:01 +01:00
parent 398ec829ae
commit 3d01edb2d9
5 changed files with 117 additions and 94 deletions

View File

@ -2,6 +2,9 @@ class_name CelestialBody extends OrbitalBody3D
@export var radius: float = 100.0
func _ready():
auto_proxy_gravity = false
func set_radius(value: float):
radius = value

View File

@ -65,47 +65,11 @@ func turn_off():
await get_tree().physics_frame
#print(" - firing: %s" % is_firing)
# --- Godot Physics Callback ---
func _physics_process(delta: float):
super(delta)
if not enabled:
is_firing = false
func _integrate_forces(state: PhysicsDirectBodyState3D):
# Call the base class integration
super(state)
# If the thruster is active, apply a constant central force in its local "up" direction.
if is_firing:
apply_thrust_force()
# Function called by the ThrusterController system to fire the thruster
func apply_thrust_force():
if is_firing:
# 1. Calculate the local force vector (magnitude and direction)
var local_force = Vector3.UP * max_thrust
# 2. FIX: Convert the force to global space using ONLY the rotation (basis).
# This ensures the force vector's magnitude is not corrupted by the thruster's global position.
# NOTE: This replaces the problematic global_transform * vector or global_transform.xform(vector)
var force_vector = global_transform.basis * local_force
# 3. Apply the force to itself.
apply_force_recursive(force_vector, global_position)
# func _draw():
# # This function is only called if the thruster is firing (due to queue_redraw)
# if not is_firing:
# return
# # --- Draw a fiery, flickering cone ---
# # The plume goes in the OPPOSITE direction of the thrust
# var plume_direction = Vector2.DOWN
# var plume_length = randf_range(20.0, 30.0) # Random length for a flickering effect
# # Define the 3 points of a triangle for the cone
# var tip = plume_direction * plume_length
# var base_offset = plume_direction.orthogonal() * 8.0
# var base1 = base_offset
# var base2 = -base_offset
# var points = PackedVector2Array([base1, tip, base2])
# # Draw the cone with a fiery color
# draw_polygon(points, PackedColorArray([Color.ORANGE_RED, Color.GOLD, Color.ORANGE_RED]))
apply_central_force(Vector3.UP * max_thrust)

View File

@ -5,8 +5,8 @@ class_name OrbitalBody3D extends RigidBody3D
# Defines the physical behavior of this body.
enum PhysicsMode {
INDEPENDENT, # An independent body with its own physics simulation (planets, characters in EVA).
COMPOSITE, # A body that aggregates mass and forces from ANCHORED children (ships, modules).
ANCHORED # A component that is "bolted on" and defers physics to a COMPOSITE parent.
COMPOSITE, # A body that aggregates mass and forces from ANCHORED children (ships, modules).
ANCHORED # A component that is "bolted on" and defers physics to a COMPOSITE parent.
}
@export var physics_mode: PhysicsMode = PhysicsMode.INDEPENDENT
@ -14,23 +14,25 @@ enum PhysicsMode {
var current_grid_authority: OrbitalBody3D = null
# Mass of this individual component
@export var base_mass: float = 1.0
# @export var mass: float = 0.0 # Aggregated mass of this body and all its OrbitalBody3D children
@export var base_mass: float = 1.0
# REFACTOR: All physics properties are now Vector3
# @export var linear_velocity: Vector3 = Vector3.ZERO
# @export var angular_velocity: Vector3 = Vector3.ZERO # Represents angular velocity around X, Y, and Z axes
# --- Gravity Proxy System ---
# If this is set, we stop calculating our own gravity and just copy this parent.
var gravity_proxy_parent: OrbitalBody3D = null
# If true, this body will automatically attach to a parent OrbitalBody3D
# as a gravity proxy. Set this to FALSE for planets/moons that need
# to calculate their own independent orbits.
@export var auto_proxy_gravity: bool = true
# We cache this frame's acceleration so our children can read it.
# This allows us to chain proxies (Pawn -> Ship -> Station).
var current_gravitational_acceleration: Vector3 = Vector3.ZERO
# Variables to accumulate forces applied during the current physics frame
var accumulated_force: Vector3 = Vector3.ZERO
var accumulated_torque: Vector3 = Vector3.ZERO
# Placeholder for Moment of Inertia.
# REFACTOR: This is a simplification. For true 3D physics, this would be an
# inertia tensor (a Basis). But for game physics, a single float
# (like your CharacterPawn3D) is much simpler to work with.
# @export var inertia: float = 1.0
func _ready():
freeze_mode = FreezeMode.FREEZE_MODE_KINEMATIC
if physics_mode == PhysicsMode.ANCHORED:
@ -41,6 +43,35 @@ func _ready():
recalculate_physical_properties()
set_physics_process(not Engine.is_editor_hint())
func _notification(what):
# Automatically update gravity proxy when the scene hierarchy changes
if what == NOTIFICATION_PARENTED:
_update_gravity_proxy()
func _update_gravity_proxy():
if not auto_proxy_gravity:
gravity_proxy_parent = null
return
# 1. Search up the tree for an OrbitalBody3D ancestor
var candidate = get_parent()
var new_proxy = null
while candidate:
if candidate is OrbitalBody3D:
# Found a valid parent physics body
new_proxy = candidate
break
candidate = candidate.get_parent()
# 2. Assign the proxy
if new_proxy != gravity_proxy_parent:
gravity_proxy_parent = new_proxy
if new_proxy:
print(name, " auto-parented gravity proxy to: ", new_proxy.name)
else:
print(name, " detached from gravity proxy (Independent Mode).")
# --- PUBLIC FORCE APPLICATION METHODS ---
# REFACTOR: All arguments are now Vector3
func apply_force_recursive(force: Vector3, pos: Vector3 = self.global_position):
@ -84,14 +115,6 @@ func _update_mass_and_inertia():
print("Node: %s, Mass: %f" % [self, mass])
func _physics_process(delta):
pass
# if not Engine.is_editor_hint():
# match physics_mode:
# PhysicsMode.INDEPENDENT:
# _integrate_forces(delta)
# PhysicsMode.COMPOSITE:
# _integrate_forces(delta)
func _integrate_forces(state: PhysicsDirectBodyState3D):
# Safety Check for Division by Zero
@ -99,18 +122,18 @@ func _integrate_forces(state: PhysicsDirectBodyState3D):
accumulated_force = Vector3.ZERO
accumulated_torque = Vector3.ZERO
return
# --- 1. DETERMINE GRAVITY ---
if is_instance_valid(gravity_proxy_parent):
# OPTIMIZED PATH: Copy the parent's acceleration (Gravity Proxy)
current_gravitational_acceleration = gravity_proxy_parent.current_gravitational_acceleration
else:
# FULL PATH: Retrieve the pre-calculated N-Body acceleration from the cache
current_gravitational_acceleration = OrbitalMechanics.get_calculated_force(self) / mass
# 3. Apply Linear Physics (F = ma)
# var linear_acceleration = accumulated_force / mass # Division is now safe
state.apply_central_force(accumulated_force)
# 4. Apply Rotational Physics (T = I * angular_acceleration)
# REFACTOR: Use the simplified 3D torque equation from your CharacterPawn3D
#if inertia.length() > 0:
# var angular_acceleration = accumulated_torque / inertia
# print("Inertia for %s: %s" % [self, inertia])
# print("Angular Acceleration for %s: %s" % [self, angular_acceleration])
# angular_velocity += angular_acceleration * state.step
# --- 2. APPLY FORCES ---
# Apply Gravity (F = m * a)
state.apply_central_force(current_gravitational_acceleration * mass)
# 5. Reset accumulated forces for the next frame
accumulated_force = Vector3.ZERO

View File

@ -5,10 +5,10 @@ var config: GameConfig
# --- Dictionaries to track players and their objects ---
var player_controllers: Dictionary = {} # Key: player_id, Value: PlayerController node
var player_pawns: Dictionary = {} # Key: player_id, Value: CharacterPawn3D node
var player_pawns: Dictionary = {} # Key: player_id, Value: CharacterPawn3D node
# This variable will hold the reference to the currently active star system.
var current_star_system : StarSystem = null
var current_star_system: StarSystem = null
var registered_spawners: Array[Spawner] = []
var waiting_players: Array[int] = [] # A queue for players waiting to spawn
@ -68,6 +68,13 @@ func _try_spawn_waiting_player():
pawn.set_multiplayer_authority(player_id)
spawn_point.add_child(pawn)
# Traverse up to find the physics body (Ship/Module) we just spawned inside
var parent_body = _get_orbital_body_ancestor(spawn_point)
if parent_body:
# Match the ship's speed so we don't slam into the wall
pawn.linear_velocity = parent_body.linear_velocity
print("GameManager: Pawn inherited velocity ", pawn.linear_velocity, " from ", parent_body.name)
print("GameManager peer %s: Player %d spawned successfully." % [multiplayer.get_unique_id(), player_id])
else:
@ -137,3 +144,12 @@ func get_all_trackable_bodies() -> Array[OrbitalBody3D]:
func request_server_action(action_name: String, args: Array = []):
# This function's body only runs on the SERVER.
print("Server received request: ", action_name, " with args: ", args)
# Helper to find the physics root (Module/Ship) from a child node (Spawner)
func _get_orbital_body_ancestor(node: Node) -> OrbitalBody3D:
var current = node
while current:
if current is OrbitalBody3D:
return current
current = current.get_parent()
return null

View File

@ -8,8 +8,17 @@ 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
# --- GRAVITY ACCELERATION CACHE ---
# The calculated acceleration for this frame. Key: Object ID, Value: Vector3
var gravity_force_cache: Dictionary = {}
# --- PUBLIC API ---
func get_calculated_force(body: OrbitalBody3D) -> Vector3:
return gravity_force_cache.get(body.get_instance_id(), Vector3.ZERO)
func _physics_process(_delta: float) -> void:
gravity_force_cache.clear()
var star_system: StarSystem = GameManager.current_star_system
if not star_system:
return
@ -22,51 +31,60 @@ func _physics_process(_delta: float) -> void:
var top_level_bodies: Array[OrbitalBody3D] = [star]
top_level_bodies.append_array(planetary_systems)
apply_n_body_forces(top_level_bodies)
calculate_n_body_forces(top_level_bodies)
for system in planetary_systems:
var system_attractors = system.get_internal_attractors()
apply_n_body_forces(system_attractors)
calculate_n_body_forces(system_attractors)
for star_orbiter in star_system.get_orbital_bodies():
star_orbiter.apply_force_recursive(calculate_n_body_force(star_orbiter, top_level_bodies))
calculate_n_body_force(star_orbiter, top_level_bodies)
func calculate_gravitational_force(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> Vector3:
if not is_instance_valid(orbiter) or not is_instance_valid(primary):
func calculate_gravitational_force(body_a: OrbitalBody3D, body_b: OrbitalBody3D) -> Vector3:
if not is_instance_valid(body_a) or not is_instance_valid(body_b):
# REFACTOR: Return Vector3.ZERO
return Vector3.ZERO
var distance_sq = orbiter.global_position.distance_squared_to(primary.global_position)
var distance_sq = body_a.global_position.distance_squared_to(body_b.global_position)
if distance_sq < 1.0:
return Vector3.ZERO
var influence_a = primary.mass / distance_sq
var influence_b = orbiter.mass / distance_sq
var influence_a = body_b.mass / distance_sq
var influence_b = body_a.mass / distance_sq
if influence_a < MIN_INFLUENCE_THRESHOLD and influence_b < MIN_INFLUENCE_THRESHOLD:
return Vector3.ZERO
var force_magnitude = (G * primary.mass * orbiter.mass) / distance_sq
# REFACTOR: direction_to is now 3D, this logic is fine
var direction = orbiter.global_position.direction_to(primary.global_position)
var force_magnitude = (G * body_b.mass * body_a.mass) / distance_sq
var direction = body_a.global_position.direction_to(body_b.global_position)
return direction * force_magnitude
func apply_n_body_forces(attractors: Array[OrbitalBody3D]):
func calculate_n_body_forces(attractors: Array[OrbitalBody3D]):
for i in range(attractors.size()):
var body_a: OrbitalBody3D = attractors[i]
if not is_instance_valid(body_a): continue
# Ensure cache entry exists (Vector3.ZERO)
if not body_a.get_instance_id() in gravity_force_cache:
gravity_force_cache[body_a.get_instance_id()] = Vector3.ZERO
for j in range(i + 1, attractors.size()):
var body_b: OrbitalBody3D = attractors[j]
if not is_instance_valid(body_b): continue
# Ensure cache entry exists
if not body_b.get_instance_id() in gravity_force_cache:
gravity_force_cache[body_b.get_instance_id()] = Vector3.ZERO
# REFACTOR: force_vector is now Vector3
var force_vector = calculate_gravitational_force(body_a, body_b)
if force_vector != Vector3.ZERO:
body_a.apply_force_recursive(force_vector)
body_b.apply_force_recursive(-force_vector)
gravity_force_cache[body_a.get_instance_id()] += (force_vector)
gravity_force_cache[body_b.get_instance_id()] -= (force_vector)
func calculate_n_body_force(body: OrbitalBody3D, attractors: Array[OrbitalBody3D]) -> Vector3:
var total_pull: Vector3 = Vector3.ZERO
@ -164,7 +182,7 @@ func _calculate_relative_orbital_path(body_to_trace: OrbitalBody3D) -> PackedVec
if specific_energy >= 0:
time_step = 0.1
else:
var semi_major_axis = -mu / (2.0 * specific_energy)
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)
@ -174,7 +192,7 @@ func _calculate_relative_orbital_path(body_to_trace: OrbitalBody3D) -> PackedVec
var distance_sq = ghost_relative_pos.length_squared()
if distance_sq < 1.0:
break
var direction = -ghost_relative_pos.normalized()
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
@ -185,7 +203,6 @@ func _calculate_relative_orbital_path(body_to_trace: OrbitalBody3D) -> PackedVec
return path_points
# --- These functions are scalar and need no changes ---
func calculate_hill_sphere(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> float:
if not is_instance_valid(orbiter) or not is_instance_valid(primary) or primary.mass <= 0:
@ -193,7 +210,7 @@ func calculate_hill_sphere(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> fl
var distance = orbiter.global_position.distance_to(primary.global_position)
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)
return distance * pow(mass_ratio, 1.0 / 3.0)
func calculate_simplified_roche_limit(primary: OrbitalBody3D) -> float:
if not is_instance_valid(primary) or primary.mass <= 0: