Skip to content

Spider webs as a decaying grid — design

Status: proposal, needs Chris + Toby sign-off (slow feel and web look are both Chris’s calls). Origin: Toby, 2026-10-08. Written up after the ATV BUILD 282 perf A/B (memory bullet-heaven-funzo-zone-slowdown).

Webs today are an Array[Dictionary] of capsules (Sim.webs, cap WEB_CAP 400). Even after the MultiMesh batching (one draw) and webs-lite, every frame/tick still walks every web object:

Cost Where Scales with
Per-pilot slow test (point-to-segment per web) ElementalSystem.web_slow_mult webs × pilots, every tick
Countdown + remove_at ElementalSystem.update_webs webs, every tick
Dedupe / trail-stretch grid rebuilt per call ElementalSystem.drop_webs webs, every tick with a spider
Render buffer rewrite (12 floats/web, GDScript loop) ZoneRenderer._update_web_layer webs, every frame
GPU fill of overlapping quads web_field*.gdshader overlap area

Honest priority note: on 2026-10-08 the ATV’s 4 fps was FunZo’s unbatched, never-expiring zones, not webs. Toggling webs-lite with ~300 webs live changed nothing measurable. This is a structural cleanup with a moderate perf win (it removes the per-web CPU loops above and the WEB_CAP eviction backstop), not an emergency. Ship the FunZo fix first.

  • Webs become a grid over the play area, stored as one packed array, drawn by one shader. No web objects are created.
  • Each cell holds a value 0..MAX (MAX ≥ 1; proposal 5). It decays 0.1 per second, floored at 0. 0.1/s means one deposit lasts 10 s, the same as today’s WEB_DURATION.
  • When a spider lays a web, every cell inside the web’s shape gains +1 (capped at MAX).
  • A pilot is slowed by the value of the cell under its centre (see “Sampling” for a refinement).

No decay pass: store each cell’s zero-crossing tick

Section titled “No decay pass: store each cell’s zero-crossing tick”

A literal per-second decrement sweeps every cell (4000×4000 arena at 16 px = 62.5k cells) in GDScript, which means a periodic CPU spike. Instead each cell stores Z, the sim tick at which its value reaches 0 (PackedInt32Array):

TICKS_PER_UNIT = 600 # 0.1/s at 60 Hz -> 1 unit per 10 s
value(cell) = max(0, Z - now) / TICKS_PER_UNIT
deposit(cell) : Z = min(max(Z, now) + TICKS_PER_UNIT, now + TICKS_PER_UNIT * MAX)

There is no per-tick or per-second work: decay is implicit in now. It is all integer, so it stays deterministic and serializable (/sim rule: plain data, no engine APIs). The shader gets now as a uniform and computes the same value per pixel, so the texture only changes when a deposit happens.

Mark the cells whose centres fall inside the real shape (circle or capsule, the existing _point_web_dist_sq test), not the bounding box. Otherwise the Elite Spider’s diagonal corridors and rays become fat rectangles. The passive trail just deposits a circle every WEB_SPACING px along the spider’s walk. The capsule-stretch / trail-join logic (TRAIL_*) and _near_existing_web dedupe go away. Repeated passes now add (up to MAX) instead of being suppressed.

  • One quad covering the visible part of the grid, sampling an R32F texture of Z. Integers below 2^24 are exact in float32, which is about 77 h of ticks.
  • Upload the texture only on ticks where a deposit happened. Hide the quad when no cell is active (track max_Z > now), so a run with no spiders pays nothing.
  • Look: a filtered value field, thresholded into soft silk edges, with a world-space strand pattern and brightness/alpha from the value. This replaces the per-web capsule silk and Web Looks v1/A/B/C (spokes and pattern seeds per web). That is a visual change for Chris to approve, and it makes the pending “pick a Web Look” decision moot.

Cell size (Toby: smaller = closer to today’s gameplay, but less efficient)

Section titled “Cell size (Toby: smaller = closer to today’s gameplay, but less efficient)”

Where cell size actually costs:

Cell Cells (4000²) Z array / texture Cells per deposit (r = 38 circle) Max edge error
8 px 250k 1.0 MB ~71 ~6 px
16 px 62.5k 250 KB ~18 ~11 px
24 px 27.8k 111 KB ~8 ~17 px
32 px 15.6k 62 KB ~4-5 ~23 px
  • The slow lookup is O(1) at any size, so per-tick cost does not grow as cells shrink.
  • Deposits scale with 1/cell², but they happen per web laid (every ~34 px of spider walk), not per frame. Even 8 px is cheap there.
  • Memory and texture upload scale with 1/cell². The upload is the real limit on the ATV: the whole texture re-uploads on any tick with a deposit, so with spiders alive that is close to every tick. Uploading only a dirty sub-rect would remove most of it, but check what Godot 4.7 exposes (RenderingServer.texture_2d_update is whole-image).
  • Gameplay error is about half a cell diagonal at the web edge.

Proposal: 16 px, as a run-start tunable (WebGrid.CELL_PX, not live, because changing it reallocates the grid). Measure the upload cost on the ATV before going smaller.

Sampling: bilinear slow mostly removes the trade-off

Section titled “Sampling: bilinear slow mostly removes the trade-off”

Instead of the single cell under the pilot’s centre, bilinearly interpolate the 4 nearest cell values. The slow then ramps smoothly across a web edge rather than stepping at cell boundaries. At 24-32 px that feels close to today’s continuous circles, so cells can stay coarse (cheap texture) without blocky gameplay. The cost is 4 reads instead of 1, still O(1), still integer-exact inputs. Recommended. If Chris prefers a hard edge, single-cell sampling is a one-line change.

Slow mapping (Chris’s call: changes feel)

Section titled “Slow mapping (Chris’s call: changes feel)”

Today a passive trail is a single non-stacking ×0.42 (WEB_SLOW_PER 0.6 ^ TRAIL_SLOW_WEIGHT 1.7), tuned to “keep today’s feel”. Elite Spider webs stack ×0.6 each, floored at WEB_SLOW_MIN 0.15. A web is full strength until it vanishes.

The grid changes two things:

  1. Overlapping trails stack (value 2-5 where spiders cross or retrace).
  2. The slow fades as a web decays (value 1 → 0 over 10 s).

Starting mapping, all tunables: slow = max(WEB_SLOW_MIN, WEB_SLOW_PER ^ (WEIGHT * value)) with WEIGHT 1.7 (a fresh single trail = today’s 0.42). Optional WEB_FULL_UNTIL to hold full strength until value < 1 if the fade feels too soft. Feel-test on device through the console before persisting.

Unchanged: only pilots are slowed (PlayerState.move_mult, as today). Enemies ignore webs.

  • sim/elemental_system.gd: drop_webs, add_web_capsule, update_webs (deleted), web_slow_mult, _near_existing_web (deleted), TRAIL_* consts.
  • sim/enemy_behaviors.gd: Elite Spider landing pad, dash corridor and rays become capsule deposits.
  • sim/sim.gd: webs array becomes a WebGrid (new extends RefCounted in /sim); reset at run start and on area change.
  • render/zone_renderer.gd: web layer, WEB_LOOK_MATS, webs-lite LOD path (deleted), and the PerfCuts.webs_lite toggle and its console row (retired).
  • render/perf_rig.gd hold_webs and the perf scenario’s webs arm: deposit a fixed pattern instead.
  • Telemetry samples.webs: becomes the active-cell count (document the meaning change in docs/architecture/dev-tools.md).
  • Tests: rewrite tests/test_web_trails.gd. The vfx_library entry and score need updating (tests/test_vfx_library_manifest.gd).
  • Determinism: if any pinned baseline lays a web, re-pin via skill bh-determinism-repin.
  • New tunables (registry + control/src/tunable_docs.js): CELL_PX (run start), MAX, TICKS_PER_UNIT, WEIGHT, and the existing WEB_SLOW_PER / WEB_SLOW_MIN.
  1. Story/room areas bound play by room walls, not ARENA_HALF. Size the grid per area bounds, or one arena-sized grid everywhere?
  2. Co-op: the grid is shared sim state, so no per-pilot issue. Confirm the networked snapshot path serializes a PackedInt32Array cheaply (62.5k × 4 B is too big to send whole; it is deterministic lockstep, so it should never need sending).
  3. MAX: 5 per Toby. Any value ≥ 1 costs the same with the zero-crossing encoding.
  • Headless: slow at a given point matches the mapping; deposits are capped at MAX; value reaches 0 exactly TICKS_PER_UNIT × value ticks after the last deposit; no per-tick loop over cells.
  • On ATV (console perf scenario, webs arm): frame and sim ms flat from 0 to 400-web-equivalent coverage; texture upload ms measured at the chosen cell size.
  • Chris feel-test of the slow mapping and the new silk look, with the old system still behind a toggle for A/B until approved.