WIP OrbitalBody3D rework
This commit is contained in:
@ -2,7 +2,9 @@
|
||||
|
||||
## 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.
|
||||
|
||||
@ -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.
|
||||
|
||||
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
|
||||
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
|
||||
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.
|
||||
- This allows for complex and emergent orbital behaviors, such as tidal forces and stable elliptical orbits.
|
||||
|
||||
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.
|
||||
|
||||
### 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
|
||||
|
||||
@ -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.
|
||||
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
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- Key Scripts:
|
||||
- OrbitalBody2D.gd: The base class for all physical objects.
|
||||
- Spaceship.gd: The central hub for a player ship.
|
||||
- Thruster.gd: A self-contained, physically simulated thruster component.
|
||||
- ThrusterController.gd: Contains advanced autopilot and manual control logic (PD controller, bang-coast-bang maneuvers).
|
||||
- NavigationComputer.gd: Manages the UI and high-level maneuver planning.
|
||||
- MapDrawer.gd: A Control node that manages the interactive map UI.
|
||||
- MapIcon.gd: The reusable UI component for map objects.
|
||||
-OrbitalBody3D.gd: The base class for all physical objects.
|
||||
- Module.gd: The central hub for a player ship, aggregating mass, inertia, and components.
|
||||
- HelmLogicShard.gd / AutopilotShard.gd: Databanks that contain the advanced autopilot and manual control logic.
|
||||
- SensorPanel.gd: A Control node that manages the interactive map UI.
|
||||
- CharacterPawn3D.gd / ZeroGMovementComponent.gd: Manages all third-person 3D physics-based character movement.
|
||||
|
||||
- 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
|
||||
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
|
||||
Since co-op and crew management are central, detailing this aspect is crucial.
|
||||
|
||||
|
||||
### 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?
|
||||
- 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.
|
||||
|
||||
- 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? This will tie into the LifeSupport system.
|
||||
|
||||
### 2. Character States:
|
||||
- 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.
|
||||
|
||||
@ -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="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"]
|
||||
|
||||
[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")
|
||||
physics_mode = 1
|
||||
mass = 1.0
|
||||
metadata/_custom_type_script = "uid://6co67nfy8ngb"
|
||||
|
||||
[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")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 2)
|
||||
disabled = true
|
||||
|
||||
[node name="OmniLight3D" type="OmniLight3D" parent="."]
|
||||
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="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)
|
||||
current = true
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("SceneReplicationConfig_ism2t")
|
||||
|
||||
26
modules/physics_testing_ship.tscn
Normal file
26
modules/physics_testing_ship.tscn
Normal 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
|
||||
@ -21,6 +21,7 @@ OrbitalMechanics="*res://scripts/singletons/orbital_mechanics.gd"
|
||||
GameManager="*res://scripts/singletons/game_manager.gd"
|
||||
Constants="*res://scripts/singletons/constants.gd"
|
||||
NetworkHandler="*res://scripts/network/network_handler.gd"
|
||||
MotionUtils="*res://scripts/singletons/motion_utils.gd"
|
||||
|
||||
[display]
|
||||
|
||||
@ -170,14 +171,15 @@ left_click={
|
||||
|
||||
[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_angular_damp=0.0
|
||||
3d/sleep_threshold_linear=0.0
|
||||
2d/default_gravity=0.0
|
||||
2d/default_gravity_vector=Vector2(0, 0)
|
||||
2d/default_linear_damp=0.0
|
||||
2d/sleep_threshold_linear=0.0
|
||||
common/physics_interpolation=true
|
||||
|
||||
[plugins]
|
||||
|
||||
|
||||
@ -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"]
|
||||
|
||||
[node name="Module" type="Node3D"]
|
||||
[node name="Module" type="RigidBody3D"]
|
||||
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"
|
||||
|
||||
@ -1,23 +1,17 @@
|
||||
[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"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ecow4"]
|
||||
size = Vector3(1, 1, 0.02)
|
||||
[ext_resource type="Script" uid="uid://cxnbunw3k7s5j" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_ecow4"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ecow4"]
|
||||
size = Vector3(1, 1, 0.02)
|
||||
|
||||
[node name="Hullplate" type="Node3D"]
|
||||
script = ExtResource("1_ecow4")
|
||||
physics_mode = 2
|
||||
metadata/_custom_type_script = "uid://wlm40n8ywr"
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_ecow4"]
|
||||
size = Vector3(1, 1, 0.02)
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="."]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D"]
|
||||
mesh = SubResource("BoxMesh_ecow4")
|
||||
skeleton = NodePath("../..")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
||||
[node name="Hullplate" type="CollisionShape3D"]
|
||||
shape = SubResource("BoxShape3D_ecow4")
|
||||
script = ExtResource("1_ecow4")
|
||||
metadata/_custom_type_script = "uid://cxnbunw3k7s5j"
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("BoxMesh_ecow4")
|
||||
|
||||
2
scenes/ship/builder/pieces/ship_piece.gd
Normal file
2
scenes/ship/builder/pieces/ship_piece.gd
Normal file
@ -0,0 +1,2 @@
|
||||
@abstract
|
||||
class_name ShipPiece extends CollisionShape3D
|
||||
1
scenes/ship/builder/pieces/ship_piece.gd.uid
Normal file
1
scenes/ship/builder/pieces/ship_piece.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dg46wkbv2ep3h
|
||||
@ -1 +1 @@
|
||||
class_name StructuralPiece extends OrbitalBody3D
|
||||
class_name StructuralPiece extends ShipPiece
|
||||
|
||||
@ -2,6 +2,7 @@ extends Area3D
|
||||
class_name Spawner
|
||||
|
||||
@onready var mp_spawner: MultiplayerSpawner = $MultiplayerSpawner
|
||||
@export var disabled: bool = false
|
||||
|
||||
# This spawner will register itself with the GameManager when it enters the scene.
|
||||
func _ready():
|
||||
@ -11,6 +12,7 @@ func _ready():
|
||||
GameManager.register_spawner(self)
|
||||
|
||||
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,
|
||||
# or a reference to the body it's orbiting for initial velocity calculation.
|
||||
|
||||
@ -87,7 +87,7 @@ func apply_thrust_force():
|
||||
var force_vector = global_transform.basis * local_force
|
||||
|
||||
# 3. Apply the force to itself.
|
||||
apply_force(force_vector, global_position)
|
||||
apply_force_recursive(force_vector, global_position)
|
||||
|
||||
# func _draw():
|
||||
# # This function is only called if the thruster is firing (due to queue_redraw)
|
||||
|
||||
@ -214,7 +214,7 @@ func _calibrate_single_thruster(thruster: Thruster) -> DataTypes.ThrusterData:
|
||||
|
||||
# --- Calculate Performance ---
|
||||
# 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)
|
||||
else:
|
||||
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 ---
|
||||
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_burn_duration = (root_module.inertia * root_module.angular_velocity) / counter_torque
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# CharacterPawn.gd
|
||||
extends CharacterBody3D
|
||||
extends OrbitalBody3D
|
||||
class_name CharacterPawn3D
|
||||
|
||||
## Core Parameters
|
||||
@ -29,7 +29,7 @@ var _pitch_yaw_input: Vector2 = Vector2.ZERO
|
||||
@onready var zero_g_movemement_component: ZeroGMovementComponent = $ZeroGMovementComponent
|
||||
|
||||
## 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
|
||||
|
||||
## Other State Variables
|
||||
@ -74,21 +74,14 @@ func _physics_process(delta: float):
|
||||
|
||||
# 4. Apply Linear Velocity & Collision (Universal)
|
||||
# Use move_and_slide for states affected by gravity/floor or zero-g collisions
|
||||
move_and_slide()
|
||||
|
||||
# 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)
|
||||
move_and_collide(linear_velocity * delta)
|
||||
|
||||
# 5. Reset Inputs
|
||||
_reset_inputs()
|
||||
|
||||
func _integrate_forces(state: PhysicsDirectBodyState3D):
|
||||
pass
|
||||
|
||||
# --- Universal Rotation ---
|
||||
func _apply_mouse_rotation():
|
||||
if _pitch_yaw_input != Vector2.ZERO:
|
||||
@ -112,27 +105,20 @@ func _integrate_angular_velocity(delta: float):
|
||||
if angular_velocity.length_squared() < 0.0001:
|
||||
angular_velocity = Vector3.ZERO
|
||||
|
||||
func _handle_basic_collision(collision: KinematicCollision3D):
|
||||
var surface_normal = collision.get_normal()
|
||||
velocity = velocity.bounce(surface_normal)
|
||||
velocity *= (1.0 - collision_energy_loss * 0.5)
|
||||
# func _handle_basic_collision(collision: KinematicCollision3D):
|
||||
# var surface_normal = collision.get_normal()
|
||||
# velocity = velocity.bounce(surface_normal)
|
||||
# velocity *= (1.0 - collision_energy_loss * 0.5)
|
||||
|
||||
# --- Public Helper for Controllers ---
|
||||
# Applies torque affecting angular velocity
|
||||
func add_torque(torque_global: Vector3, delta: float):
|
||||
# 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)
|
||||
if effective_inertia <= 0: effective_inertia = 1.0 # Safety prevent division by zero
|
||||
# Apply change directly to angular velocity using the global torque
|
||||
# func add_torque(torque_global: Vector3, delta: float):
|
||||
# # 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)
|
||||
# if effective_inertia <= 0: effective_inertia = 1.0 # Safety prevent division by zero
|
||||
# # Apply change directly to angular velocity using the global torque
|
||||
|
||||
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
|
||||
# angular_velocity += (torque_global / effective_inertia) * delta
|
||||
|
||||
# --- 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
|
||||
|
||||
@ -23,7 +23,7 @@ properties/2/path = NodePath("CameraPivot:rotation")
|
||||
properties/2/spawn = true
|
||||
properties/2/replication_mode = 2
|
||||
|
||||
[node name="CharacterPawn3D" type="CharacterBody3D"]
|
||||
[node name="CharacterPawn3D" type="RigidBody3D"]
|
||||
script = ExtResource("1_4frsu")
|
||||
metadata/_custom_type_script = "uid://cdmmiixa75f3x"
|
||||
|
||||
@ -44,6 +44,7 @@ top_level = true
|
||||
spring_length = 3.0
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="CameraPivot/SpringArm"]
|
||||
current = true
|
||||
far = 200000.0
|
||||
|
||||
[node name="GripDetector" type="Area3D" parent="."]
|
||||
|
||||
@ -6,9 +6,9 @@ class_name EVAMovementComponent
|
||||
var pawn: CharacterPawn3D
|
||||
|
||||
## EVA Parameters (Moved from ZeroGPawn)
|
||||
@export var orientation_speed: float = 2.0 # Used for orienting body to camera
|
||||
@export var move_speed: float = 2.0
|
||||
@export var roll_torque: float = 2.5
|
||||
@export var orientation_speed: float = 20.0 # Used for orienting body to camera
|
||||
@export var linear_acceleration: float = 20.0
|
||||
@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 inertia_multiplier: float = 1.0 # How much the suit adds to pawn's base inertia (placeholder)
|
||||
@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
|
||||
|
||||
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
|
||||
var roll_torque_global = -pawn.global_basis.z * (roll_input) * roll_torque # Sign fixed
|
||||
pawn.add_torque(roll_torque_global, delta)
|
||||
|
||||
## 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
|
||||
var roll_torque_global = -pawn.basis.z * (roll_input) * roll_torque_acceleration * delta # Sign fixed
|
||||
pawn.apply_torque(roll_torque_global)
|
||||
|
||||
## Called by Pawn when entering FLOATING state with suit
|
||||
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
|
||||
|
||||
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 ---
|
||||
# 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
|
||||
pawn.add_torque(roll_torque_vector, delta)
|
||||
pawn.apply_torque(roll_acceleration)
|
||||
|
||||
|
||||
# --- Auto-Orientation Logic ---
|
||||
@ -112,8 +104,8 @@ func _orient_pawn(delta: float):
|
||||
|
||||
# --- THE FIX: Adjust how target_up is calculated ---
|
||||
# Calculate velocity components relative to camera orientation
|
||||
var _forward_velocity_component = pawn.velocity.dot(target_forward)
|
||||
var _right_velocity_component = pawn.velocity.dot(pawn.camera_anchor.global_basis.x)
|
||||
# var _forward_velocity_component = pawn.velocity.dot(target_forward)
|
||||
# var _right_velocity_component = pawn.velocity.dot(pawn.camera_anchor.global_basis.x)
|
||||
|
||||
# Only apply strong "feet trailing" if significant forward/backward movement dominates
|
||||
# and we are actually moving.
|
||||
|
||||
@ -15,7 +15,8 @@ var nearby_grips: Array[GripArea3D] = []
|
||||
@export var reach_orient_speed: float = 10.0 # Speed pawn orients to grip
|
||||
|
||||
# --- 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_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 ---
|
||||
var _seeking_climb_input: Vector2 = Vector2.ZERO # The move_input held when seeking started
|
||||
|
||||
@export var launch_charge_rate: float = 20.0
|
||||
@export var max_launch_speed: float = 15.0
|
||||
@export var launch_charge_rate: float = 1.5
|
||||
@export var max_launch_speed: float = 4.0
|
||||
var launch_direction: Vector3 = Vector3.ZERO
|
||||
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)
|
||||
|
||||
|
||||
## 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 ===
|
||||
func _on_enter_state(state : MovementState):
|
||||
print("ZeroGMovementComponent activated for state: ", MovementState.keys()[state])
|
||||
if state == MovementState.GRIPPING:
|
||||
pawn.velocity = Vector3.ZERO
|
||||
pawn.angular_velocity = Vector3.ZERO
|
||||
# TODO: Use forces to match velocity to grip
|
||||
# if state == MovementState.GRIPPING:
|
||||
# pawn.velocity = Vector3.ZERO
|
||||
# pawn.angular_velocity = Vector3.ZERO
|
||||
# else: # e.g., REACHING_MOVE?
|
||||
# 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) ---
|
||||
var error_pos = target_position - pawn.global_position
|
||||
# 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)
|
||||
target_velocity_pos -= pawn.velocity * gripping_angular_damping # 'angular_damping' here acts as Kd
|
||||
# Apply force via acceleration
|
||||
pawn.velocity = pawn.velocity.lerp(target_velocity_pos, delta * 10.0) # Smoothly apply correction
|
||||
# target_velocity_pos -= pawn.linear_velocity * gripping_angular_damping # 'angular_damping' here acts as Kd
|
||||
# TODO: Add less force the smaller error_pos is to stop ocillating around target pos
|
||||
pawn.apply_central_force((force / pawn.mass) * delta)
|
||||
|
||||
# --- 3. Apply Angular Force (PD Controller) ---
|
||||
if not is_zero_approx(roll_input):
|
||||
# Manual Roll Input (applies torque)
|
||||
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:
|
||||
# Auto-Orient (PD Controller)
|
||||
_apply_orientation_torque(target_basis, delta)
|
||||
@ -245,7 +249,9 @@ func _apply_climb_physics(delta: float, move_input: Vector2):
|
||||
|
||||
# 5. Apply Movement Force
|
||||
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)
|
||||
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.
|
||||
print("Seeking Climb ended, no grip found. Reverting to IDLE.")
|
||||
|
||||
|
||||
# --- Grip Helpers
|
||||
|
||||
## The single, authoritative function for grabbing a grip.
|
||||
@ -410,7 +415,8 @@ func _start_climb(move_input: Vector2):
|
||||
|
||||
func _stop_climb(release_grip: bool):
|
||||
# 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
|
||||
if release_grip:
|
||||
_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
|
||||
|
||||
func _apply_orientation_torque(target_basis: Basis, delta: float):
|
||||
var current_quat = pawn.global_transform.basis.get_rotation_quaternion()
|
||||
var target_quat = target_basis.get_rotation_quaternion()
|
||||
var error_quat = target_quat * current_quat.inverse()
|
||||
var torque = MotionUtils.calculate_pd_rotation_torque(
|
||||
target_basis,
|
||||
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
|
||||
# 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)
|
||||
pawn.apply_torque(torque * delta)
|
||||
|
||||
# --- Launch helpers ---
|
||||
func _start_charge(move_input: Vector2):
|
||||
@ -452,20 +452,19 @@ func _start_charge(move_input: Vector2):
|
||||
|
||||
func _handle_launch_charge(delta: float):
|
||||
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):
|
||||
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
|
||||
_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.
|
||||
# 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)
|
||||
# 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 ---
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
# orbital_body_3d.gd
|
||||
# REFACTOR: Extends Node3D instead of Node2D
|
||||
class_name OrbitalBody3D
|
||||
extends Node3D
|
||||
class_name OrbitalBody3D extends RigidBody3D
|
||||
|
||||
# Defines the physical behavior of this body.
|
||||
enum PhysicsMode {
|
||||
@ -16,11 +15,11 @@ 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 mass: float = 0.0 # Aggregated mass of this body and all its OrbitalBody3D children
|
||||
|
||||
# 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
|
||||
# @export var linear_velocity: Vector3 = Vector3.ZERO
|
||||
# @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
|
||||
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
|
||||
# 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
|
||||
# @export var inertia: float = 1.0
|
||||
|
||||
func _ready():
|
||||
freeze_mode = FreezeMode.FREEZE_MODE_KINEMATIC
|
||||
if physics_mode == PhysicsMode.ANCHORED:
|
||||
freeze = true
|
||||
|
||||
pass
|
||||
|
||||
recalculate_physical_properties()
|
||||
set_physics_process(not Engine.is_editor_hint())
|
||||
|
||||
# --- PUBLIC FORCE APPLICATION METHODS ---
|
||||
# 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.
|
||||
match physics_mode:
|
||||
PhysicsMode.INDEPENDENT:
|
||||
@ -50,14 +55,14 @@ func apply_force(force: Vector3, pos: Vector3 = self.global_position):
|
||||
var p = get_parent()
|
||||
while p:
|
||||
if p is OrbitalBody3D:
|
||||
# Recursively call the parent's apply_force method.
|
||||
p.apply_force(force, pos)
|
||||
# Recursively call the parent's apply_force_recursive method.
|
||||
p.apply_force_recursive(force, pos)
|
||||
return # Stop at the first OrbitalBody3D parent
|
||||
p = p.get_parent()
|
||||
|
||||
push_error("Anchored OrbitalBody3D has become dislodged and is now Composite.")
|
||||
physics_mode = PhysicsMode.COMPOSITE
|
||||
apply_force(force, position)
|
||||
apply_force_recursive(force, position)
|
||||
|
||||
func _add_forces(force: Vector3, pos: Vector3 = Vector3.ZERO):
|
||||
# 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])
|
||||
|
||||
func _physics_process(delta):
|
||||
if not Engine.is_editor_hint():
|
||||
match physics_mode:
|
||||
PhysicsMode.INDEPENDENT:
|
||||
_integrate_forces(delta)
|
||||
PhysicsMode.COMPOSITE:
|
||||
_integrate_forces(delta)
|
||||
pass
|
||||
# if not Engine.is_editor_hint():
|
||||
# match physics_mode:
|
||||
# PhysicsMode.INDEPENDENT:
|
||||
# _integrate_forces(delta)
|
||||
# PhysicsMode.COMPOSITE:
|
||||
# _integrate_forces(delta)
|
||||
|
||||
func _integrate_forces(delta):
|
||||
func _integrate_forces(state: PhysicsDirectBodyState3D):
|
||||
# Safety Check for Division by Zero
|
||||
var sim_mass = mass
|
||||
if sim_mass <= 0.0:
|
||||
if mass <= 0.0:
|
||||
accumulated_force = Vector3.ZERO
|
||||
accumulated_torque = Vector3.ZERO
|
||||
return
|
||||
|
||||
# 3. Apply Linear Physics (F = ma)
|
||||
var linear_acceleration = accumulated_force / sim_mass # Division is now safe
|
||||
linear_velocity += linear_acceleration * delta
|
||||
global_position += linear_velocity * delta
|
||||
|
||||
# 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 > 0:
|
||||
var angular_acceleration = accumulated_torque / inertia
|
||||
angular_velocity += angular_acceleration * delta
|
||||
|
||||
# REFACTOR: Apply 3D rotation using the integrated angular velocity
|
||||
# (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)
|
||||
#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
|
||||
|
||||
# 5. Reset accumulated forces for the next frame
|
||||
accumulated_force = Vector3.ZERO
|
||||
accumulated_torque = Vector3.ZERO
|
||||
|
||||
|
||||
func recalculate_physical_properties():
|
||||
if physics_mode != PhysicsMode.COMPOSITE:
|
||||
mass = base_mass
|
||||
if inertia <= 0.0:
|
||||
inertia = 1.0
|
||||
if inertia == Vector3.ZERO:
|
||||
inertia = Vector3(1.0, 1.0, 1.0)
|
||||
return
|
||||
|
||||
var all_parts: Array[OrbitalBody3D] = []
|
||||
@ -130,7 +129,7 @@ func recalculate_physical_properties():
|
||||
|
||||
if all_parts.is_empty():
|
||||
mass = base_mass
|
||||
inertia = 1.0
|
||||
inertia = Vector3(1.0, 1.0, 1.0)
|
||||
return
|
||||
|
||||
# --- 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
|
||||
|
||||
# --- 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:
|
||||
var local_pos = part.global_position - self.global_position
|
||||
# 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 ---
|
||||
self.mass = total_mass
|
||||
self.inertia = total_inertia * 0.01
|
||||
if self.inertia <= 0.0: # Safety check
|
||||
self.inertia = 1.0
|
||||
self.inertia = total_inertia * 0.01
|
||||
if self.inertia == Vector3.ZERO: # Safety check
|
||||
inertia = Vector3(1.0, 1.0, 1.0)
|
||||
|
||||
# A recursive helper function to get an array of all OrbitalBody3D children
|
||||
func _collect_anchored_parts(parts_array: Array):
|
||||
parts_array.append(self)
|
||||
for child in get_children():
|
||||
if child is OrbitalBody3D and child.physics_mode == PhysicsMode.ANCHORED:
|
||||
child._collect_anchored_parts(parts_array)
|
||||
child._collect_anchored_parts(parts_array)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[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://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="Script" uid="uid://bfc6u1f8sigxj" path="res://scripts/singletons/game_config.gd" id="3_75b4c"]
|
||||
|
||||
|
||||
97
scripts/singletons/motion_utils.gd
Normal file
97
scripts/singletons/motion_utils.gd
Normal 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
|
||||
1
scripts/singletons/motion_utils.gd.uid
Normal file
1
scripts/singletons/motion_utils.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://c465dh6n0s7hy
|
||||
@ -29,7 +29,7 @@ func _physics_process(_delta: float) -> void:
|
||||
apply_n_body_forces(system_attractors)
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
if force_vector != Vector3.ZERO:
|
||||
body_a.apply_force(force_vector)
|
||||
body_b.apply_force(-force_vector)
|
||||
body_a.apply_force_recursive(force_vector)
|
||||
body_b.apply_force_recursive(-force_vector)
|
||||
|
||||
func calculate_n_body_force(body: OrbitalBody3D, attractors: Array[OrbitalBody3D]) -> Vector3:
|
||||
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()
|
||||
|
||||
for i in range(num_steps):
|
||||
var ghost_body = OrbitalBody3D.new()
|
||||
ghost_body.global_position = ghost_position
|
||||
ghost_body.mass = body_to_trace.mass
|
||||
# for i in range(num_steps):
|
||||
# var ghost_body = OrbitalBody3D.new()
|
||||
# ghost_body.global_position = ghost_position
|
||||
# ghost_body.mass = body_to_trace.mass
|
||||
|
||||
var total_force = calculate_n_body_gravity_forces(ghost_body)
|
||||
var acceleration = total_force / ghost_body.mass
|
||||
# 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_velocity += acceleration * time_step
|
||||
# ghost_position += ghost_velocity * time_step
|
||||
# path_points.append(ghost_position)
|
||||
|
||||
ghost_body.free()
|
||||
# ghost_body.free()
|
||||
|
||||
return path_points
|
||||
|
||||
|
||||
Reference in New Issue
Block a user