The registered fields, in registration order — which is the order they are summed in, and therefore part of the determinism contract (§33: floating point addition is not associative).
The tracked worlds, in the order their bodies are visited.
Registers field, sampled in units, at the end of the summation order.
The same field object may be registered twice — with different units, or
with the same ones — and is then sampled twice per body per step. Nothing
deduplicates: a field is a pure function (§33), two registrations of one
function are two terms of the sum, and silently collapsing them would make
2 × drag unspellable.
Optionaloptions: ForceFieldAddOptionsfield, so a construction call can be inlined
§39 lifecycle: forgets every world and every field.
The worlds are not disposed — a world outlives the systems that read it
(PhysicsSystem says the same), and this one never owned any of them.
Samples every registered field at every dynamic body of every tracked world and queues the total through §26 (§39 step 5).
for each tracked world, in tracking order:
world.forEachActiveBody: every dynamic, awake body, in registration order (§33)
point = the visit's world-space centre of mass (§25)
velocity = body.linearVelocity, copied (start-of-step value)
total = Σ over fields, in registration order:
sample(point, velocity, simulationTime) × (units === "acceleration" ? mass : 1)
if total ≠ 0: body.applyForce(total)
§26 splits applyForce from applyForceAtPoint, and this is the former:
the force acts at the centre of mass and induces no torque. Sampling at the
point the force acts at is the only choice that keeps a field's answer and
its effect describing the same place — for an off-centre or compound body
(§24) the transform origin can be anywhere, including outside the shape.
PhysicsWorld.forEachActiveBody reads it from the solver, and is also
where the "dynamic and awake" filter and its §22/§32 arguments live.
RigidBody.linearVelocity is refreshed from the solver in the previous
step's publish pass, so a velocity-dependent field (drag, wind) sees the
state the step begins in — the same rule the particle path states, and what
makes a drag field's closed form checkable. The copy costs one vector write
per body and makes §27's "do not mutate velocity" advisory rather than
load-bearing: a badly written field corrupts this system's scratch instead
of the component's mirror of solver state.
By forEachActiveBody, which states the argument: a force on a static or
kinematic body does nothing, and RigidBody.applyForce does not
implicitly wake a sleeper (WP-5.2). A persistent field (gravity, wind)
that visited every sleeper and then called wake() would defeat §32.
The opt-in is per-entry ForceFieldAddOptions.wakesSleepingBodies:
only those fields walk forEachSleepingDynamicBody, and only a non-zero
contribution calls RigidBody.wake(). Two entries that disagree do not
share a visit — the flag is not a system-wide OR.
Nothing is allocated after construction except the batched SoA scratch,
which grows to the next power of two that covers world.size the first
time a ForceField.sampleAll field meets that many registrations
and is then reused. A system with no fields, or with no tracked world,
does no solver call at all: the field list is tested before the worlds
are walked, so registering the system and using it later costs one
array-length comparison per step.
When at least one registered field offers sampleAll, that world's
active bodies are gathered into stride-3 SoA buffers (binary64, so the
packed values equal centerOfMass / linearVelocity bit-for-bit) and
each such field is invoked once per step instead of once per body. A
field without the fast path is still sampled with ForceField.sample, in its own place in the registration order, into
the same newton accumulator — one custom field neither reorders the
sum nor disables batching for its neighbours. Per-body summation order
is unchanged: field 0 then field 1 then field 2, which is why the
force-fields golden does not move.
§39 lifecycle: nothing to prepare.
Removes the first registration of field, preserving the order of the
rest. Returns whether one was found.
Adds world to the end of the visit order, or leaves the order alone if it
is already tracked. Returns the world, so a tracking call can be inlined.
Stops visiting world's bodies. Returns whether it was tracked, so a
teardown path may call it unconditionally. The world itself is untouched:
forces already queued on its bodies are still drained by its next step,
because they are §26 commands like any other.
Applies §27 force fields to every dynamic rigid body of every tracked world, once per fixed step (§39 step 5, §26, §27).
See the module header for the design: why this is a system rather than a step inside
PhysicsWorld, why the cross-package field contract is structural, and why the units are a required argument.