create-state-machine
Skill2 starsChanged 6 months agoMITView on GitHub
Scaffold a node-based finite state machine — StateMachine parent + State child template. Each state is a node, transitions are signals. Inspector-tweakable, testable in isolation.
In this repository33 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- bootstrap-godot-projectSkill
- codebase-surveySkill
- create-autoloadSkill
- create-componentSkill
- create-resourceSkill
- create-sceneSkill
- export-configSkill
- feature-planSkill
- feature-specSkill
- game-brainstormingSkill
- gdd-writerSkill
- genre-pack-3d-actionSkill
- genre-pack-platformerSkill
- genre-pack-topdownSkill
- genre-pack-turnbasedSkill
- godot-patternsSkill
- gut-test-writerSkill
- networking-foundationSkill
- performance-auditSkill
- save-schema-migrationSkill
- setup-collision-layersSkill
- setup-git-godotSkill
- setup-input-mapSkill
- setup-localizationSkill
- setup-save-systemSkill
- sfx-generatorSkill
- shader-writerSkill
- subagent-dev-modeSkill
- ui-patterns-godotSkill
- update-docsSkill
- using-godot-superpowersSkill
- writing-game-planSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 0 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 188
Use addon-curator agent to install one.
- Very specific: line 209
Transitions during enter(): don't call change_state from inside enter() — it can cause re-entry.
- Very specific: line 212
AnimationPlayer transitions: prefer driving animations from enter() so re-entering a state replays the animation.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Create State Machine
Generate a node-based FSM. Compared to class-based / dictionary-based FSMs:
✅ Each state is a Node — visible in scene tree, Inspector-tweakable ✅ States can have their own children (timers, animation refs) ✅ Transitions go through a single signal — easy to log/debug ✅ Substituting a state for testing = swap a node
Files
scripts/state_machine/state_machine.gd
class_name StateMachine
extends Node
## Holds State child nodes; forwards engine callbacks to current state.
## States transition by emitting `transitioned(next_state_name)`.
signal state_changed(old: StringName, new: StringName)
@export var initial_state: NodePath
@export var debug: bool = false
var current_state: State
var states: Dictionary[StringName, State] = {}
func _ready() -> void:
for child in get_children():
if child is State:
states[StringName(child.name)] = child
child.transitioned.connect(_on_state_transitioned)
if initial_state:
current_state = get_node(initial_state) as State
elif not states.is_empty():
current_state = states.values()[0]
if current_state:
current_state.enter({})
func _process(delta: float) -> void:
if current_state:
current_state.update(delta)
func _physics_process(delta: float) -> void:
if current_state:
current_state.physics_update(delta)
func _unhandled_input(event: InputEvent) -> void:
if current_state:
current_state.handle_input(event)
func change_state(target: StringName, payload: Dictionary = {}) -> void:
if not states.has(target):
push_warning("StateMachine: missing state '%s'" % target)
return
var old := StringName(current_state.name) if current_state else &""
if current_state:
current_state.exit()
current_state = states[target]
if debug:
print("[FSM] %s -> %s" % [old, target])
state_changed.emit(old, target)
current_state.enter(payload)
func _on_state_transitioned(target: StringName, payload: Dictionary) -> void:
change_state(target, payload)scripts/state_machine/state.gd
class_name State
extends Node
## Base class. Override the lifecycle callbacks.
## To transition: emit `transitioned(target_state_name, optional_payload_dict)`.
signal transitioned(target: StringName, payload: Dictionary)
func enter(_payload: Dictionary) -> void:
pass
func exit() -> void:
pass
func update(_delta: float) -> void:
pass
func physics_update(_delta: float) -> void:
pass
func handle_input(_event: InputEvent) -> void:
passConcrete state example
scripts/states/player_idle.gd:
class_name PlayerIdleState
extends State
@export var move_threshold: float = 0.05
func enter(_payload: Dictionary) -> void:
var player := owner as CharacterBody2D
var anim := player.get_node("AnimationPlayer") as AnimationPlayer
anim.play("idle")
player.velocity.x = 0.0
func physics_update(_delta: float) -> void:
var input_x := Input.get_axis("move_left", "move_right")
if absf(input_x) > move_threshold:
transitioned.emit(&"Run", {})
if Input.is_action_just_pressed("jump") and (owner as CharacterBody2D).is_on_floor():
transitioned.emit(&"Jump", {})
if not (owner as CharacterBody2D).is_on_floor():
transitioned.emit(&"Fall", {})scripts/states/player_run.gd:
class_name PlayerRunState
extends State
@export var max_speed: float = 250.0
@export var acceleration: float = 1500.0
func enter(_payload: Dictionary) -> void:
(owner.get_node("AnimationPlayer") as AnimationPlayer).play("run")
func physics_update(delta: float) -> void:
var player := owner as CharacterBody2D
var input_x := Input.get_axis("move_left", "move_right")
if absf(input_x) < 0.05:
transitioned.emit(&"Idle", {})
return
player.velocity.x = move_toward(player.velocity.x, input_x * max_speed, acceleration * delta)
if Input.is_action_just_pressed("jump") and player.is_on_floor():
transitioned.emit(&"Jump", {})
if not player.is_on_floor():
transitioned.emit(&"Fall", {})Scene structure
Player (CharacterBody2D)
├── ...other components...
└── StateMachine (StateMachine, initial_state = "Idle")
├── Idle (State, script: player_idle.gd)
├── Run (State, script: player_run.gd)
├── Jump (State, script: player_jump.gd)
└── Fall (State, script: player_fall.gd)Names of child nodes (Idle, Run, etc.) are the keys used in change_state(&"Idle").
Hierarchical / parallel state machines
For complex characters (e.g. ground state machine + weapon state machine running in parallel), nest:
Player
└── StateMachines (Node)
├── Movement (StateMachine)
│ ├── Idle / Run / Jump / Fall
└── Combat (StateMachine)
├── Unarmed / Holding / Aiming / AttackingEach StateMachine runs independently; states emit signals that the other machine listens to (e.g. movement's Hit state pauses combat).
Behavior tree alternative
For AI with many fallback behaviors (search → patrol → idle), a state machine becomes spaghetti. Use a behavior tree:
Use addon-curator agent to install one.
Testing a state machine
extends GutTest
const PlayerScene := preload("res://scenes/player/player.tscn")
func test_idle_to_run_on_input() -> void:
var player := add_child_autofree(PlayerScene.instantiate())
var sm := player.get_node("StateMachine") as StateMachine
assert_eq(StringName(sm.current_state.name), &"Idle")
Input.action_press("move_right")
await get_tree().physics_frame
assert_eq(StringName(sm.current_state.name), &"Run")
Input.action_release("move_right")Common pitfalls
- Transitions during enter(): don't call
change_statefrom insideenter()— it can cause re-entry. Defer withcall_deferred. - State node names must be unique within the StateMachine — they're the dictionary keys.
ownerin aStatescript is the scene's owner (the entity), not the StateMachine. Useful for accessing siblings.- AnimationPlayer transitions: prefer driving animations from
enter()so re-entering a state replays the animation; otherwise useAnimationTreestate machine.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

