mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat(arena): resize for 5v5, cut a real goal-wall hole, and bake collision geometry
Retraining from scratch removes the constraint that blocked these: the existing checkpoints in Game/bots/promoted/ no longer need byte-identical geometry. - Fix the goal aperture constants (were 1.85/1.65, now match objects/goal.tscn's actual sensor exactly at 1.75/1.5) so a ball crossing the visible edge can't fail to score. - Cut a real navigable hole in each end wall's collision (_build_end_wall_colliders), replacing the previous solid box — the ball now genuinely enters the net instead of triggering the sensor a hair before hitting a solid wall. Correct for both FLOOR and ELEVATED goal modes via _goal_surround_bounds. A separate, slightly wider GOAL_VISUAL_APERTURE_* pair keeps the collision hole exact while still giving goal.gd's bezel/rim frame clearance to be seen against the hull cut. - Resize the play volume 1.5x (INNER_HALF_X/Z/HEIGHT 12/18/12 -> 18/27/18) to comfortably fit a 5v5 roster: updates every hand-authored literal in arena_boundary.tscn/arena_base.tscn/the elevated arena variants that doesn't derive from those constants, adds 5 spawn markers per team, and rescales ship_observations.gd's normalization scales and arena_02's cosmetic decoration/nebula-dust shader uniforms to match. - Bake the ~170 runtime-generated CollisionShape3D nodes into the scene via a new bake_colliders()/tools/bake_arena_boundary.gd instead of rebuilding them on every load — a real load-time cost repeated in every parallel headless training env. Colliders must be direct children of the StaticBody3D to register at all, so bake_colliders() parents them onto self and tags them with a group for idempotent re-baking, rather than grouping them under an intermediate container node. _ready() self-heals: it skips regenerating only when an existing bake's goal_mode metadata matches the current one, so the FLOOR-mode bake in the shared scene is never silently reused by an ELEVATED arena variant.
This commit is contained in:
+1187
-21
File diff suppressed because it is too large
Load Diff
@@ -8,13 +8,13 @@
|
||||
goal_mode = 1
|
||||
|
||||
[node name="GoalTeam0" parent="." index="5"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6.0, 18)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.0, 27)
|
||||
|
||||
[node name="GoalTeam1" parent="." index="6"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 6.0, -18)
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 9.0, -27)
|
||||
|
||||
[node name="Spawn1" parent="SpawnsTeam0" index="0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.0, 2.8, 13.5)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6.0, 2.8, 20.25)
|
||||
|
||||
[node name="Spawn1" parent="SpawnsTeam1" index="0"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -4.0, 2.8, -13.5)
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -6.0, 2.8, -20.25)
|
||||
|
||||
+12
-12
@@ -32,7 +32,7 @@ gradient = SubResource("Gradient_dust")
|
||||
|
||||
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_dust"]
|
||||
emission_shape = 3
|
||||
emission_box_extents = Vector3(26, 11, 28)
|
||||
emission_box_extents = Vector3(39, 16.5, 42)
|
||||
direction = Vector3(0, 1, 0)
|
||||
spread = 180.0
|
||||
initial_velocity_min = 0.1
|
||||
@@ -68,48 +68,48 @@ light_color = Color(0.55, 0.35, 0.65, 1)
|
||||
light_energy = 0.45
|
||||
|
||||
[node name="NebulaDust" type="GPUParticles3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 5, 0)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 7.5, 0)
|
||||
amount = 500
|
||||
lifetime = 22.0
|
||||
preprocess = 8.0
|
||||
randomness = 1.0
|
||||
visibility_aabb = AABB(-30, -14, -32, 60, 28, 64)
|
||||
visibility_aabb = AABB(-45, -21, -48, 90, 42, 96)
|
||||
process_material = SubResource("ParticleProcessMaterial_dust")
|
||||
draw_pass_1 = SubResource("QuadMesh_dust")
|
||||
|
||||
[node name="Decoration" type="Node3D" parent="."]
|
||||
|
||||
[node name="NebulaStationA" parent="Decoration" instance=ExtResource("8_station")]
|
||||
transform = Transform3D(1.8, 0, 0, 0, 1.8, 0, 0, 0, 1.8, 28, 5, -6)
|
||||
transform = Transform3D(1.8, 0, 0, 0, 1.8, 0, 0, 0, 1.8, 42, 7.5, -9)
|
||||
rotation = Vector3(0, -0.436332, 0)
|
||||
|
||||
[node name="NebulaDebrisA" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(1.2, 0, 0, 0, 1.2, 0, 0, 0, 1.2, -22, 9, 11)
|
||||
transform = Transform3D(1.2, 0, 0, 0, 1.2, 0, 0, 0, 1.2, -33, 13.5, 16.5)
|
||||
rotation = Vector3(0.261799, 0.698132, 0.087266)
|
||||
|
||||
[node name="NebulaDebrisB" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 34, 3, 15)
|
||||
transform = Transform3D(0.8, 0, 0, 0, 0.8, 0, 0, 0, 0.8, 51, 4.5, 22.5)
|
||||
rotation = Vector3(-0.174533, 1.919862, 0.349066)
|
||||
|
||||
[node name="NebulaDebrisC" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, -30, 11, -21)
|
||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, -45, 16.5, -31.5)
|
||||
rotation = Vector3(0.087266, -1.047198, -0.261799)
|
||||
|
||||
[node name="NebulaStationB" parent="Decoration" instance=ExtResource("8_station")]
|
||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, -32, 7, 9)
|
||||
transform = Transform3D(1.5, 0, 0, 0, 1.5, 0, 0, 0, 1.5, -48, 10.5, 13.5)
|
||||
rotation = Vector3(0.05, 1.134464, -0.02)
|
||||
|
||||
[node name="NebulaStationC" parent="Decoration" instance=ExtResource("8_station")]
|
||||
transform = Transform3D(1.3, 0, 0, 0, 1.3, 0, 0, 0, 1.3, 9, 8, 42)
|
||||
transform = Transform3D(1.3, 0, 0, 0, 1.3, 0, 0, 0, 1.3, 13.5, 12, 63)
|
||||
rotation = Vector3(-0.03, 2.740167, 0.06)
|
||||
|
||||
[node name="NebulaDebrisD" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(1.0, 0, 0, 0, 1.0, 0, 0, 0, 1.0, 13, 15, -35)
|
||||
transform = Transform3D(1.0, 0, 0, 0, 1.0, 0, 0, 0, 1.0, 19.5, 22.5, -52.5)
|
||||
rotation = Vector3(0.4, 1.3, -0.2)
|
||||
|
||||
[node name="NebulaDebrisE" parent="Decoration" instance=ExtResource("9_debris")]
|
||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, -11, 4, 33)
|
||||
transform = Transform3D(0.9, 0, 0, 0, 0.9, 0, 0, 0, 0.9, -16.5, 6, 49.5)
|
||||
rotation = Vector3(-0.3, -0.9, 0.15)
|
||||
|
||||
[node name="NebulaPlanet" parent="Decoration" instance=ExtResource("11_planet")]
|
||||
transform = Transform3D(30, 0, 0, 0, 30, 0, 0, 0, 30, -55, 38, -80)
|
||||
transform = Transform3D(30, 0, 0, 0, 30, 0, 0, 0, 30, -82.5, 57, -120)
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
goal_mode = 1
|
||||
|
||||
[node name="GoalTeam0" parent="." index="5"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6.0, 18)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.0, 27)
|
||||
|
||||
[node name="GoalTeam1" parent="." index="6"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 6.0, -18)
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 9.0, -27)
|
||||
|
||||
[node name="Spawn1" parent="SpawnsTeam0" index="0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.0, 2.8, 13.5)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6.0, 2.8, 20.25)
|
||||
|
||||
[node name="Spawn1" parent="SpawnsTeam1" index="0"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -4.0, 2.8, -13.5)
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -6.0, 2.8, -20.25)
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
goal_mode = 1
|
||||
|
||||
[node name="GoalTeam0" parent="." index="5"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6.0, 18)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.0, 27)
|
||||
|
||||
[node name="GoalTeam1" parent="." index="6"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 6.0, -18)
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 9.0, -27)
|
||||
|
||||
[node name="Spawn1" parent="SpawnsTeam0" index="0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.0, 2.8, 13.5)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 6.0, 2.8, 20.25)
|
||||
|
||||
[node name="Spawn1" parent="SpawnsTeam1" index="0"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -4.0, 2.8, -13.5)
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -6.0, 2.8, -20.25)
|
||||
|
||||
@@ -53,16 +53,16 @@ shadow_enabled = false
|
||||
environment = SubResource("Environment_base")
|
||||
|
||||
[node name="ReflectionProbe" type="ReflectionProbe" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 6, 0)
|
||||
size = Vector3(26, 14, 40)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9, 0)
|
||||
size = Vector3(38, 20, 58)
|
||||
box_projection = true
|
||||
update_mode = 0
|
||||
|
||||
[node name="GoalTeam0" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.79, 18)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.79, 27)
|
||||
|
||||
[node name="GoalTeam1" parent="." instance=ExtResource("6_p57ef")]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.79, -18)
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.79, -27)
|
||||
team = 1
|
||||
|
||||
[node name="BallSpawn" type="Marker3D" parent="."]
|
||||
@@ -71,9 +71,33 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2, 0)
|
||||
[node name="SpawnsTeam0" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.8, 13.5)
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -9, 2.8, 20.25)
|
||||
|
||||
[node name="Spawn2" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -4.5, 2.8, 20.25)
|
||||
|
||||
[node name="Spawn3" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2.8, 20.25)
|
||||
|
||||
[node name="Spawn4" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 4.5, 2.8, 20.25)
|
||||
|
||||
[node name="Spawn5" type="Marker3D" parent="SpawnsTeam0"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 9, 2.8, 20.25)
|
||||
|
||||
[node name="SpawnsTeam1" type="Node3D" parent="."]
|
||||
|
||||
[node name="Spawn1" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 2.8, -13.5)
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -9, 2.8, -20.25)
|
||||
|
||||
[node name="Spawn2" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -4.5, 2.8, -20.25)
|
||||
|
||||
[node name="Spawn3" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 2.8, -20.25)
|
||||
|
||||
[node name="Spawn4" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 4.5, 2.8, -20.25)
|
||||
|
||||
[node name="Spawn5" type="Marker3D" parent="SpawnsTeam1"]
|
||||
transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 9, 2.8, -20.25)
|
||||
|
||||
+139
-20
@@ -5,9 +5,9 @@ extends StaticBody3D
|
||||
# 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
|
||||
const INNER_HALF_X := 18.0
|
||||
const INNER_HALF_Z := 27.0
|
||||
const INNER_HEIGHT := 18.0
|
||||
# Goal-centre distance from arena centre. Flush with the end walls (see
|
||||
# arena_01.tscn's goal transforms), so there is no floating gap between the
|
||||
# goal and the wall for a ball to ramp across before reaching the sensor.
|
||||
@@ -45,14 +45,30 @@ const BASE_RADIUS := 2.0
|
||||
# The end-wall fillets stop short of the goal mouth so floor-level shots
|
||||
# roll flat into the goal sensor (3.5 m wide) instead of ramping over it.
|
||||
const GOAL_MOUTH_HALF_WIDTH := 2.5
|
||||
# The aperture the visual shell leaves for the goal itself, and the opaque
|
||||
# bulkhead surrounding it. Mirrors objects/goal.tscn's 3.5 x 1.5 sensor with a
|
||||
# little clearance, so no sliver of panel shows through the goal frame. The
|
||||
# surround has to be opaque hull: backed by the translucent field panel
|
||||
# The real navigable hole in the wall's collision (see
|
||||
# _build_end_wall_colliders) — must track objects/goal.tscn's BoxShape3D
|
||||
# (3.5 x 1.5, i.e. half-width 1.75, height 1.5) exactly. Anything wider than
|
||||
# the scoring sensor lets the ball cross the wall opening without entering
|
||||
# the sensor and failing to score; this is collision-authoritative, do not
|
||||
# widen it for cosmetic reasons — see GOAL_VISUAL_APERTURE_* below for that.
|
||||
# Half-width matches the sensor exactly; height is a few cm off in FLOOR mode
|
||||
# (the sensor is vertically offset by the goal node's own y=0.79 placement,
|
||||
# while the hole itself is measured from the floor at y=0 — see
|
||||
# _goal_surround_bounds), which is immaterial at the ball's radius but means
|
||||
# "exact match" isn't literally true.
|
||||
const GOAL_APERTURE_HALF_WIDTH := 1.75
|
||||
const GOAL_APERTURE_HEIGHT := 1.5
|
||||
# The (larger) aperture the visual shell/opaque bulkhead actually cuts —
|
||||
# deliberately wider than the true collision hole above, so goal.gd's bezel/
|
||||
# rim frame (built starting exactly at the sensor's own half-extents) has
|
||||
# clearance to be seen against the hull's cut edge instead of sitting flush
|
||||
# with it. Cosmetic only: collision still uses the exact GOAL_APERTURE_*
|
||||
# pair above, so this can't reopen the "ball crosses without scoring" bug.
|
||||
const GOAL_VISUAL_APERTURE_HALF_WIDTH := GOAL_APERTURE_HALF_WIDTH + 0.1
|
||||
const GOAL_VISUAL_APERTURE_HEIGHT := GOAL_APERTURE_HEIGHT + 0.15
|
||||
# The surround has to be opaque hull: backed by the translucent field panel
|
||||
# instead, the goal's recess and net showed straight through the wall beside
|
||||
# the mouth and read as a second, duplicated net.
|
||||
const GOAL_APERTURE_HALF_WIDTH := 1.85
|
||||
const GOAL_APERTURE_HEIGHT := 1.65
|
||||
# ELEVATED only — in FLOOR mode the surround spans the deck to the fillet
|
||||
# tangent, so its height is BASE_RADIUS and needs no constant.
|
||||
const GOAL_SURROUND_HALF_HEIGHT := 1.6
|
||||
@@ -102,12 +118,28 @@ var _field_material: ShaderMaterial
|
||||
# pattern so the viewport lookup isn't repeated every frame.
|
||||
var _camera: Camera3D
|
||||
|
||||
# Group every generated collider is tagged with. A CollisionShape3D only
|
||||
# registers a shape with a CollisionObject3D that is its DIRECT parent — an
|
||||
# earlier version of this code grouped generated colliders under an
|
||||
# intermediate Node3D container for identification, which silently made
|
||||
# every one of them inert (no shape ever reached the StaticBody3D). They must
|
||||
# be direct children of `self`; the group tag is how bake_colliders()
|
||||
# identifies (and clears, for idempotent re-baking) its own prior output
|
||||
# without an intermediate node.
|
||||
const GENERATED_COLLIDER_GROUP := "_arena_generated_collider"
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
add_to_group("arena_boundary")
|
||||
_build_corner_colliders()
|
||||
_build_fillet_colliders(BASE_FILLET)
|
||||
_build_fillet_colliders(CEILING_FILLET)
|
||||
# goal_mode affects the generated geometry itself (see _fillet_runs'
|
||||
# `split` and _build_end_wall_colliders' hole placement), but it's an
|
||||
# instance-level override each arena_0X_elevated.tscn applies to the
|
||||
# shared arena_boundary.tscn's Boundary node — so a bake done in FLOOR
|
||||
# mode (the shared .tscn's default) must not be silently reused by an
|
||||
# ELEVATED instance. Only skip regenerating when the existing bake was
|
||||
# actually made in the current goal_mode.
|
||||
if get_meta("baked_goal_mode", -1) != goal_mode:
|
||||
bake_colliders()
|
||||
# Visuals are pure decoration and training spawns many headless instances
|
||||
# that never render one, so skip the mesh build there entirely. Collision
|
||||
# is unaffected either way.
|
||||
@@ -117,6 +149,29 @@ func _ready() -> void:
|
||||
_build_visual_shell()
|
||||
|
||||
|
||||
# Builds every generated collider (corner curves, base/ceiling fillets, end-
|
||||
# wall goal holes) as direct children of this ArenaBoundary (required for
|
||||
# them to register with the StaticBody3D at all) and records the goal_mode
|
||||
# they were built for. Idempotent: clears any of its own prior output first
|
||||
# (identified via GENERATED_COLLIDER_GROUP), so calling this repeatedly — a
|
||||
# stale bake whose goal_mode doesn't match (see _ready), or re-running
|
||||
# tools/bake_arena_boundary.gd on an already-baked scene — replaces rather
|
||||
# than duplicates. _ready() calls this itself for any scene that hasn't been
|
||||
# baked yet, so behaviour is identical either way; baking only skips redoing
|
||||
# this work on every subsequent load, which matters most for the many
|
||||
# parallel headless training envs that would otherwise pay it on every
|
||||
# episode reset.
|
||||
func bake_colliders() -> void:
|
||||
for child in get_children():
|
||||
if child.is_in_group(GENERATED_COLLIDER_GROUP):
|
||||
child.free()
|
||||
_build_corner_colliders()
|
||||
_build_fillet_colliders(BASE_FILLET)
|
||||
_build_fillet_colliders(CEILING_FILLET)
|
||||
_build_end_wall_colliders()
|
||||
set_meta("baked_goal_mode", goal_mode)
|
||||
|
||||
|
||||
# Wall+ceiling-only proximity force field ("artificial gravity" grav-plating,
|
||||
# weaker than the floor's plain default gravity, which this deliberately
|
||||
# leaves untouched). Ship and Ball each call this with their own
|
||||
@@ -248,8 +303,10 @@ func _fillet_runs(rise: float) -> Array[Dictionary]:
|
||||
|
||||
|
||||
# --- collision ---------------------------------------------------------------
|
||||
# These build the tangent-box collider rings only. Their geometry is what
|
||||
# trained policies in Game/bots/ were fitted against, so it must not change.
|
||||
# Builds the tangent-box collider rings and each end wall's real goal hole.
|
||||
# Geometry is free to evolve now that bots are retrained from scratch — just
|
||||
# re-run tools/bake_arena_boundary.gd afterward so the baked scene (see
|
||||
# bake_colliders) reflects the change.
|
||||
|
||||
|
||||
# Vertical quarter-cylinder curves across the four wall-wall corners.
|
||||
@@ -332,9 +389,65 @@ func _add_curve_collider(
|
||||
box.size = Vector3(SURFACE_THICKNESS, run_length, face_width)
|
||||
collision.shape = box
|
||||
collision.transform = Transform3D(segment_basis, origin)
|
||||
collision.add_to_group(GENERATED_COLLIDER_GROUP)
|
||||
add_child(collision)
|
||||
|
||||
|
||||
# A plain axis-aligned box collider centred on `center`. Used for the flat
|
||||
# end-wall panels below, which aren't tangent to a curve so don't need
|
||||
# _add_curve_collider's outward/along framing.
|
||||
func _add_box_collider(center: Vector3, size: Vector3) -> void:
|
||||
var collision := CollisionShape3D.new()
|
||||
var box := BoxShape3D.new()
|
||||
box.size = size
|
||||
collision.shape = box
|
||||
collision.transform = Transform3D(Basis.IDENTITY, center)
|
||||
collision.add_to_group(GENERATED_COLLIDER_GROUP)
|
||||
add_child(collision)
|
||||
|
||||
|
||||
# One rectangular end-wall panel spanning x in [x0, x1] and y in [y0, y1] at
|
||||
# the given wall_z, SURFACE_THICKNESS deep. No-ops for a degenerate (zero or
|
||||
# negative area) range, which happens whenever the goal hole's aperture
|
||||
# extends to the wall's own boundary (e.g. FLOOR mode's aperture bottom sits
|
||||
# at the wall's own y0).
|
||||
func _add_end_wall_panel(wall_z: float, x0: float, x1: float, y0: float, y1: float) -> void:
|
||||
if x1 <= x0 or y1 <= y0:
|
||||
return
|
||||
_add_box_collider(
|
||||
Vector3((x0 + x1) / 2.0, (y0 + y1) / 2.0, wall_z),
|
||||
Vector3(x1 - x0, y1 - y0, SURFACE_THICKNESS)
|
||||
)
|
||||
|
||||
|
||||
# Real navigable hole in each end wall, sized to the goal's actual scoring
|
||||
# aperture (GOAL_APERTURE_HALF_WIDTH/HEIGHT, which now track
|
||||
# objects/goal.tscn's sensor — see that constant's comment) instead of the
|
||||
# previous single solid box that let the ball trigger the goal sensor but
|
||||
# never actually fly through the wall. _goal_surround_bounds() already knows
|
||||
# the aperture's vertical placement for both FLOOR (floor-level) and ELEVATED
|
||||
# (raised) goal modes, so this works unmodified for either. Left/right panels
|
||||
# carry the full wall height; top/bottom panels close off the remainder of
|
||||
# the aperture band above/below the hole. In FLOOR mode the bottom panel
|
||||
# (y -1..0) is a harmless duplicate — FloorShape already occupies that
|
||||
# region — rather than a true no-op: _add_end_wall_panel still emits it,
|
||||
# since aperture_bottom (0) is above y_bottom (-1). Left in rather than
|
||||
# special-cased since it costs one extra collider and can't create a gap.
|
||||
func _build_end_wall_colliders() -> void:
|
||||
var half_width := INNER_HALF_X + 1.0
|
||||
var y_bottom := -1.0
|
||||
var y_top := INNER_HEIGHT
|
||||
var bounds := _goal_surround_bounds(GOAL_APERTURE_HEIGHT)
|
||||
var aperture_bottom: float = bounds.z
|
||||
var aperture_top: float = bounds.w
|
||||
for sz in [-1.0, 1.0]:
|
||||
var wall_z: float = sz * (INNER_HALF_Z + 0.5)
|
||||
_add_end_wall_panel(wall_z, -half_width, -GOAL_APERTURE_HALF_WIDTH, y_bottom, y_top)
|
||||
_add_end_wall_panel(wall_z, GOAL_APERTURE_HALF_WIDTH, half_width, y_bottom, y_top)
|
||||
_add_end_wall_panel(wall_z, -GOAL_APERTURE_HALF_WIDTH, GOAL_APERTURE_HALF_WIDTH, aperture_top, y_top)
|
||||
_add_end_wall_panel(wall_z, -GOAL_APERTURE_HALF_WIDTH, GOAL_APERTURE_HALF_WIDTH, y_bottom, aperture_bottom)
|
||||
|
||||
|
||||
# --- visual shell ------------------------------------------------------------
|
||||
# One mesh covering the inner surface of the play volume exactly once. Every
|
||||
# piece is cut to meet its neighbours edge-on, so no two translucent surfaces
|
||||
@@ -444,21 +557,27 @@ func _add_goal_aprons(st: SurfaceTool) -> void:
|
||||
# panel, the recess and net behind it showed through the wall and the net read
|
||||
# as duplicated either side of the frame.
|
||||
func _add_goal_surrounds(st: SurfaceTool) -> void:
|
||||
var bounds := _goal_surround_bounds()
|
||||
var bounds := _goal_surround_bounds(GOAL_VISUAL_APERTURE_HEIGHT)
|
||||
for side in [-1.0, 1.0]:
|
||||
_add_aperture_panel(st,
|
||||
Vector3(0, 0, side * INNER_HALF_Z), Vector3(-side, 0, 0), Vector3.UP,
|
||||
Vector3(0, 0, -side), GOAL_MOUTH_HALF_WIDTH, bounds.x, bounds.y,
|
||||
GOAL_APERTURE_HALF_WIDTH, bounds.z, bounds.w)
|
||||
GOAL_VISUAL_APERTURE_HALF_WIDTH, bounds.z, bounds.w)
|
||||
|
||||
|
||||
# (surround bottom, surround top, aperture bottom, aperture top) on the end
|
||||
# wall. In FLOOR mode the surround spans deck to fillet tangent with the goal
|
||||
# sitting on the deck; in ELEVATED it is a band centred on the raised goal.
|
||||
func _goal_surround_bounds() -> Vector4:
|
||||
# `aperture_height` is the caller's choice of GOAL_APERTURE_HEIGHT (the true,
|
||||
# collision-matching size — see _build_end_wall_colliders) or
|
||||
# GOAL_VISUAL_APERTURE_HEIGHT (the cosmetically-widened cut — see
|
||||
# _add_goal_surrounds); it only affects the returned aperture bottom/top
|
||||
# (z/w), not the surround bottom/top (x/y), which are independent of it in
|
||||
# both modes.
|
||||
func _goal_surround_bounds(aperture_height: float) -> Vector4:
|
||||
if goal_mode == GoalMode.FLOOR:
|
||||
return Vector4(0.0, BASE_RADIUS, 0.0, GOAL_APERTURE_HEIGHT)
|
||||
var half := GOAL_APERTURE_HEIGHT / 2.0
|
||||
return Vector4(0.0, BASE_RADIUS, 0.0, aperture_height)
|
||||
var half := aperture_height / 2.0
|
||||
return Vector4(
|
||||
GOAL_CENTER_Y - GOAL_SURROUND_HALF_HEIGHT,
|
||||
GOAL_CENTER_Y + GOAL_SURROUND_HALF_HEIGHT,
|
||||
@@ -485,7 +604,7 @@ func _add_wall_panels(st: SurfaceTool) -> void:
|
||||
var y0 := BASE_RADIUS
|
||||
var y1 := INNER_HEIGHT - BASE_RADIUS
|
||||
var elevated := goal_mode == GoalMode.ELEVATED
|
||||
var bounds := _goal_surround_bounds()
|
||||
var bounds := _goal_surround_bounds(GOAL_VISUAL_APERTURE_HEIGHT)
|
||||
for side in [-1.0, 1.0]:
|
||||
_add_aperture_panel(st,
|
||||
Vector3(side * INNER_HALF_X, 0, 0), Vector3(0, 0, side), Vector3.UP,
|
||||
|
||||
@@ -11,8 +11,8 @@ uniform float soft_fade_distance : hint_range(0.0, 5.0) = 1.0;
|
||||
// see ArenaBoundary.INNER_HALF_X/INNER_HALF_Z/INNER_HEIGHT. Dust is fully
|
||||
// hidden inside that box and fades in over arena_clear_distance beyond it,
|
||||
// so it reads as drifting around the station rather than through the pitch.
|
||||
uniform vec3 arena_half_extents = vec3(12.0, 6.0, 18.0);
|
||||
uniform vec3 arena_centre = vec3(0.0, 6.0, 0.0);
|
||||
uniform vec3 arena_half_extents = vec3(18.0, 9.0, 27.0);
|
||||
uniform vec3 arena_centre = vec3(0.0, 9.0, 0.0);
|
||||
uniform float arena_clear_distance : hint_range(0.0, 10.0) = 2.0;
|
||||
|
||||
varying float v_flicker;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
extends SceneTree
|
||||
|
||||
# One-off bake: runs ArenaBoundary's collider generation once and commits the
|
||||
# result into objects/arena_boundary.tscn, so every arena load (and every
|
||||
# parallel headless training env) skips regenerating ~170 CollisionShape3D
|
||||
# nodes at runtime — ArenaBoundary._ready() already skips this itself once a
|
||||
# bake matching the current goal_mode exists (see bake_colliders).
|
||||
#
|
||||
# Safe to re-run any time a geometry constant changes (arena size, corner/
|
||||
# fillet radii, goal aperture, segment counts, etc.) — bake_colliders() clears
|
||||
# its own prior output first, so this replaces rather than duplicates:
|
||||
# godot --headless --path Game --script res://tools/bake_arena_boundary.gd
|
||||
|
||||
const ARENA_BOUNDARY_PATH := "res://objects/arena_boundary.tscn"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var packed: PackedScene = load(ARENA_BOUNDARY_PATH)
|
||||
var instance := packed.instantiate()
|
||||
instance.bake_colliders()
|
||||
# PackedScene.pack() only serializes nodes whose `owner` is set to the
|
||||
# scene root — newly created nodes have no owner by default, so without
|
||||
# this, pack() silently produces a scene with none of the just-built
|
||||
# colliders in it.
|
||||
_set_owner_recursive(instance, instance)
|
||||
|
||||
var rebaked := PackedScene.new()
|
||||
var pack_result := rebaked.pack(instance)
|
||||
if pack_result != OK:
|
||||
push_error("bake_arena_boundary: pack() failed with error %d" % pack_result)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
var save_result := ResourceSaver.save(rebaked, ARENA_BOUNDARY_PATH)
|
||||
if save_result != OK:
|
||||
push_error("bake_arena_boundary: save() failed with error %d" % save_result)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("bake_arena_boundary: baked colliders into %s" % ARENA_BOUNDARY_PATH)
|
||||
quit()
|
||||
|
||||
|
||||
func _set_owner_recursive(node: Node, scene_root: Node) -> void:
|
||||
for child in node.get_children():
|
||||
child.owner = scene_root
|
||||
_set_owner_recursive(child, scene_root)
|
||||
Reference in New Issue
Block a user