mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
fix(training): avoid ship-ship overlap when placing a multi-ship roster
_place_ships_random/_place_air_drill sampled each ship's randomized episode- start position independently, so a team_size > 1 roster could spawn interpenetrating (ships are ~1x1x4). Both now resample (up to 20 attempts, matching the existing corner/fillet rejection-sampling pattern) against every ship already placed that reset, rejecting anything within MIN_SHIP_SEPARATION (4.5m, matching the arena spawn-marker spacing) of one.
This commit is contained in:
@@ -86,6 +86,13 @@ const CORNER_LIMIT := ArenaBoundary.INNER_HALF_X + ArenaBoundary.INNER_HALF_Z \
|
||||
const FILLET_CLEAR_Y := ArenaBoundary.BASE_RADIUS + FIELD_MIN_Y
|
||||
const MAX_RANDOM_BALL_SPEED := 12.0
|
||||
const MAX_RANDOM_SHIP_SPEED := 8.0
|
||||
# Minimum centre-to-centre separation enforced between ships placed in the
|
||||
# same randomized reset (team_size > 1) — without it, _place_ships_random/
|
||||
# _place_air_drill sample each ship independently and can spawn them
|
||||
# interpenetrating. Twice the ~2.05m worst-case rotated half-extent noted
|
||||
# above clears any relative orientation; matches arena_base.tscn's spawn
|
||||
# marker spacing, which uses the same margin for the same reason.
|
||||
const MIN_SHIP_SEPARATION := 4.5
|
||||
|
||||
# Sim runs at 60 physics ticks per sim-second regardless of speedup.
|
||||
const TICKS_PER_SIM_SECOND := 60.0
|
||||
@@ -392,17 +399,27 @@ func _place_air_drill() -> void:
|
||||
var ball_velocity := _random_direction() * randf_range(0.0, MAX_RANDOM_BALL_SPEED * 0.5)
|
||||
_place_body(ball, Transform3D(Basis.IDENTITY, ball_position), ball_velocity, Vector3.ZERO)
|
||||
|
||||
# Each ship's lateral offset is sampled independently, so with more than
|
||||
# one ship per team (team_size > 1) two could otherwise land within their
|
||||
# own hulls of each other — resample against every ship already placed
|
||||
# this reset (see MIN_SHIP_SEPARATION).
|
||||
var placed: Array[Vector3] = []
|
||||
for ship in ships:
|
||||
if ship in _inert_ships:
|
||||
continue
|
||||
var lateral_offset := Vector3(randf_range(-1, 1), 0.0, randf_range(-1, 1))
|
||||
lateral_offset = lateral_offset.normalized() if lateral_offset.length_squared() > 0.001 else Vector3.FORWARD
|
||||
lateral_offset *= randf_range(6.0, 14.0)
|
||||
var ship_position := Vector3(
|
||||
clampf(ball_position.x + lateral_offset.x, -FIELD_HALF_X, FIELD_HALF_X),
|
||||
randf_range(FIELD_MIN_Y, 4.0),
|
||||
clampf(ball_position.z + lateral_offset.z, -FIELD_HALF_Z, FIELD_HALF_Z)
|
||||
)
|
||||
var ship_position := Vector3.ZERO
|
||||
for _attempt in 20:
|
||||
var lateral_offset := Vector3(randf_range(-1, 1), 0.0, randf_range(-1, 1))
|
||||
lateral_offset = lateral_offset.normalized() if lateral_offset.length_squared() > 0.001 else Vector3.FORWARD
|
||||
lateral_offset *= randf_range(6.0, 14.0)
|
||||
ship_position = Vector3(
|
||||
clampf(ball_position.x + lateral_offset.x, -FIELD_HALF_X, FIELD_HALF_X),
|
||||
randf_range(FIELD_MIN_Y, 4.0),
|
||||
clampf(ball_position.z + lateral_offset.z, -FIELD_HALF_Z, FIELD_HALF_Z)
|
||||
)
|
||||
if _far_enough_from(ship_position, placed):
|
||||
break
|
||||
placed.append(ship_position)
|
||||
var orientation := Basis.from_euler(Vector3(
|
||||
randf_range(-0.4, 0.4), randf_range(-PI, PI), randf_range(-0.4, 0.4)
|
||||
))
|
||||
@@ -429,6 +446,10 @@ func _place_ball_near_goal() -> void:
|
||||
|
||||
|
||||
func _place_ships_random() -> void:
|
||||
# Placed one at a time, resampling each against every position already
|
||||
# placed this reset (see MIN_SHIP_SEPARATION) — otherwise a team_size > 1
|
||||
# roster is sampled independently per ship and can spawn interpenetrating.
|
||||
var placed: Array[Vector3] = []
|
||||
for ship in ships:
|
||||
# Inert opponents (opponent_mode=inert) stay parked at their arena
|
||||
# spawn instead of drifting into the play area as a stray obstacle —
|
||||
@@ -441,13 +462,17 @@ func _place_ships_random() -> void:
|
||||
randf_range(-0.4, 0.4)
|
||||
))
|
||||
var velocity := _random_direction() * randf_range(0.0, MAX_RANDOM_SHIP_SPEED)
|
||||
_place_body(ship, Transform3D(orientation, _random_position()), velocity, Vector3.ZERO)
|
||||
var position := _random_position(placed)
|
||||
placed.append(position)
|
||||
_place_body(ship, Transform3D(orientation, position), velocity, Vector3.ZERO)
|
||||
|
||||
|
||||
func _random_position() -> Vector3:
|
||||
func _random_position(exclude: Array[Vector3] = []) -> Vector3:
|
||||
# Resample anything too close to a corner curve or wall-base fillet (see
|
||||
# CORNER_LIMIT / FILLET_CLEAR_Y); the violating region is a few percent
|
||||
# of the volume, so 20 attempts effectively never fall through.
|
||||
# CORNER_LIMIT / FILLET_CLEAR_Y), or too close to an already-placed ship
|
||||
# this same reset (see MIN_SHIP_SEPARATION); the violating region is a
|
||||
# few percent of the volume, so 20 attempts effectively never fall
|
||||
# through even placing a full 5v5 roster one at a time.
|
||||
var position := Vector3.ZERO
|
||||
for _attempt in 20:
|
||||
position = Vector3(
|
||||
@@ -455,11 +480,18 @@ func _random_position() -> Vector3:
|
||||
randf_range(FIELD_MIN_Y, FIELD_MAX_Y),
|
||||
randf_range(-FIELD_HALF_Z, FIELD_HALF_Z)
|
||||
)
|
||||
if _spawn_position_clear(position):
|
||||
if _spawn_position_clear(position) and _far_enough_from(position, exclude):
|
||||
break
|
||||
return position
|
||||
|
||||
|
||||
func _far_enough_from(position: Vector3, others: Array[Vector3]) -> bool:
|
||||
for other in others:
|
||||
if position.distance_squared_to(other) < MIN_SHIP_SEPARATION * MIN_SHIP_SEPARATION:
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _spawn_position_clear(position: Vector3) -> bool:
|
||||
if absf(position.x) + absf(position.z) > CORNER_LIMIT:
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user