mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-11 18:53:42 +00:00
feat(*): Replace the test terrain arena with an enclosed standard-size space-platform arena (shared ArenaBoundary floor/walls/ceiling scene, starfield sky, ball CCD) and derive TrainingMode placement bounds from it, dropping the out-of-bounds reward guard
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
class_name ArenaBoundary
|
||||
extends StaticBody3D
|
||||
|
||||
# The standard arena play volume (inner faces of the enclosure). Every arena
|
||||
# instances objects/arena_boundary.tscn so all arenas share one size; code
|
||||
# that needs field dimensions derives them from these constants rather than
|
||||
# restating numbers.
|
||||
const INNER_HALF_X := 12.0
|
||||
const INNER_HALF_Z := 18.0
|
||||
const INNER_HEIGHT := 12.0
|
||||
# Goal-centre distance from arena centre; the end walls sit 1 m behind, so a
|
||||
# ball pinned against them still overlaps the goal sensor.
|
||||
const GOAL_LINE_Z := 17.0
|
||||
@@ -0,0 +1 @@
|
||||
uid://f32otkpo3lvr
|
||||
@@ -11,8 +11,9 @@ extends RefCounted
|
||||
# The same rotation must be inverted when interpreting actions (see canon —
|
||||
# it is its own inverse).
|
||||
|
||||
# Normalization scales. Arena bounds: goals at z ≈ ±15.56, ship spawns at
|
||||
# z = ±12; positions are soft-normalized to roughly [-1, 1].
|
||||
# Normalization scales. Standard arena volume (see ArenaBoundary): x ±12,
|
||||
# z ±18, height 12, goals at z ±17; positions are soft-normalized to roughly
|
||||
# [-1, 1]. Do not retune without retraining every model in Game/bots/.
|
||||
const POSITION_SCALE := Vector3(20.0, 10.0, 20.0)
|
||||
const BALL_SPEED_SCALE := 30.0
|
||||
const GOAL_DISTANCE_SCALE := 40.0
|
||||
|
||||
@@ -24,24 +24,26 @@ extends GameMode
|
||||
@export_range(0.0, 1.0) var kickoff_state_chance := 0.2
|
||||
@export_range(0.0, 1.0) var ball_near_goal_chance := 0.2
|
||||
|
||||
# Placement bounds, inset from the arena (goals at z ≈ ±15.56).
|
||||
const FIELD_HALF_X := 10.0
|
||||
const FIELD_HALF_Z := 13.0
|
||||
# Placement bounds for randomized episode starts, derived from the standard
|
||||
# enclosure (ArenaBoundary). The inset keeps a randomly oriented ship (1x1x4
|
||||
# box, worst-case half-extent ~2.05) from spawning intersecting the walls,
|
||||
# ceiling, or goal sensors.
|
||||
const SPAWN_INSET := 2.5
|
||||
const FIELD_HALF_X := ArenaBoundary.INNER_HALF_X - SPAWN_INSET
|
||||
const FIELD_HALF_Z := ArenaBoundary.GOAL_LINE_Z - SPAWN_INSET
|
||||
const FIELD_MIN_Y := 1.5
|
||||
const FIELD_MAX_Y := 8.0
|
||||
const FIELD_MAX_Y := ArenaBoundary.INNER_HEIGHT - SPAWN_INSET
|
||||
const MAX_RANDOM_BALL_SPEED := 12.0
|
||||
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 has no walls/ceiling yet (see TODO.md), so an untrained policy can
|
||||
# simply fly away. Leaving this volume ends the episode with a small penalty.
|
||||
const BOUNDS_HALF_X := 25.0
|
||||
const BOUNDS_HALF_Z := 25.0
|
||||
const BOUNDS_MIN_Y := -5.0
|
||||
const BOUNDS_MAX_Y := 25.0
|
||||
@export var out_of_bounds_penalty := 1.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] = []
|
||||
|
||||
@@ -102,13 +104,10 @@ func _attack_goal_position(team: int) -> Vector3:
|
||||
|
||||
|
||||
func _physics_process(_delta):
|
||||
_respawn_escaped_bodies()
|
||||
if _eval:
|
||||
_episode_ticks += 1
|
||||
for ship in ships:
|
||||
if is_instance_valid(ship) and _out_of_bounds(ship.global_position):
|
||||
_place_body(ship, _ship_spawn_transforms[ship], Vector3.ZERO, Vector3.ZERO)
|
||||
var ball_lost := is_instance_valid(ball) and _out_of_bounds(ball.global_position)
|
||||
if _episode_ticks > int(episode_length_seconds * TICKS_PER_SIM_SECOND) or ball_lost:
|
||||
if _episode_ticks > int(episode_length_seconds * TICKS_PER_SIM_SECOND):
|
||||
_eval_draws += 1
|
||||
_end_eval_episode()
|
||||
return
|
||||
@@ -128,23 +127,23 @@ func _physics_process(_delta):
|
||||
_reset_episode()
|
||||
return
|
||||
|
||||
# A ship leaving the play volume is penalized and respawned at its kickoff
|
||||
# spawn (the episode continues); a lost ball ends the episode for both.
|
||||
for agent in _agents:
|
||||
if _out_of_bounds(agent.ship.global_position):
|
||||
agent.reward -= out_of_bounds_penalty
|
||||
_place_body(agent.ship, _ship_spawn_transforms[agent.ship], Vector3.ZERO, Vector3.ZERO)
|
||||
if is_instance_valid(ball) and _out_of_bounds(ball.global_position) and not _agents.is_empty():
|
||||
for agent in _agents:
|
||||
agent.done = true
|
||||
_reset_episode()
|
||||
|
||||
# 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 _out_of_bounds(position: Vector3) -> bool:
|
||||
return absf(position.x) > BOUNDS_HALF_X \
|
||||
or absf(position.z) > BOUNDS_HALF_Z \
|
||||
or position.y < BOUNDS_MIN_Y \
|
||||
or position.y > BOUNDS_MAX_Y
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user