Spider webs as a decaying grid — design
Spider webs as a decaying grid — design
Section titled “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.
The model (Toby’s proposal)
Section titled “The model (Toby’s proposal)”- 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).
Implementation choices
Section titled “Implementation choices”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 svalue(cell) = max(0, Z - now) / TICKS_PER_UNITdeposit(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.
Deposit shape
Section titled “Deposit shape”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.
Rendering
Section titled “Rendering”- One quad covering the visible part of the grid, sampling an
R32Ftexture ofZ. 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_updateis 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:
- Overlapping trails stack (value 2-5 where spiders cross or retrace).
- 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.
Touch list
Section titled “Touch list”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:websarray becomes aWebGrid(newextends RefCountedin/sim); reset at run start and on area change.render/zone_renderer.gd: web layer,WEB_LOOK_MATS, webs-lite LOD path (deleted), and thePerfCuts.webs_litetoggle and its console row (retired).render/perf_rig.gdhold_websand the perf scenario’swebsarm: deposit a fixed pattern instead.- Telemetry
samples.webs: becomes the active-cell count (document the meaning change indocs/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 existingWEB_SLOW_PER/WEB_SLOW_MIN.
Open questions
Section titled “Open questions”- Story/room areas bound play by room walls, not
ARENA_HALF. Size the grid per area bounds, or one arena-sized grid everywhere? - Co-op: the grid is shared sim state, so no per-pilot issue. Confirm the networked snapshot path
serializes a
PackedInt32Arraycheaply (62.5k × 4 B is too big to send whole; it is deterministic lockstep, so it should never need sending). - MAX: 5 per Toby. Any value ≥ 1 costs the same with the zero-crossing encoding.
Acceptance
Section titled “Acceptance”- Headless: slow at a given point matches the mapping; deposits are capped at MAX; value reaches 0
exactly
TICKS_PER_UNIT × valueticks after the last deposit; no per-tick loop over cells. - On ATV (console perf scenario,
websarm): 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.