fix(game-mode): recover ships/ball that escape through an open goal in every mode

The goal mouths are now a real navigable hole in the end walls, sized to the
ball rather than the ship — a ship's 1x1 cross-section fits through it, and
there's nothing behind the net to stop it. The escape failsafe previously
only existed in TrainingMode (where a physics regression just wastes
training time); now that any ship can genuinely fly out through an open
goal, every mode needs it or a stray ship/ball falls into the void with no
way back short of quitting. Moved up to GameMode as the shared default
_physics_process, removing TrainingMode's now-duplicate copy.
This commit is contained in:
Josh Creek
2026-08-05 09:16:00 +01:00
parent 0f7603d3cb
commit 661c588fef
2 changed files with 34 additions and 24 deletions
+34
View File
@@ -175,3 +175,37 @@ func _reset_body(body: RigidBody3D, to: Transform3D) -> void:
func _unhandled_input(event):
if event.is_action_pressed("ui_cancel"):
get_tree().change_scene_to_file(ScenePaths.MAIN_MENU)
# Escape failsafe. The arena is meant to be fully enclosed, but the goal
# mouths are now a real navigable hole in the end walls (see
# ArenaBoundary._build_end_wall_colliders) sized to the ball, not the ship —
# a ship's 1x1 cross-section fits through it, and there is nothing behind the
# net to stop it. Previously this only existed in TrainingMode (a physics
# regression there just wastes training time); now that any ship can
# genuinely fly out through an open goal, every mode needs it, or a stray
# ship/ball falls into the void with no way back short of quitting. Runs by
# default every tick; TrainingMode overrides _physics_process entirely and
# calls this itself alongside its own episode logic.
const ESCAPE_MARGIN := 15.0
func _physics_process(_delta: float) -> void:
_respawn_escaped_bodies()
func _respawn_escaped_bodies() -> void:
for ship in ships:
if is_instance_valid(ship) and _is_escaped(ship.global_position):
push_warning("GameMode: ship escaped the enclosed arena — check boundary colliders")
_reset_body(ship, _ship_spawn_transforms[ship])
if is_instance_valid(ball) and _is_escaped(ball.global_position):
push_warning("GameMode: ball escaped the enclosed arena — check boundary colliders")
_reset_body(ball, arena.get_ball_spawn())
func _is_escaped(position: Vector3) -> bool:
return absf(position.x) > ArenaBoundary.INNER_HALF_X + ESCAPE_MARGIN \
or absf(position.z) > ArenaBoundary.INNER_HALF_Z + ESCAPE_MARGIN \
or position.y < -ESCAPE_MARGIN \
or position.y > ArenaBoundary.INNER_HEIGHT + ESCAPE_MARGIN
-24
View File
@@ -90,12 +90,6 @@ const MAX_RANDOM_SHIP_SPEED := 8.0
# Sim runs at 60 physics ticks per sim-second regardless of speedup.
const TICKS_PER_SIM_SECOND := 60.0
# The arena is physically enclosed, so nothing should ever get this far out.
# If a body escapes anyway (physics regression, boundary edit), it is warned
# about and respawned with no reward change and no episode end — a multi-hour
# training run must survive it, and the escape must not shape rewards.
const ESCAPE_MARGIN := 15.0
var _agents: Array[ShipAIController] = []
# Eval mode state (see header comment)
@@ -320,24 +314,6 @@ func _physics_process(_delta):
return
# Escape failsafe: see ESCAPE_MARGIN.
func _respawn_escaped_bodies() -> void:
for ship in ships:
if is_instance_valid(ship) and _escaped(ship.global_position):
push_warning("TrainingMode: ship escaped the enclosed arena — check boundary colliders")
_place_body(ship, _ship_spawn_transforms[ship], Vector3.ZERO, Vector3.ZERO)
if is_instance_valid(ball) and _escaped(ball.global_position):
push_warning("TrainingMode: ball escaped the enclosed arena — check boundary colliders")
_place_body(ball, arena.get_ball_spawn(), Vector3.ZERO, Vector3.ZERO)
func _escaped(position: Vector3) -> bool:
return absf(position.x) > ArenaBoundary.INNER_HALF_X + ESCAPE_MARGIN \
or absf(position.z) > ArenaBoundary.INNER_HALF_Z + ESCAPE_MARGIN \
or position.y < -ESCAPE_MARGIN \
or position.y > ArenaBoundary.INNER_HEIGHT + ESCAPE_MARGIN
func _on_goal_scored(conceding_team: int) -> void:
if _eval:
_eval_goals[1 - conceding_team] += 1