doc 3 of 5

Part 02 — Knockback and push

You will build: the impulse + damping (the shove that bleeds out, the survivors stage 03 machinery as a feel system) and the composition — the frame where the chase, the crowd, and the hit all push one enemy at once, and the order that makes the hit read. You'll learn: why knockback is a velocity, not a position · the damping rate as the push's personality · the clamp-once rule · and the dead zone the push has to respect (or not).

Why this exists#

The survivors stage 03 installed the machinery — a knockback vector, damped by move_toward toward zero, added to the chase — and stage 04 gave it a pusher (the hitbox's shove). This part is the feel of that machinery: why the shove reads as damage, why the damping rate is a design number, and what happens when three forces share a frame. The steering shelf's three rules (seek, the weight, the limit) are the movement version; this is the impact version, where the limit is the hit and the weight is how hard it lands.

Build it#

A — The impulse, and why it's a velocity#

A knockback is an impulse: a change in velocity, applied once, that the entity then decays. The survivors stage 03 shape, as the feel system:

# in the Enemy (stage 03's machine, the feel reading)
var knockback: Vector2 = Vector2.ZERO
var knockback_resistance := 1   # the damping's divisor — the push's personality

# in _physics_process, beside the chase:
knockback = knockback.move_toward(Vector2.ZERO, 60.0 / knockback_resistance * delta)
fake_velocity += knockback

The three facts that make it read:

  • It's a velocity, added to the movement, not a position set. A knockback that moves the entity (position += shove) teleports — the golem jumps three metres and the player's eye lost it. A knockback that's a velocity (the fake_velocity += knockback) integrates over frames — the golem is pushed, frame by frame, and the eye tracks the push. The teleport-vs-push distinction is the position-vs-velocity one, the same as the audio shelf's fact-vs-promise: the position is the lie, the velocity is the truth.
  • It decays, at a rate. The move_toward toward zero is the damping — the shove bleeds out over frames instead of persisting. A knockback with no decay is a permanent velocity offset (the golem keeps drifting at the shove's speed forever — the "why is it floating" bug); one with too-fast a decay is a twitch (the shove is over before the eye registers it). The decay rate is the push's length, and the length is the feel.
  • The resistance divides the decay. knockback_resistance is the per-species data (the data-driven shelf's part 01, question 2 — per-kind): a golem at resistance 1 decays at 60 px/s², a brute at resistance 3 decays at 20 — the same shove lands hard on the golem and barely registers on the brute. The resistance is the mass, in the data, and the shove's reading (how far it goes) is the mass made visible. A hit that moves every enemy the same distance is a hit the player can't read; one that moves the golem three metres and the brute one is a hit that reports the target.

B — The composition: three forces, one frame#

Now the frame the part is named for: a golem being chased, in a crowd, hit by the sword — three forces on one entity, in one _physics_process. The chase (the stage 03 seek, toward the player), the separation (the stage 03 nudge, off the crowd), and the knockback (the hit's shove, off the player). The naive composition adds them and clamps each — and the clamp is the bug:

Predict before you run: the knockback is 90 px/s, the chase is 40, the separation is 30 (weighted). If you clamp each force to a max_force of 120 before summing, what's the shove's effective strength — and if you clamp the sum instead, what's it worth?

Each force is under 120, so the per-force clamp does nothing this frame — but the principle is the bug: a per-force clamp caps every force at the limit, so a hit's shove (the one force that's supposed to exceed the steady-state forces) is capped at the same value as the chase. The shove's excess — the part that makes it read as a hit — is the part the per-force clamp removes. The clamp-once rule: the forces compose unclamped, and the sum is clamped to max_force at the end. The shove can exceed the chase (its excess is the feel), and the sum's clamp is the speed limit (the golem doesn't leave the scene), not the force limit (the hit doesn't get capped at the chase's value).

The frame, drawn — the golem heading east (toward the player, heading: 0), the chase pulling east, the knockback shoving west (off the player, the hit's direction), the separation pushing south (off the crowd below):

A hit golem, one frame, three forces. 3 steering forces leaving one agent in a single frame. Angles are measured clockwise from the agent's +X axis, and its heading is drawn at 0°. chase 40 × 1.0 = 40 at 0°, knockback 90 × 1.0 = 90 at 180°, separation 30 × 1.5 = 45 at 90°. Their weighted sum is 67 long at 138°, inside max_force 120. With every weight set to 1.0 the same forces resolve to 58, 11 degrees away from the weighted answer.The weighted sum (chase 40 east, knockback 90 west, separation 45 south) resolves to a shove down-and-away — the separation's weight (1.5) tilts the answer off the pure knockback line, so the golem slides *off the crowd* as it's hit, not straight back into it. The dashed "all weights 1.0" arrow is the naive composition: the same forces, no weight, resolving 11 degrees off — the weight is the direction, the steering shelf's part 02 argument, on a hit frame.

Read it the way the eye reads the hit: the knockback (90 west) is the hit, the chase (40 east) is the golem still trying to get to you, and the separation (45 south, weighted) is the crowd. The sum is down-and-away — the golem slides off the hit and off the crowd, and the slide is the readability: a golem hit in a pile that slides through the pile is a golem the eye lost; one that slides off it is a golem the eye tracked through the hit. The separation's weight (1.5) is what buys the "off the crowd" — the steering shelf's part 02 "the weight changes the direction of the answer," on a frame where the direction is the feel.

The composition's order, in code:

var steering := Vector2.ZERO
steering += chase            # the seek, stage 03
steering += separation       # the nudge, stage 03 (weighted)
steering += knockback        # the impulse, part A (decaying)
steering = steering.limit_length(max_force)   # the clamp-once, part B
fake_velocity = steering     # the velocity, integrated, not set

The limit_length is the clamp-once — one limit, on the sum, at the end. The fake_velocity = steering is the velocity (part A's fact) — the golem is steered, not placed.

C — The dead zone, and the push that respects it (or doesn't)#

The survivors stage 03 dead zone: the chase stops at 15 px (the if distance > 15 gate), so a golem at rest is 15 px from the player, not inside the hitbox. Now the push: a hit at the dead zone's edge shoves the golem through the 15 px and back — the knockback is an impulse (part A), and the impulse isn't gated by the chase's dead zone. The golem flies past the player, the chase re-engages on the far side, and the golem comes back.

That's correct, and it's the feel's honesty: the dead zone gates the chase (the steady- state seek), not the forces (the impulses). A push that respected the dead zone (a shove that stopped at 15 px) would be a shove that couldn't shove — the hit's excess (part B's clamp-once, the feel) is the part that crosses the zone, and gating it is gating the feel. The survivors stage 03's stuck-list named it: "the knockback shoves enemies through the dead zone and they resume chasing from inside — correct behaviour! The dead zone only gates the chase term; the impulse is independent." The push and the zone are different layers — the zone is the movement's rest point, the push is the force's transient — and the layering is the steering shelf's "the seek is steady, the forces are events" made concrete on a hit.

D — The push's direction, and the space it's in#

The shove's direction is from the hit, toward the target — the survivors stage 04's hitbox: global_position.direction_to(hurtbox.global_position) * 10.0. The direction is the hit's direction (where the sword came from), not the target's facing (where the golem was looking) — a golem hit from the left shoves right, regardless of which way it faced. The space is the global (both the hitbox's and the hurtbox's global_position), because the shove is a world-direction and a local-direction shove is the stage 03 "orbit instead of chase" bug, wearing a knockback costume. The direction's home is the hitbox (the thing that knows where the hit came from), and the target receives it — the data-driven shelf's "the event has a home, the consequence follows it," the shove following the hit.

E — Commit#

git add . && git commit -m "feel 02: knockback impulse, the clamp-once, the dead zone's layer"

Checkpoint — definition of done#

  • The knockback is a velocity (the fake_velocity += knockback, part A) — a hit pushes the golem over frames, and the eye tracks it; the position-set version (the teleport) is met and refused
  • The decay is at a rate (the move_toward toward zero), and the resistance is per- species data (part A's mass) — the same shove moves the golem far and the brute near, and the difference is the data, not a branch
  • The clamp is once, on the sum (part B) — the shove's excess survives (a hit reads harder than the chase), and the per-force clamp is met and refused (the capped shove, the feel removed)
  • The composition's frame is drawn (the vector-composition) and read the way the eye reads it — the separation's weight buys the "off the crowd," the 11-degree delta is the weight's direction
  • The dead zone gates the chase, not the push (part C) — a hit at the zone's edge shoves through and the golem comes back, and you can say why that's the feel's honesty
  • The shove's direction is the hit's direction, in global space (part D) — a golem hit from the left goes right, faced or not
  • Zero warnings; committed

Stretch (no instructions)#

A push that stacks, with a cap: two hits in the same frame (the survivor's clump, the weapon's double-swing) — the knockbacks add (the second shove is on top of the first's velocity), and the sum's clamp (part B) is the cap. The feel is the stack (a double-hit shoves further than a single, the damage made visible) and the cap is the scene (the golem doesn't leave). Find the cap where the stack reads as impact and the golem stays on screen — the tuning loop, part 04, on the max_force.

If you get stuck#

  • The golem teleports on a hit (the eye loses it) → the knockback is a position set (position += shove), part A's lie. The shove is a velocity (fake_velocity += knockback), integrated over frames. The position is the jump; the velocity is the push.
  • The shove persists (the golem drifts forever after the hit) → the decay is missing (the move_toward toward zero), or the rate is 0 (the knockback_resistance is infinite, part A's divisor). The impulse decays; a shove that doesn't is a velocity offset, not a knockback.
  • Every enemy shoves the same distance (the mass is invisible) → the knockback_resistance is the same on every species (part A's data, question 2, answered "per-instance" when it's per-kind). The resistance is on the stats (the species' definition), and the brute's 3 is the data that makes the same shove read as "that was a brute."
  • The hit caps at the chase's value (the shove never exceeds the seek) → the per-force clamp (part B's bug), the limit_length on each force before the sum. The clamp is once, on the sum; the shove's excess is the feel, and the per-force limit is the feel removed.
  • The golem shoves into the crowd (through the separation) → the separation's weight is 1.0 (the naive composition, the diagram's dashed arrow) — the shove's direction is the pure knockback, straight into the pile. The weight (1.5, part B) buys the "off the crowd"; the steering shelf's part 02 is the argument, the diagram is the frame.