| Engine | Godot 4.2 or newer |
|---|---|
| Language | GDScript |
| Time to run | ~5 minutes |
| Level | Intermediate — comfortable with node hierarchies and inheritance |
| Output | 5 files (Player + 4 states), ~150 lines total |
| Dependencies | None. No plugins. |
The prompt
Open a fresh chat at claude.ai/new and paste this in. Don't add anything before or after.
$ Build me a finite-state-machine player controller for Godot 4.2+ in GDScript. // shape - Player is a CharacterBody2D with child node "State" (Node) holding state scripts. - One file per state: State_Idle.gd, State_Run.gd, State_Jump.gd, State_Fall.gd. - A small base class State.gd that defines: - enter(player), exit(player), update(player, delta), physics_update(player, delta) - Player.gd holds the current_state, calls its update + physics_update each frame, and exposes a change_state(name) method. // behavior - Idle: zero velocity. Transitions to Run on horizontal input, Jump on jump pressed, Fall if not on floor. - Run: applies horizontal velocity, flips sprite. Same transitions as Idle. - Jump: applies upward velocity once on enter. Transitions to Fall when velocity.y > 0. - Fall: gravity only. Transitions to Idle on landing (is_on_floor() and no input), Run on landing with input. // debug - A simple Label child that shows the current state name. Update it in change_state. // physics - Use move_and_slide(). Read input in physics_update. - Constants at the top of Player.gd: SPEED = 200, JUMP_VELOCITY = -400, GRAVITY = 980. // return format - 5 files, in separate code blocks, with file paths as headers (e.g. # res://player/Player.gd). - No prose before or after the code blocks.
What this gets you
A platformer-style player that's structured the way you'd actually want to maintain it. Each state is its own file, so adding a Dash state means writing one new file and adding one transition line, not unscrambling a 200-line _physics_process.
This is the structure used by most mid-to-large indie Godot games. If you're past the "single script throws everything into _physics_process" stage, this is the next move.
What good output looks like
If Claude got it right, the output should pass these checks before you paste it:
- States inherit from a common base. Look for
extends Stateat the top of each State_*.gd file. Without inheritance you can't swap states polymorphically. - change_state lives on Player.gd, not on individual states. States should request a transition by calling
player.change_state("Run"), not by reaching across to siblings. - enter() runs setup, exit() runs cleanup. If the Jump state applies its upward velocity in
physics_update, you'll get infinite jumps. Velocity should be applied once in enter(). - Gravity is applied in Fall and Jump, not Idle and Run. If gravity runs in every state, the player will sink through the floor on Idle.
- The Label updates as the state changes. Easiest sanity check that the FSM is actually transitioning.
- No
matchon state strings inside Player.gd. If Claude wrotematch current_state: "idle": ...it didn't actually build a state machine, just renamed if-statements. Ask it to refactor.
How to wire it up
- Save the 5 files at the paths Claude returned (typically
res://player/). - Create a scene with this hierarchy:
Player (CharacterBody2D)→Sprite2D,CollisionShape2D,State (Node)→ 4 child State nodes (one per state script),Label. - Attach
Player.gdto the root, and one State_*.gd to each of the 4 child State nodes. - In the editor, drag the State node onto Player.gd's exported
state_rootfield. - Add an Input Map action called
jumpbound to space. - Run the scene. The Label should read "Idle". Press D to enter Run, space to enter Jump.
Common gotchas
The player snaps back to Idle every frame
This happens when transition logic in Idle.update() doesn't actually break after calling change_state. In GDScript, change_state won't halt the rest of update() unless you return right after it. Add an early return.
Jump triggers, but the player doesn't actually rise
Almost always because velocity.y is being overwritten by gravity in Jump's physics_update before move_and_slide is called. Make sure gravity is only applied in Fall, and Jump should transition to Fall as soon as velocity.y crosses zero.
The state machine works but feels stiff
Add a coyote-time grace window in Fall (e.g. 0.1s where Jump is still allowed even though the player technically left the floor). This is the single biggest "feel" upgrade for a platformer and worth a follow-up prompt.
Where to go next
- Dash state. One new file: enter() applies a velocity burst, physics_update counts down a timer, transitions to Fall when done.
- Wall slide / wall jump. Two new states (WallSlide, WallJump), plus is_on_wall() checks in Fall.
- Hurt / Dead. Add a global event bus (
autoload Events) so any damage source can request a state change without coupling to player. - Hierarchical FSM. Once you have 8+ states, group related ones (Grounded → Idle/Run, Airborne → Jump/Fall) so transitions can be shared.
Why this prompt is shaped this way
Three small choices that make this prompt land more reliably:
- Specifying the file structure up front. Without "5 files in separate code blocks with file paths as headers", Claude tends to dump everything into one file or wrap it in a markdown narrative. Telling it the output format saves you a follow-up.
- Naming the constants.
SPEED = 200, GRAVITY = 980keeps Claude from picking weird defaults that feel either floaty or rigid. - Calling out the bad pattern. Saying "no
matchon state strings" preempts the most common shortcut Claude takes when asked for a state machine.
Use it however you want
The prompt is CC0 — free to remix, no attribution. The output Claude returns is yours. If it helped, a link to pixeldex.app in your jam build's credits keeps the lights on.