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:
parent
54b883d545
commit
7d1a584c15
11 changed files with 350 additions and 78 deletions
|
|
@ -5,6 +5,13 @@ extends Node2D
|
|||
# core stays genuinely empty (nothing drawn inside CORE_RADIUS), and an
|
||||
# accretion disc of rotating arc segments - not closed rings - stands in
|
||||
# for a surface, brighter and thicker toward the centre.
|
||||
#
|
||||
# Each ring is a NeonPiece that is SPUN, not redrawn. Issues #6 and #10 made
|
||||
# that the rule for the buildings and it applies here more than anywhere: the
|
||||
# disc was ten glowing paths of twenty-five points re-tessellated every frame
|
||||
# - forty antialiased polylines - to turn five rings at five different rates.
|
||||
# Rotation is a node property, so now nothing is rasterised after the first
|
||||
# frame and the disc turns for free.
|
||||
|
||||
var disc_phase: float = 0.0
|
||||
|
||||
|
|
@ -14,36 +21,54 @@ const RING_STEP: float = 16.0
|
|||
const DISC_COLOR: Color = Color(0.55, 0.15, 0.85)
|
||||
const DISC_HOT_COLOR: Color = Color(0.9, 0.6, 1.0)
|
||||
|
||||
# One per ring, with the rate and direction it turns at.
|
||||
var _rings: Array = []
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_build()
|
||||
|
||||
|
||||
func configure(_shape_seed: int) -> void:
|
||||
queue_redraw()
|
||||
if _rings.is_empty():
|
||||
_build()
|
||||
|
||||
|
||||
func extractor_anchor() -> Vector2:
|
||||
return Vector2(0.0, -(CORE_RADIUS + float(RING_COUNT - 1) * RING_STEP + 12.0))
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
disc_phase += delta
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
func _build() -> void:
|
||||
for child in get_children():
|
||||
child.queue_free()
|
||||
_rings.clear()
|
||||
for i in range(RING_COUNT):
|
||||
var radius: float = CORE_RADIUS + float(i) * RING_STEP
|
||||
# 1.0 at the innermost ring, fading to 0.0 at the outermost - drives
|
||||
# both colour (hotter inward) and stroke weight/brightness.
|
||||
var t: float = 1.0 - float(i) / float(maxi(RING_COUNT - 1, 1))
|
||||
var ring_color: Color = DISC_COLOR.lerp(DISC_HOT_COLOR, t * 0.65)
|
||||
# Alternating spin direction per ring and a speed tied to t reads as
|
||||
var piece: NeonPiece = NeonPiece.create(self, Vector2.ZERO,
|
||||
DISC_COLOR.lerp(DISC_HOT_COLOR, t * 0.65))
|
||||
# The two arcs are built at their resting angles; the ring node turns
|
||||
# them. Alternating direction per ring and a speed tied to t reads as
|
||||
# genuine turbulence rather than one disc rotating as a rigid whole.
|
||||
var spin: float = disc_phase * (0.12 + t * 0.22) * (1.0 if i % 2 == 0 else -1.0)
|
||||
for arc_i in range(2):
|
||||
var start: float = spin + float(arc_i) * PI + 0.15
|
||||
var start: float = float(arc_i) * PI + 0.15
|
||||
var end: float = start + PI - 0.3
|
||||
var points := PackedVector2Array()
|
||||
var steps: int = 24
|
||||
for j in range(steps + 1):
|
||||
var angle: float = lerpf(start, end, float(j) / float(steps))
|
||||
points.append(Vector2(cos(angle), sin(angle)) * radius)
|
||||
NeonDraw.draw_glowing_path(self, points, ring_color, 0.5 + t * 0.5, 0.45 + t * 0.35)
|
||||
piece.add_path(points, 0.5 + t * 0.5, 0.45 + t * 0.35)
|
||||
_rings.append({
|
||||
"piece": piece,
|
||||
"rate": (0.12 + t * 0.22) * (1.0 if i % 2 == 0 else -1.0),
|
||||
})
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
disc_phase += delta
|
||||
# One property per ring. Nothing is rasterised again.
|
||||
for ring in _rings:
|
||||
(ring.piece as NeonPiece).rotation = disc_phase * float(ring.rate)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
extends Node2D
|
||||
|
||||
var gas_phase: float = 0.0
|
||||
var _rim: NeonPiece
|
||||
|
||||
|
||||
func configure(_shape_seed: int) -> void:
|
||||
|
|
@ -16,12 +17,24 @@ func _process(delta: float) -> void:
|
|||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var rim := PackedVector2Array()
|
||||
# The bands genuinely deform - each point moves along its own sine - so they
|
||||
# are the one thing here that has to be re-tessellated to animate. The rim
|
||||
# does not move at all, so it is a NeonPiece child rasterised once rather
|
||||
# than a thirty-two point polygon rebuilt every frame alongside them
|
||||
# (issues #6/#10).
|
||||
func _ensure_rim() -> void:
|
||||
if _rim != null:
|
||||
return
|
||||
var points := PackedVector2Array()
|
||||
for i in range(32):
|
||||
var angle := TAU * float(i) / 32.0
|
||||
rim.append(Vector2(cos(angle) * 118.0, sin(angle) * 118.0))
|
||||
NeonDraw.draw_glowing_polygon(self, rim, Color(0.4, 0.78, 1.0), 0.9)
|
||||
points.append(Vector2(cos(angle) * 118.0, sin(angle) * 118.0))
|
||||
_rim = NeonPiece.create(self, Vector2.ZERO, Color(0.4, 0.78, 1.0))
|
||||
_rim.add_polygon(points, 0.9)
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
_ensure_rim()
|
||||
for row in [-62.0, -28.0, 8.0, 43.0, 73.0]:
|
||||
var band := PackedVector2Array()
|
||||
var half_width: float = sqrt(maxf(0.0, 118.0 * 118.0 - row * row))
|
||||
|
|
|
|||
|
|
@ -5,6 +5,15 @@ extends Node2D
|
|||
# and armoured rather than a natural silhouette (#63: this is an ENEMY
|
||||
# installation, not a celestial body), with a rotating dish and blinking
|
||||
# warning lights to read as occupied/hostile at a glance.
|
||||
#
|
||||
# Built out of NeonPiece children rather than redrawn each frame. Issues #6
|
||||
# and #10 established that rule for the buildings: every NeonDraw call
|
||||
# expands into four antialiased polylines, so re-running _draw() to advance
|
||||
# an animation re-tessellates the whole thing. This visual was doing exactly
|
||||
# that - nine draw calls a frame, of which eight were fixed geometry - to
|
||||
# rotate one dish and fade six lights. Both are node properties: the dish is
|
||||
# a child that spins, the lights are a child whose modulate breathes, and the
|
||||
# hull is rasterised once and never touched again.
|
||||
|
||||
var phase: float = 0.0
|
||||
|
||||
|
|
@ -14,42 +23,72 @@ const CORE_COLOR: Color = Color(0.9, 0.25, 0.18)
|
|||
const WARNING_COLOR: Color = Color(1.0, 0.2, 0.15)
|
||||
const DARK_CORE: Color = Color(0.14, 0.02, 0.02)
|
||||
|
||||
var _dish: NeonPiece
|
||||
var _lights: NeonPiece
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_build()
|
||||
|
||||
|
||||
func configure(_shape_seed: int) -> void:
|
||||
queue_redraw()
|
||||
if _dish == null:
|
||||
_build()
|
||||
|
||||
|
||||
func extractor_anchor() -> Vector2:
|
||||
return Vector2(0.0, -(RADIUS + 40.0))
|
||||
|
||||
|
||||
func _build() -> void:
|
||||
for child in get_children():
|
||||
child.queue_free()
|
||||
|
||||
# The armoured hull and its core: fixed, so drawn once.
|
||||
var shell: NeonPiece = NeonPiece.create(self, Vector2.ZERO, HULL_COLOR)
|
||||
shell.core_color = DARK_CORE
|
||||
shell.add_polygon(_ring(RADIUS, 0.0), 0.85)
|
||||
var core: NeonPiece = NeonPiece.create(self, Vector2.ZERO, CORE_COLOR)
|
||||
core.core_color = DARK_CORE
|
||||
core.add_polygon(_ring(RADIUS * 0.55, PI / 6.0), 0.9)
|
||||
|
||||
# The dish, drawn along +X and then SPUN by rotating this node - the
|
||||
# rotation is what used to cost a full re-tessellation every frame.
|
||||
_dish = NeonPiece.create(self, Vector2.ZERO, CORE_COLOR)
|
||||
_dish.core_color = DARK_CORE
|
||||
var tip: Vector2 = Vector2(RADIUS * 1.35, 0.0)
|
||||
_dish.add_line(Vector2.ZERO, tip, 0.55, 0.8)
|
||||
_dish.add_polygon(_circle(tip, 11.0), 1.0)
|
||||
|
||||
# The warning lights blink by fading the whole piece, not by redrawing
|
||||
# six circles at a new alpha.
|
||||
_lights = NeonPiece.create(self, Vector2.ZERO, WARNING_COLOR)
|
||||
_lights.core_color = DARK_CORE
|
||||
for i in range(6):
|
||||
var angle: float = TAU * float(i) / 6.0
|
||||
_lights.add_polygon(_circle(Vector2(cos(angle), sin(angle)) * RADIUS, 5.0), 1.0)
|
||||
|
||||
|
||||
func _ring(radius: float, offset: float) -> PackedVector2Array:
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(6):
|
||||
var angle: float = TAU * float(i) / 6.0 + offset
|
||||
points.append(Vector2(cos(angle), sin(angle)) * radius)
|
||||
return points
|
||||
|
||||
|
||||
func _circle(centre: Vector2, radius: float) -> PackedVector2Array:
|
||||
var points: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(12):
|
||||
var angle: float = TAU * float(i) / 12.0
|
||||
points.append(centre + Vector2(cos(angle), sin(angle)) * radius)
|
||||
return points
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
phase += delta
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func _draw() -> void:
|
||||
var hull: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(6):
|
||||
var angle: float = TAU * float(i) / 6.0
|
||||
hull.append(Vector2(cos(angle), sin(angle)) * RADIUS)
|
||||
NeonDraw.draw_glowing_polygon(self, hull, HULL_COLOR, 0.85, DARK_CORE)
|
||||
|
||||
var inner: PackedVector2Array = PackedVector2Array()
|
||||
for i in range(6):
|
||||
var angle: float = TAU * float(i) / 6.0 + PI / 6.0
|
||||
inner.append(Vector2(cos(angle), sin(angle)) * RADIUS * 0.55)
|
||||
NeonDraw.draw_glowing_polygon(self, inner, CORE_COLOR, 0.9, DARK_CORE)
|
||||
|
||||
# A slow-rotating dish - reads as actively transmitting, not derelict.
|
||||
var dish_angle: float = phase * 0.6
|
||||
var dish_tip: Vector2 = Vector2(cos(dish_angle), sin(dish_angle)) * RADIUS * 1.35
|
||||
NeonDraw.draw_glowing_line(self, Vector2.ZERO, dish_tip, CORE_COLOR, 0.8, 0.55, DARK_CORE)
|
||||
NeonDraw.draw_glowing_circle(self, dish_tip, 11.0, CORE_COLOR, 1.0, DARK_CORE)
|
||||
|
||||
# Blinking hull-corner warning lights.
|
||||
var blink: float = 0.4 + 0.6 * absf(sin(phase * 4.0))
|
||||
for i in range(6):
|
||||
var angle: float = TAU * float(i) / 6.0
|
||||
var at: Vector2 = Vector2(cos(angle), sin(angle)) * RADIUS
|
||||
NeonDraw.draw_glowing_circle(self, at, 5.0, WARNING_COLOR, blink, DARK_CORE)
|
||||
# Two node properties. Nothing is rasterised again.
|
||||
if _dish != null:
|
||||
_dish.rotation = phase * 0.6
|
||||
if _lights != null:
|
||||
_lights.modulate.a = 0.4 + 0.6 * absf(sin(phase * 4.0))
|
||||
|
|
|
|||
Loading…
Reference in a new issue