mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(physics): make upright a real state, and actually start ships on the floor
Six rounds of reward shaping (~700M steps) failed to produce upright ground driving. A critical review of the simulation rather than the reward found why: 1. The hull was a 1x1x4 box with inertia (1,1,1) and no restoring torque anywhere, so belly-down and rolled-90 were geometrically identical resting states. "Upright" was not a physically distinguished state at all - the reward was paying for a property the simulation did not have. 2. ~65% of episodes spawned ships via _random_position, which samples Y uniformly over the full 18m volume (mean ~8.7m). The measured airborne_fraction ~0.44 was largely that spawn distribution, and every ground-handling term fades out above 3m, so the shaping being tuned barely ever applied. 3. air_drill_chance 0.20 spawned deliberately unreachable-without-climbing states in the stage meant to teach ground driving, and its own air_touch_fraction (0.0002) shows the drills were never solved. Fixes land in the physics and the task distribution, not the reward: - ship.tscn: hull 1x1x4 -> 1.6x0.6x4 so it has one stable resting face; inertia (1,1,1) -> (7,1,7), physically correct for the hull, making tumbling reluctant while keeping yaw snappy. - ship.gd: new altitude-faded righting torque (spring-damper toward belly-down, faded out by 3m so aerials keep full attitude freedom). This is the grav-plating analogue of Rocket League's auto-righting and helps human pilots land cleanly too. - training_mode.gd: new ground_start_chance branch spawning ships level and resting on the floor with a floor-level ball - the state the handling stage's rewards are actually written for. - generation5.py: ground-start-chance 0.50, air-drill-chance 0.20 -> 0.0. Reward terms are left exactly as they were; they should finally pull in a direction the ship can go.
This commit is contained in:
@@ -26,6 +26,21 @@ extends RigidBody3D
|
||||
@export var ceiling_pull_strength = 11.5 # Ceiling grav-plating strength; nets above gravity so a ship can hold a ceiling
|
||||
@export var ceiling_pull_range = 3.0 # Metres from the ceiling where pull begins
|
||||
|
||||
# Grav-plating righting torque: a spring-damper that rolls/pitches the hull
|
||||
# back toward belly-down, strongest at floor level and faded to nothing by
|
||||
# righting_range so genuine aerials keep full attitude freedom. Without it
|
||||
# "upright" is not a physically distinguished state at all — the hull is a
|
||||
# box with no restoring torque, so belly-down and rolled-90 are equally
|
||||
# stable and a policy has no dynamics-level reason to prefer either. Six
|
||||
# rounds of RL reward shaping (see TRAINING.md) failed to buy upright
|
||||
# ground handling for exactly this reason; the fix belongs in the physics,
|
||||
# not the reward. Same idea as the wall/ceiling pull above — the plating
|
||||
# orients you, not just attracts you — and it helps human pilots land
|
||||
# cleanly too.
|
||||
@export var righting_strength: float = 20.0 # Righting spring gain (0 disables)
|
||||
@export var righting_damping: float = 6.0 # Opposes tumble while righting
|
||||
@export var righting_range: float = 3.0 # Metres above the floor where righting fades out
|
||||
|
||||
# Non-tinted hull meshes, runtime-merged into one ArrayMesh by
|
||||
# _build_merged_hull() (Nose/TailFin stay separate MeshInstance3Ds since
|
||||
# _apply_team_color() retints them per-team and must keep addressing them by
|
||||
@@ -337,6 +352,7 @@ func _integrate_forces(state):
|
||||
|
||||
# === ROTATION (Turning) ===
|
||||
apply_rotation_forces(state, _current_action.rotation)
|
||||
apply_righting_torque(state)
|
||||
|
||||
# === DRAG AND LIMITS ===
|
||||
apply_drag_and_limits(state, _current_action.rotation)
|
||||
@@ -385,6 +401,31 @@ func apply_surface_pull(state: PhysicsDirectBodyState3D) -> void:
|
||||
state.apply_central_force(pull * mass)
|
||||
|
||||
|
||||
# Spring-damper torque toward belly-down (see righting_strength). The spring
|
||||
# term basis.y x UP is a world-space axis whose magnitude is sin(tilt) and
|
||||
# whose direction is the shortest rotation back to upright, so it is zero
|
||||
# when already level, peaks on its side, and — being a cross product —
|
||||
# vanishes again when perfectly inverted. The damping term is applied only
|
||||
# about that same righting axis, so it bleeds off tumble without taxing
|
||||
# deliberate yaw. Fades linearly to nothing by righting_range metres up,
|
||||
# matching every other ground-handling term's altitude ramp (see
|
||||
# ShipAIController.GROUND_HANDLING_HEIGHT).
|
||||
func apply_righting_torque(state: PhysicsDirectBodyState3D) -> void:
|
||||
if righting_strength <= 0.0:
|
||||
return
|
||||
var height_factor := 1.0 - clampf(global_position.y / righting_range, 0.0, 1.0)
|
||||
if height_factor <= 0.0:
|
||||
return
|
||||
|
||||
var righting_axis := global_transform.basis.y.cross(Vector3.UP)
|
||||
var torque := righting_axis * righting_strength
|
||||
# Damp only the component of spin around the righting axis.
|
||||
if righting_axis.length_squared() > 0.0001:
|
||||
var axis := righting_axis.normalized()
|
||||
torque -= axis * state.angular_velocity.dot(axis) * righting_damping
|
||||
state.apply_torque(torque * height_factor)
|
||||
|
||||
|
||||
func apply_rotation_forces(state: PhysicsDirectBodyState3D, rotation_input: Vector3):
|
||||
if rotation_input.length() < 0.01:
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user