Stop redrawing what only needs moving

Reviewed against the rule issues #6 and #10 set for the buildings: geometry
is rasterised once and thereafter only moved, scaled or faded, because every
NeonDraw call expands into four antialiased polylines.

The planet visuals never got that refactor. The black hole rebuilt ten
glowing paths of twenty-five points every frame - forty polylines - to spin
five rings; each ring is a NeonPiece that rotates now. The research lab spent
nine draw calls a frame, eight of them fixed geometry, to turn one dish and
blink six lights; the dish is a child that rotates and the lights a child
whose modulate breathes. The gas giant's bands genuinely deform, so they
still draw, but its constant rim moved out to a child. NeonPiece gained
add_path, which it needed for open arcs.

And I had broken the rule myself. ScannerGuidance re-queued a redraw of its
arrow every frame for a colour that changes only with the destination - and
it runs with PROCESS_MODE_ALWAYS, so it was re-tessellating through every
dialog that stops the rest of the game. Guarded, and pinned by a test that
counts CanvasItem's own draw signal: 36 rasterisations in 0.6s before, one
after.

Two smaller things from the same pass: the station menu drew its condition
bar by hand where the ship lab already uses a ProgressBar node, so it uses
one now and its _draw is gone; and station/upkeep refreshes were wired into
_refresh_garrison(), which nothing but a test calls, so both menus showed
whatever was true when they opened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019AJMdB7iuHuLFZM1aBu3GD
This commit is contained in:
Loïc Wisniewski 2026-09-08 11:35:45 +02:00
commit 7d1a584c15
11 changed files with 350 additions and 78 deletions

View file

@ -14,6 +14,13 @@ const EDGE_MARGIN: float = 34.0
@onready var target_label: Label = $TargetLabel
var fleet: Node2D
# The colour the arrow was last rasterised with. Issues #6/#10: a glyph is
# drawn once and thereafter moved and rotated, both of which reuse the cached
# geometry. Re-queueing a redraw every frame for a colour that changes only
# when the DESTINATION changes threw that away - and this node runs even
# while the game is stopped for a dialog, so it was the one piece of UI
# re-tessellating itself through the quietest moments in the game.
var _drawn_glow: Color = Color(0, 0, 0, 0)
func _ready() -> void:
@ -53,8 +60,9 @@ func _process(_delta: float) -> void:
# authored colour rather than being forced into one.
var definition: Resource = PLANET_DEFINITIONS.get_definition(
String(target.get("definition_id", "")))
if definition != null:
indicator.glow_color = definition.map_color
if definition != null and definition.map_color != _drawn_glow:
_drawn_glow = definition.map_color
indicator.glow_color = _drawn_glow
indicator.queue_redraw()
var sectors: float = toward_target.length() / WorldGen.CHUNK_SIZE
target_label.text = "%s · %.1f SECTORS" % [

View file

@ -12,23 +12,25 @@ extends "res://ui/menus/ModalMenu.gd"
# it does, so the bar row below shows what each of them has left, worst first,
# which is the number that decides whether another needs building.
# One bar, wide, for the vessel closest to being spent - the one that decides
# One bar, for the vessel closest to being spent - the one that decides
# whether another needs building. The per-vessel figures are listed as TEXT
# below it: a row of small bars was unreadable at the size a phone draws this
# menu, and "cycles left" is a number the player wants to actually read.
const BAR_WIDTH: float = 420.0
const BAR_HEIGHT: float = 40.0
const BAR_TOP: float = 494.0
# beneath it: a row of small bars was unreadable at the size a phone draws
# this menu, and "cycles left" is a number the player wants to read.
#
# A ProgressBar NODE rather than rects in _draw(), the way the ship lab draws
# its research progress. Issues #6/#10 set the rule for the rest of the game -
# geometry is rasterised once and thereafter only moved, scaled or recoloured
# - and a menu that refreshes while it is open would otherwise re-tessellate
# its bar every frame to move one edge.
const WORN_COLOR: Color = Color(1.0, 0.35, 0.25)
const FRESH_COLOR: Color = Color(0.5, 1.0, 0.55)
const PLATE_COLOR: Color = Color(0.02, 0.02, 0.06, 0.9)
const FRAME_COLOR: Color = Color(0.85, 0.96, 1.0, 0.85)
@onready var title: Label = $TitleLabel
@onready var assigned_label: Label = $AssignedLabel
@onready var status: Label = $StatusLabel
@onready var condition_label: Label = $ConditionLabel
@onready var condition_list: Label = $ConditionList
@onready var condition_bar: ProgressBar = $ConditionBar
@onready var less_button: IconButton = $LessButton
@onready var more_button: IconButton = $MoreButton
@ -89,6 +91,7 @@ func refresh() -> void:
var left: Array = GameState.station_condition(building_id)
var full: int = maxi(GameState.station_work_capacity(building_id), 1)
var unit: String = "RUNS" if vessel_type == GameState.VESSEL_TRANSPORTER else "REPAIRS"
condition_bar.visible = not left.is_empty()
if left.is_empty():
condition_label.text = ""
condition_list.text = ""
@ -101,27 +104,11 @@ func refresh() -> void:
lines.append("%s %d %d / %d %s" % [
name.to_upper(), i + 1, int(left[i]), full, unit])
condition_list.text = "\n".join(lines)
queue_redraw()
func _draw() -> void:
if building_id == 0:
return
# One bar per vessel standing here, worst first: what is left of the work
# it was built to do. Drawn rather than built from nodes because the count
# changes as vessels arrive and wear out, and a row of bars is cheaper to
# draw than to keep a pool of Controls in step with.
var left: Array = GameState.station_condition(building_id)
if left.is_empty():
return
var full: int = maxi(GameState.station_work_capacity(building_id), 1)
# The worst of them. station_condition() answers worst first.
var fraction: float = clampf(float(left[0]) / float(full), 0.0, 1.0)
var x: float = (size.x - BAR_WIDTH) * 0.5
draw_rect(Rect2(x - 3.0, BAR_TOP - 3.0, BAR_WIDTH + 6.0, BAR_HEIGHT + 6.0),
PLATE_COLOR)
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH, BAR_HEIGHT),
Color(FRESH_COLOR.r, FRESH_COLOR.g, FRESH_COLOR.b, 0.16))
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH * fraction, BAR_HEIGHT),
WORN_COLOR.lerp(FRESH_COLOR, fraction))
draw_rect(Rect2(x, BAR_TOP, BAR_WIDTH, BAR_HEIGHT), FRAME_COLOR, false, 2.0)
# Worst first, so this is the one about to go.
var fraction: float = clampf(float(left[0]) / float(full), 0.0, 1.0)
condition_bar.value = fraction
# The fill is recoloured rather than redrawn: a StyleBox is shared
# geometry, and setting its colour does not re-tessellate anything.
var fill: StyleBoxFlat = condition_bar.get_theme_stylebox("fill")
if fill != null:
fill.bg_color = WORN_COLOR.lerp(FRESH_COLOR, fraction)

View file

@ -1,8 +1,27 @@
[gd_scene load_steps=3 format=3]
[gd_scene load_steps=5 format=3]
[ext_resource type="Script" path="res://ui/IconButton.gd" id="icon"]
[ext_resource type="Script" path="res://ui/menus/StationMenu.gd" id="menu"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_condition_background"]
bg_color = Color(0.025, 0.045, 0.09, 0.92)
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.18, 0.48, 0.62, 0.9)
corner_radius_top_left = 8
corner_radius_top_right = 8
corner_radius_bottom_right = 8
corner_radius_bottom_left = 8
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_condition_fill"]
bg_color = Color(0.5, 1, 0.55, 0.88)
corner_radius_top_left = 6
corner_radius_top_right = 6
corner_radius_bottom_right = 6
corner_radius_bottom_left = 6
[node name="StationMenu" type="Control" groups=["modal_menus"]]
visible = false
mouse_filter = 2
@ -83,6 +102,21 @@ theme_override_colors/font_color = Color(0.55, 0.78, 0.86, 1)
text = ""
horizontal_alignment = 1
[node name="ConditionBar" type="ProgressBar" parent="."]
visible = false
mouse_filter = 2
anchor_left = 0.5
anchor_right = 0.5
offset_left = -210.0
offset_top = 494.0
offset_right = 210.0
offset_bottom = 534.0
theme_override_styles/background = SubResource("StyleBoxFlat_condition_background")
theme_override_styles/fill = SubResource("StyleBoxFlat_condition_fill")
max_value = 1.0
step = 0.001
show_percentage = false
[node name="ConditionList" type="Label" parent="."]
mouse_filter = 2
anchor_right = 1.0