Stage 03 — The Sword
You will build: a sword swing with real active frames, a reusable Hitbox component, and input buffering — the invisible feature every good action game has and no one notices. You'll learn: the Hitbox component · active frames · input buffering — and design decision 3 (your cancel policy) gets its first draft. Warm-up — from memory:
- Draw your state machine as it stands: states, arrows, triggers.
- During DASH, what happens to a dash input, and which line of code decides that?
Why this exists#
An attack is a shape that hurts, briefly. The industry pattern: an Area2D — the hitbox — positioned where the weapon sweeps, switched on for a handful of frames mid-swing, then off. No damage math lives in it; it's a bell that rings "I touched something." Built as its own small scene, the same component becomes the slime's bite in stage 05 unchanged — that reusability is the architecture's whole thesis, starting now.
Buffering is the other half: at speed, players press attack a few frames early — during a dash, during the last swing. Games that drop those inputs feel broken; games that remember the most recent one for ~0.15s feel telepathic.
Build it#
A — Collision layers, named now#
Project settings, 2D physics layer names: world, player, enemy, player_hit,
enemy_hit. The architecture doc states the hitbox/hurtbox layer rule and asks you to work
out why the hurtbox does the looking — do that reasoning now, write one sentence in the
architecture doc, and set this stage's hitbox accordingly: layer player_hit, mask nothing.
B — The Hitbox component#
components/hitbox.tscn: Area2D root + CollisionShape2D, script exporting
@export var data: AttackData (make the AttackData Resource first: damage, knockback
force — numbers you'll tune in stage 04). Give it activate() / deactivate() that toggle
monitoring and the shape's disabled — then, in the running scene, use the Remote tree
to confirm what each actually toggles; the difference will matter someday and today is the
cheap day to see it.
C — The swing#
Player gains ATTACK: a SwordPivot (Node2D) child rotated to facing, the Hitbox
instanced under it, offset ~20px. The state's spine is a timeline, driven by a short Timer
chain or awaited timers — your call:
windup (0.06s, hitbox off) → active (0.10s, hitbox ON) → recovery (0.12s, hitbox off) → RUN
Movement during a swing: frozen, or a heavy slow — decide with your hands. And the first half of decision 3: does dash cancel recovery? Try both for ten minutes; write your current lean in the architecture doc (stage 06 revisits it once hits have weight).
D — The buffer#
One variable and one timestamp: when attack (or dash) is pressed and can't fire, remember it and when; when the current state ends, fire the remembered action if it's fresher than ~0.15s. Test honestly: mash attack during a swing — exactly one follow-up should come out, never two, never zero.
E — Proof of touch#
No health yet — prove the bell rings: connect the hitbox's area_entered/body_entered
to a print, park a plain StaticBody2D dummy in the arena, and swing at it from all four
facings. (It won't print until the dummy can be seen by those masks — make the dummy
detectable in whatever way your layer reasoning says is right. If your answer is "put it
on enemy... but nothing masks enemy yet" — good catch: that's next stage's Hurtbox
arriving. For today, let the hitbox mask enemy temporarily and note the TODO.)
git add . && git commit -m "stage 03: sword swing, hitbox component, input buffer"
Checkpoint — definition of done#
- The swing has three visible phases (tint them) and the hitbox is on only in the middle
- Swings come out toward all four facings, positioned correctly
- Mash-test passes: one buffered follow-up, never two
- The layer sentence is written in the architecture doc, and the hitbox's layer/mask match it
- Zero warnings; committed. Explain: why does the hitbox not deal damage itself — what goes wrong later if it does?
Stretch (no instructions)#
A two-swing combo: attack during recovery chains a second, mirrored swing with slightly different timing. Your buffer already wants to do this.
If you get stuck#
- Prints double per swing → the area is seeing body and area, or your active window toggles twice; log the signal name with the print.
- Hitbox lags a frame behind facing → rotate the pivot when facing changes, not when the swing starts... or is it the reverse you want? (Try attacking mid-turn both ways.)
- Buffer fires stale inputs → you stored the press but not the time; both halves matter.