WIP OrbitalBody3D rework

This commit is contained in:
olof.pettersson
2025-11-15 18:12:00 +01:00
parent 1342ca2610
commit 3647aa599d
21 changed files with 364 additions and 195 deletions

View File

@ -2,7 +2,9 @@
## 1. Game Vision & Concept ## 1. Game Vision & Concept
Project Millimeters of Aluminum is a top-down 2D spaceship simulation game that emphasizes realistic orbital mechanics, deep ship management, and cooperative crew gameplay. Players take on roles as members of a multi-species crew aboard a modular, physically simulated spaceship. Project Millimeters of Aluminum is a third-person 3D spaceship simulation game that emphasizes realistic physics, deep ship management, and cooperative crew gameplay. Players take on roles as members of a multi-species crew aboard a modular, physically simulated spaceship.
The game's aesthetic is inspired by the functional, industrial look of real-world space hardware and sci-fi like The Expanse, focusing on diegetic interfaces and detailed, functional components. The core experience is about planning and executing complex maneuvers in a hazardous, procedurally generated star system, where understanding the ship's systems is as important as piloting skill.
The game's aesthetic is inspired by the technical, gritty, and high-contrast 2D style of games like Barotrauma, focusing on diegetic interfaces and detailed, functional components. The core experience is about planning and executing complex maneuvers in a hazardous, procedurally generated star system, where understanding the ship's systems is as important as piloting skill. The game's aesthetic is inspired by the technical, gritty, and high-contrast 2D style of games like Barotrauma, focusing on diegetic interfaces and detailed, functional components. The core experience is about planning and executing complex maneuvers in a hazardous, procedurally generated star system, where understanding the ship's systems is as important as piloting skill.
@ -12,19 +14,73 @@ The gameplay is centered around a Plan -> Execute -> Manage loop:
1. Plan: The crew uses the Navigation Computer to analyze their orbit and plan complex maneuvers, such as a Hohmann transfer to another planet. They must account for launch windows, fuel costs, and travel time. 1. Plan: The crew uses the Navigation Computer to analyze their orbit and plan complex maneuvers, such as a Hohmann transfer to another planet. They must account for launch windows, fuel costs, and travel time.
2. Execute: The crew engages the autopilot or manually pilots the ship. The Thruster Controller executes the planned burns, performing precise, fuel-optimal rotations and main engine thrusts to alter the ship's trajectory. 2. Execute: The crew engages the autopilot or manually pilots the ship. The Helm executes the planned burns, performing precise, fuel-optimal rotations and main engine thrusts to alter the ship's trajectory.
3. Manage: While underway, the crew manages the ship's modular systems, monitors resources like fuel and power, and responds to emergent events like hull breaches or system failures. 3. Manage: While underway, the crew moves about the ship's 3D interior, manages modular systems, monitors resources, and responds to emergent events like hull breaches or system failures.
## 3. Key Features
### 3. Key Features
### 1. Procedural Star System ### 1. Procedural Star System
The game world is a procedurally generated star system created by the StarSystemGenerator. Each system features a central star, a variable number of planets, moons, and asteroid belts, creating a unique environment for each playthrough. The game world is a procedurally generated star system created by the StarSystemGenerator. Each system features a central star, a variable number of planets, moons, and asteroid belts, creating a unique environment for each playthrough.
### 2. N-Body Physics Simulation ### 2. N-Body Physics Simulation
Major bodies in orbit (CelestialBody class) are goveerened by a simplified n-body gravity simulation. Physical objects with player interaction (ships, crew characters, detached components, and eventually stations) are governed by a realistic N-body gravitational simulation, managed by the OrbitalMechanics library.
- Objects inherit from a base OrbitalBody2D class, ensuring consistent physics. Major bodies in orbit (CelestialBody class) are governed by a 3D n-body gravity simulation, managed by the OrbitalMechanics library. Objects inherit from a base OrbitalBody3D class, ensuring consistent physics. The simulation allows for complex and emergent orbital behaviors.
- This allows for complex and emergent orbital behaviors, such as tidal forces and stable elliptical orbits.
### 3. Modular Spaceship
The player's ship is not a monolithic entity but a collection of distinct, physically simulated components attached to a root Module node.
The Module class extends OrbitalBody3D and aggregates mass and inertia from all child Component and StructuralPiece nodes.
Ship logic is decentralized into data-driven "databanks," such as the HelmLogicShard and AutopilotShard.
Hardware, like a Thruster, is a 3D Component that applies force to the root Module.
### 4. Advanced Navigation Computer
This is the primary crew interface for long-range travel, presented as a diegetic 2D screen (SensorPanel) within the 3D world.
Maneuver Planning: The computer can calculate various orbital transfers, each with strategic trade-offs:
Hohmann Transfer
Brachistochrone (Torchship) Trajectory
Tactical Map: A fully interactive UI map featuring:
Zoom-to-cursor and click-and-drag panning.
Predictive orbital path drawing.
Icon culling and detailed tooltips.
### 5. Physics-Based 3D Character Control
Character control is built on a robust, physics-based 3D system designed for complex zero-G environments.
Pawn/Controller Architecture: Player control is split between a PlayerController3D (which gathers hardware input and sends it via RPC) and a CharacterPawn3D (a CharacterBody3D that acts as the physics integrator).
Modular Movement: The pawn's movement logic is handled by component "brains." The ZeroGMovementComponent manages all zero-G interaction, while the EVAMovementComponent acts as a "dumb tool" providing thruster forces.
Physics-Based Gripping: Players can grab onto designated GripArea3D nodes. This is not an animation lock; a PD controller applies forces to the player's body to move them to the grip point and align them with its orientation.
Zero-G Traversal: The ZeroGMovementComponent features a state machine for IDLE (coasting), CLIMBING (moving between grips), REACHING (pending implementation), and CHARGING_LAUNCH (pushing off surfaces).
### 6. Runtime Component Design & Engineering
(This future-facing concept remains valid from the original design)
To move beyond pre-defined ship parts, the game will feature an in-game system for players to design, prototype, and manufacture their own components. This is achieved through a "Component Blueprint" architecture that separates a component's data definition from its physical form.
Component Blueprints: A ComponentBlueprint is a Resource file (.tres) that acts as a schematic.
Generic Template Scenes: The game will use a small number of generic, unconfigured "template" scenes (e.g., generic_thruster.tscn).
The Design Lab: Players will use a dedicated SystemStation to create and modify blueprints.
Networked Construction: A global ComponentFactory on the server will instantiate and configure components based on player-chosen blueprints, which are then replicated by the MultiplayerSpawner.
### 3. Modular Spaceship ### 3. Modular Spaceship
@ -78,19 +134,17 @@ To move beyond pre-defined ship parts, the game will feature an in-game system f
3. A global `ComponentFactory` singleton on the server takes the blueprint, instantiates the correct generic template scene, and applies the blueprint's property overrides to the new instance. 3. A global `ComponentFactory` singleton on the server takes the blueprint, instantiates the correct generic template scene, and applies the blueprint's property overrides to the new instance.
4. This fully-configured node is then passed to the `MultiplayerSpawner`, which replicates the object across the network, ensuring all clients see the correctly customized component. 4. This fully-configured node is then passed to the `MultiplayerSpawner`, which replicates the object across the network, ensuring all clients see the correctly customized component.
## 4. Technical Overview ## 4. Technical Overview
- Architecture: The project uses a decoupled, modular architecture. A GameManager handles global state, while ship systems are managed by ControlPanel and Databank resources loaded by a SystemStation.
- Architecture: The project uses a decoupled, modular architecture heavily reliant on a global SignalBus for inter-scene communication and a GameManager for global state. Ships feature their own local ShipSignalBus for internal component communication.
- Key Scripts: - Key Scripts:
- OrbitalBody2D.gd: The base class for all physical objects. -OrbitalBody3D.gd: The base class for all physical objects.
- Spaceship.gd: The central hub for a player ship. - Module.gd: The central hub for a player ship, aggregating mass, inertia, and components.
- Thruster.gd: A self-contained, physically simulated thruster component. - HelmLogicShard.gd / AutopilotShard.gd: Databanks that contain the advanced autopilot and manual control logic.
- ThrusterController.gd: Contains advanced autopilot and manual control logic (PD controller, bang-coast-bang maneuvers). - SensorPanel.gd: A Control node that manages the interactive map UI.
- NavigationComputer.gd: Manages the UI and high-level maneuver planning. - CharacterPawn3D.gd / ZeroGMovementComponent.gd: Manages all third-person 3D physics-based character movement.
- MapDrawer.gd: A Control node that manages the interactive map UI.
- MapIcon.gd: The reusable UI component for map objects.
- Art Style: Aims for a Barotraumainspired aesthetic using 2D ragdolls (Skeleton2D, PinJoint2D), detailed sprites with normal maps, and high-contrast dynamic lighting (PointLight2D, LightOccluder2D). - Art Style: Aims for a functional, industrial 3D aesthetic. Character movement is physics-based using CharacterBody3D and Area3D grip detection. Ship interiors will be built from 3D modules and viewed from an over-the-shoulder camera.
## 5. Game Progression & Economy ## 5. Game Progression & Economy
This is the biggest area for potential expansion. A new section could detail how the player engages with the world and improves their situation over time. This is the biggest area for potential expansion. A new section could detail how the player engages with the world and improves their situation over time.
@ -126,11 +180,12 @@ You mention "emergent events" in the gameplay loop. It would be beneficial to de
## 7. Crew Interaction & Ship Interior ## 7. Crew Interaction & Ship Interior
Since co-op and crew management are central, detailing this aspect is crucial. Since co-op and crew management are central, detailing this aspect is crucial.
### 1. Ship Interior Management: ### 1. Ship Interior Management:
- Diegetic Interfaces: You mention this in the vision. It's worth specifying how the crew will interact with systems. Will they need to be at a specific console (like the Navigation Computer) to use it? Do repairs require a character to physically be at the damaged module? - Diegetic Interfaces: The crew will interact with systems from a third-person, over-the-shoulder perspective. They must be at a specific SystemStation to use its panels. Repairs will require a character to physically be at the damaged module.
- Atmospherics & Life Support: How is the ship's interior environment simulated? Will fires or toxic gas leaks be a possibility? This ties directly into your LifeSupport system. - Atmospherics & Life Support: How is the ship's interior environment simulated? This will tie into the LifeSupport system.
### 2. Character States: ### 2. Character States:
- Health & Injury: How are characters affected by hazards? Can they be injured in high-G maneuvers or from system failures? - Health & Injury: How are characters affected by hazards? Can they be injured in high-G maneuvers or from system failures?
- EVA (Extra-Vehicular Activity): Detail the mechanics for EVAs. What equipment is needed? How is movement handled in zero-G? This would be a perfect role for the "Hard Vacuum Monster" species. - EVA (Extra-Vehicular Activity): This is a core feature. The EVAMovementComponent provides force-based thruster control for linear movement and roll torque. The ZeroGMovementComponent manages gripping, climbing, and launching from the ship's exterior and interior surfaces.
- Movement for the "Hard Vacuum Monster" species can be refined from a version of the reaching component where it can grab any nearby surface and can generate enough suction strength to remain attached to a moving object.

View File

@ -1,13 +1,17 @@
[gd_scene load_steps=4 format=3 uid="uid://bkwogkfqk2uxo"] [gd_scene load_steps=5 format=3 uid="uid://bkwogkfqk2uxo"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_ktv2t"] [ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_ktv2t"]
[ext_resource type="PackedScene" uid="uid://bsyufiv0m1018" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_shb7f"] [ext_resource type="PackedScene" uid="uid://bsyufiv0m1018" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_shb7f"]
[ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="3_ism2t"] [ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="3_ism2t"]
[node name="3dTestShip" type="Node3D"] [sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_ism2t"]
properties/0/path = NodePath(".:position")
properties/0/spawn = true
properties/0/replication_mode = 1
[node name="3dTestShip" type="RigidBody3D"]
script = ExtResource("1_ktv2t") script = ExtResource("1_ktv2t")
physics_mode = 1 physics_mode = 1
mass = 1.0
metadata/_custom_type_script = "uid://6co67nfy8ngb" metadata/_custom_type_script = "uid://6co67nfy8ngb"
[node name="Hullplate" parent="." instance=ExtResource("2_shb7f")] [node name="Hullplate" parent="." instance=ExtResource("2_shb7f")]
@ -462,6 +466,7 @@ transform = Transform3D(0.99989736, 0, 0.014328662, -0.014328662, -4.371139e-08,
[node name="Spawner" parent="." instance=ExtResource("3_ism2t")] [node name="Spawner" parent="." instance=ExtResource("3_ism2t")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
disabled = true
[node name="OmniLight3D" type="OmniLight3D" parent="."] [node name="OmniLight3D" type="OmniLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 1, -3) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 1, -3)
@ -478,3 +483,6 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4, 1, 4)
[node name="Camera3D" type="Camera3D" parent="."] [node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3) transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)
current = true current = true
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
replication_config = SubResource("SceneReplicationConfig_ism2t")

View File

@ -0,0 +1,26 @@
[gd_scene load_steps=4 format=3 uid="uid://xcgmicfdqqb1"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_ogx5r"]
[ext_resource type="PackedScene" uid="uid://bsyufiv0m1018" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_nyqc6"]
[ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="3_3bya3"]
[node name="PhysicsTestingShip" type="RigidBody3D"]
script = ExtResource("1_ogx5r")
base_mass = 200.0
metadata/_custom_type_script = "uid://6co67nfy8ngb"
[node name="Hullplate" parent="." instance=ExtResource("2_nyqc6")]
transform = Transform3D(1, 0, 0, 0, -4.371139e-08, 1, 0, -1, -4.371139e-08, 0, -1, 0)
[node name="Spawner" parent="." instance=ExtResource("3_3bya3")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.021089494, 0)
[node name="OmniLight3D" type="OmniLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0, -2)
[node name="OmniLight3D2" type="OmniLight3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, -2)
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)
current = true

View File

@ -21,6 +21,7 @@ OrbitalMechanics="*res://scripts/singletons/orbital_mechanics.gd"
GameManager="*res://scripts/singletons/game_manager.gd" GameManager="*res://scripts/singletons/game_manager.gd"
Constants="*res://scripts/singletons/constants.gd" Constants="*res://scripts/singletons/constants.gd"
NetworkHandler="*res://scripts/network/network_handler.gd" NetworkHandler="*res://scripts/network/network_handler.gd"
MotionUtils="*res://scripts/singletons/motion_utils.gd"
[display] [display]
@ -170,14 +171,15 @@ left_click={
[physics] [physics]
common/physics_jitter_fix=0.0 3d/default_gravity=0.0
3d/default_gravity_vector=Vector3(0, 0, 0)
3d/default_linear_damp=0.0 3d/default_linear_damp=0.0
3d/default_angular_damp=0.0
3d/sleep_threshold_linear=0.0 3d/sleep_threshold_linear=0.0
2d/default_gravity=0.0 2d/default_gravity=0.0
2d/default_gravity_vector=Vector2(0, 0) 2d/default_gravity_vector=Vector2(0, 0)
2d/default_linear_damp=0.0 2d/default_linear_damp=0.0
2d/sleep_threshold_linear=0.0 2d/sleep_threshold_linear=0.0
common/physics_interpolation=true
[plugins] [plugins]

View File

@ -1,7 +1,11 @@
[gd_scene load_steps=2 format=3 uid="uid://cm0rohkr6khd1"] [gd_scene load_steps=2 format=3 uid="uid://dfnc0ipvwuhwd"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_b1h2b"] [ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_b1h2b"]
[node name="Module" type="Node3D"] [node name="Module" type="RigidBody3D"]
script = ExtResource("1_b1h2b") script = ExtResource("1_b1h2b")
mass = 1.0 ship_name = null
hull_integrity = null
physics_mode = null
base_mass = null
metadata/_custom_type_script = "uid://wlm40n8ywr"

View File

@ -1,23 +1,17 @@
[gd_scene load_steps=4 format=3 uid="uid://bsyufiv0m1018"] [gd_scene load_steps=4 format=3 uid="uid://bsyufiv0m1018"]
[ext_resource type="Script" uid="uid://wlm40n8ywr" path="res://scripts/orbital_body_2d.gd" id="1_ecow4"] [ext_resource type="Script" uid="uid://cxnbunw3k7s5j" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_ecow4"]
[sub_resource type="BoxMesh" id="BoxMesh_ecow4"]
size = Vector3(1, 1, 0.02)
[sub_resource type="BoxShape3D" id="BoxShape3D_ecow4"] [sub_resource type="BoxShape3D" id="BoxShape3D_ecow4"]
size = Vector3(1, 1, 0.02) size = Vector3(1, 1, 0.02)
[node name="Hullplate" type="Node3D"] [sub_resource type="BoxMesh" id="BoxMesh_ecow4"]
script = ExtResource("1_ecow4") size = Vector3(1, 1, 0.02)
physics_mode = 2
metadata/_custom_type_script = "uid://wlm40n8ywr"
[node name="StaticBody3D" type="StaticBody3D" parent="."] [node name="Hullplate" type="CollisionShape3D"]
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D"]
mesh = SubResource("BoxMesh_ecow4")
skeleton = NodePath("../..")
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
shape = SubResource("BoxShape3D_ecow4") shape = SubResource("BoxShape3D_ecow4")
script = ExtResource("1_ecow4")
metadata/_custom_type_script = "uid://cxnbunw3k7s5j"
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
mesh = SubResource("BoxMesh_ecow4")

View File

@ -0,0 +1,2 @@
@abstract
class_name ShipPiece extends CollisionShape3D

View File

@ -0,0 +1 @@
uid://dg46wkbv2ep3h

View File

@ -1 +1 @@
class_name StructuralPiece extends OrbitalBody3D class_name StructuralPiece extends ShipPiece

View File

@ -2,6 +2,7 @@ extends Area3D
class_name Spawner class_name Spawner
@onready var mp_spawner: MultiplayerSpawner = $MultiplayerSpawner @onready var mp_spawner: MultiplayerSpawner = $MultiplayerSpawner
@export var disabled: bool = false
# This spawner will register itself with the GameManager when it enters the scene. # This spawner will register itself with the GameManager when it enters the scene.
func _ready(): func _ready():
@ -11,6 +12,7 @@ func _ready():
GameManager.register_spawner(self) GameManager.register_spawner(self)
func can_spawn() -> bool: func can_spawn() -> bool:
return get_overlapping_bodies().is_empty() return false if disabled else get_overlapping_bodies().is_empty()
# We can add properties to the spawner later, like which faction it belongs to, # We can add properties to the spawner later, like which faction it belongs to,
# or a reference to the body it's orbiting for initial velocity calculation. # or a reference to the body it's orbiting for initial velocity calculation.

View File

@ -87,7 +87,7 @@ func apply_thrust_force():
var force_vector = global_transform.basis * local_force var force_vector = global_transform.basis * local_force
# 3. Apply the force to itself. # 3. Apply the force to itself.
apply_force(force_vector, global_position) apply_force_recursive(force_vector, global_position)
# func _draw(): # func _draw():
# # This function is only called if the thruster is firing (due to queue_redraw) # # This function is only called if the thruster is firing (due to queue_redraw)

View File

@ -214,7 +214,7 @@ func _calibrate_single_thruster(thruster: Thruster) -> DataTypes.ThrusterData:
# --- Calculate Performance --- # --- Calculate Performance ---
# Torque = inertia * angular_acceleration (alpha = dw/dt) # Torque = inertia * angular_acceleration (alpha = dw/dt)
if root_module.inertia > 0: if root_module.inertia.length_squared() > 0:
data.measured_torque_vector = root_module.inertia * (delta_angular_velocity / test_burn_duration) data.measured_torque_vector = root_module.inertia * (delta_angular_velocity / test_burn_duration)
else: else:
data.measured_torque_vector = Vector3.ZERO data.measured_torque_vector = Vector3.ZERO
@ -235,7 +235,7 @@ func _calibrate_single_thruster(thruster: Thruster) -> DataTypes.ThrusterData:
# --- Cleanup: Counter the spin from the test fire --- # --- Cleanup: Counter the spin from the test fire ---
if data.measured_torque_vector.length() > 0.001: if data.measured_torque_vector.length_squared() > 0.001:
var counter_torque = -data.measured_torque_vector var counter_torque = -data.measured_torque_vector
var counter_burn_duration = (root_module.inertia * root_module.angular_velocity) / counter_torque var counter_burn_duration = (root_module.inertia * root_module.angular_velocity) / counter_torque

View File

@ -1,5 +1,5 @@
# CharacterPawn.gd # CharacterPawn.gd
extends CharacterBody3D extends OrbitalBody3D
class_name CharacterPawn3D class_name CharacterPawn3D
## Core Parameters ## Core Parameters
@ -29,7 +29,7 @@ var _pitch_yaw_input: Vector2 = Vector2.ZERO
@onready var zero_g_movemement_component: ZeroGMovementComponent = $ZeroGMovementComponent @onready var zero_g_movemement_component: ZeroGMovementComponent = $ZeroGMovementComponent
## Physics State (Managed by Pawn) ## Physics State (Managed by Pawn)
var angular_velocity: Vector3 = Vector3.ZERO # var angular_velocity: Vector3 = Vector3.ZERO
@export var angular_damping: float = 0.95 # Base damping @export var angular_damping: float = 0.95 # Base damping
## Other State Variables ## Other State Variables
@ -74,21 +74,14 @@ func _physics_process(delta: float):
# 4. Apply Linear Velocity & Collision (Universal) # 4. Apply Linear Velocity & Collision (Universal)
# Use move_and_slide for states affected by gravity/floor or zero-g collisions # Use move_and_slide for states affected by gravity/floor or zero-g collisions
move_and_slide() move_and_collide(linear_velocity * delta)
# Check for collision response AFTER move_and_slide
var collision_count = get_slide_collision_count()
if collision_count > 0:
var collision = get_slide_collision(collision_count - 1) # Get last collision
# Delegate or handle basic bounce
if eva_suit_component:
eva_suit_component.handle_collision(collision, collision_energy_loss)
else:
_handle_basic_collision(collision)
# 5. Reset Inputs # 5. Reset Inputs
_reset_inputs() _reset_inputs()
func _integrate_forces(state: PhysicsDirectBodyState3D):
pass
# --- Universal Rotation --- # --- Universal Rotation ---
func _apply_mouse_rotation(): func _apply_mouse_rotation():
if _pitch_yaw_input != Vector2.ZERO: if _pitch_yaw_input != Vector2.ZERO:
@ -112,27 +105,20 @@ func _integrate_angular_velocity(delta: float):
if angular_velocity.length_squared() < 0.0001: if angular_velocity.length_squared() < 0.0001:
angular_velocity = Vector3.ZERO angular_velocity = Vector3.ZERO
func _handle_basic_collision(collision: KinematicCollision3D): # func _handle_basic_collision(collision: KinematicCollision3D):
var surface_normal = collision.get_normal() # var surface_normal = collision.get_normal()
velocity = velocity.bounce(surface_normal) # velocity = velocity.bounce(surface_normal)
velocity *= (1.0 - collision_energy_loss * 0.5) # velocity *= (1.0 - collision_energy_loss * 0.5)
# --- Public Helper for Controllers --- # --- Public Helper for Controllers ---
# Applies torque affecting angular velocity # Applies torque affecting angular velocity
func add_torque(torque_global: Vector3, delta: float): # func add_torque(torque_global: Vector3, delta: float):
# Calculate effective inertia (base + suit multiplier if applicable) # # Calculate effective inertia (base + suit multiplier if applicable)
var effective_inertia = base_inertia * (eva_suit_component.inertia_multiplier if eva_suit_component else 1.0) # var effective_inertia = base_inertia * (eva_suit_component.inertia_multiplier if eva_suit_component else 1.0)
if effective_inertia <= 0: effective_inertia = 1.0 # Safety prevent division by zero # if effective_inertia <= 0: effective_inertia = 1.0 # Safety prevent division by zero
# Apply change directly to angular velocity using the global torque # # Apply change directly to angular velocity using the global torque
angular_velocity += (torque_global / effective_inertia) * delta # angular_velocity += (torque_global / effective_inertia) * delta
# --- Movement Implementations (Keep non-EVA ones here) ---
func _apply_walking_movement(_delta: float): pass # TODO
func _apply_ladder_floating_drag(delta: float):
velocity = velocity.lerp(Vector3.ZERO, delta * 2.0);
angular_velocity = angular_velocity.lerp(Vector3.ZERO, delta * 2.0)
func _apply_ladder_movement(_delta: float): pass # TODO
# --- Input Setters/Resets (Add vertical to set_movement_input) --- # --- Input Setters/Resets (Add vertical to set_movement_input) ---
func set_movement_input(move: Vector2, roll: float, vertical: float): _move_input = move; _roll_input = roll; _vertical_input = vertical func set_movement_input(move: Vector2, roll: float, vertical: float): _move_input = move; _roll_input = roll; _vertical_input = vertical

View File

@ -23,7 +23,7 @@ properties/2/path = NodePath("CameraPivot:rotation")
properties/2/spawn = true properties/2/spawn = true
properties/2/replication_mode = 2 properties/2/replication_mode = 2
[node name="CharacterPawn3D" type="CharacterBody3D"] [node name="CharacterPawn3D" type="RigidBody3D"]
script = ExtResource("1_4frsu") script = ExtResource("1_4frsu")
metadata/_custom_type_script = "uid://cdmmiixa75f3x" metadata/_custom_type_script = "uid://cdmmiixa75f3x"
@ -44,6 +44,7 @@ top_level = true
spring_length = 3.0 spring_length = 3.0
[node name="Camera3D" type="Camera3D" parent="CameraPivot/SpringArm"] [node name="Camera3D" type="Camera3D" parent="CameraPivot/SpringArm"]
current = true
far = 200000.0 far = 200000.0
[node name="GripDetector" type="Area3D" parent="."] [node name="GripDetector" type="Area3D" parent="."]

View File

@ -6,9 +6,9 @@ class_name EVAMovementComponent
var pawn: CharacterPawn3D var pawn: CharacterPawn3D
## EVA Parameters (Moved from ZeroGPawn) ## EVA Parameters (Moved from ZeroGPawn)
@export var orientation_speed: float = 2.0 # Used for orienting body to camera @export var orientation_speed: float = 20.0 # Used for orienting body to camera
@export var move_speed: float = 2.0 @export var linear_acceleration: float = 20.0
@export var roll_torque: float = 2.5 @export var roll_torque_acceleration: float = 2.5
@export var angular_damping: float = 0.95 # Base damping applied by pawn, suit might add more? @export var angular_damping: float = 0.95 # Base damping applied by pawn, suit might add more?
@export var inertia_multiplier: float = 1.0 # How much the suit adds to pawn's base inertia (placeholder) @export var inertia_multiplier: float = 1.0 # How much the suit adds to pawn's base inertia (placeholder)
@export var stabilization_kp: float = 5.0 @export var stabilization_kp: float = 5.0
@ -58,19 +58,11 @@ func apply_thrusters(pawn: CharacterPawn3D, delta: float, move_input: Vector2, v
var combined_move_dir = move_dir_horizontal + move_dir_vertical var combined_move_dir = move_dir_horizontal + move_dir_vertical
if combined_move_dir != Vector3.ZERO: if combined_move_dir != Vector3.ZERO:
pawn.velocity += combined_move_dir.normalized() * move_speed * delta pawn.apply_central_force(combined_move_dir * linear_acceleration * delta)
# Apply Roll Torque # Apply Roll Torque
var roll_torque_global = -pawn.global_basis.z * (roll_input) * roll_torque # Sign fixed var roll_torque_global = -pawn.basis.z * (roll_input) * roll_torque_acceleration * delta # Sign fixed
pawn.add_torque(roll_torque_global, delta) pawn.apply_torque(roll_torque_global)
## Called by Pawn to handle collision response in FLOATING state
func handle_collision(collision: KinematicCollision3D, collision_energy_loss: float):
if not is_instance_valid(pawn): return
var surface_normal = collision.get_normal()
var reflected_velocity = pawn.velocity.bounce(surface_normal)
reflected_velocity *= (1.0 - collision_energy_loss)
pawn.velocity = reflected_velocity # Update pawn's velocity directly
## Called by Pawn when entering FLOATING state with suit ## Called by Pawn when entering FLOATING state with suit
func on_enter_state(): func on_enter_state():
@ -94,13 +86,13 @@ func _apply_floating_movement(delta: float, move_input: Vector2, vertical_input:
var combined_move_dir = move_dir_horizontal + move_dir_vertical var combined_move_dir = move_dir_horizontal + move_dir_vertical
if combined_move_dir != Vector3.ZERO: if combined_move_dir != Vector3.ZERO:
pawn.velocity += combined_move_dir.normalized() * move_speed * delta pawn.apply_central_force(combined_move_dir.normalized() * linear_acceleration * delta)
# --- Apply Roll Torque --- # --- Apply Roll Torque ---
# Calculate torque magnitude based on input # Calculate torque magnitude based on input
var roll_torque_vector = pawn.transform.basis.z * (-roll_input) * roll_torque var roll_acceleration = pawn.basis.z * (-roll_input) * roll_torque_acceleration * delta
# Apply the global torque vector using the pawn's helper function # Apply the global torque vector using the pawn's helper function
pawn.add_torque(roll_torque_vector, delta) pawn.apply_torque(roll_acceleration)
# --- Auto-Orientation Logic --- # --- Auto-Orientation Logic ---
@ -112,8 +104,8 @@ func _orient_pawn(delta: float):
# --- THE FIX: Adjust how target_up is calculated --- # --- THE FIX: Adjust how target_up is calculated ---
# Calculate velocity components relative to camera orientation # Calculate velocity components relative to camera orientation
var _forward_velocity_component = pawn.velocity.dot(target_forward) # var _forward_velocity_component = pawn.velocity.dot(target_forward)
var _right_velocity_component = pawn.velocity.dot(pawn.camera_anchor.global_basis.x) # var _right_velocity_component = pawn.velocity.dot(pawn.camera_anchor.global_basis.x)
# Only apply strong "feet trailing" if significant forward/backward movement dominates # Only apply strong "feet trailing" if significant forward/backward movement dominates
# and we are actually moving. # and we are actually moving.

View File

@ -15,7 +15,8 @@ var nearby_grips: Array[GripArea3D] = []
@export var reach_orient_speed: float = 10.0 # Speed pawn orients to grip @export var reach_orient_speed: float = 10.0 # Speed pawn orients to grip
# --- Grip damping parameters --- # --- Grip damping parameters ---
@export var gripping_linear_damping: float = 5.0 # How quickly velocity stops @export var gripping_linear_damping: float = 50.0 # How quickly velocity stops
@export var gripping_linear_kd: float = 2 * sqrt(gripping_linear_damping) # How quickly velocity stops
@export var gripping_angular_damping: float = 5.0 # How quickly spin stops @export var gripping_angular_damping: float = 5.0 # How quickly spin stops
@export var gripping_orient_speed: float = 2.0 # How quickly pawn rotates to face grip @export var gripping_orient_speed: float = 2.0 # How quickly pawn rotates to face grip
@ -32,8 +33,8 @@ var next_grip_target: GripArea3D = null # The grip we are trying to transition t
# --- Seeking Climb State --- # --- Seeking Climb State ---
var _seeking_climb_input: Vector2 = Vector2.ZERO # The move_input held when seeking started var _seeking_climb_input: Vector2 = Vector2.ZERO # The move_input held when seeking started
@export var launch_charge_rate: float = 20.0 @export var launch_charge_rate: float = 1.5
@export var max_launch_speed: float = 15.0 @export var max_launch_speed: float = 4.0
var launch_direction: Vector3 = Vector3.ZERO var launch_direction: Vector3 = Vector3.ZERO
var launch_charge: float = 0.0 var launch_charge: float = 0.0
@ -87,19 +88,13 @@ func process_movement(delta: float, move_input: Vector2, vertical_input: float,
_handle_launch_charge(delta) _handle_launch_charge(delta)
## Called by Pawn for collision (optional, might not be needed if grabbing stops movement)
func handle_collision(collision: KinematicCollision3D, collision_energy_loss: float):
# Basic bounce if somehow colliding while using this controller
var surface_normal = collision.get_normal()
pawn.velocity = pawn.velocity.bounce(surface_normal)
pawn.velocity *= (1.0 - collision_energy_loss * 0.5)
# === STATE MACHINE === # === STATE MACHINE ===
func _on_enter_state(state : MovementState): func _on_enter_state(state : MovementState):
print("ZeroGMovementComponent activated for state: ", MovementState.keys()[state]) print("ZeroGMovementComponent activated for state: ", MovementState.keys()[state])
if state == MovementState.GRIPPING: # TODO: Use forces to match velocity to grip
pawn.velocity = Vector3.ZERO # if state == MovementState.GRIPPING:
pawn.angular_velocity = Vector3.ZERO # pawn.velocity = Vector3.ZERO
# pawn.angular_velocity = Vector3.ZERO
# else: # e.g., REACHING_MOVE? # else: # e.g., REACHING_MOVE?
# state = MovementState.IDLE # Or SEARCHING? # state = MovementState.IDLE # Or SEARCHING?
@ -205,17 +200,26 @@ func _apply_grip_physics(delta: float, _move_input: Vector2, roll_input: float):
# --- 2. Apply Linear Force (PD Controller) --- # --- 2. Apply Linear Force (PD Controller) ---
var error_pos = target_position - pawn.global_position var error_pos = target_position - pawn.global_position
# Simple P-controller for velocity (acts as a spring) # Simple P-controller for velocity (acts as a spring)
var target_velocity_pos = error_pos * gripping_linear_damping # 'damping' here acts as Kp
# We get the force from the PD controller and apply it as acceleration.
var force = MotionUtils.calculate_pd_position_force(
target_position,
pawn.global_position,
pawn.linear_velocity, # Use linear_velocity (from RigidBody3D)
gripping_linear_damping, # Kp
gripping_linear_damping # Kd
)
# Simple D-controller (damping) # Simple D-controller (damping)
target_velocity_pos -= pawn.velocity * gripping_angular_damping # 'angular_damping' here acts as Kd # target_velocity_pos -= pawn.linear_velocity * gripping_angular_damping # 'angular_damping' here acts as Kd
# Apply force via acceleration # TODO: Add less force the smaller error_pos is to stop ocillating around target pos
pawn.velocity = pawn.velocity.lerp(target_velocity_pos, delta * 10.0) # Smoothly apply correction pawn.apply_central_force((force / pawn.mass) * delta)
# --- 3. Apply Angular Force (PD Controller) --- # --- 3. Apply Angular Force (PD Controller) ---
if not is_zero_approx(roll_input): if not is_zero_approx(roll_input):
# Manual Roll Input (applies torque) # Manual Roll Input (applies torque)
var roll_torque_global = pawn.global_transform.basis.z * (-roll_input) * gripping_orient_speed # Use global Z var roll_torque_global = pawn.global_transform.basis.z * (-roll_input) * gripping_orient_speed # Use global Z
pawn.add_torque(roll_torque_global, delta) pawn.apply_torque(roll_torque_global * delta)
else: else:
# Auto-Orient (PD Controller) # Auto-Orient (PD Controller)
_apply_orientation_torque(target_basis, delta) _apply_orientation_torque(target_basis, delta)
@ -245,7 +249,9 @@ func _apply_climb_physics(delta: float, move_input: Vector2):
# 5. Apply Movement Force # 5. Apply Movement Force
var target_velocity = climb_direction * climb_speed var target_velocity = climb_direction * climb_speed
pawn.velocity = pawn.velocity.lerp(target_velocity, delta * climb_acceleration) var error_vel = target_velocity - pawn.linear_velocity
var force = error_vel * climb_acceleration # Kp = climb_acceleration
pawn.apply_central_force(force * delta)
# 6. Apply Angular Force (Auto-Orient to current grip) # 6. Apply Angular Force (Auto-Orient to current grip)
var grip_base_transform = current_grip.global_transform var grip_base_transform = current_grip.global_transform
@ -266,7 +272,6 @@ func _process_seeking_climb(_delta: float, move_input: Vector2):
# No grip found. Transition to IDLE. # No grip found. Transition to IDLE.
print("Seeking Climb ended, no grip found. Reverting to IDLE.") print("Seeking Climb ended, no grip found. Reverting to IDLE.")
# --- Grip Helpers # --- Grip Helpers
## The single, authoritative function for grabbing a grip. ## The single, authoritative function for grabbing a grip.
@ -410,7 +415,8 @@ func _start_climb(move_input: Vector2):
func _stop_climb(release_grip: bool): func _stop_climb(release_grip: bool):
# print("ZeroGMoveController: Stopping Climb. Release Grip: ", release_grip) # print("ZeroGMoveController: Stopping Climb. Release Grip: ", release_grip)
pawn.velocity = pawn.velocity.lerp(Vector3.ZERO, 0.5) # Apply some braking # TODO: Implement using forces
# pawn.velocity = pawn.velocity.lerp(Vector3.ZERO, 0.5) # Apply some braking
next_grip_target = null next_grip_target = null
if release_grip: if release_grip:
_release_current_grip() # Transitions to IDLE _release_current_grip() # Transitions to IDLE
@ -418,21 +424,15 @@ func _stop_climb(release_grip: bool):
current_state = MovementState.GRIPPING # Go back to stationary gripping current_state = MovementState.GRIPPING # Go back to stationary gripping
func _apply_orientation_torque(target_basis: Basis, delta: float): func _apply_orientation_torque(target_basis: Basis, delta: float):
var current_quat = pawn.global_transform.basis.get_rotation_quaternion() var torque = MotionUtils.calculate_pd_rotation_torque(
var target_quat = target_basis.get_rotation_quaternion() target_basis,
var error_quat = target_quat * current_quat.inverse() pawn.global_basis,
pawn.angular_velocity, # Use angular_velocity (from RigidBody3D)
gripping_orient_speed, # Kp
gripping_orient_speed # Kd
)
# Ensure we take the shortest path for rotation. If W is negative, the pawn.apply_torque(torque * delta)
# quaternion represents the "long way around". Negating it gives the same
# orientation but via the shorter rotational path.
if error_quat.w < 0: error_quat = -error_quat
var error_angle = error_quat.get_angle()
var error_axis = error_quat.get_axis()
var torque_proportional = error_axis.normalized() * error_angle * gripping_orient_speed
var torque_derivative = -pawn.angular_velocity * gripping_angular_damping
var total_torque_global = (torque_proportional + torque_derivative)
pawn.add_torque(total_torque_global, delta)
# --- Launch helpers --- # --- Launch helpers ---
func _start_charge(move_input: Vector2): func _start_charge(move_input: Vector2):
@ -452,20 +452,19 @@ func _start_charge(move_input: Vector2):
func _handle_launch_charge(delta: float): func _handle_launch_charge(delta: float):
launch_charge = min(launch_charge + launch_charge_rate * delta, max_launch_speed) launch_charge = min(launch_charge + launch_charge_rate * delta, max_launch_speed)
pawn.velocity = Vector3.ZERO
pawn.angular_velocity = Vector3.ZERO
func _execute_launch(move_input: Vector2): func _execute_launch(move_input: Vector2):
if not is_instance_valid(current_grip): return # Safety check if not is_instance_valid(current_grip): return # Safety check
pawn.velocity = launch_direction * launch_charge # Apply launch velocity to pawn
launch_charge = 0.0 launch_charge = 0.0
_release_current_grip(move_input) # Release AFTER calculating direction _release_current_grip(move_input) # Release AFTER calculating direction
pawn.apply_impulse(launch_direction * launch_charge)
# Instead of going to IDLE, go to SEEKING_CLIMB to find the next grip. # Instead of going to IDLE, go to SEEKING_CLIMB to find the next grip.
# The move_input that started the launch is what we'll use for the seek direction. # The move_input that started the launch is what we'll use for the seek direction.
# _seeking_climb_input = (pawn.global_basis.y.dot(launch_direction) * Vector2.UP) + (pawn.global_basis.x.dot(launch_direction) * Vector2.RIGHT) # _seeking_climb_input = (pawn.global_basis.y.dot(launch_direction) * Vector2.UP) + (pawn.global_basis.x.dot(launch_direction) * Vector2.RIGHT)
# current_state = MovementState.SEEKING_CLIMB # current_state = MovementState.SEEKING_CLIMB
print("ZeroGMovementComponent: Launched with speed ", pawn.velocity.length(), " and now SEEKING_CLIMB") print("ZeroGMovementComponent: Launched with speed ", pawn.linear_velocity.length(), " and now SEEKING_CLIMB")
# --- Signal Handlers --- # --- Signal Handlers ---

View File

@ -1,7 +1,6 @@
# orbital_body_3d.gd # orbital_body_3d.gd
# REFACTOR: Extends Node3D instead of Node2D # REFACTOR: Extends Node3D instead of Node2D
class_name OrbitalBody3D class_name OrbitalBody3D extends RigidBody3D
extends Node3D
# Defines the physical behavior of this body. # Defines the physical behavior of this body.
enum PhysicsMode { enum PhysicsMode {
@ -16,11 +15,11 @@ var current_grid_authority: OrbitalBody3D = null
# Mass of this individual component # Mass of this individual component
@export var base_mass: float = 1.0 @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 mass: float = 0.0 # Aggregated mass of this body and all its OrbitalBody3D children
# REFACTOR: All physics properties are now Vector3 # REFACTOR: All physics properties are now Vector3
@export var linear_velocity: Vector3 = Vector3.ZERO # @export var linear_velocity: Vector3 = Vector3.ZERO
@export var angular_velocity: Vector3 = Vector3.ZERO # Represents angular velocity around X, Y, and Z axes # @export var angular_velocity: Vector3 = Vector3.ZERO # Represents angular velocity around X, Y, and Z axes
# Variables to accumulate forces applied during the current physics frame # Variables to accumulate forces applied during the current physics frame
var accumulated_force: Vector3 = Vector3.ZERO var accumulated_force: Vector3 = Vector3.ZERO
@ -30,15 +29,21 @@ var accumulated_torque: Vector3 = Vector3.ZERO
# REFACTOR: This is a simplification. For true 3D physics, this would be an # REFACTOR: This is a simplification. For true 3D physics, this would be an
# inertia tensor (a Basis). But for game physics, a single float # inertia tensor (a Basis). But for game physics, a single float
# (like your CharacterPawn3D) is much simpler to work with. # (like your CharacterPawn3D) is much simpler to work with.
@export var inertia: float = 1.0 # @export var inertia: float = 1.0
func _ready(): func _ready():
freeze_mode = FreezeMode.FREEZE_MODE_KINEMATIC
if physics_mode == PhysicsMode.ANCHORED:
freeze = true
pass
recalculate_physical_properties() recalculate_physical_properties()
set_physics_process(not Engine.is_editor_hint()) set_physics_process(not Engine.is_editor_hint())
# --- PUBLIC FORCE APPLICATION METHODS --- # --- PUBLIC FORCE APPLICATION METHODS ---
# REFACTOR: All arguments are now Vector3 # REFACTOR: All arguments are now Vector3
func apply_force(force: Vector3, pos: Vector3 = self.global_position): func apply_force_recursive(force: Vector3, pos: Vector3 = self.global_position):
# This is the force routing logic. # This is the force routing logic.
match physics_mode: match physics_mode:
PhysicsMode.INDEPENDENT: PhysicsMode.INDEPENDENT:
@ -50,14 +55,14 @@ func apply_force(force: Vector3, pos: Vector3 = self.global_position):
var p = get_parent() var p = get_parent()
while p: while p:
if p is OrbitalBody3D: if p is OrbitalBody3D:
# Recursively call the parent's apply_force method. # Recursively call the parent's apply_force_recursive method.
p.apply_force(force, pos) p.apply_force_recursive(force, pos)
return # Stop at the first OrbitalBody3D parent return # Stop at the first OrbitalBody3D parent
p = p.get_parent() p = p.get_parent()
push_error("Anchored OrbitalBody3D has become dislodged and is now Composite.") push_error("Anchored OrbitalBody3D has become dislodged and is now Composite.")
physics_mode = PhysicsMode.COMPOSITE physics_mode = PhysicsMode.COMPOSITE
apply_force(force, position) apply_force_recursive(force, position)
func _add_forces(force: Vector3, pos: Vector3 = Vector3.ZERO): func _add_forces(force: Vector3, pos: Vector3 = Vector3.ZERO):
# If we are the root, accumulate the force and calculate torque on the total body. # If we are the root, accumulate the force and calculate torque on the total body.
@ -80,49 +85,43 @@ func _update_mass_and_inertia():
print("Node: %s, Mass: %f" % [self, mass]) print("Node: %s, Mass: %f" % [self, mass])
func _physics_process(delta): func _physics_process(delta):
if not Engine.is_editor_hint(): pass
match physics_mode: # if not Engine.is_editor_hint():
PhysicsMode.INDEPENDENT: # match physics_mode:
_integrate_forces(delta) # PhysicsMode.INDEPENDENT:
PhysicsMode.COMPOSITE: # _integrate_forces(delta)
_integrate_forces(delta) # PhysicsMode.COMPOSITE:
# _integrate_forces(delta)
func _integrate_forces(delta): func _integrate_forces(state: PhysicsDirectBodyState3D):
# Safety Check for Division by Zero # Safety Check for Division by Zero
var sim_mass = mass if mass <= 0.0:
if sim_mass <= 0.0:
accumulated_force = Vector3.ZERO accumulated_force = Vector3.ZERO
accumulated_torque = Vector3.ZERO accumulated_torque = Vector3.ZERO
return return
# 3. Apply Linear Physics (F = ma) # 3. Apply Linear Physics (F = ma)
var linear_acceleration = accumulated_force / sim_mass # Division is now safe # var linear_acceleration = accumulated_force / mass # Division is now safe
linear_velocity += linear_acceleration * delta state.apply_central_force(accumulated_force)
global_position += linear_velocity * delta
# 4. Apply Rotational Physics (T = I * angular_acceleration) # 4. Apply Rotational Physics (T = I * angular_acceleration)
# REFACTOR: Use the simplified 3D torque equation from your CharacterPawn3D # REFACTOR: Use the simplified 3D torque equation from your CharacterPawn3D
if inertia > 0: #if inertia.length() > 0:
var angular_acceleration = accumulated_torque / inertia var angular_acceleration = accumulated_torque / inertia
angular_velocity += angular_acceleration * delta # print("Inertia for %s: %s" % [self, inertia])
# print("Angular Acceleration for %s: %s" % [self, angular_acceleration])
# REFACTOR: Apply 3D rotation using the integrated angular velocity # angular_velocity += angular_acceleration * state.step
# (This is the same method your CharacterPawn3D uses)
if angular_velocity.length_squared() > 0.0001:
rotate(angular_velocity.normalized(), angular_velocity.length() * delta)
# Optional: Add damping
# angular_velocity *= (1.0 - 0.1 * delta)
# 5. Reset accumulated forces for the next frame # 5. Reset accumulated forces for the next frame
accumulated_force = Vector3.ZERO accumulated_force = Vector3.ZERO
accumulated_torque = Vector3.ZERO accumulated_torque = Vector3.ZERO
func recalculate_physical_properties(): func recalculate_physical_properties():
if physics_mode != PhysicsMode.COMPOSITE: if physics_mode != PhysicsMode.COMPOSITE:
mass = base_mass mass = base_mass
if inertia <= 0.0: if inertia == Vector3.ZERO:
inertia = 1.0 inertia = Vector3(1.0, 1.0, 1.0)
return return
var all_parts: Array[OrbitalBody3D] = [] var all_parts: Array[OrbitalBody3D] = []
@ -130,7 +129,7 @@ func recalculate_physical_properties():
if all_parts.is_empty(): if all_parts.is_empty():
mass = base_mass mass = base_mass
inertia = 1.0 inertia = Vector3(1.0, 1.0, 1.0)
return return
# --- Step 1: Calculate Total Mass and LOCAL Center of Mass --- # --- Step 1: Calculate Total Mass and LOCAL Center of Mass ---
@ -147,7 +146,7 @@ func recalculate_physical_properties():
local_center_of_mass = weighted_local_pos_sum / total_mass local_center_of_mass = weighted_local_pos_sum / total_mass
# --- Step 2: Calculate Total Moment of Inertia around the LOCAL CoM --- # --- Step 2: Calculate Total Moment of Inertia around the LOCAL CoM ---
var total_inertia = 0.0 var total_inertia: Vector3 = Vector3.ZERO
for part in all_parts: for part in all_parts:
var local_pos = part.global_position - self.global_position var local_pos = part.global_position - self.global_position
# REFACTOR: This logic (Parallel Axis Theorem) is still correct for Vector3 # REFACTOR: This logic (Parallel Axis Theorem) is still correct for Vector3
@ -156,13 +155,13 @@ func recalculate_physical_properties():
# --- Step 3: Assign the final values --- # --- Step 3: Assign the final values ---
self.mass = total_mass self.mass = total_mass
self.inertia = total_inertia * 0.01 self.inertia = total_inertia * 0.01
if self.inertia <= 0.0: # Safety check if self.inertia == Vector3.ZERO: # Safety check
self.inertia = 1.0 inertia = Vector3(1.0, 1.0, 1.0)
# A recursive helper function to get an array of all OrbitalBody3D children # A recursive helper function to get an array of all OrbitalBody3D children
func _collect_anchored_parts(parts_array: Array): func _collect_anchored_parts(parts_array: Array):
parts_array.append(self) parts_array.append(self)
for child in get_children(): for child in get_children():
if child is OrbitalBody3D and child.physics_mode == PhysicsMode.ANCHORED: if child is OrbitalBody3D and child.physics_mode == PhysicsMode.ANCHORED:
child._collect_anchored_parts(parts_array) child._collect_anchored_parts(parts_array)

View File

@ -1,7 +1,7 @@
[gd_resource type="Resource" script_class="GameConfig" load_steps=5 format=3 uid="uid://cv15sck8rl2b7"] [gd_resource type="Resource" script_class="GameConfig" load_steps=5 format=3 uid="uid://cv15sck8rl2b7"]
[ext_resource type="PackedScene" uid="uid://chgycmkkaf7jv" path="res://scenes/characters/pilot_ball.tscn" id="1_s0mxw"] [ext_resource type="PackedScene" uid="uid://chgycmkkaf7jv" path="res://scenes/characters/pilot_ball.tscn" id="1_s0mxw"]
[ext_resource type="PackedScene" uid="uid://bkwogkfqk2uxo" path="res://modules/3d_test_ship.tscn" id="2_75b4c"] [ext_resource type="PackedScene" uid="uid://xcgmicfdqqb1" path="res://modules/physics_testing_ship.tscn" id="2_75b4c"]
[ext_resource type="PackedScene" uid="uid://dnre6svquwdtb" path="res://scenes/characters/player_controller.tscn" id="2_sk8k5"] [ext_resource type="PackedScene" uid="uid://dnre6svquwdtb" path="res://scenes/characters/player_controller.tscn" id="2_sk8k5"]
[ext_resource type="Script" uid="uid://bfc6u1f8sigxj" path="res://scripts/singletons/game_config.gd" id="3_75b4c"] [ext_resource type="Script" uid="uid://bfc6u1f8sigxj" path="res://scripts/singletons/game_config.gd" id="3_75b4c"]

View File

@ -0,0 +1,97 @@
extends Node
## Calculates the required delta-v vector to match a target's velocity.
func get_velocity_match_delta_v(
current_velocity: Vector3,
target_velocity: Vector3
) -> Vector3:
# The required change in velocity is simply the difference.
return target_velocity - current_velocity
## Calculates the torque required to rotate to a target basis and stop.
## (A spring-damper system for rotation)
func calculate_pd_rotation_torque(
target_basis: Basis,
current_basis: Basis,
current_angular_velocity: Vector3,
kp: float, # Proportional gain (the "spring" stiffness)
kd: float # Derivative gain (the "damper" strength)
) -> Vector3:
# Find the shortest rotational "error"
var current_quat = current_basis.get_rotation_quaternion()
var target_quat = target_basis.get_rotation_quaternion()
var error_quat = target_quat * current_quat.inverse()
if error_quat.w < 0:
error_quat = -error_quat
var error_angle = error_quat.get_angle()
var error_axis = error_quat.get_axis()
# Safety check: if we are already aligned, do nothing
if error_axis.is_zero_approx():
# Return a torque that damps the current spin
return -current_angular_velocity * kd
# P-Term (Spring): Applies torque to correct the angle
var torque_p = error_axis.normalized() * error_angle * kp
# D-Term (Damper): Applies torque to stop the current spin
var torque_d = -current_angular_velocity * kd
return torque_p + torque_d
## Calculates the torque required to aim at a target position and track it.
## This is a high-level function that uses the PD rotation controller.
func calculate_tracking_torque(
tracker_transform: Transform3D,
target_global_position: Vector3,
current_angular_velocity: Vector3,
tracker_up_vector: Vector3,
kp: float, # How "strong" the tracker's motors are
kd: float # How "damped" the tracker's motors are
) -> Vector3:
# 1. Determine the direction we *want* to look
var look_at_direction = tracker_transform.origin.direction_to(target_global_position)
# Safety check (if target is at our origin)
if look_at_direction.is_zero_approx():
return Vector3.ZERO
# 2. Calculate the desired orientation
var target_basis = Basis.looking_at(look_at_direction, tracker_up_vector)
# 3. Use the generic PD controller to get the torque needed
return calculate_pd_rotation_torque(
target_basis,
tracker_transform.basis,
current_angular_velocity,
kp,
kd
)
## Calculates the force required to move to a target position and stop.
## (A spring-damper system)
func calculate_pd_position_force(
target_pos: Vector3,
current_pos: Vector3,
current_velocity: Vector3,
kp: float, # Proportional gain (the "spring" stiffness)
kd: float # Derivative gain (the "damper" strength)
) -> Vector3:
# P-Term (Spring): Pulls you towards the target
# The force is proportional to the distance (error)
var error_pos = target_pos - current_pos
var force_p = error_pos * kp
# D-Term (Damper): Pushes against your current velocity
# This stops you from overshooting and oscillating
var force_d = -current_velocity * kd
# The final force is the sum of the spring and the damper
return force_p + force_d

View File

@ -0,0 +1 @@
uid://c465dh6n0s7hy

View File

@ -29,7 +29,7 @@ func _physics_process(_delta: float) -> void:
apply_n_body_forces(system_attractors) apply_n_body_forces(system_attractors)
for star_orbiter in star_system.get_orbital_bodies(): for star_orbiter in star_system.get_orbital_bodies():
star_orbiter.apply_force(calculate_n_body_force(star_orbiter, top_level_bodies)) star_orbiter.apply_force_recursive(calculate_n_body_force(star_orbiter, top_level_bodies))
func calculate_gravitational_force(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> Vector3: func calculate_gravitational_force(orbiter: OrbitalBody3D, primary: OrbitalBody3D) -> Vector3:
if not is_instance_valid(orbiter) or not is_instance_valid(primary): if not is_instance_valid(orbiter) or not is_instance_valid(primary):
@ -65,8 +65,8 @@ func apply_n_body_forces(attractors: Array[OrbitalBody3D]):
var force_vector = calculate_gravitational_force(body_a, body_b) var force_vector = calculate_gravitational_force(body_a, body_b)
if force_vector != Vector3.ZERO: if force_vector != Vector3.ZERO:
body_a.apply_force(force_vector) body_a.apply_force_recursive(force_vector)
body_b.apply_force(-force_vector) body_b.apply_force_recursive(-force_vector)
func calculate_n_body_force(body: OrbitalBody3D, attractors: Array[OrbitalBody3D]) -> Vector3: func calculate_n_body_force(body: OrbitalBody3D, attractors: Array[OrbitalBody3D]) -> Vector3:
var total_pull: Vector3 = Vector3.ZERO var total_pull: Vector3 = Vector3.ZERO
@ -123,19 +123,19 @@ func _calculate_n_body_orbital_path(body_to_trace: OrbitalBody3D) -> PackedVecto
var path_points = PackedVector3Array() var path_points = PackedVector3Array()
for i in range(num_steps): # for i in range(num_steps):
var ghost_body = OrbitalBody3D.new() # var ghost_body = OrbitalBody3D.new()
ghost_body.global_position = ghost_position # ghost_body.global_position = ghost_position
ghost_body.mass = body_to_trace.mass # ghost_body.mass = body_to_trace.mass
var total_force = calculate_n_body_gravity_forces(ghost_body) # var total_force = calculate_n_body_gravity_forces(ghost_body)
var acceleration = total_force / ghost_body.mass # var acceleration = total_force / ghost_body.mass
ghost_velocity += acceleration * time_step # ghost_velocity += acceleration * time_step
ghost_position += ghost_velocity * time_step # ghost_position += ghost_velocity * time_step
path_points.append(ghost_position) # path_points.append(ghost_position)
ghost_body.free() # ghost_body.free()
return path_points return path_points