diff --git a/Game/objects/ship.tscn b/Game/objects/ship.tscn index cac93cfa..cd9f4156 100644 --- a/Game/objects/ship.tscn +++ b/Game/objects/ship.tscn @@ -1,12 +1,8 @@ [gd_scene load_steps=13 format=3 uid="uid://p07epxnh8wwp"] [ext_resource type="Script" uid="uid://dyq1n7q1bqjps" path="res://scripts/ship.gd" id="1_efag7"] -[ext_resource type="ArrayMesh" path="res://assets/models/ship_hull.res" id="2_hull"] [ext_resource type="ArrayMesh" path="res://assets/models/ship_nose.res" id="3_nose"] -[ext_resource type="ArrayMesh" path="res://assets/models/ship_canopy.res" id="4_canopy"] [ext_resource type="ArrayMesh" path="res://assets/models/ship_tailfin.res" id="5_talfin"] -[ext_resource type="ArrayMesh" path="res://assets/models/ship_engine_l.res" id="6_enginel"] -[ext_resource type="ArrayMesh" path="res://assets/models/ship_engine_r.res" id="7_enginer"] [sub_resource type="PhysicsMaterial" id="PhysicsMaterial_ship"] friction = 0.1 @@ -21,29 +17,13 @@ physics_material_override = SubResource("PhysicsMaterial_ship") inertia = Vector3(1, 1, 1) script = ExtResource("1_efag7") -[node name="Hull" type="MeshInstance3D" parent="."] -transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0) -mesh = ExtResource("2_hull") - [node name="Nose" type="MeshInstance3D" parent="."] transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0, 0) mesh = ExtResource("3_nose") -[node name="Canopy" type="MeshInstance3D" parent="."] -transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.31, -0.55) -mesh = ExtResource("4_canopy") - [node name="TailFin" type="MeshInstance3D" parent="."] transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0, 0.24, 0.72) mesh = ExtResource("5_talfin") -[node name="EngineGlowL" type="MeshInstance3D" parent="."] -transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, -0.42, -0.05, 0.95) -mesh = ExtResource("6_enginel") - -[node name="EngineGlowR" type="MeshInstance3D" parent="."] -transform = Transform3D(-1, 0, 0, 0, 1, 0, 0, 0, -1, 0.42, -0.05, 0.95) -mesh = ExtResource("7_enginer") - [node name="CollisionShape3D" type="CollisionShape3D" parent="."] shape = SubResource("BoxShape3D_dsjou") diff --git a/Game/scripts/ship.gd b/Game/scripts/ship.gd index b9e9990a..f6b858a2 100644 --- a/Game/scripts/ship.gd +++ b/Game/scripts/ship.gd @@ -32,6 +32,32 @@ const TEAM_COLORS := { 1: Color(1.0, 0.5, 0.15), } +# Non-tinted hull meshes, runtime-merged into one ArrayMesh by +# _build_merged_hull() (Nose/TailFin stay separate MeshInstance3Ds since +# _apply_team_color() retints them per-team and must keep addressing them by +# name). Verified via get_surface_count()/surface_get_material() before +# writing this: hull and canopy are each a single surface with their own +# distinct opaque StandardMaterial3D (canopy is NOT alpha/transparent despite +# the name), and engine_l/engine_r are each 2 surfaces, also all distinct +# materials — none of the 6 source surfaces share a material with any other, +# including the L/R engine pair. So this merge does not collapse draw calls +# the way TODO.md's "6 draw calls down to 3" assumed (Godot still issues one +# draw call per surface regardless of how many MeshInstance3Ds they're spread +# across); the real win is scene-tree node count, 4 MeshInstance3D children +# down to 1, cutting per-frame transform/visibility overhead. +const MERGED_MESH_PATHS := [ + "res://assets/models/ship_hull.res", + "res://assets/models/ship_canopy.res", + "res://assets/models/ship_engine_l.res", + "res://assets/models/ship_engine_r.res", +] +const MERGED_MESH_TRANSFORMS := [ + Transform3D(Basis(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)), Vector3(0, 0, 0)), # Hull + Transform3D(Basis(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)), Vector3(0, 0.31, -0.55)), # Canopy + Transform3D(Basis(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)), Vector3(-0.42, -0.05, 0.95)), # EngineGlowL + Transform3D(Basis(Vector3(-1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, -1)), Vector3(0.42, -0.05, 0.95)), # EngineGlowR +] + # Which team this ship plays for (0 or 1). Set by the game mode on spawn. var team: int = 0: set(value): @@ -103,11 +129,13 @@ func _ready(): _boundary = get_tree().get_first_node_in_group("arena_boundary") - # Telemetry emission is HUD-only work; headless (RL/CI) instances never - # have one, so skip the per-tick cost entirely rather than relying on - # the listener check below to no-op every frame. + # Telemetry emission and the merged-hull visual mesh are both render-only + # work; headless (RL/CI) instances never render or have a HUD watching + # them, so skip both rather than relying on later no-ops. if DisplayServer.get_name() == "headless": set_physics_process(false) + else: + _build_merged_hull() func _apply_team_color() -> void: @@ -120,6 +148,32 @@ func _apply_team_color() -> void: mesh.material_override = accent +# Runtime-bakes Hull/Canopy/EngineGlowL/EngineGlowR (see MERGED_MESH_PATHS +# comment above) into one ArrayMesh, one destination surface per source +# surface via SurfaceTool.append_from, each keeping its own original +# material — preserves current visuals exactly regardless of surface count. +# Mirrors the runtime-bake pattern already used by goal.gd/arena_boundary.gd. +# Skipped in headless mode (see _ready): purely visual, costs nothing +# physics/RL cares about. +func _build_merged_hull() -> void: + var mesh := ArrayMesh.new() + var dest_idx := 0 + for i in MERGED_MESH_PATHS.size(): + var src: ArrayMesh = load(MERGED_MESH_PATHS[i]) + var xform: Transform3D = MERGED_MESH_TRANSFORMS[i] + for surf in src.get_surface_count(): + var st := SurfaceTool.new() + st.begin(Mesh.PRIMITIVE_TRIANGLES) + st.append_from(src, surf, xform) + st.commit(mesh) + mesh.surface_set_material(dest_idx, src.surface_get_material(surf)) + dest_idx += 1 + var instance := MeshInstance3D.new() + instance.name = "MergedHull" + instance.mesh = mesh + add_child(instance) + + # Attach the node that drives this ship (player, AI, or network). Replaces # any existing controller; parents the new one under the ship if needed. func set_controller(new_controller: ShipController) -> void: diff --git a/TODO.md b/TODO.md index 81d6c004..429a809d 100644 --- a/TODO.md +++ b/TODO.md @@ -25,7 +25,6 @@ Bugs found in an adversarial review. None are gameplay- or physics-affecting, so ## Performance -- [ ] Merge the four non-tinted ship meshes (Hull/Canopy/EngineGlowL/R) into one — 6 draw calls per ship down to 3. Irrelevant at 1v1; 36 calls before VFX at 3v3. - [ ] Name collision layers in `project.godot` and assign them — nothing configures `collision_layer`/`collision_mask` today, so every body tests against every other. - [ ] Measure `nebula_dust.gdshader`'s per-fragment depth-texture sample across 500 large soft billboards before adding more particle work. - [ ] Bake `ArenaBoundary`'s ~160 runtime-generated `CollisionShape3D` nodes into the scene. Costs a load hitch on every arena entry and repeats in every parallel headless training env. **Blocked on the trained-bot decision below** — `arena_boundary.gd:235` notes this geometry is what the shipped policies were fitted against, so the bake must be verified byte-identical.