Action RPG — Architecture
Fast-paced top-down action: dash through attacks, hit hard, get out. Think small-scale Hyper Light Drifter energy at 32×32. The design has one organizing idea: combat is a conversation between components — health, hitboxes, and hurtboxes are reusable pieces that don't know whether they're attached to the player, a slime, or a training dummy. Build them once, compose them everywhere.
Systems map#
| System | Pattern | Why |
|---|---|---|
| Movement | CharacterBody2D with acceleration and friction (velocity.move_toward) | Fast games live or die on feel; instant velocity (the farming approach) feels stiff here — ramps make weight |
| Player states | State machine from day one: RUN / DASH / ATTACK (+HURT later) | Fast-paced means overlapping intentions — dash pressed mid-swing, hit mid-dash. Without a state machine this becomes flag soup by stage 3 |
| Dash | A state with a duration timer, a cooldown timer, and invincibility frames | The defining verb of the genre; also the cleanest possible state-machine lesson — enter, commit, exit |
| Attacks | Hitbox component (Area2D + damage data), switched on for the swing's active frames | Damage dealing as an attachable scene: the sword swing, an enemy's bite, and a trap are the same component with different numbers |
| Taking damage | Hurtbox component (Area2D) + Health component (plain Node with damaged/died signals) | Damage receiving decoupled from everything: the HUD listens to the player's Health, the death poof listens to the slime's — same code |
| Knockback | See decision 1 | The feel of impact; wrong choice makes combat mushy |
| Enemy AI | Per-enemy state machine: IDLE / CHASE / WINDUP / ATTACK / STUNNED — style per decision 2 | The windup state IS the gameplay: readable telegraphs are what make fast combat fair |
| Game feel | Hitstop, screenshake, flash — a small Juice toolkit | "Fast-paced" is mostly perception; these three sell every hit |
| Arena | Wave spawner + score + restart | The smallest shape that makes combat a game; rooms/dungeons come later |
Later modules (designed for, not built): stats & leveling · loot/equipment · dungeon rooms & doors · ranged enemies · a boss · saving · audio.
Scene tree (slice)#
Main (Node2D)
├── Arena (Node2D)
│ ├── Ground (TileMapLayer)
│ ├── Walls (StaticBody2D)
│ ├── Enemies (Node2D) ← spawner fills this
│ └── Player (CharacterBody2D)
│ ├── Sprite2D
│ ├── CollisionShape2D
│ ├── Hurtbox (Area2D) ← the reusable component
│ ├── Health (Node) ← ditto
│ └── SwordPivot (Node2D) ← rotates to facing
│ └── Hitbox (Area2D) ← on only during active frames
├── Camera2D ← with trauma-based shake (stage 06)
└── HUD (CanvasLayer) ← hearts, score
Collision layers (named in project settings; stage 03 is where they first matter)#
world · player · enemy · player_hit (player's outgoing damage) · enemy_hit.
The rule that prevents every friendly-fire bug: hitboxes live on a *_hit layer and mask
nothing; hurtboxes live nowhere and mask the opposing side's hit layer. Work out why
that direction (hurtbox does the looking) before stage 04 — it's checkable in one
experiment.
Data model#
- Definitions:
AttackData extends Resource(damage, knockback force, hitstop ms) — the sword's swing and a slime's lunge are.tresfiles ·EnemyStats(speed, health, detection radius) — three slime variants, zero new code. - Runtime: current health lives in each
Healthnode · score and wave live with the arena · nothing global in the slice.
The design decisions — yours to make#
- Knockback mechanism (stage 04): (A) inject velocity into the victim and let friction bleed it off — simple, composes with movement, can fight the AI's own motion; (B) a dedicated STUNNED/knockback state that owns the body until it ends — more machinery, fully predictable, and the AI's stun comes free. Your call and reason: ______
- Enemy AI style (stage 05): (A) enum +
match, like your player — consistent, one file; (B) node-based: one child node per state — states become editable scenes, per-state code stays tiny, and adding a second enemy type gets cheaper. This is the right moment to try node-based even if you keep enums for the player — comparing them on real code is the lesson. Your call and reason: ______ - Cancel policy (stages 03/06): can dash cancel an attack's active frames? Attack cancel dash? This is your game's identity — strict commitment (Souls-ish) vs fluid cancels (Hades-ish). Play both for ten minutes before deciding; write down which and why. Your call and reason: ______
Build order#
Walking skeleton: an arena you can run and dash around, one dummy you can hit, numbers appearing. Then: components & damage → a real enemy → juice → waves. Every stage ends playable; combat should feel a little better every single sitting.