doc 5 of 5

Part 04 — Physics and areas

You will build: the body-vs-area cost model, the mask as a performance control, the monitoring-off target, and the one-area consolidation the survivors stretch measures. You'll learn: why the physics row is the row that amplifies · the broadphase's pair count · and the honest boundary — the physics question with no paper answer.

Why this exists#

Part 02's table named the physics row as the one that amplifies expensively — the row you don't see in your code because it's not your loop. This part is that row, opened up: the cost model for why a body is more than an area, why the mask is a performance control (not just a correctness one), and where the physics cost lives (the broadphase, the pair count) so the fix goes to the pair count and not the float ops. The survivors game's decision 1 (fake velocity, stage 02) is the model's worked example — guessed in stage 02, measured in stage 08 — and the 3D farm's ground is its quiet client (one body, one box, the cost model at count one).

Build it#

A — The body vs the area, in cost#

A CharacterBody2D/CharacterBody3D is an area plus a physics-server entry plus a collision response. The three, and what each costs:

  • The area (the Area2D/Area3D base) — a broadphase entry: the physics server's spatial index knows the shape exists, and every frame it tests the shape against the shapes its mask connects to. This is the cost every physics entity pays.
  • The server entry (the body's mass, velocity, the solver's state) — the body is in the dynamics solve: the server integrates its velocity, resolves its collisions, and applies the response. A body that's moving (the survivor's golem, if it were a body) is in the solve every tick; a body that's static (the 3D farm's ground, a StaticBody3D) is in the broadphase but not the dynamics solve — the static body is the area's cost without the server entry's.
  • The collision response (move_and_slide's push-out, the body-vs-body resolution) — the work that makes bodies push each other, and the work the survivor's decision 1 bought out of. The golem is an Area2D with fake velocity: no server entry, no response, the broadphase cost only. 300 golems as bodies is 300 server entries and 300×300/2 potential body-vs-body resolutions in the solve; 300 golems as areas is 300 broadphase entries and zero body-vs-body work (the areas don't resolve against each other — the stage 03 separation nudge is your code, and it's cheap).

The model, stated: a body is an area plus a solve entry plus a response, and the response is the super-linear term. The survivor's decision 1 (the overview's "A is honest physics; B is honest performance") is the model's choice: B buys out the solve and the response for the cost of not pushing (the golems overlap until the nudge separates them, stage 03's honest "this is not the general pile-up fix"). At 300, the buy-out is the difference between a scene that runs and a scene that doesn't — stage 08's measurement, the model's number.

B — The mask, as a performance control#

The survivor's stage 04 layer table is a correctness contract ("a hitbox's mask lists exactly the hurtboxes it may hit") — and it's a performance control for the same reason. The broadphase tests pairs: an area on layer L with mask M is tested against every shape on a layer in M. The pair count is the product of the populations the mask connects:

  • A hitbox masking everything (layers 1, 5, 6, 7, 13, 15, 21, 22) is tested against the floor, the player body, every golem, every drop — 300+ pairs per hitbox per frame, most of them never overlapping (the broadphase tests the potential, the spatial index's candidate pairs, before the narrowphase confirms). The hitbox's mask is doing work for shapes it will never hit.
  • A hitbox masking only layer 15 (the enemy hurtboxes) is tested against the 300 hurtboxes — the pairs it needs, and not the floor or the drops. The mask cut the pair count from "everything" to "the targets," and the cut is the performance.

The rule, from stage 04, as a performance law: a mask is a list of the shapes you may hit, and every layer in the list is a population you're tested against. Add a layer to a mask to make a bug go away (the "mask everything and see if it works" reflex) and you've added a population to the pair count — the bug's fix is a cost, and the cost is silent (the broadphase doesn't warn you it's testing 300 extra pairs). The mask is the one physics setting that's both a design fact (who may hit whom) and a performance fact (how many pairs the server tests), and the two facts are the same list. Audit your masks the way part 01 audits its lines: which populations does this mask connect me to, and do I need each one?

C — monitoring off: the target costs nothing to be seen#

The survivor's hurtboxes (stage 04) have monitoring = false — they never initiate an overlap test, they only appear in other areas' get_overlapping_areas(). The cost model: an area with monitoring on runs its own overlap tests every frame (it's a querier); one with monitoring off is queried (it's in the broadphase, found by others' tests, but runs no tests of its own). A target — a hurtbox, a drop, a pickup zone — is queried, not a querier, and monitoring off is the target's correct setting: it costs nothing to be seen, and the seeing is the other area's job (the hitbox's mask, part B).

The mistake is a target with monitoring on "to be safe" — the hurtbox running its own overlap tests every frame, finding the same shapes the hitbox finds, and costing the pair count twice (once as the hitbox's query, once as the hurtbox's). The "safe" setting is the expensive one, and it's silent, the way the mask-everything is silent. A shape that's only ever hit has monitoring off; a shape that hits or senses has it on. The survivor's PickUpArea (a senser — monitoring on, mask 22) and the hurtboxes (targets — monitoring off) are the two roles, and the role is the setting.

D — Interpolation, and the jitter it's for#

The survivor's project setting has physics_interpolation = true (and the 3D farm's camera has physics_interpolation_mode set). Interpolation is the render side of the physics clock: the physics runs at 60 Hz (part 01's clock), the render runs at the display's rate (144 Hz on a fast monitor), and interpolation renders the entity between physics ticks — a 144 Hz display gets 144 smooth positions from 60 physics ones, instead of 60 positions held for 2–3 frames each (the jitter: the entity steps, the display's extra frames show the step as a stutter).

The cost is the honest one: interpolation stores the previous physics position and lerps, a per-entity per-frame cost — small, but real, and the row part 02's table would list under "float ops, linearly, cheaply." The setting is on in the survivor because the golems' chase is the visible motion (a jittery chase reads as a broken game) and the cost is the cheap row; it would be off (or the display capped at 60) if the jitter weren't visible and the cost were the finding. Part 01's guard, on the interpolation's lerp, is the measurement that decides — the row is cheap, the setting stays; the row is the finding, the display caps.

E — The consolidation: two areas, one#

The survivor's stage 08 stretch is the mask model applied to the entity: the golem has an outer area (the separation sensor, layer 13) and a hurtbox (the target, layer 15) — two areas, two broadphase entries, two sets of pairs. The consolidation: one area, whose mask covers both jobs — the outer area's layer is 13 and 15, and the mask is the player hitbox (6) for the hurtbox job and the enemy (13) for the separation job. The broadphase entry count drops by half (300 golems → 300 areas instead of 600), and the pair count drops with it. The cost is the mask's pair count (part B): the consolidated area is now tested against the enemy population (the separation) and the player hitbox (the damage), and the "exactly the shapes you may hit" contract (stage 04) is now "the shapes you may hit or sense" — wider, and the width is the separation's population. Measure it: the stretch's "two areas per enemy became one; the broadphase entry count dropped by half" is the model's prediction, and part 01's guard on the Physics line is the measurement that confirms or refutes it. (It confirms, usually — the entry count is the dominant term — but the mask's wider pair count is the cost the confirmation names.)

F — The boundary: the question with no paper answer#

The almanac's architecture doc lists three permanent refusals of any in-page simulation, and the first is "anything touching move_and_slide() (it returns a velocity you did not assign)." The refusal is this part's boundary: some physics questions have no paper answer. "What does move_and_slide do when two bodies push a third into a wall?" is a question the engine answers, on the tick, with a velocity the code didn't set — and a diagram or a prose model of the answer is a confident lie (the architecture doc's words), because the answer is the solver's, and the solver is the thing you can only measure, not derive. The paper model (this part) says which costs exist and how they scale; the engine says what the tick does. The boundary is the line between the two: model the cost, measure the behaviour, and never let a prose answer stand in for a tick's.

G — Commit#

git add . && git commit -m "perf 04: body vs area, the mask as control, monitoring off, the consolidation"

Checkpoint — definition of done#

  • The body-vs-area model is stated for your scene: which entities are bodies (the solve entry, the response) and which are areas (the broadphase only), and why (the survivor's decision 1, the 3D farm's ground as a static body) — the cost is the reason, not the habit
  • Every mask is audited: the populations it connects to, and the one you removed (a layer that was "for safety" and cost a population) — the mask is the performance control, part B
  • Every target has monitoring off (the hurtboxes, the drops), and every senser has it on (the hitbox, the PickUpArea) — the role is the setting, part C, and the "safe" monitoring- on target is found and fixed
  • Interpolation's cost is measured (part 01's guard on the lerp) and the setting is decided by the number, not the habit — on because the jitter's visible and the row's cheap, or off because the row's the finding
  • The consolidation (two areas, one) is measured: the model's prediction (half the entries) confirmed or refuted by part 01's guard on the Physics line — the number is the answer, part F's boundary respected
  • You can name the question with no paper answer (the move_and_slide tick, the solver's velocity) and say why the model stops at the cost and the engine owns the behaviour
  • Zero warnings; committed

Stretch (no instructions)#

The sleep: a dynamic body that stops moving sleeps (the physics server drops it from the active solve until a force wakes it) — the 3D farm's dropped item (a future harvest, a crate) is a dynamic body that should sleep at rest. Find the sleep setting (the body's can_sleep, the project's auto sleep threshold), put a body to rest, and measure the Physics line with it sleeping vs awake — the sleep is the cost model's idle case, the row that's there until the entity stops, and the measurement is the saving the sleep earns. (A body that never sleeps is a server entry that never leaves the solve; the sleep is the monitoring-off of the dynamics — the target that costs nothing while at rest.)

If you get stuck#

  • The Physics line is high and the entity count is low → the cost isn't the count, it's the mask (part B's pair count): a few entities with wide masks test against many populations. Audit the masks before the count — the pair product, not the entity sum, is the number.
  • Consolidating two areas into one raised the Physics line → the mask's wider pair count (part E's cost) exceeded the entry-count saving. The model's prediction (half the entries) held for the entries and failed for the pairs — the mask connected the consolidated area to a population (the separation's enemy layer) whose pair count outweighed the entry saving. The fix is the mask, not the consolidation: the separation's population is the one to narrow (a separation layer only the separation needs, part B's "exactly the shapes"), or the consolidation is reversed (the two areas were cheaper than the one wide-masked area).
  • move_and_slide does something the model didn't predict (a body pushed into a wall takes a velocity the code didn't set) → part F's boundary: the solver's answer, not the model's. The model said which costs exist; the tick said what happens. Measure the tick (the velocity, the position, the frame), file it as the engine's behaviour, and don't let the prose model claim it — the confident lie is the failure mode.
  • The jitter is worse with interpolation on → the display rate and the physics rate are in a resonance (a 120 Hz display, 60 Hz physics, the interpolation's lerp landing on every other frame). Cap the display to the physics rate (run/max_fps, the 3D farm's project setting at 60) or the interpolation's cost is buying a resonance, not smoothness. Part 01's measurement, on the render line, names the frame the jitter lands on.