godot-patterns
Skill2 starsChanged 6 months agoMITView on GitHub
Godot 4.x reference — GDScript style, scene composition, signals, autoloads, common 2D/3D patterns, Tween, Area input, and Godot 3.x to 4.x migration. Auto-loads when working with .gd or .tscn files.
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
- create-state-machineSkill
- export-configSkill
- feature-planSkill
- feature-specSkill
- game-brainstormingSkill
- gdd-writerSkill
- genre-pack-3d-actionSkill
- genre-pack-platformerSkill
- genre-pack-topdownSkill
- genre-pack-turnbasedSkill
- 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
4 rules. About 0 are common, 1 is distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
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
- Distinctive: line 87
Scene: when there's a hierarchy or designer-tweakable values.
- Very specific: line 99
Scene transitions: prefer change_scene_to_packed(packed_scene) over change_scene_to_file(path) (no re-parse).
- Very specific: line 171
Use Tween for gameplay (created on demand, garbage-collected).
- Very specific: line 250
Use gdscript-migrator agent for systematic migration of legacy code.
≈ “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
Godot 4.x Patterns Reference
Source-of-truth: Godot docs and GDScript style guide.
GDScript style essentials
class_name Player
extends CharacterBody2D
## Player controller. Extends docstring at file top is rendered in editor help.
signal health_changed(old: int, new: int)
signal died
enum State { IDLE, RUN, JUMP, FALL }
const MAX_SPEED := 250.0
const JUMP_VELOCITY := -400.0
@export var max_health: int = 100
@export_range(0.0, 1.0) var move_smoothing: float = 0.15
@export var bullet_scene: PackedScene
var current_health: int
var _state: State = State.IDLE
@onready var _sprite: Sprite2D = $Sprite2D
@onready var _hurtbox: Area2D = %Hurtbox # unique name access
func _ready() -> void:
current_health = max_health
_hurtbox.area_entered.connect(_on_hurtbox_area_entered)
func take_damage(amount: int) -> void:
var prev := current_health
current_health = maxi(0, current_health - amount)
health_changed.emit(prev, current_health)
if current_health == 0:
died.emit()
func _on_hurtbox_area_entered(area: Area2D) -> void:
if area.is_in_group("enemy_hitbox"):
take_damage(area.damage)Naming
| Item | Convention | Example |
| File / folder | snake_case | player_controller.gd |
class_name, classes | PascalCase | class_name PlayerController |
| Functions, vars, signals | snake_case | take_damage, health_changed |
| Constants | CONSTANT_CASE | MAX_SPEED |
| Enum type / members | PascalCase / CONSTANT_CASE | enum State { IDLE, RUN } |
| Private | leading _ | _internal_state |
| Node names in scene | PascalCase | PlayerCamera |
| Signal names | past tense | health_changed, enemy_killed |
Member order (style guide)
@tool / @icon → class_name / extends → docstring → signals → enums → consts → static vars → @export vars → vars → @onready vars → _init → _ready → other virtual _* → public methods → private _methods → inner classes.
Two blank lines between functions. Tabs for indent. 100-char soft limit. Double quotes by default.
Type hints (mandatory in 4.x)
@export var max_health: int = 100
@onready var _sprite: Sprite2D = $Sprite2D
var velocity := Vector2.ZERO # inferred type
var enemies: Array[Enemy] = [] # typed array
var stats: Dictionary[String, int] = {} # typed dict (4.4+)
func heal(amount: int) -> void: ...Scene composition
- Composition over inheritance. Build behavior by composing component child nodes (
HealthComponent,HitboxComponent,StateMachine) rather than 3+-deepextendschains. - Scene when there's a hierarchy or designer-tweakable values; script-only Node when behavior is purely imperative; Resource when it's pure data.
- One responsibility per scene. Cross-scene wiring via signals or autoloads, never
get_node("/root/Main/Player")traversal. - Instance with
preload(), notload(), when path is static:
const BulletScene := preload("res://scenes/bullet.tscn")
func shoot() -> void:
var b := BulletScene.instantiate()
add_child(b)- Scene transitions: prefer
change_scene_to_packed(packed_scene)overchange_scene_to_file(path)(no re-parse). Wrap in autoloadSceneManagerwith fade transition.
Signals
# Declaration (typed args!)
signal health_changed(old: int, new: int)
# Emission
health_changed.emit(prev, current)
# Connection (in code, in _ready, never via editor)
button.pressed.connect(_on_button_pressed)
button.pressed.connect(_attack.bind("heavy")) # Callable.bind for partial args
button.pressed.connect(_on_pressed, CONNECT_ONE_SHOT)Decision rule:
| Pattern | Use when |
Direct @onready ref | Stable parent-child or unique-name (%) sibling |
| Local signal | One emitter, N listeners; same scene tree neighborhood |
| EventBus signal | Emitter + listener live in unrelated scenes |
Group + call_group | Many-to-many broadcasts; listeners come and go |
Autoloads
Register in Project Settings → Globals → Autoload. Use the create-autoload skill to scaffold and register at once. Common ones:
| Autoload | Role |
EventBus | Cross-scene signals only |
GameState | Run-scoped data: score, level, flags |
SceneManager | Scene swaps + transitions |
AudioManager | Bus volumes, SFX/music pool |
SaveManager | Serialize/deserialize via Resource |
InputManager | Action remap, device tracking |
SettingsManager | Config (resolution, language, accessibility) |
Don't autoload anything tied to a specific scene's lifecycle, anything heavy in _process, or anything you'd want multiple instances of. Never queue_free() an autoload.
3D input via Area3D / Area2D
func _ready() -> void:
var area: Area3D = $Area3D
area.input_event.connect(_on_area_input_event)
area.mouse_entered.connect(_on_mouse_entered)
func _on_area_input_event(_camera: Camera3D, event: InputEvent, _pos: Vector3,
_normal: Vector3, _shape_idx: int) -> void:
if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT:
clicked.emit(self)For 2D use Area2D.input_event(viewport, event, shape_idx). Set input_pickable = true on the Area.
Tween (gameplay animation)
# Parallel scale + position
var tw := create_tween().set_parallel()
tw.tween_property(self, "scale", Vector3.ONE * 1.2, 0.15)
tw.tween_property(self, "position:y", target_y, 0.25).set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
# Sequence with callback
var seq := create_tween()
seq.tween_property(self, "modulate:a", 0.0, 0.3)
seq.tween_callback(queue_free)Use Tween for gameplay (created on demand, garbage-collected). Use AnimationPlayer for hand-keyframed animations and AnimationTree for blending state machines.
Resources (custom data classes)
# resources/item.gd
class_name Item
extends Resource
@export var id: StringName
@export var display_name: String
@export var icon: Texture2D
@export var max_stack: int = 1
@export_multiline var description: StringThen @export var starting_item: Item lets designers drag a .tres instance in the Inspector. Caveat: .duplicate() resources mutated at runtime — load() returns the same shared instance.
_process vs _physics_process vs _unhandled_input
| Callback | Use for |
_process(delta) | Visuals, UI, camera follow, non-physics interpolation |
_physics_process(delta) | Movement, collision queries, physics-tied AI (fixed timestep) |
_unhandled_input(event) | Input handling (only fires when not consumed by UI) |
_input(event) | Input that must run even before UI handles it |
Disable when idle: set_process(false), set_physics_process(false), or process_mode = PROCESS_MODE_DISABLED.
Pause
get_tree().paused = true
# Per-node behavior:
# PROCESS_MODE_INHERIT (default), PROCESS_MODE_PAUSABLE,
# PROCESS_MODE_WHEN_PAUSED, PROCESS_MODE_ALWAYS, PROCESS_MODE_DISABLED
$PauseMenu.process_mode = Node.PROCESS_MODE_WHEN_PAUSED
$Music.process_mode = Node.PROCESS_MODE_ALWAYSProject structure (recommended)
project.godot
addons/ # plugins (gut, dialogic, phantom_camera, ...)
autoload/ # singletons (event_bus.gd, game_state.gd)
scenes/ # .tscn grouped by feature
player/
enemies/
ui/
levels/
scripts/ # reusable scripts not bound to one scene
components/ # health_component.gd, hurtbox.gd
state_machine/
resources/ # .tres data (items/, abilities/)
assets/ # raw art/audio (sprites/, audio/, models/)
test/ # GUT tests
shaders/ # .gdshader files
.gdignore # in folders Godot should not importGodot 3.x → 4.x cheatsheet
| Wrong (3.x) | Right (4.x) |
yield(timer, "timeout") | await timer.timeout |
export var x = 5 | @export var x: int = 5 |
onready var n = $N | @onready var n: Node = $N |
connect("signal", obj, "method") | signal_name.connect(callable) |
instance() | instantiate() |
Spatial | Node3D |
KinematicBody2D + move_and_slide(velocity) | CharacterBody2D; set velocity; move_and_slide() no args |
PoolStringArray | PackedStringArray |
rand_range(a, b) | randf_range(a, b) |
tool | @tool |
Reference | RefCounted |
change_scene(path) | change_scene_to_file(path) or better change_scene_to_packed(packed) |
Use gdscript-migrator agent for systematic migration of legacy code.
Anti-patterns to avoid
get_node()/find_child()in_process— cache via@onready.- Movement in
_process— use_physics_process. - Polling
Input.is_action_pressedin_processfor one-shot actions — use_unhandled_inputandevent.is_action_pressed. - Cross-scene traversal
get_node("/root/Main/Player")— use unique names, autoload, or signals. - Untyped
Array/Dictionaryfor homogeneous data — useArray[T]/Dictionary[K, V]. Dictionaryliterals for game data — use customResource.- JSON for full save state — loses
Vector2/Color/typed objects. UseResourcesave. extendschains 3+ deep — compose with child component nodes.- Connecting a signal in both editor and code — pick one (code preferred).
- Forgetting
process_modeon UI/menu — pause kills the menu too. - Mutating shared
Resourceinstances — call.duplicate()for per-instance state. Array.append()repeatedly in hot loops — preallocate or reuse.instantiate()mid-frame in a hot loop — pool instead (seegenre-pack-*skills).
See also
create-component,create-state-machine,create-resource— scaffolding skillssetup-collision-layers,setup-input-map,setup-save-system— system setupcode-revieweragent — full GDScript review against this reference
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.

