
Godot
- 92 installs
- 52 repo stars
- Updated March 4, 2026
- bfollington/terma
godot is a Claude Code skill that provides Godot Engine file-format knowledge, architecture patterns, and validation tools for game development.
About
godot is a Claude Code skill for developing games with Godot Engine. It gives file-format expertise for .gd, .tscn, and .tres files, architecture patterns like component-based and signal-driven design, and validation scripts. A developer uses it when implementing game systems, debugging scene or resource load errors, or creating Godot components.
- Explains Godot .gd, .tscn, and .tres file formats and their strict rules
- Provides component-based and signal-driven architecture patterns
- Includes validate_tres.py and validate_tscn.py validation scripts
Godot by the numbers
- 92 all-time installs (skills.sh)
- Ranked #134 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
godot capabilities & compatibility
- Capabilities
- debugging · refactoring
- Use cases
- debugging · refactoring
What godot says it does
This skill should be used when working on Godot Engine projects.
Godot projects use a mix of GDScript code files (.gd) and text-based resource files (.tscn for scenes, .tres for resources).
python3 scripts/validate_tres.py resources/spells/fireball.tres
npx skills add https://github.com/bfollington/terma --skill godotAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 92 |
|---|---|
| repo stars | ★ 52 |
| Last updated | March 4, 2026 |
| Repository | bfollington/terma ↗ |
What it does
Implement Godot game systems and fix scene/resource file errors with format-aware guidance.
Who is it for?
Editing Godot .tscn/.tres files and implementing component-based game systems.
Skip if: Interactively driving a running Godot game (see godot-interactive for that).
When should I use this skill?
You are working on a Godot project or debugging a scene/resource load error.
What you get
Valid Godot scene and resource files plus clean component-based, signal-driven game systems.
- Godot scenes
- resource files
- GDScript components
By the numbers
- Bundles 2 validation scripts (validate_tres.py, validate_tscn.py)
Files
Godot Engine Development Skill
Specialized guidance for developing games and applications with Godot Engine, with emphasis on effective collaboration between LLM coding assistants and Godot's unique file structure.
Overview
Godot projects use a mix of GDScript code files (.gd) and text-based resource files (.tscn for scenes, .tres for resources). While GDScript is straightforward, the resource files have strict formatting requirements that differ significantly from GDScript syntax. This skill provides file format expertise, proven architecture patterns, validation tools, code templates, and debugging workflows to enable effective development of Godot projects.
When to Use This Skill
Invoke this skill when:
- Working on any Godot Engine project
- Creating or modifying .tscn (scene) or .tres (resource) files
- Implementing game systems (interactions, attributes, spells, inventory, etc.)
- Debugging "file failed to load" or similar resource errors
- Setting up component-based architectures
- Creating signal-driven systems
- Implementing resource-based data (items, spells, abilities)
Key Principles
1. Understand File Format Differences
GDScript (.gd) - Full Programming Language:
extends Node
class_name MyClass
var speed: float = 5.0
const MAX_HEALTH = 100
func _ready():
print("Ready")Scene Files (.tscn) - Strict Serialization Format:
[ext_resource type="Script" path="res://script.gd" id="1"]
[node name="Player" type="CharacterBody3D"]
script = ExtResource("1") # NOT preload()!Resource Files (.tres) - NO GDScript Syntax:
[ext_resource type="Script" path="res://item.gd" id="1"]
[resource]
script = ExtResource("1") # NOT preload()!
item_name = "Sword" # NOT var item_name = "Sword"!2. Critical Rules for .tres and .tscn Files
NEVER use in .tres/.tscn files:
preload()- UseExtResource("id")insteadvar,const,func- These are GDScript keywords- Untyped arrays - Use
Array[Type]([...])syntax
ALWAYS use in .tres/.tscn files:
ExtResource("id")for external resourcesSubResource("id")for inline resources- Typed arrays:
Array[Resource]([...]) - Proper ExtResource declarations before use
3. Separation of Concerns
Keep logic in .gd files, data in .tres files:
src/
spells/
spell_resource.gd # Class definition + logic
spell_effect.gd # Effect logic
resources/
spells/
fireball.tres # Data only, references scripts
ice_spike.tres # Data onlyThis makes LLM editing much safer and clearer.
4. Component-Based Architecture
Break functionality into small, focused components:
Player (CharacterBody3D)
├─ HealthAttribute (Node) # Component
├─ ManaAttribute (Node) # Component
├─ Inventory (Node) # Component
└─ StateMachine (Node) # Component
├─ IdleState (Node)
├─ MoveState (Node)
└─ AttackState (Node)Benefits:
- Each component is a small, focused file
- Easy to understand and modify
- Clear responsibilities
- Reusable across different entities
5. Signal-Driven Communication
Use signals for loose coupling:
# Component emits signals
signal health_changed(current, max)
signal death()
# Parent connects to signals
func _ready():
$HealthAttribute.health_changed.connect(_on_health_changed)
$HealthAttribute.death.connect(_on_death)Benefits:
- No tight coupling between systems
- Easy to add new listeners
- Self-documenting (signals show available events)
- UI can connect without modifying game logic
Using Bundled Resources
Validation Scripts
Validate .tres and .tscn files before testing in Godot to catch syntax errors early.
Validate .tres file:
python3 scripts/validate_tres.py resources/spells/fireball.tresValidate .tscn file:
python3 scripts/validate_tscn.py scenes/player/player.tscnUse these scripts when:
- After creating or editing .tres/.tscn files programmatically
- When debugging "failed to load" errors
- Before committing scene/resource changes
- When user reports issues with custom resources
Reference Documentation
Load reference files when needed for detailed information:
`references/file-formats.md` - Deep dive into .gd, .tscn, .tres syntax:
- Complete syntax rules for each file type
- Common mistakes with examples
- Safe vs risky editing patterns
- ExtResource and SubResource usage
`references/architecture-patterns.md` - Proven architectural patterns:
- Component-based interaction system
- Attribute system (health, mana, etc.)
- Resource-based effect system (spells, items)
- Inventory system
- State machine pattern
- Examples of combining patterns
Read these references when:
- Implementing new game systems
- Unsure about .tres/.tscn syntax
- Debugging file format errors
- Planning architecture for new features
Code Templates
Use templates as starting points for common patterns. Templates are in assets/templates/:
`component_template.gd` - Base component with signals, exports, activation:
# Copy and customize for new components
cp assets/templates/component_template.gd src/components/my_component.gd`attribute_template.gd` - Numeric attribute (health, mana, stamina):
# Use for any numeric attribute with min/max
cp assets/templates/attribute_template.gd src/attributes/stamina_attribute.gd`interaction_template.gd` - Interaction component base class:
# Extend for custom interactions (pickup, door, switch, etc.)
cp assets/templates/interaction_template.gd src/interactions/lever_interaction.gd`spell_resource.tres` - Example spell with effects:
# Use as reference for creating new spell data
cat assets/templates/spell_resource.tres`item_resource.tres` - Example item resource:
# Use as reference for creating new item data
cat assets/templates/item_resource.tresWorkflows
Workflow 1: Creating a New Component System
Example: Adding a health system to enemies.
Steps:
1. Read architecture patterns reference:
# Check for similar patterns
Read references/architecture-patterns.md
# Look for "Attribute System" section2. Create base class using template:
cp assets/templates/attribute_template.gd src/attributes/attribute.gd
# Customize the base class3. Create specialized subclass:
# Create health_attribute.gd extending attribute.gd
# Add health-specific signals (damage_taken, death)4. Add to scene via .tscn edit:
[ext_resource type="Script" path="res://src/attributes/health_attribute.gd" id="4_health"]
[node name="HealthAttribute" type="Node" parent="Enemy"]
script = ExtResource("4_health")
value_max = 50.0
value_start = 50.05. Test immediately in Godot editor
6. If issues, validate the scene file:
python3 scripts/validate_tscn.py scenes/enemies/base_enemy.tscnWorkflow 2: Creating Resource Data Files (.tres)
Example: Creating a new spell.
Steps:
1. Reference the template:
cat assets/templates/spell_resource.tres2. Create new .tres file with proper structure:
[gd_resource type="Resource" script_class="SpellResource" load_steps=3 format=3]
[ext_resource type="Script" path="res://src/spells/spell_resource.gd" id="1"]
[ext_resource type="Script" path="res://src/spells/spell_effect.gd" id="2"]
[sub_resource type="Resource" id="Effect_1"]
script = ExtResource("2")
effect_type = 0
magnitude_min = 15.0
magnitude_max = 25.0
[resource]
script = ExtResource("1")
spell_name = "Fireball"
spell_id = "fireball"
mana_cost = 25.0
effects = Array[ExtResource("2")]([SubResource("Effect_1")])3. Validate before testing:
python3 scripts/validate_tres.py resources/spells/fireball.tres4. Fix any errors reported by validator
5. Test in Godot editor
Workflow 3: Debugging Resource Loading Issues
When user reports "resource failed to load" or similar errors.
Steps:
1. Read the file reported in error:
# Check file syntax
Read resources/spells/problem_spell.tres2. Run validation script:
python3 scripts/validate_tres.py resources/spells/problem_spell.tres3. Check for common mistakes:
- Using
preload()instead ofExtResource() - Using
var,const,funckeywords - Missing ExtResource declarations
- Incorrect array syntax (not typed)
4. Read file format reference if needed:
Read references/file-formats.md
# Focus on "Resource Files (.tres)" section
# Check "Common Mistakes Reference"5. Fix errors and re-validate
Workflow 4: Implementing from Architecture Patterns
When implementing a known pattern (interaction system, state machine, etc.).
Steps:
1. Read the relevant pattern:
Read references/architecture-patterns.md
# Find the specific pattern (e.g., "Component-Based Interaction System")2. Copy relevant template:
cp assets/templates/interaction_template.gd src/interactions/door_interaction.gd3. Customize the template:
- Override
_perform_interaction() - Add custom exports for configuration
- Add custom signals if needed
4. Create scene structure following pattern:
[node name="Door" type="StaticBody3D"]
script = ExtResource("base_interactable.gd")
[node name="DoorInteraction" type="Node" parent="."]
script = ExtResource("door_interaction.gd")
interaction_text = "Open Door"5. Test incrementally
Unit Testing with GUT
Use GUT (Godot Unit Testing) for testing pure logic — any RefCounted or Resource class that doesn't depend on the scene tree is a good candidate. Card systems, scoring, state machines, map generators, challenge logic, etc.
Installation
1. Download from GitHub releases (v9.5.0 works with Godot 4.5; v9.6.0+ requires newer Godot) 2. Place addons/gut/ in your project 3. Enable plugin in Project Settings → Plugins 4. Create .gutconfig.json at project root:
{
"dirs": ["res://test/unit"],
"prefix": "test_",
"suffix": ".gd",
"log_level": 1
}Writing Tests
Tests live in test/unit/, files prefixed test_, extending GutTest:
extends GutTest
func test_score_calculation() -> void:
var scoring := Scoring.new()
assert_eq(scoring.calculate(3, 0), 100, "Base score for 3 moves")
assert_gt(scoring.calculate(2, 0), scoring.calculate(3, 0), "Fewer moves = higher score")
func test_deck_shuffle() -> void:
var rng := RandomNumberGenerator.new()
rng.seed = 42
var deck := CardSystem.create_shuffled_deck(rng)
assert_eq(deck.size(), 52, "Full deck")Key conventions:
- Test methods must start with
test_ - Helper methods use
_prefix (not discovered as tests) - Use
before_each()/after_each()for setup/teardown - Key asserts:
assert_eq,assert_ne,assert_true,assert_false,assert_gt,assert_lt,assert_null,assert_not_null
Running Tests
# Run all tests (returns exit code 0 on pass, 1 on fail)
godot -d -s --path "$PWD" addons/gut/gut_cmdln.gd -gexit
# Run a specific test file
godot -d -s --path "$PWD" addons/gut/gut_cmdln.gd -gtest=res://test/unit/test_scoring.gd -gexit
# Run tests matching a pattern
godot -d -s --path "$PWD" addons/gut/gut_cmdln.gd -gdir=res://test/unit -gselect=weekly -gexitWhat to Test
Prefer testing pure logic classes that extend RefCounted or Resource:
- Game rules and scoring
- Card/deck/hand management
- Map generation (verify properties, not exact output)
- State snapshot/restore
- Upgrade/progression math
Avoid testing scene-tree-dependent code in unit tests (rendering, UI, input handling).
Common Pitfalls and Solutions
Pitfall 1: Using GDScript Syntax in .tres Files
Problem:
# ❌ WRONG
script = preload("res://script.gd")
var items = [1, 2, 3]Solution:
# ✅ CORRECT
[ext_resource type="Script" path="res://script.gd" id="1"]
script = ExtResource("1")
items = Array[int]([1, 2, 3])Prevention: Run validation script before testing.
Pitfall 2: Missing ExtResource Declarations
Problem:
[resource]
script = ExtResource("1_script") # Not declared!Solution:
[ext_resource type="Script" path="res://script.gd" id="1_script"]
[resource]
script = ExtResource("1_script")Detection: Validation script will catch this.
Pitfall 3: Editing Complex .tscn Hierarchies
Problem: Modifying instanced scene children can break when editor re-saves.
Solution:
- Make only simple property edits in .tscn files
- For complex changes, use Godot editor
- Test immediately after text edits
- Use git to track changes and revert if needed
Pitfall 4: Untyped Arrays in .tres Files
Problem:
effects = [SubResource("Effect_1")] # Missing typeSolution:
effects = Array[Resource]([SubResource("Effect_1")])Prevention: Validation script warns about this.
Pitfall 5: Forgetting Instance Property Overrides
Problem: When instancing a scene, forgetting to override child node properties. The instance uses default values (often null), causing silent bugs.
# level.tscn
[node name="KeyPickup" parent="." instance=ExtResource("6_pickup")]
# Oops! PickupInteraction.item_resource is null - pickup won't work!Solution: Always configure instanced scene properties using the index syntax:
[node name="KeyPickup" parent="." instance=ExtResource("6_pickup")]
[node name="PickupInteraction" parent="KeyPickup" index="0"]
item_resource = ExtResource("7_key")Detection:
- Test the instance in-game immediately
- Read
references/file-formats.md"Instance Property Overrides" section for details - When creating scene instances, ask: "Does this scene have configurable components that need properties set?"
Prevention: After instancing any scene with configurable children (PickupInteraction, DoorInteraction, etc.), always verify critical properties are overridden.
Pitfall 6: CPUParticles3D color_ramp Not Displaying Colors
Problem: Setting color_ramp on CPUParticles3D, but particles still appear white or don't show the gradient colors.
[node name="CPUParticles3D" type="CPUParticles3D" parent="."]
mesh = SubResource("SphereMesh_1")
color_ramp = SubResource("Gradient_1") # Gradient is set but doesn't work!Root Cause: The mesh needs a material with vertex_color_use_as_albedo = true to apply particle colors to the mesh surface.
Solution: Add a StandardMaterial3D to the mesh with vertex color enabled:
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_1"]
vertex_color_use_as_albedo = true
[sub_resource type="SphereMesh" id="SphereMesh_1"]
material = SubResource("StandardMaterial3D_1")
radius = 0.12
height = 0.24
[node name="CPUParticles3D" type="CPUParticles3D" parent="."]
mesh = SubResource("SphereMesh_1")
color_ramp = SubResource("Gradient_1") # Now works!Prevention: When creating CPUParticles3D with color or color_ramp, always add a material with vertex_color_use_as_albedo = true to the mesh.
Best Practices
1. Consult References for Common Issues
When encountering issues, consult the reference documentation:
`references/common-pitfalls.md` - Common Godot gotchas and solutions:
- Initialization and @onready timing issues
- Node reference and get_node() problems
- Signal connection issues
- Resource loading and modification
- CharacterBody3D movement
- Transform and basis confusion
- Input handling
- Type safety issues
- Scene instancing pitfalls
- Tween issues
`references/godot4-physics-api.md` - Physics API quick reference:
- Correct raycast API (
PhysicsRayQueryParameters3D) - Shape queries and collision detection
- Collision layers and masks
- Area3D vs RigidBody3D vs CharacterBody3D
- Common physics patterns
- Performance tips
Load these when:
- Getting null reference errors
- Implementing physics/collision systems
- Debugging timing issues with @onready
- Working with CharacterBody3D movement
- Setting up raycasts or shape queries
2. Always Validate After Editing .tres/.tscn
python3 scripts/validate_tres.py path/to/file.tres
python3 scripts/validate_tscn.py path/to/file.tscn2. Use Templates as Starting Points
Don't write components from scratch - adapt templates:
cp assets/templates/component_template.gd src/my_component.gd3. Read References for Detailed Syntax
When unsure about syntax, load the reference:
Read references/file-formats.md4. Follow Separation of Concerns
- Logic → .gd files
- Data → .tres files
- Scene structure → .tscn files (prefer editor for complex changes)
5. Use Signals for Communication
Prefer signals over direct method calls:
# ✅ Good - Loose coupling
signal item_picked_up(item)
item_picked_up.emit(item)
# ❌ Avoid - Tight coupling
get_parent().get_parent().add_to_inventory(item)6. Test Incrementally
After each change: 1. Validate with scripts 2. Test in Godot editor 3. Verify functionality 4. Commit to git
7. Use Export Variables Liberally
Make configuration visible and editable:
@export_group("Movement")
@export var speed: float = 5.0
@export var jump_force: float = 10.0
@export_group("Combat")
@export var damage: int = 10Using the Godot CLI
The godot command-line tool is available for running the game and performing various operations without opening the editor.
Running the Game
Run the current project:
godot --path . --headlessRun a specific scene:
godot --path . --scene scenes/main_menu.tscnRun with debug flags:
# Show collision shapes
godot --path . --debug-collisions
# Show navigation debug visuals
godot --path . --debug-navigation
# Show path lines
godot --path . --debug-pathsChecking/Validating Code
Check GDScript syntax without running:
godot --path . --check-only --script path/to/script.gdRun headless tests (for automated testing):
godot --path . --headless --quit --script path/to/test_script.gdEditor Operations from CLI
Import resources without opening editor:
godot --path . --import --headless --quitExport project:
# Export release build
godot --path . --export-release "Preset Name" builds/game.exe
# Export debug build
godot --path . --export-debug "Preset Name" builds/game_debug.exeCommon CLI Workflows
Workflow: Quick Test Run
# Run the project and quit after testing
godot --path . --quit-after 300 # Runs for 300 frames then quitsWorkflow: Automated Resource Import
# Import all resources and exit (useful in CI/CD)
godot --path . --import --headless --quitWorkflow: Script Validation
# Validate a GDScript file before committing
godot --path . --check-only --script src/player/player.gdWorkflow: Headless Server
# Run as dedicated server (no rendering)
godot --path . --headless --scene scenes/multiplayer_server.tscnCLI Usage Tips
1. Always specify `--path .` when running from project directory to ensure Godot finds project.godot 2. Use `--headless` for CI/CD and automated testing (no window, no rendering) 3. Use `--quit` or `--quit-after N` to exit automatically after task completion 4. Combine `--check-only` with `--script` to validate GDScript syntax quickly 5. Use debug flags (--debug-collisions, --debug-navigation) to visualize systems during development 6. Check exit codes - Non-zero indicates errors (useful for CI/CD scripts)
Example: Pre-commit Hook for GDScript Validation
#!/bin/bash
# Validate all changed .gd files before committing
for file in $(git diff --cached --name-only --diff-filter=ACM | grep '\.gd$'); do
if ! godot --path . --check-only --script "$file" --headless --quit; then
echo "GDScript validation failed for $file"
exit 1
fi
doneQuick Reference
File Type Decision Tree
Writing game logic? → Use .gd file
Storing data (item stats, spell configs)? → Use .tres file
Creating scene structure? → Use .tscn file (prefer Godot editor for complex structures)
Syntax Quick Check
In .gd files: Full GDScript - var, func, preload(), etc. ✅
In .tres/.tscn files:
preload()❌ → UseExtResource("id")✅var,const,func❌ → Just property values ✅[1, 2, 3]❌ →Array[int]([1, 2, 3])✅
When to Use Each Validation Script
`validate_tres.py` - For resource files:
- Items, spells, abilities
- Custom resource data
- After creating .tres files
`validate_tscn.py` - For scene files:
- Player, enemies, levels
- UI scenes
- After editing .tscn files
When to Read Each Reference
`file-formats.md` - When:
- Creating/editing .tres/.tscn files
- Getting "failed to load" errors
- Unsure about syntax rules
`architecture-patterns.md` - When:
- Implementing new game systems
- Planning component structure
- Looking for proven patterns
Summary
Work with Godot projects effectively by:
1. Understanding file formats - .gd is code, .tres/.tscn are data with strict syntax 2. Using validation tools - Catch errors before testing 3. Following patterns - Use proven architectures from references 4. Starting from templates - Adapt rather than create from scratch 5. Testing incrementally - Validate, test, commit frequently
The key insight: Godot's text-based files are LLM-friendly when you respect the syntax differences between GDScript and resource serialization formats.
extends Node
class_name AttributeTemplate
## Base attribute system for managing numeric values with min/max constraints.
##
## Usage:
## 1. Attach to character/object as child node
## 2. Set value_max and value_start in Inspector
## 3. Connect to signals in parent script
## 4. Call add()/subtract() to modify value
##
## Example:
## var health = $HealthAttribute
## health.attribute_changed.connect(_on_health_changed)
## health.subtract(10.0) # Take 10 damage
# Signals
signal attribute_changed(attribute_name: String, value_current: float, value_max: float, value_increased: bool)
signal attribute_reached_zero(attribute_name: String)
signal attribute_reached_max(attribute_name: String)
# Configuration
@export_group("Attribute Settings")
@export var attribute_name: String = "Attribute"
@export var value_max: float = 100.0
@export var value_start: float = 100.0
@export_group("Regeneration")
@export var auto_regenerate: bool = false
@export var regen_rate: float = 1.0 # Per second
@export var regen_delay: float = 3.0 # Delay after damage
# Current value with automatic clamping and signal emission
var value_current: float:
set(value):
var old_value = value_current
value_current = clamp(value, 0, value_max)
var increased = value_current > old_value
attribute_changed.emit(attribute_name, value_current, value_max, increased)
if value_current <= 0:
attribute_reached_zero.emit(attribute_name)
elif value_current >= value_max and old_value < value_max:
attribute_reached_max.emit(attribute_name)
# Internal state for regeneration
var _regen_timer: float = 0.0
## Initialize the attribute
func _ready() -> void:
value_current = value_start
print("[", attribute_name, "] Initialized: ", value_current, "/", value_max)
## Handle regeneration
func _process(delta: float) -> void:
if not auto_regenerate:
return
if value_current >= value_max:
_regen_timer = 0.0
return
_regen_timer += delta
if _regen_timer >= regen_delay:
add(regen_rate * delta)
## Add to the attribute value
func add(amount: float) -> void:
if amount <= 0:
return
value_current += amount
## Subtract from the attribute value
func subtract(amount: float) -> void:
if amount <= 0:
return
value_current -= amount
_regen_timer = 0.0 # Reset regen timer on damage
## Set to a specific value
func set_value(new_value: float) -> void:
value_current = new_value
## Set to maximum
func set_to_max() -> void:
value_current = value_max
## Set to zero
func set_to_zero() -> void:
value_current = 0.0
## Get current percentage (0.0 to 1.0)
func get_percentage() -> float:
if value_max <= 0:
return 0.0
return value_current / value_max
## Check if attribute is depleted
func is_depleted() -> bool:
return value_current <= 0
## Check if attribute is full
func is_full() -> bool:
return value_current >= value_max
## Get remaining value
func get_remaining() -> float:
return value_max - value_current
extends Node
class_name ComponentTemplate
## A reusable component template following Godot best practices.
##
## Usage:
## 1. Attach as child node to the object that needs this functionality
## 2. Export variables appear in Inspector for easy configuration
## 3. Use signals for loose coupling with parent/other systems
# Signals for event-driven communication
signal component_activated
signal component_deactivated
signal component_updated(data: Dictionary)
# Export variables for Inspector configuration
@export_group("Component Settings")
@export var is_enabled: bool = true
@export var auto_start: bool = true
@export_group("Behavior")
@export var update_interval: float = 1.0
# Internal state
var _timer: float = 0.0
var _is_active: bool = false
## Called when the node enters the scene tree
func _ready() -> void:
if auto_start:
activate()
## Called every frame
func _process(delta: float) -> void:
if not _is_active or not is_enabled:
return
_timer += delta
if _timer >= update_interval:
_update()
_timer = 0.0
## Activate the component
func activate() -> void:
if _is_active:
return
_is_active = true
component_activated.emit()
print("[", name, "] Component activated")
## Deactivate the component
func deactivate() -> void:
if not _is_active:
return
_is_active = false
component_deactivated.emit()
print("[", name, "] Component deactivated")
## Internal update logic - override in subclasses
func _update() -> void:
var data = {
"timestamp": Time.get_ticks_msec(),
"component": name
}
component_updated.emit(data)
## Public interface for parent to call
func do_something() -> void:
if not is_enabled:
push_warning("Component not enabled")
return
# Implementation here
pass
extends Node
class_name InteractionTemplate
## Base class for interaction components following the component pattern.
##
## Usage:
## 1. Create subclass extending InteractionTemplate
## 2. Override _perform_interaction() with custom logic
## 3. Attach as child to BaseInteractable object
## 4. Set interaction_text and other exports in Inspector
##
## Example subclass:
## extends InteractionTemplate
## class_name DoorInteraction
##
## @export var door_id: String
## @export var requires_key: bool = false
##
## func _perform_interaction(player) -> void:
## if requires_key and not player.has_key(door_id):
## return
## get_parent().open()
# Signals
signal was_interacted_with(player: Node)
signal interaction_enabled
signal interaction_disabled
# Configuration
@export_group("Interaction Settings")
@export var interaction_text: String = "Interact"
@export var is_enabled: bool = true
@export var single_use: bool = false
@export_group("Requirements")
@export var required_item_id: String = ""
@export var minimum_level: int = 0
# Internal state
var _has_been_used: bool = false
## Attempt to interact with this component
## Returns true if interaction was successful
func interact(player: Node) -> bool:
if not can_interact(player):
return false
_perform_interaction(player)
was_interacted_with.emit(player)
if single_use:
_has_been_used = true
disable()
return true
## Check if interaction is possible
func can_interact(player: Node) -> bool:
if not is_enabled:
return false
if single_use and _has_been_used:
return false
if required_item_id and not _player_has_item(player, required_item_id):
return false
if minimum_level > 0 and not _player_meets_level(player, minimum_level):
return false
return true
## Get the text to display for this interaction
func get_interaction_text() -> String:
return interaction_text
## Enable this interaction
func enable() -> void:
is_enabled = true
interaction_enabled.emit()
## Disable this interaction
func disable() -> void:
is_enabled = false
interaction_disabled.emit()
## Reset for reuse (if not single_use)
func reset() -> void:
if not single_use:
_has_been_used = false
enable()
## OVERRIDE THIS: Perform the actual interaction logic
func _perform_interaction(player: Node) -> void:
print("[", get_parent().name, "] Interacted by ", player.name)
# Override in subclasses with specific interaction logic
## Helper: Check if player has required item
func _player_has_item(player: Node, item_id: String) -> bool:
if not player.has_method("has_item"):
return false
return player.has_item(item_id)
## Helper: Check if player meets level requirement
func _player_meets_level(player: Node, level: int) -> bool:
if not player.has_method("get_level"):
return false
return player.get_level() >= level
[gd_resource type="Resource" script_class="ItemResource" load_steps=2 format=3 uid="uid://template_item"]
[ext_resource type="Script" path="res://src/items/item_resource.gd" id="1_item"]
[resource]
script = ExtResource("1_item")
item_id = "example_item"
item_name = "Example Item"
description = "A template item resource"
stackable = true
max_stack = 99
[gd_resource type="Resource" script_class="SpellResource" load_steps=3 format=3 uid="uid://template_spell"]
[ext_resource type="Script" path="res://src/spells/spell_resource.gd" id="1_spell"]
[ext_resource type="Script" path="res://src/spells/spell_effect.gd" id="2_effect"]
[sub_resource type="Resource" id="Effect_damage"]
script = ExtResource("2_effect")
effect_type = 0
magnitude_min = 10.0
magnitude_max = 20.0
duration = 0.0
[resource]
script = ExtResource("1_spell")
spell_name = "Magic Missile"
spell_id = "magic_missile"
mana_cost = 15.0
spell_color = Color(0.5, 0.5, 1, 1)
projectile_speed = 20.0
effects = Array[ExtResource("2_effect")]([SubResource("Effect_damage")])
Godot Architecture Patterns
Proven architectural patterns for building maintainable Godot projects, especially when working with LLM coding assistants.
Core Principles
1. Component-Based Design
Break functionality into small, reusable components attached as Node children.
Benefits:
- Each component is a focused, understandable file
- Easy to add/remove features
- Clear responsibilities
- LLM-friendly (small files, clear purpose)
2. Signal-Driven Communication
Use signals for loose coupling between systems.
Benefits:
- Self-documenting (signals show available events)
- Easy to connect UI without modifying game logic
- No complex dependency injection needed
- Clear event flow
3. Resource-Based Data
Separate logic (.gd) from data (.tres).
Benefits:
- Data files are simple and safe to edit
- Easy to create variants
- Designer-friendly
- LLM can edit data without touching logic
---
Pattern 1: Component-Based Interaction System
Use for: Objects that can be interacted with in different ways.
Structure
BaseInteractable.gd (parent script)
├─ is_interactable: bool
├─ interaction_nodes: Array[InteractionComponent]
└─ interact(player) -> void
InteractionComponent.gd (child node script)
├─ interaction_text: String
├─ is_enabled: bool
├─ interact(player) -> bool
└─ signal: was_interacted_withImplementation
BaseInteractable.gd:
extends Node3D
class_name BaseInteractable
@export var is_interactable: bool = true
var interaction_nodes: Array[InteractionComponent] = []
func _ready():
# Gather all interaction components
for child in get_children():
if child is InteractionComponent:
interaction_nodes.append(child)
func interact(player) -> void:
if not is_interactable:
return
for interaction in interaction_nodes:
if interaction.is_enabled:
interaction.interact(player)
break # Only one interaction per pressInteractionComponent.gd:
extends Node
class_name InteractionComponent
signal was_interacted_with(player)
@export var interaction_text: String = "Interact"
@export var is_enabled: bool = true
func interact(player) -> bool:
if not is_enabled:
return false
_perform_interaction(player)
was_interacted_with.emit(player)
return true
func _perform_interaction(player) -> void:
# Override in subclasses
passExample Subclass - PickupInteraction.gd:
extends InteractionComponent
class_name PickupInteraction
@export var item_resource: ItemResource
func _perform_interaction(player) -> void:
if not item_resource:
push_error("No item resource assigned")
return
player.inventory.add_item(item_resource)
get_parent().queue_free() # Remove the pickupScene Setup (.tscn)
[ext_resource type="Script" path="res://src/base_interactable.gd" id="1"]
[ext_resource type="Script" path="res://src/pickup_interaction.gd" id="2"]
[ext_resource type="Resource" path="res://resources/items/key.tres" id="3"]
[node name="KeyPickup" type="StaticBody3D"]
script = ExtResource("1")
[node name="PickupInteraction" type="Node" parent="."]
script = ExtResource("2")
interaction_text = "Pick up Key"
item_resource = ExtResource("3")
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
# ... mesh configurationUsage Benefits
- Single object can have multiple interaction types
- Easy to add new interaction types (just create new subclass)
- Interactions can be enabled/disabled dynamically
- Clear separation of concerns
---
Pattern 2: Attribute System
Use for: Health, mana, stamina, or any numeric attribute with min/max values.
Structure
Attribute.gd (base class)
├─ attribute_name: String
├─ value_current: float (with setter)
├─ value_max: float
├─ value_start: float
├─ signal: attribute_changed(name, current, max, increased)
└─ signal: attribute_reached_zero(name)
HealthAttribute.gd (specialized)
├─ extends Attribute
├─ signal: damage_taken(amount)
└─ signal: death()Implementation
Attribute.gd:
extends Node
class_name Attribute
signal attribute_changed(attribute_name: String, value_current: float, value_max: float, value_increased: bool)
signal attribute_reached_zero(attribute_name: String)
@export var attribute_name: String = "Attribute"
@export var value_max: float = 100.0
@export var value_start: float = 100.0
var value_current: float:
set(value):
var old_value = value_current
value_current = clamp(value, 0, value_max)
var increased = value_current > old_value
attribute_changed.emit(attribute_name, value_current, value_max, increased)
if value_current <= 0:
attribute_reached_zero.emit(attribute_name)
func _ready():
value_current = value_start
func add(amount: float) -> void:
value_current += amount
func subtract(amount: float) -> void:
value_current -= amountHealthAttribute.gd:
extends Attribute
class_name HealthAttribute
signal damage_taken(amount: float)
signal death()
func _ready():
super._ready()
attribute_name = "Health"
attribute_reached_zero.connect(_on_death)
func take_damage(amount: float) -> void:
subtract(amount)
damage_taken.emit(amount)
func heal(amount: float) -> void:
add(amount)
func _on_death() -> void:
death.emit()Parent Integration
BaseEnemy.gd:
extends CharacterBody3D
class_name BaseEnemy
var attributes: Dictionary = {}
func _ready():
# Gather all attributes
for child in get_children():
if child is Attribute:
attributes[child.attribute_name] = child
child.attribute_changed.connect(_on_attribute_changed)
# Connect to health-specific signals
if attributes.has("Health"):
attributes["Health"].death.connect(_on_death)
func _on_attribute_changed(attr_name: String, current: float, max_val: float, increased: bool):
# Update UI or trigger effects
pass
func _on_death():
# Drop loot, play animation, etc.
queue_free()Scene Setup (.tscn)
[ext_resource type="Script" path="res://src/base_enemy.gd" id="1"]
[ext_resource type="Script" path="res://src/health_attribute.gd" id="2"]
[node name="Enemy" type="CharacterBody3D"]
script = ExtResource("1")
[node name="HealthAttribute" type="Node" parent="."]
script = ExtResource("2")
value_max = 50.0
value_start = 50.0---
Pattern 3: Resource-Based Effect System
Use for: Spells, abilities, items, or any combinable effects.
Structure
SpellEffect.gd (individual effect)
├─ effect_type: enum
├─ magnitude_min/max: float
├─ duration: float
└─ apply_effect(target, caster)
SpellResource.gd (combines effects)
├─ effects: Array[SpellEffect]
├─ mana_cost: float
├─ projectile_speed: float
└─ spell_color: ColorImplementation
SpellEffect.gd:
extends Resource
class_name SpellEffect
enum EffectType {
DAMAGE,
HEAL,
RESTORE_MANA,
DAMAGE_OVER_TIME,
HEAL_OVER_TIME,
SLOW,
SPEED_BOOST,
STUN,
# ... more types
}
@export var effect_type: EffectType = EffectType.DAMAGE
@export var magnitude_min: float = 10.0
@export var magnitude_max: float = 10.0
@export var duration: float = 0.0
@export var tick_rate: float = 1.0 # For over-time effects
func apply_effect(target: Node, caster: Node) -> void:
var magnitude = randf_range(magnitude_min, magnitude_max)
match effect_type:
EffectType.DAMAGE:
_apply_damage(target, magnitude)
EffectType.HEAL:
_apply_heal(target, magnitude)
EffectType.SLOW:
_apply_slow(target, magnitude, duration)
# ... handle other types
func _apply_damage(target: Node, amount: float) -> void:
if target.has_method("take_damage"):
target.take_damage(amount)
func _apply_heal(target: Node, amount: float) -> void:
if target.has_node("HealthAttribute"):
target.get_node("HealthAttribute").heal(amount)
func _apply_slow(target: Node, percent: float, dur: float) -> void:
# Apply slow effect logic
passSpellResource.gd:
extends Resource
class_name SpellResource
@export var spell_id: String = ""
@export var spell_name: String = "Spell"
@export var mana_cost: float = 10.0
@export var effects: Array[SpellEffect] = []
@export var projectile_speed: float = 20.0
@export var spell_color: Color = Color.WHITE
func cast(caster: Node, target_position: Vector3) -> void:
# Spawn projectile or apply effects immediately
pass
func apply_effects(target: Node, caster: Node) -> void:
for effect in effects:
effect.apply_effect(target, caster)Data File (.tres)
[gd_resource type="Resource" script_class="SpellResource" load_steps=4 format=3]
[ext_resource type="Script" path="res://src/spells/spell_resource.gd" id="1"]
[ext_resource type="Script" path="res://src/spells/spell_effect.gd" id="2"]
[sub_resource type="Resource" id="Effect_damage"]
script = ExtResource("2")
effect_type = 0
magnitude_min = 15.0
magnitude_max = 25.0
[sub_resource type="Resource" id="Effect_slow"]
script = ExtResource("2")
effect_type = 12
magnitude_min = 50.0
magnitude_max = 50.0
duration = 3.0
[resource]
script = ExtResource("1")
spell_name = "Ice Bolt"
spell_id = "ice_bolt"
mana_cost = 20.0
spell_color = Color(0.5, 0.7, 1, 1)
projectile_speed = 15.0
effects = Array[ExtResource("2")]([SubResource("Effect_damage"), SubResource("Effect_slow")])Benefits
- Create new spells with just data (no code changes)
- Combinable effects create emergent gameplay
- Easy for LLM to generate new spell variants
- Designer-friendly (edit in Inspector)
---
Pattern 4: Inventory System
Use for: Player/enemy inventories, containers, shops.
Structure
ItemResource.gd (data)
├─ item_id: String
├─ item_name: String
├─ description: String
├─ icon: Texture2D
└─ stackable: bool
Inventory.gd (logic)
├─ items: Array[ItemResource]
├─ add_item(item)
├─ remove_item(item_id)
├─ has_item(item_id) -> bool
└─ signal: item_added/removedImplementation
ItemResource.gd:
extends Resource
class_name ItemResource
@export var item_id: String = ""
@export var item_name: String = "Item"
@export var description: String = ""
@export var icon: Texture2D
@export var stackable: bool = false
@export var max_stack: int = 99Inventory.gd:
extends Node
class_name Inventory
signal item_added(item: ItemResource)
signal item_removed(item_id: String)
var items: Array[ItemResource] = []
func add_item(item: ItemResource) -> bool:
if not item:
return false
items.append(item)
item_added.emit(item)
return true
func remove_item(item_id: String) -> bool:
for i in range(items.size()):
if items[i].item_id == item_id:
var removed = items[i]
items.remove_at(i)
item_removed.emit(item_id)
return true
return false
func has_item(item_id: String) -> bool:
for item in items:
if item.item_id == item_id:
return true
return false
func get_item(item_id: String) -> ItemResource:
for item in items:
if item.item_id == item_id:
return item
return null---
Pattern 5: State Machine
Use for: AI behavior, player states, animation control.
Structure
StateMachine.gd
├─ current_state: State
├─ states: Dictionary
├─ change_state(state_name)
└─ _process/_physics_process
State.gd (base)
├─ enter()
├─ exit()
├─ process(delta)
└─ physics_process(delta)Implementation
State.gd:
extends Node
class_name State
signal state_finished(next_state: String)
func enter() -> void:
pass
func exit() -> void:
pass
func process(delta: float) -> void:
pass
func physics_process(delta: float) -> void:
passStateMachine.gd:
extends Node
class_name StateMachine
@export var initial_state: String = ""
var states: Dictionary = {}
var current_state: State = null
func _ready():
# Gather all state children
for child in get_children():
if child is State:
states[child.name] = child
child.state_finished.connect(_on_state_finished)
if initial_state and states.has(initial_state):
change_state(initial_state)
func _process(delta: float):
if current_state:
current_state.process(delta)
func _physics_process(delta: float):
if current_state:
current_state.physics_process(delta)
func change_state(new_state_name: String) -> void:
if current_state:
current_state.exit()
current_state = states.get(new_state_name)
if current_state:
current_state.enter()
func _on_state_finished(next_state: String) -> void:
change_state(next_state)Example State - IdleState.gd:
extends State
class_name IdleState
@export var idle_time: float = 2.0
var timer: float = 0.0
func enter() -> void:
timer = 0.0
print("Entering idle state")
func process(delta: float) -> void:
timer += delta
if timer >= idle_time:
state_finished.emit("Patrol")---
Combining Patterns
These patterns work together naturally:
# Enemy with attributes, state machine, and loot
BaseEnemy (CharacterBody3D)
├─ HealthAttribute (Attribute component)
├─ ManaAttribute (Attribute component)
├─ StateMachine
│ ├─ IdleState
│ ├─ PatrolState
│ ├─ ChaseState
│ └─ AttackState
└─ LootDropper (Component)
└─ loot_table: Array[LootEntry]Each component handles one responsibility, making the system:
- Easy to understand
- Simple to modify
- Clear to debug
- LLM-friendly to work with
Common Godot Pitfalls and Solutions
This document catalogs frequent mistakes, gotchas, and their solutions when working with Godot 4.x projects.
Initialization and @onready Timing
The Problem
@onready variables are initialized when _ready() is called, but the order of initialization across the scene tree can cause issues.
Common Pitfall: Null References from Parent Methods
Problem:
# In child component
@onready var player: CharacterBody3D = get_parent()
@onready var camera: Camera3D = player.get_camera() # ❌ Returns null!
func _ready():
# camera is null here - get_camera() wasn't available during @onreadyWhy it fails:
@onreadyruns before_ready()in the scene tree- If
get_camera()returns a node that's dynamically set up, it may not exist yet - Parent initialization might not be complete
Solution: Use dynamic getters
# ✅ Better approach
@onready var player: CharacterBody3D = get_parent()
func _get_camera() -> Camera3D:
if player and player.has_method("get_camera"):
return player.get_camera()
return null
func perform_action():
var camera = _get_camera()
if camera:
# Use cameraSolution: Initialize in _ready()
# ✅ Alternative approach
var player: CharacterBody3D
var camera: Camera3D
func _ready():
player = get_parent()
if player and player.has_method("get_camera"):
camera = player.get_camera()Common Pitfall: Accessing Child Nodes Too Early
Problem:
# Parent node
@onready var child_component = $ChildComponent
func _ready():
child_component.setup() # ❌ Might fail if child's _ready() hasn't run
# Child node
var is_initialized: bool = false
func _ready():
# Complex initialization
is_initialized = true
func setup():
if not is_initialized:
push_error("Called setup() before initialization!")Solution: Use call_deferred or signals
# ✅ Parent waits for child to be ready
func _ready():
await get_tree().process_frame # Wait one frame
child_component.setup()
# ✅ Or use signals
func _ready():
child_component.initialized.connect(_on_child_ready)
func _on_child_ready():
# Child is definitely ready now_ready() Execution Order
Key principle: _ready() is called bottom-up in the scene tree (children before parents).
SceneRoot
├─ Parent (ready called THIRD)
├─ Child1 (ready called FIRST)
└─ Child2 (ready called SECOND)Implications:
- Children's
_ready()complete before parent's_ready()starts - Parent can safely access child nodes in
_ready() - Children should NOT assume parent is ready during their
_ready()
Example:
# Child component
func _ready():
var parent = get_parent()
# ❌ Don't call parent.initialize() - parent's _ready() hasn't run yet
# ✅ Instead, emit a signal or wait
ready.emit()
# Parent
func _ready():
for child in get_children():
# ✅ Children are fully ready here
if child.has_method("configure"):
child.configure(some_data)Node References and get_node()
Common Pitfall: Hardcoded NodePaths Breaking
Problem:
@onready var health_bar = $"../UI/HealthBar" # ❌ Fragile - breaks if hierarchy changesSolution: Use groups or signals
# ✅ Add HealthBar to "ui_health" group in editor
func _ready():
var health_bar = get_tree().get_first_node_in_group("ui_health")
# ✅ Or use signals
signal health_changed(current: float, max: float)
func take_damage(amount: float):
health -= amount
health_changed.emit(health, max_health) # UI listens to thisCommon Pitfall: Using get_node() in @onready with Complex Paths
Problem:
@onready var camera = get_node("../../CameraPivot/Camera3D") # ❌ Error-proneSolution: Find node by type or group
# ✅ Find by type
func _get_camera() -> Camera3D:
var current = get_parent()
while current:
if current is Camera3D:
return current
for child in current.get_children():
if child is Camera3D:
return child
current = current.get_parent()
return null
# ✅ Or add camera to "camera" group and find it
func _ready():
var camera = get_tree().get_first_node_in_group("main_camera")Signal Connection Issues
Common Pitfall: Connecting Signals in Wrong Order
Problem:
func _ready():
$Button.pressed.connect(_on_button_pressed)
$Button.pressed.emit() # ❌ Connection might not be active yetSolution: Wait one frame or use call_deferred
func _ready():
$Button.pressed.connect(_on_button_pressed)
await get_tree().process_frame
$Button.pressed.emit() # ✅ Connection is active
# Or
func _ready():
$Button.pressed.connect(_on_button_pressed)
$Button.pressed.emit.call_deferred() # ✅ Emits after _ready() completesCommon Pitfall: Memory Leaks from Signal Connections
Problem:
func _ready():
some_node.signal_name.connect(callback)
# ❌ If this node is freed but some_node remains, connection persistsSolution: Disconnect in cleanup or use weak references
var connected_node: Node
func _ready():
connected_node = some_node
connected_node.signal_name.connect(callback)
func _exit_tree():
if connected_node and connected_node.signal_name.is_connected(callback):
connected_node.signal_name.disconnect(callback)
# Or use one-shot connections
func _ready():
some_node.signal_name.connect(callback, CONNECT_ONE_SHOT)Resource Loading and Modification
Common Pitfall: Modifying Shared Resources
Problem:
# item_resource.tres is shared across all instances
@export var item: ItemResource
func _ready():
item.quantity += 1 # ❌ Modifies the .tres file for ALL instances!Solution: Duplicate resources when modifying
@export var item: ItemResource
var local_item: ItemResource
func _ready():
local_item = item.duplicate() # ✅ Create instance-specific copy
local_item.quantity += 1 # Only affects this instanceCommon Pitfall: preload() in .tres Files
Problem:
[resource]
script = preload("res://script.gd") # ❌ WRONG - .tres files don't support preload()Solution: Use ExtResource
[ext_resource type="Script" path="res://script.gd" id="1"]
[resource]
script = ExtResource("1") # ✅ CorrectCharacterBody3D Movement
Common Pitfall: Velocity Not Persisting
Problem:
func _physics_process(delta):
var velocity = Vector3.ZERO # ❌ Resets velocity every frame!
velocity.x = input.x * speed
move_and_slide()Solution: Use the velocity property
func _physics_process(delta):
velocity.x = input.x * speed # ✅ Modifies persistent velocity
velocity.y -= gravity * delta
move_and_slide()Common Pitfall: Floor Detection Issues
Problem:
func _physics_process(delta):
if is_on_floor():
velocity.y = 0 # ❌ Causes jittering on slopesSolution: Only reset vertical velocity when needed
func _physics_process(delta):
# Apply gravity
if not is_on_floor():
velocity.y -= gravity * delta
# Jump
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force # ✅ Only set when jumping
move_and_slide()Transform and Basis Confusion
Common Pitfall: Wrong Direction Vector
Problem:
var forward = transform.basis.z # ❌ This is actually backward in Godot!Solution: Negate Z for forward
var forward = -transform.basis.z # ✅ Forward direction
var right = transform.basis.x # ✅ Right direction
var up = transform.basis.y # ✅ Up directionCommon Pitfall: Mixing Local and Global Transforms
Problem:
position += Vector3.FORWARD * speed # ❌ Moves in global forward, not localSolution: Use basis to transform direction
position += -transform.basis.z * speed # ✅ Moves in local forward direction
# Or use global_transform for global operations
global_position += Vector3.FORWARD * speed # ✅ Explicitly globalInput Handling
Common Pitfall: Input Processed in _process() and _physics_process()
Problem:
func _process(delta):
if Input.is_action_just_pressed("jump"):
jump() # ❌ Might miss input if physics runs at different rate
func _physics_process(delta):
# Physics codeSolution: Handle input where it's used
# For movement: use _physics_process
func _physics_process(delta):
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_force
move_and_slide()
# For UI/non-physics: use _input or _unhandled_input
func _input(event):
if event.is_action_pressed("menu"):
open_menu()Common Pitfall: Mouse Capture Issues
Problem:
func _ready():
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
# ❌ Escape key doesn't work - mouse is captured foreverSolution: Toggle mouse mode with escape
func _input(event):
if event.is_action_pressed("ui_cancel"):
if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
else:
Input.mouse_mode = Input.MOUSE_MODE_CAPTUREDType Safety and Static Typing
Common Pitfall: Weak Typing Hiding Errors
Problem:
var node = get_node("Player")
node.health = 100 # ❌ No error if Player doesn't have health propertySolution: Use static typing
var player: Player = get_node("Player") as Player
if player:
player.health = 100 # ✅ Error if Player class doesn't have healthCommon Pitfall: Null Checks Missing
Problem:
var target = get_tree().get_first_node_in_group("player")
var distance = global_position.distance_to(target.global_position) # ❌ Crashes if no playerSolution: Always check for null
var target = get_tree().get_first_node_in_group("player")
if target:
var distance = global_position.distance_to(target.global_position) # ✅ SafeScene Instancing
Common Pitfall: Forgetting to Add Instanced Scene to Tree
Problem:
var scene = preload("res://enemy.tscn")
var enemy = scene.instantiate()
enemy.position = spawn_point # ❌ Enemy exists but isn't in the scene tree!Solution: Add to tree
var scene = preload("res://enemy.tscn")
var enemy = scene.instantiate()
add_child(enemy) # ✅ Add to tree
enemy.global_position = spawn_point # Use global_position after add_childCommon Pitfall: Setting Position Before Adding to Tree
Problem:
var enemy = scene.instantiate()
enemy.position = Vector3(10, 0, 10) # ❌ Local position, might be wrong after add_child
add_child(enemy)Solution: Set global position after adding
var enemy = scene.instantiate()
add_child(enemy)
enemy.global_position = Vector3(10, 0, 10) # ✅ Global position after in treeTween Issues
Common Pitfall: Creating Tweens Without Cleanup
Problem:
func animate():
var tween = create_tween()
tween.tween_property(self, "position", target, 1.0)
# ❌ If called repeatedly, creates multiple tweens conflictingSolution: Kill previous tweens or check is_valid()
var current_tween: Tween
func animate():
if current_tween and current_tween.is_valid():
current_tween.kill() # ✅ Stop previous animation
current_tween = create_tween()
current_tween.tween_property(self, "position", target, 1.0)Common Pitfall: Tween Callbacks Not Firing
Problem:
func swing_weapon():
var tween = create_tween()
tween.tween_property(weapon, "rotation", target_rotation, 0.5)
tween.tween_callback(finish_swing) # ❌ Might not fire if tween is killedSolution: Use await or check tween validity
func swing_weapon():
var tween = create_tween()
tween.tween_property(weapon, "rotation", target_rotation, 0.5)
await tween.finished # ✅ Waits for tween to complete
finish_swing()Summary: Quick Checklist
When encountering issues, check:
- [ ] Is
@onreadycausing timing issues? → Use dynamic getters or initialize in_ready() - [ ] Are you accessing parent methods in child's
_ready()? → Wait a frame or use signals - [ ] Is a node reference null? → Check scene tree structure and initialization order
- [ ] Using
get_node()with complex paths? → Use groups or find by type - [ ] Modifying a shared resource? → Duplicate it first
- [ ] Movement not working? → Check you're using
velocityproperty, not local variable - [ ] Direction vector wrong? → Remember
-transform.basis.zis forward - [ ] Tween not working? → Kill previous tweens, use await for callbacks
- [ ] Input missed? → Process input where it's used (_physics_process for movement)
- [ ] Null reference error? → Add null checks before accessing properties
- [ ] Instance not appearing? → Remember to
add_child()and useglobal_position
When in Doubt
1. Print debug info: print() is your friend 2. Check the scene tree: Use Remote tab in editor while game runs 3. Enable visible collision shapes: Debug → Visible Collision Shapes 4. Read error messages carefully: They often point to the exact issue 5. Consult official docs: https://docs.godotengine.org/
Godot File Formats Reference
This reference provides detailed syntax information for Godot's text-based file formats.
GDScript Files (.gd)
Standard Python-like script files. Full GDScript syntax is supported.
Basic Structure
extends BaseClass
class_name MyClass
signal my_signal(param: Type)
@export var speed: float = 5.0
@export_group("Combat")
@export var damage: int = 10
var _internal_var: String = ""
func _ready() -> void:
print("Ready")
func my_function(param: int) -> bool:
return param > 0Key Features
- Full object-oriented programming
- Type hints (optional but recommended)
- Signals for event-driven communication
- Export variables for Inspector editing
- Inheritance with
extends - Class naming with
class_name
Scene Files (.tscn)
Text serialization of scene node hierarchies. STRICT FORMATTING REQUIRED.
File Structure
[gd_scene load_steps=N format=3 uid="uid://..."]
[ext_resource type="Type" path="res://path" id="1_name"]
[ext_resource type="Type" path="res://path" id="2_other"]
[sub_resource type="SomeType" id="SubResource_1"]
property = value
[node name="Root" type="NodeType"]
script = ExtResource("1_name")
property = value
[node name="Child" type="ChildType" parent="."]
property = value
[node name="Grandchild" type="Node" parent="Child"]
another_property = 123Critical Rules
1. Header Format
[gd_scene load_steps=3 format=3 uid="uid://unique_identifier"]load_steps: Number of resources to load (ExtResource + SubResource count)format: Always 3 for Godot 4.xuid: Unique identifier (DO NOT change - breaks references)
2. ExtResource Declarations
[ext_resource type="Script" path="res://src/my_script.gd" id="1_script"]
[ext_resource type="PackedScene" path="res://scenes/other.tscn" id="2_scene"]- Must be declared before use
idformat:number_descriptive_name- Common types:
Script,PackedScene,Texture2D,Material
3. ExtResource References
script = ExtResource("1_script")
texture = ExtResource("3_texture")- NEVER use `preload()` - that's GDScript syntax
- ALWAYS use `ExtResource("id")`
4. SubResource Declarations
[sub_resource type="BoxShape3D" id="SubResource_collision"]
size = Vector3(1, 2, 1)- For resources defined inline within the scene
- Referenced with
SubResource("id")
5. Node Hierarchy
[node name="Player" type="CharacterBody3D"]
script = ExtResource("1_player")
[node name="CollisionShape" type="CollisionShape3D" parent="."]
shape = SubResource("SubResource_collision")
[node name="Model" type="MeshInstance3D" parent="."]
mesh = ExtResource("2_mesh")
[node name="Texture" type="Sprite2D" parent="Model"]
texture = ExtResource("3_texture")- Root node: no parent
- Direct children:
parent="." - Nested children:
parent="ParentNodeName" - Deep nesting:
parent="Parent/Child"
6. Scene Instancing
[node name="Enemy" type="Node3D"]
[node name="BaseEnemy" parent="." instance=ExtResource("5_enemy_scene")]
[node name="SomeChild" parent="BaseEnemy" index="2"]
property = "modified value"instance=ExtResource(): Instantiate another sceneindex="N": Modify instanced scene's child (see Instance Property Overrides below)
Instance Property Overrides
CRITICAL CONCEPT: When you instance a scene in a .tscn file, the instance starts with ALL default property values from the source scene. To customize an instance, you MUST explicitly override properties using special syntax.
The Problem
# item_pickup.tscn (source scene)
[node name="ItemPickup" type="StaticBody3D"]
[node name="PickupInteraction" type="Node" parent="."]
script = ExtResource("1_pickup")
item_resource = null # Default value
quantity = 1If you instance this scene WITHOUT overriding properties:
# level.tscn
[node name="KeyPickup" parent="." instance=ExtResource("6_pickup")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)The item_resource will be null - the instance uses the default! This is a common source of bugs.
The Solution: Property Overrides
To customize an instanced scene's child nodes, use the index parameter:
# level.tscn
[ext_resource type="PackedScene" path="res://scenes/item_pickup.tscn" id="6_pickup"]
[ext_resource type="Resource" path="res://resources/test_key.tres" id="7_key"]
[node name="KeyPickup" parent="." instance=ExtResource("6_pickup")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 3)
[node name="PickupInteraction" parent="KeyPickup" index="0"]
item_resource = ExtResource("7_key")
quantity = 1Key points:
parent="KeyPickup"- References the instanced scene nodeindex="0"- Indicates this is modifying the first child (0-indexed)- Property assignments override the defaults
Finding the Correct Index
The index parameter corresponds to the child's position in the parent's child list (0-indexed). To find it:
1. Open the source scene in the Godot editor 2. Look at the Scene tree - the order of children determines the index 3. Count from 0 - first child is index="0", second is index="1", etc.
Or examine the source .tscn file to see the node order.
Multiple Property Overrides
You can override multiple children of an instance:
[node name="Door" parent="." instance=ExtResource("5_door")]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0, 0)
[node name="DoorInteraction" parent="Door" index="0"]
required_key = "gold_key"
interaction_text = "Unlock Gold Door"
[node name="MeshInstance3D" parent="Door" index="1"]
material_override = ExtResource("8_gold_material")Common Patterns
Configuring pickup items:
[node name="HealthPotion" parent="." instance=ExtResource("6_pickup")]
[node name="PickupInteraction" parent="HealthPotion" index="0"]
item_resource = ExtResource("7_health_potion")
quantity = 3Configuring doors:
[node name="LockedDoor" parent="." instance=ExtResource("5_door")]
[node name="DoorInteraction" parent="LockedDoor" index="0"]
starts_locked = true
required_key = "dungeon_key"Configuring enemy spawns:
[node name="BossSlime" parent="." instance=ExtResource("8_slime")]
[node name="Slime" parent="BossSlime" index="0"]
max_health = 200.0
move_speed = 3.0When NOT to Use Index
Don't use index for properties on the root node of the instance - those can be set directly:
# ✅ CORRECT - Root node properties set directly
[node name="Slime" parent="." instance=ExtResource("8_slime")]
max_health = 50.0
move_speed = 2.0
# ❌ WRONG - Don't use index for root properties
[node name="Slime" parent="." instance=ExtResource("8_slime")]
[node name="Slime" parent="Slime" index="0"] # This is confusing and wrong!
max_health = 50.0Safe vs Risky Edits
✅ SAFE Edits
# Add new ExtResource declaration
[ext_resource type="Script" path="res://new.gd" id="10_new"]
# Modify simple property values
[node name="Player"]
move_speed = 6.0
# Add new top-level node
[node name="NewNode" type="Node" parent="."]
script = ExtResource("10_new")
# Add new child to existing node
[node name="Component" type="Node" parent="Player"]⚠️ RISKY Edits
# Modifying instanced scene children (can break on editor re-save)
[node name="Mesh" parent="Enemy/Model" index="3"]
# Complex node restructuring
# Moving nodes between parents❌ DANGEROUS - Never Do
# Changing UIDs (breaks all references)
uid="uid://different_id"
# Deleting ExtResource declarations still in use
# Restructuring instanced scenes heavilyResource Files (.tres)
Serialized resource data. NO GDSCRIPT SYNTAX ALLOWED.
File Structure
[gd_resource type="Resource" script_class="ClassName" load_steps=N format=3 uid="uid://..."]
[ext_resource type="Script" path="res://path/to/script.gd" id="1_script"]
[sub_resource type="Resource" id="SubRes_1"]
script = ExtResource("1_script")
property = value
[resource]
script = ExtResource("1_script")
property1 = "value"
property2 = 42
nested_resource = SubResource("SubRes_1")Critical Rules
1. NO GDScript Syntax
# ❌ WRONG - GDScript syntax not allowed
script = preload("res://script.gd")
var my_value = 10
const SPEED = 5.0
func do_something():
pass
# ✅ CORRECT - Resource syntax only
script = ExtResource("1_script")
my_value = 10
speed = 5.02. ExtResource for External References
# ❌ WRONG
[resource]
script = preload("res://spell_effect.gd")
# ✅ CORRECT
[ext_resource type="Script" path="res://spell_effect.gd" id="1_effect"]
[resource]
script = ExtResource("1_effect")3. SubResource for Nested Resources
# Declare SubResource first
[sub_resource type="Resource" id="Effect_1"]
script = ExtResource("1_effect_script")
magnitude = 10.0
# Use in main resource
[resource]
script = ExtResource("1_main_script")
effect = SubResource("Effect_1")4. Typed Arrays
# ❌ WRONG - Untyped array
effects = [SubResource("Effect_1"), SubResource("Effect_2")]
# ✅ CORRECT - Must specify type
effects = Array[Resource]([SubResource("Effect_1"), SubResource("Effect_2")])
# ✅ ALSO CORRECT - Using ExtResource for type
[ext_resource type="Script" path="res://effect.gd" id="2_effect"]
effects = Array[ExtResource("2_effect")]([SubResource("Effect_1")])5. Common Data Types
# Primitives
my_int = 42
my_float = 3.14
my_bool = true
my_string = "text"
# Vectors
position = Vector2(10, 20)
position3d = Vector3(1, 2, 3)
# Colors
color = Color(1, 0, 0, 1) # RGBA
color_hex = Color("#ff0000")
# Arrays
numbers = Array[int]([1, 2, 3])
strings = Array[String](["a", "b", "c"])
# Resources
script = ExtResource("1_script")
nested = SubResource("SubRes_1")Complete Example
[gd_resource type="Resource" script_class="SpellResource" load_steps=4 format=3 uid="uid://abc123"]
[ext_resource type="Script" path="res://src/spells/spell_resource.gd" id="1_spell"]
[ext_resource type="Script" path="res://src/spells/spell_effect.gd" id="2_effect"]
[sub_resource type="Resource" id="Effect_damage"]
script = ExtResource("2_effect")
effect_type = 0
magnitude_min = 15.0
magnitude_max = 25.0
duration = 0.0
[sub_resource type="Resource" id="Effect_slow"]
script = ExtResource("2_effect")
effect_type = 12
magnitude_min = 50.0
magnitude_max = 50.0
duration = 3.0
[resource]
script = ExtResource("1_spell")
spell_name = "Ice Bolt"
spell_id = "ice_bolt"
mana_cost = 20.0
spell_color = Color(0.5, 0.7, 1, 1)
projectile_speed = 15.0
effects = Array[ExtResource("2_effect")]([SubResource("Effect_damage"), SubResource("Effect_slow")])Common Mistakes Reference
1. preload() in .tres/.tscn
# ❌ WRONG
script = preload("res://script.gd")
# ✅ CORRECT
[ext_resource type="Script" path="res://script.gd" id="1"]
script = ExtResource("1")2. GDScript Keywords in .tres
# ❌ WRONG
var speed = 5.0
const MAX_HEALTH = 100
# ✅ CORRECT
speed = 5.0
max_health = 1003. Untyped Arrays in .tres
# ❌ WRONG
items = [SubResource("Item1"), SubResource("Item2")]
# ✅ CORRECT
items = Array[Resource]([SubResource("Item1"), SubResource("Item2")])4. Missing ExtResource Declarations
# ❌ WRONG - Using without declaring
[node name="Player"]
script = ExtResource("1_player") # Not declared!
# ✅ CORRECT - Declare first
[ext_resource type="Script" path="res://player.gd" id="1_player"]
[node name="Player"]
script = ExtResource("1_player")5. Invalid Parent References
# ❌ WRONG - Parent doesn't exist
[node name="Child" parent="NonExistentNode"]
# ✅ CORRECT - Use valid parent
[node name="Parent" type="Node"]
[node name="Child" parent="Parent"]6. Forgetting Instance Property Overrides
# ❌ WRONG - Instance uses default (null) values
[node name="KeyPickup" parent="." instance=ExtResource("6_pickup")]
# item_resource will be null! Pickup won't work!
# ✅ CORRECT - Override the property
[node name="KeyPickup" parent="." instance=ExtResource("6_pickup")]
[node name="PickupInteraction" parent="KeyPickup" index="0"]
item_resource = ExtResource("7_key")This is a very common bug! Instanced scenes start with default values. If a scene has configurable components (like PickupInteraction with item_resource, or DoorInteraction with required_key), you MUST override those properties in the instance. See "Instance Property Overrides" section for details.
Godot 4.x Physics API Quick Reference
This reference covers common physics operations in Godot 4.x, focusing on correct API usage and common patterns.
Raycasting
PhysicsRayQueryParameters3D (Correct Class Name)
Important: The class is PhysicsRayQueryParameters3D, NOT PhysicsRayQuery3D.
Basic Raycast Setup
# Get the physics space
var space_state = get_world_3d().direct_space_state
# Create ray query parameters
var query = PhysicsRayQueryParameters3D.create(from_position, to_position)
# Optional: Exclude specific bodies
query.exclude = [self, other_body]
# Optional: Set collision mask (which layers to check)
query.collision_mask = 1 # Layer 1 only
query.collision_mask = 0b0011 # Layers 1 and 2
# Perform the raycast
var result = space_state.intersect_ray(query)
# Check if hit something
if result:
var hit_object = result.collider
var hit_position = result.position
var hit_normal = result.normalCommon Raycast Patterns
Camera-based raycast (first-person interaction):
var camera = get_viewport().get_camera_3d()
var from = camera.global_position
var to = from + (-camera.global_transform.basis.z * range)
var query = PhysicsRayQueryParameters3D.create(from, to)
query.exclude = [player]
var result = space_state.intersect_ray(query)Downward raycast (ground detection):
var from = global_position
var to = global_position + Vector3.DOWN * 10.0
var query = PhysicsRayQueryParameters3D.create(from, to)
var result = space_state.intersect_ray(query)
if result:
var distance_to_ground = from.distance_to(result.position)Line-of-sight check:
func has_line_of_sight(target: Node3D) -> bool:
var space_state = get_world_3d().direct_space_state
var from = global_position
var to = target.global_position
var query = PhysicsRayQueryParameters3D.create(from, to)
query.exclude = [self, target]
var result = space_state.intersect_ray(query)
return not result # True if nothing blockingShape Queries
PhysicsShapeQueryParameters3D
For checking if a shape overlaps with objects (useful for area attacks, detection zones).
# Create shape query
var query = PhysicsShapeQueryParameters3D.new()
# Set the shape (sphere, box, capsule, etc.)
var sphere = SphereShape3D.new()
sphere.radius = 2.0
query.shape = sphere
# Set transform (position and rotation)
query.transform = Transform3D(Basis(), global_position)
# Optional: collision mask
query.collision_mask = 1
# Perform query
var space_state = get_world_3d().direct_space_state
var results = space_state.intersect_shape(query)
# Iterate results
for result in results:
var collider = result.collider
# Do something with each overlapping objectCommon Shape Query Patterns
Area attack (sphere around player):
func perform_area_attack(radius: float, damage: float):
var query = PhysicsShapeQueryParameters3D.new()
var sphere = SphereShape3D.new()
sphere.radius = radius
query.shape = sphere
query.transform = Transform3D(Basis(), global_position)
query.collision_mask = 2 # Enemies layer
var space_state = get_world_3d().direct_space_state
var results = space_state.intersect_shape(query)
for result in results:
if result.collider.has_method("take_damage"):
result.collider.take_damage(damage)Cone detection (enemy field of view):
# Use multiple raycasts in a cone pattern
func detect_in_cone(angle_degrees: float, range: float) -> Array:
var detected = []
var rays = 5 # Number of rays in cone
for i in range(rays):
var angle = -angle_degrees/2 + (angle_degrees / (rays-1)) * i
var direction = global_transform.basis.z.rotated(Vector3.UP, deg_to_rad(angle))
var from = global_position
var to = from + direction * range
var query = PhysicsRayQueryParameters3D.create(from, to)
var result = space_state.intersect_ray(query)
if result:
detected.append(result.collider)
return detectedCollision Layers and Masks
Understanding layers is critical for efficient physics.
Layer Setup
# In project settings, name your layers:
# Layer 1: World (walls, floors)
# Layer 2: Player
# Layer 3: Enemies
# Layer 4: Projectiles
# Layer 5: Interactables
# Set what layers an object is ON
collision_layer = 0b00010 # Layer 2 (Player)
# Set what layers an object can COLLIDE WITH
collision_mask = 0b00101 # Layers 1 (World) and 3 (Enemies)Common Layer Patterns
Player should collide with world and enemies:
# Player
collision_layer = 0b00010 # Layer 2
collision_mask = 0b00101 # Layers 1 (world) and 3 (enemies)Enemy projectile should hit player but not other enemies:
# Enemy Projectile
collision_layer = 0b01000 # Layer 4
collision_mask = 0b00011 # Layers 1 (world) and 2 (player)Interaction raycast should only hit interactables:
var query = PhysicsRayQueryParameters3D.create(from, to)
query.collision_mask = 0b10000 # Layer 5 (interactables) onlyArea3D vs RigidBody3D vs StaticBody3D vs CharacterBody3D
When to Use Each
StaticBody3D
- Non-moving collision objects (walls, floors, obstacles)
- Cannot be moved by physics
- Very efficient
CharacterBody3D
- Player characters, NPCs with custom movement
- Controlled by code, not physics engine
- Has
move_and_slide()for smooth movement - Use for anything you want direct control over
RigidBody3D
- Objects controlled by physics (crates, barrels, ragdolls)
- Affected by gravity, forces, collisions
- Good for destructible/pushable objects
Area3D
- Trigger zones (doesn't block movement)
- Detection volumes (pickup radius, damage zones)
- Cannot collide physically, only detect overlaps
Common Patterns
Damage zone (Area3D):
extends Area3D
signal body_damaged(body: Node3D, damage: float)
@export var damage_per_second: float = 10.0
func _ready():
body_entered.connect(_on_body_entered)
body_exited.connect(_on_body_exited)
var bodies_inside: Array[Node3D] = []
func _on_body_entered(body: Node3D):
bodies_inside.append(body)
func _on_body_exited(body: Node3D):
bodies_inside.erase(body)
func _process(delta: float):
for body in bodies_inside:
if body.has_method("take_damage"):
body.take_damage(damage_per_second * delta)
body_damaged.emit(body, damage_per_second * delta)Pickup detection (Area3D child of player):
# As child of CharacterBody3D (player)
extends Area3D
func _ready():
collision_layer = 0 # Not on any layer
collision_mask = 0b10000 # Only detect interactables
body_entered.connect(_on_pickup_entered)
func _on_pickup_entered(body: Node3D):
if body.has_method("pickup"):
body.pickup(get_parent()) # Pass player to pickupPerformance Tips
1. Use collision layers efficiently - Don't check unnecessary layers 2. Limit raycast distance - Shorter rays are faster 3. Cache space_state - Don't call get_world_3d().direct_space_state every frame if possible 4. Use Areas for detection - More efficient than frequent raycasts 5. Exclude irrelevant bodies - Use query.exclude to skip known objects
Common Gotchas
1. Wrong class name: It's PhysicsRayQueryParameters3D, not PhysicsRayQuery3D 2. Forgetting collision mask: Raycast won't hit anything if mask is 0 3. Self-collision: Always exclude self from queries 4. Result dictionary: Check if result: before accessing result.collider 5. Global vs local positions: Raycasts use global coordinates 6. Basis direction: -transform.basis.z is forward, not transform.basis.z
Debugging Physics
# Visualize raycasts in editor
func _draw_debug_ray(from: Vector3, to: Vector3, hit: bool):
var immediate = ImmediateMesh.new()
var material = StandardMaterial3D.new()
material.albedo_color = Color.RED if hit else Color.GREEN
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
# Add debug line drawing here
# (Use MeshInstance3D with ImmediateMesh for runtime visualization)
# Print collision layers
func debug_print_layers():
print("collision_layer: ", collision_layer, " (binary: ", String.num_int64(collision_layer, 2), ")")
print("collision_mask: ", collision_mask, " (binary: ", String.num_int64(collision_mask, 2), ")")Further Reading
- Official Godot 4 Physics Docs: https://docs.godotengine.org/en/stable/tutorials/physics/
- Understanding collision layers: https://docs.godotengine.org/en/stable/tutorials/physics/physics_introduction.html#collision-layers-and-masks
#!/usr/bin/env python3
"""
Validates Godot .tres (resource) files for common syntax errors.
Common mistakes this catches:
- Using preload() instead of ExtResource()
- Using GDScript syntax (var, const, func) in resource files
- Missing ExtResource declarations
- Incorrect array type syntax
"""
import sys
import re
from pathlib import Path
class TresValidator:
def __init__(self, file_path):
self.file_path = Path(file_path)
self.errors = []
self.warnings = []
def validate(self):
"""Run all validation checks."""
if not self.file_path.exists():
self.errors.append(f"File not found: {self.file_path}")
return False
content = self.file_path.read_text()
self._check_preload_usage(content)
self._check_gdscript_keywords(content)
self._check_array_syntax(content)
self._check_resource_references(content)
return len(self.errors) == 0
def _check_preload_usage(self, content):
"""Check for illegal preload() usage."""
preload_pattern = r'preload\s*\('
matches = list(re.finditer(preload_pattern, content, re.IGNORECASE))
for match in matches:
line_num = content[:match.start()].count('\n') + 1
self.errors.append(
f"Line {line_num}: Found 'preload()' - use ExtResource() instead in .tres files"
)
def _check_gdscript_keywords(self, content):
"""Check for GDScript keywords that shouldn't be in .tres files."""
# Split into lines and check each
lines = content.split('\n')
keywords = ['var ', 'const ', 'func ', 'class_name ', 'extends ']
for i, line in enumerate(lines, 1):
# Skip comments
if line.strip().startswith('#'):
continue
for keyword in keywords:
if keyword in line and not line.strip().startswith('['):
self.errors.append(
f"Line {i}: Found GDScript keyword '{keyword.strip()}' - "
f"not allowed in .tres files"
)
def _check_array_syntax(self, content):
"""Check for proper typed array syntax."""
# Look for array assignments without type
untyped_array_pattern = r'=\s*\[[^\]]*\]'
lines = content.split('\n')
for i, line in enumerate(lines, 1):
# Skip resource headers
if line.strip().startswith('[') and line.strip().endswith(']'):
continue
if re.search(untyped_array_pattern, line):
# Check if it's preceded by Array[Type]
if 'Array[' not in line:
self.warnings.append(
f"Line {i}: Array may need type specification - "
f"use Array[Type]([...]) syntax"
)
def _check_resource_references(self, content):
"""Check that ExtResource IDs are declared."""
# Find all ExtResource usages
usage_pattern = r'ExtResource\s*\(\s*"([^"]+)"\s*\)'
usages = re.findall(usage_pattern, content)
# Find all ExtResource declarations
decl_pattern = r'\[ext_resource[^\]]*id\s*=\s*"([^"]+)"'
declarations = re.findall(decl_pattern, content)
# Check for undefined references
for resource_id in set(usages):
if resource_id not in declarations:
self.errors.append(
f"ExtResource('{resource_id}') used but not declared - "
f"add [ext_resource ...] declaration"
)
def print_results(self):
"""Print validation results."""
print(f"\n{'='*60}")
print(f"Validating: {self.file_path}")
print(f"{'='*60}\n")
if self.errors:
print("❌ ERRORS:")
for error in self.errors:
print(f" • {error}")
print()
if self.warnings:
print("⚠️ WARNINGS:")
for warning in self.warnings:
print(f" • {warning}")
print()
if not self.errors and not self.warnings:
print("✅ No issues found!\n")
elif not self.errors:
print("✅ No errors (only warnings)\n")
else:
print(f"❌ Found {len(self.errors)} error(s)\n")
return len(self.errors) == 0
def main():
if len(sys.argv) < 2:
print("Usage: validate_tres.py <file.tres>")
sys.exit(1)
validator = TresValidator(sys.argv[1])
validator.validate()
success = validator.print_results()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Validates Godot .tscn (scene) files for common structural errors.
Common mistakes this catches:
- Missing ExtResource declarations for used resources
- Invalid parent references
- Malformed node entries
- UID format issues
"""
import sys
import re
from pathlib import Path
class TscnValidator:
def __init__(self, file_path):
self.file_path = Path(file_path)
self.errors = []
self.warnings = []
def validate(self):
"""Run all validation checks."""
if not self.file_path.exists():
self.errors.append(f"File not found: {self.file_path}")
return False
content = self.file_path.read_text()
self._check_header(content)
self._check_resource_references(content)
self._check_node_structure(content)
self._check_parent_references(content)
return len(self.errors) == 0
def _check_header(self, content):
"""Check for valid scene file header."""
lines = content.split('\n')
if not lines:
self.errors.append("Empty file")
return
first_line = lines[0].strip()
if not first_line.startswith('[gd_scene'):
self.errors.append(
"Invalid header - should start with [gd_scene load_steps=N format=3 ...]"
)
# Check for UID
if 'uid=' not in first_line:
self.warnings.append("Missing UID in header - may cause reference issues")
def _check_resource_references(self, content):
"""Check that all ExtResource and SubResource references are declared."""
# Find all ExtResource usages
ext_usage_pattern = r'ExtResource\s*\(\s*"([^"]+)"\s*\)'
ext_usages = re.findall(ext_usage_pattern, content)
# Find all ExtResource declarations
ext_decl_pattern = r'\[ext_resource[^\]]*id\s*=\s*"([^"]+)"'
ext_declarations = re.findall(ext_decl_pattern, content)
# Check for undefined ExtResource references
for resource_id in set(ext_usages):
if resource_id not in ext_declarations:
self.errors.append(
f"ExtResource('{resource_id}') used but not declared"
)
# Find all SubResource usages
sub_usage_pattern = r'SubResource\s*\(\s*"([^"]+)"\s*\)'
sub_usages = re.findall(sub_usage_pattern, content)
# Find all SubResource declarations
sub_decl_pattern = r'\[sub_resource[^\]]*id\s*=\s*"([^"]+)"'
sub_declarations = re.findall(sub_decl_pattern, content)
# Check for undefined SubResource references
for resource_id in set(sub_usages):
if resource_id not in sub_declarations:
self.errors.append(
f"SubResource('{resource_id}') used but not declared"
)
def _check_node_structure(self, content):
"""Check for valid node entries."""
lines = content.split('\n')
node_pattern = r'^\[node\s+name="([^"]+)"'
for i, line in enumerate(lines, 1):
match = re.match(node_pattern, line)
if match:
# Check if node has required attributes
if 'type=' not in line and 'instance=' not in line and 'parent=' in line:
# Child nodes without type or instance might be invalid
self.warnings.append(
f"Line {i}: Node '{match.group(1)}' has parent but no type or instance"
)
def _check_parent_references(self, content):
"""Check that parent references are valid."""
lines = content.split('\n')
# Collect all node names
node_pattern = r'\[node\s+name="([^"]+)"'
nodes = []
for line in lines:
match = re.match(node_pattern, line)
if match:
nodes.append(match.group(1))
# Check parent references
parent_pattern = r'parent="([^"]+)"'
for i, line in enumerate(lines, 1):
match = re.search(parent_pattern, line)
if match:
parent = match.group(1)
# "." is valid (root), others should exist
if parent != "." and '/' not in parent:
# Simple parent reference
if parent not in nodes:
self.warnings.append(
f"Line {i}: Parent '{parent}' not found in scene"
)
def print_results(self):
"""Print validation results."""
print(f"\n{'='*60}")
print(f"Validating: {self.file_path}")
print(f"{'='*60}\n")
if self.errors:
print("❌ ERRORS:")
for error in self.errors:
print(f" • {error}")
print()
if self.warnings:
print("⚠️ WARNINGS:")
for warning in self.warnings:
print(f" • {warning}")
print()
if not self.errors and not self.warnings:
print("✅ No issues found!\n")
elif not self.errors:
print("✅ No errors (only warnings)\n")
else:
print(f"❌ Found {len(self.errors)} error(s)\n")
return len(self.errors) == 0
def main():
if len(sys.argv) < 2:
print("Usage: validate_tscn.py <file.tscn>")
sys.exit(1)
validator = TscnValidator(sys.argv[1])
validator.validate()
success = validator.print_results()
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
Related skills
FAQ
Which Godot file formats does it cover?
It covers .gd (GDScript), .tscn (scenes), and .tres (resource) files and their differing syntax rules.
Does it help catch file errors?
Yes, it bundles validate_tres.py and validate_tscn.py to catch syntax errors before testing in Godot.