From fbb783b9475b5aabb40e3902930901e796146f90 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:15:43 +0100 Subject: [PATCH] perf(ship): gate telemetry emission on listeners and headless mode Ship._emit_telemetry_data() ran get_euler()+trig every physics tick for every ship regardless of whether a HUD was watching, wasting work on AI ships and every headless training instance. Disable _physics_process outright when headless, and skip emission the rest of the time unless a signal actually has a listener. --- Game/scripts/ship.gd | 19 ++++++++++++++++++- TODO.md | 11 +---------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/Game/scripts/ship.gd b/Game/scripts/ship.gd index 7f63f19f..9751e258 100644 --- a/Game/scripts/ship.gd +++ b/Game/scripts/ship.gd @@ -83,6 +83,12 @@ 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. + if DisplayServer.get_name() == "headless": + set_physics_process(false) + func _apply_team_color() -> void: if not is_inside_tree(): @@ -112,7 +118,18 @@ func set_controller(new_controller: ShipController) -> void: func _physics_process(_delta): - _emit_telemetry_data() + if _has_telemetry_listeners(): + _emit_telemetry_data() + + +# Covers a second/AI ship with no HUD watching it - headless mode is already +# handled by disabling _physics_process entirely in _ready. +func _has_telemetry_listeners() -> bool: + return speed_changed.get_connections().size() > 0 \ + or altitude_changed.get_connections().size() > 0 \ + or attitude_changed.get_connections().size() > 0 \ + or heading_changed.get_connections().size() > 0 \ + or thrust_changed.get_connections().size() > 0 func _integrate_forces(state): diff --git a/TODO.md b/TODO.md index 5f3660d1..94f2528d 100644 --- a/TODO.md +++ b/TODO.md @@ -23,18 +23,9 @@ Bugs found in an adversarial review. None are gameplay- or physics-affecting, so - [ ] Reuse a member `ShipAction` in `ship.gd` (controllerless path) and `player_ship_controller.gd` instead of allocating one per physics tick; `ai_ship_controller.gd` already does this correctly. - [ ] Delete the duplicate 1 MB texture — `assets/textures/planet_surface.png` and `assets/models/nebula_planet_planet_surface.png` are byte-identical. -## DRY / structure - -- [x] `arena_base.tscn` + inherited themes. `arena_01/02/03.tscn` each restate ~40 identical lines (both lights, the reflection probe, goal transforms, ball/ship spawn markers); only sky, ambient, three glow numbers, tint and decoration differ. The `_elevated` variants already prove the inherited-scene pattern works here — `arena_01_elevated.tscn` is 20 lines to `arena_01.tscn`'s 100. Unblocks cheap new arenas. -- [x] Hoist bot construction into `GameMode` — `match_mode._make_opponent_controller` and `spectate_mode._make_bot` are the same function (same existence check, same three fields, same warning + inert fallback). -- [x] Hoist score-keeping into `GameMode` — both modes declare `score := {0:0, 1:0}`, a `score_changed` signal, and the identical increment/emit/print block. -- [x] `HudInstrument extends Control` base for the three HUD widgets: each repeats `const SMOOTHING := 12.0` and the same `1.0 - exp(-SMOOTHING * delta)` lerp/redraw `_process`, and `hud_heading_tape.gd` reaches across to a static angle helper parked on `HudAttitudeIndicator`. -- [x] `MAIN_MENU_SCENE_PATH` is declared in both `game_mode.gd` and `settings_menu.gd`. -- [x] Comment `main_menu.gd`'s `DIFFICULTIES` to say the tiers deliberately share `easy.json` and differ only by handicap (superseded once real `medium`/`hard` tiers land — see the AI section above). - ## Performance -- [ ] Gate `Ship._emit_telemetry_data()` — it runs `get_euler()` + trig per ship per physics tick for every ship, including AI ships nobody displays and `--headless` training where no HUD exists. Gate on having signal connections, and `set_physics_process(false)` when headless (`arena_boundary.gd` already does this correctly for its `_process`). +- [x] Gate `Ship._emit_telemetry_data()` — it runs `get_euler()` + trig per ship per physics tick for every ship, including AI ships nobody displays and `--headless` training where no HUD exists. Gate on having signal connections, and `set_physics_process(false)` when headless (`arena_boundary.gd` already does this correctly for its `_process`). - [ ] Stop the HUD instruments redrawing once settled: all five `queue_redraw()` every frame forever, re-recording canvas items with `draw_string` glyph work, and `HUD.tscn` sets `process_mode = 3` so it continues while paused. - [ ] Shared per-team materials instead of `Ship._apply_team_color()` allocating a fresh `StandardMaterial3D` and assigning it as `material_override` (and running at least twice per ship — once from `_ready`, once from the `team` setter). Removes the allocation and lets same-team ships batch. - [ ] Merge each goal's visuals into one `ArrayMesh` with a hull surface and a net surface — currently 11 `MeshInstance3D`s and 4 materials per goal, ~22 draw calls for a static prop. `arena_boundary.gd:333` already demonstrates the `SurfaceTool` technique in this codebase.