mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-16 13:02:02 +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 FILLET_CLEAR_Y := ArenaBoundary.BASE_RADIUS + FIELD_MIN_Y
|
||||||
const MAX_RANDOM_BALL_SPEED := 12.0
|
const MAX_RANDOM_BALL_SPEED := 12.0
|
||||||
const MAX_RANDOM_SHIP_SPEED := 8.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.
|
# Sim runs at 60 physics ticks per sim-second regardless of speedup.
|
||||||
const TICKS_PER_SIM_SECOND := 60.0
|
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)
|
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)
|
_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:
|
for ship in ships:
|
||||||
if ship in _inert_ships:
|
if ship in _inert_ships:
|
||||||
continue
|
continue
|
||||||
|
var ship_position := Vector3.ZERO
|
||||||
|
for _attempt in 20:
|
||||||
var lateral_offset := Vector3(randf_range(-1, 1), 0.0, randf_range(-1, 1))
|
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 = lateral_offset.normalized() if lateral_offset.length_squared() > 0.001 else Vector3.FORWARD
|
||||||
lateral_offset *= randf_range(6.0, 14.0)
|
lateral_offset *= randf_range(6.0, 14.0)
|
||||||
var ship_position := Vector3(
|
ship_position = Vector3(
|
||||||
clampf(ball_position.x + lateral_offset.x, -FIELD_HALF_X, FIELD_HALF_X),
|
clampf(ball_position.x + lateral_offset.x, -FIELD_HALF_X, FIELD_HALF_X),
|
||||||
randf_range(FIELD_MIN_Y, 4.0),
|
randf_range(FIELD_MIN_Y, 4.0),
|
||||||
clampf(ball_position.z + lateral_offset.z, -FIELD_HALF_Z, FIELD_HALF_Z)
|
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(
|
var orientation := Basis.from_euler(Vector3(
|
||||||
randf_range(-0.4, 0.4), randf_range(-PI, PI), randf_range(-0.4, 0.4)
|
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:
|
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:
|
for ship in ships:
|
||||||
# Inert opponents (opponent_mode=inert) stay parked at their arena
|
# Inert opponents (opponent_mode=inert) stay parked at their arena
|
||||||
# spawn instead of drifting into the play area as a stray obstacle —
|
# 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)
|
randf_range(-0.4, 0.4)
|
||||||
))
|
))
|
||||||
var velocity := _random_direction() * randf_range(0.0, MAX_RANDOM_SHIP_SPEED)
|
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
|
# 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
|
# CORNER_LIMIT / FILLET_CLEAR_Y), or too close to an already-placed ship
|
||||||
# of the volume, so 20 attempts effectively never fall through.
|
# 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
|
var position := Vector3.ZERO
|
||||||
for _attempt in 20:
|
for _attempt in 20:
|
||||||
position = Vector3(
|
position = Vector3(
|
||||||
@@ -455,11 +480,18 @@ func _random_position() -> Vector3:
|
|||||||
randf_range(FIELD_MIN_Y, FIELD_MAX_Y),
|
randf_range(FIELD_MIN_Y, FIELD_MAX_Y),
|
||||||
randf_range(-FIELD_HALF_Z, FIELD_HALF_Z)
|
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
|
break
|
||||||
return position
|
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:
|
func _spawn_position_clear(position: Vector3) -> bool:
|
||||||
if absf(position.x) + absf(position.z) > CORNER_LIMIT:
|
if absf(position.x) + absf(position.z) > CORNER_LIMIT:
|
||||||
return false
|
return false
|
||||||
|
|||||||
Reference in New Issue
Block a user