godot-modernize-gdscript
Modernizes GDScript 1.0 (Godot 3.x) code to GDScript 2.0 (Godot 4.x) syntax. Converts yield to await, onready to @onready, export to @export, setget to property syntax, and adds optional static typing. Essential for Godot 3.x to 4.x migration projects.
SKILL.md
Full skill instructions
Godot GDScript 2.0 Modernizer
Converts GDScript 1.0 (Godot 3.x) patterns to GDScript 2.0 (Godot 4.x) syntax.
Core Conversions
This skill performs five key modernizations:
| GDScript 1.0 | GDScript 2.0 | Pattern |
|---|---|---|
yield(object, "signal") | await object.signal | Async operations |
onready var | @onready var | Node references |
export var | @export var | Inspector variables |
var x setget set_x, get_x | Property syntax | Getters/setters |
var health = 100 | var health: int = 100 | Static typing |
UPON INVOCATION - START HERE
When this skill is invoked, IMMEDIATELY execute:
1. Verify Godot Project (5 seconds)
ls project.godot 2>/dev/null && echo "✓ Godot project detected" || echo "✗ Not a Godot project"
If NOT a Godot project:
- Inform user this skill only works on Godot projects
- STOP here
If IS a Godot project:
- Proceed to step 2
2. Detect GDScript Version (10 seconds)
# Check for Godot 3.x patterns
echo "=== Detecting GDScript 1.0 Patterns ==="
echo "yield statements:"
grep -rn "yield(" --include="*.gd" . | wc -l
echo "onready declarations:"
grep -rn "^onready var" --include="*.gd" . | wc -l
echo "export declarations:"
grep -rn "^export var\|^export(int)\|^export(float)" --include="*.gd" . | wc -l
echo "setget properties:"
grep -rn "setget" --include="*.gd" . | wc -l
echo "untyped variables:"
grep -rn "^var [a-z]" --include="*.gd" . | grep -v ":" | wc -l
3. Present Findings
Show the user:
=== GDScript 2.0 Modernization Analysis ===
Project: [project name]
Current: GDScript 1.0 (Godot 3.x style)
Patterns to modernize:
- yield statements: X
- onready variables: X
- export variables: X
- setget properties: X
- untyped variables: X
Total: X modernizations needed
Modernization includes:
✓ yield → await conversion
✓ onready → @onready
✓ export → @export
✓ setget → property syntax
✓ Optional static typing
✓ Git commit per file
✓ Backup before changes
Would you like me to:
1. Modernize all files (recommended)
2. Show detailed breakdown first
3. Select specific conversions
4. Cancel
4. Wait for User Choice
- If 1 (Proceed): Start Phase 2 immediately
- If 2 (Details): Show file-by-file breakdown, then offer to proceed
- If 3 (Selective): Ask which conversions to apply
- If 4 (Cancel): Exit skill
Phase 1: Analysis & Inventory
1.1 Create File Inventory
# Find all .gd files
find . -name "*.gd" -type f | sort > /tmp/gdscript_files.txt
wc -l /tmp/gdscript_files.txt
echo "files found"
1.2 Analyze Each File
For each .gd file, detect patterns:
# Analyze patterns per file
for file in $(cat /tmp/gdscript_files.txt); do
echo "=== $file ==="
grep -c "yield(" "$file" 2>/dev/null || echo 0
grep -c "^onready var" "$file" 2>/dev/null || echo 0
grep -c "^export" "$file" 2>/dev/null || echo 0
grep -c "setget" "$file" 2>/dev/null || echo 0
done
1.3 Create Modernization Plan
Modernization Plan:
===================
1. yield → await conversions (X files)
2. onready → @onready (X files)
3. export → @export (X files)
4. setget → properties (X files)
5. Static typing (X files)
Total files to modify: X
Estimated time: Auto (user doesn't wait)
Backup created: YES (git tag)
Rollback available: YES
Phase 2: Modernization Operations
Conversion A: yield → await
Detection:
grep -rn "yield(" --include="*.gd" .
Common Patterns:
| Old Pattern | New Pattern |
|---|---|
yield(get_tree().create_timer(1.0), "timeout") | await get_tree().create_timer(1.0).timeout |
yield(timer, "timeout") | await timer.timeout |
yield(animation_player, "animation_finished") | await animation_player.animation_finished |
yield(get_tree(), "idle_frame") | await get_tree().process_frame |
Transformation Process:
-
Extract yield statement
# Before yield(get_tree().create_timer(2.0), "timeout") -
Convert to await
# After await get_tree().create_timer(2.0).timeout -
Handle variable assignment
# Before var result = yield(async_function(), "completed") # After var result = await async_function()
Implementation:
# Pseudo-code for replacement
def convert_yield_to_await(line):
# Pattern: yield(object, "signal_name")
# Convert to: await object.signal_name
import re
pattern = r'yield\(([^,]+),\s*"([^"]+)"\)'
replacement = r'await \1.\2'
return re.sub(pattern, replacement, line)
Conversion B: onready → @onready
Detection:
grep -rn "^onready var" --include="*.gd" .
Transformation:
# Before
onready var player = $Player
onready var health_bar = $UI/HealthBar
# After
@onready var player: Node = $Player
@onready var health_bar: ProgressBar = $UI/HealthBar
Process:
- Replace
onreadywith@onready - Add type hints where detectable
- Keep variable name and initialization
Type Inference (Optional):
# If $Player is CharacterBody2D in scene
@onready var player: CharacterBody2D = $Player
# If $Timer is Timer node
@onready var _timer: Timer = $Timer
Conversion C: export → @export
Detection:
grep -rn "^export" --include="*.gd" .
Transformation:
# Before
export var speed = 200
export(int) var max_health = 100
export(float) var jump_force = 500.0
export(String) var character_name = "Player"
export(NodePath) var target_path
# After
@export var speed: float = 200.0
@export var max_health: int = 100
@export var jump_force: float = 500.0
@export var character_name: String = "Player"
@export var target_path: NodePath
Export Types Mapping:
| Old Export | New Export | Type |
|---|---|---|
export(int) | @export var x: int | Integer |
export(float) | @export var x: float | Float |
export(String) | @export var x: String | String |
export(bool) | @export var x: bool | Boolean |
export(Color) | @export var x: Color | Color |
export(Vector2) | @export var x: Vector2 | Vector2 |
export(Vector3) | @export var x: Vector3 | Vector3 |
export(NodePath) | @export var x: NodePath | NodePath |
export var | @export var x: Type | Inferred |
Conversion D: setget → Property Syntax
Detection:
grep -rn "setget" --include="*.gd" .
Transformation:
# Before (GDScript 1.0)
var health = 100 setget set_health, get_health
func set_health(value):
health = clamp(value, 0, max_health)
health_changed.emit(health)
func get_health():
return health
# After (GDScript 2.0)
var health: int = 100:
set(value):
health = clamp(value, 0, max_health)
health_changed.emit(health)
get:
return health
Process:
- Remove
setgetfrom variable declaration - Add colon after type
- Inline setter and getter
- Use
set(value):andget:syntax
Complex Example:
# Before
var score = 0 setget set_score
func set_score(value):
score = max(0, value)
update_ui()
# After
var score: int = 0:
set(value):
score = max(0, value)
update_ui()
Conversion E: Static Typing (Optional)
Detection:
grep -rn "^var [a-z_]* = " --include="*.gd" . | grep -v ":"
Type Inference:
| Value | Inferred Type |
|---|---|
= 100 | int |
= 3.14 | float |
= "text" | String |
= true | bool |
= Vector2(x, y) | Vector2 |
= $Node | Node type from scene |
Transformation:
# Before
var health = 100
var speed = 200.5
var name = "Player"
var active = true
var position = Vector2.ZERO
# After
var health: int = 100
var speed: float = 200.5
var name: String = "Player"
var active: bool = true
var position: Vector2 = Vector2.ZERO
Phase 3: Execution & Safety
3.1 Create Git Baseline
# Create backup tag
git tag gdscript1-baseline-$(date +%Y%m%d-%H%M%S)
# Stage any current changes
git add .
git commit -m "Baseline: Pre-GDScript 2.0 modernization" || echo "No changes to commit"
3.2 Process Files Sequentially
For each .gd file:
# Process file
python3 << 'EOF'
import re
def modernize_gdscript(content):
# Conversion 1: yield → await
content = re.sub(
r'yield\(([^,]+),\s*"([^"]+)"\)',
r'await \1.\2',
content
)
# Conversion 2: onready → @onready
content = re.sub(
r'^onready var',
'@onready var',
content,
flags=re.MULTILINE
)
# Conversion 3: export → @export (simple)
content = re.sub(
r'^export var',
'@export var',
content,
flags=re.MULTILINE
)
return content
# Read, process, write
with open("script.gd", "r") as f:
content = f.read()
new_content = modernize_gdscript(content)
with open("script.gd", "w") as f:
f.write(new_content)
EOF
3.3 Commit Per File
# After each file modification
git add "$file"
git commit -m "Modernize: $file to GDScript 2.0
- Converted yield to await
- Updated onready to @onready
- Migrated export to @export
- Applied static typing"
3.4 Validation After Each File
# Validate syntax
godot --headless --quit-after 2 project.godot 2>&1 | grep -i "error" && {
echo "Syntax error detected in $file"
git reset --hard HEAD~1
echo "Reverted changes to $file"
}
Phase 4: Verification & Testing
4.1 Syntax Validation
# Check all modified files for syntax errors
for file in $(git diff --name-only HEAD~10..HEAD | grep "\.gd$"); do
echo "Checking $file..."
# Basic syntax check
gdscript_tool --check "$file" 2>/dev/null || echo "Manual review needed: $file"
done
4.2 Pattern Verification
# Verify no old patterns remain
echo "Checking for remaining GDScript 1.0 patterns..."
echo "yield statements remaining:"
grep -rn "yield(" --include="*.gd" . | wc -l
echo "onready remaining:"
grep -rn "^onready var" --include="*.gd" . | wc -l
echo "export remaining:"
grep -rn "^export var\|^export(" --include="*.gd" . | wc -l
4.3 Godot Project Test
# Open project in Godot to verify
godot --editor project.godot &
sleep 5
# Check for errors
# (User should visually verify no red errors in Output panel)
Examples
Example 1: Complete Script Modernization
Before (GDScript 1.0):
extends CharacterBody2D
export var speed = 200
export(int) var health = 100
onready var sprite = $Sprite
onready var animation = $AnimationPlayer
var damage = 10 setget set_damage
func set_damage(value):
damage = clamp(value, 0, 100)
func _ready():
yield(get_tree().create_timer(1.0), "timeout")
start_game()
func take_damage(amount):
health -= amount
if health <= 0:
yield(play_death_animation(), "completed")
queue_free()
func play_death_animation():
animation.play("death")
yield(animation, "animation_finished")
After (GDScript 2.0):
extends CharacterBody2D
@export var speed: float = 200.0
@export var health: int = 100
@onready var sprite: Sprite2D = $Sprite
@onready var animation: AnimationPlayer = $AnimationPlayer
var damage: int = 10:
set(value):
damage = clamp(value, 0, 100)
func _ready():
await get_tree().create_timer(1.0).timeout
start_game()
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
await play_death_animation()
queue_free()
func play_death_animation() -> void:
animation.play("death")
await animation.animation_finished
Example 2: Signal Connection Modernization
Before:
func _ready():
button.connect("pressed", self, "_on_button_pressed")
timer.connect("timeout", self, "_on_timeout")
func _on_button_pressed():
pass
func _on_timeout():
pass
After:
func _ready():
button.pressed.connect(_on_button_pressed)
timer.timeout.connect(_on_timeout)
func _on_button_pressed() -> void:
pass
func _on_timeout() -> void:
pass
Success Criteria
Modernization complete when:
- ✓ Zero
yield(statements remain (converted to await) - ✓ Zero
onready varremain (converted to @onready) - ✓ Zero
export varremain (converted to @export) - ✓ Zero
setgetremain (converted to property syntax) - ✓ All scripts compile without errors
- ✓ No functional changes (behavior identical)
- ✓ Git history shows clear modernization commits
Common Issues & Solutions
Issue 1: yield with function calls
Problem:
var result = yield(load_data(), "completed")
Solution:
var result = await load_data()
Issue 2: Coroutines with state
Problem:
func process():
yield(do_step_1(), "completed")
yield(do_step_2(), "completed")
Solution:
func process() -> void:
await do_step_1()
await do_step_2()
Issue 3: setget with only getter or setter
Problem:
var score = 0 setget , get_score # Only getter
Solution:
var score: int = 0:
get:
return score
Rollback Procedure
If modernization causes issues:
# Find baseline tag
git tag | grep "gdscript1-baseline"
# Reset to pre-modernization state
git reset --hard <baseline-tag>
# Or revert specific commits
git revert <modernization-commit-hash>
Integration with Other Skills
Use before:
godot-refactor- Modernize syntax first, then refactor architecturegodot-migrate-tilemap- Update code before TileMap migration
Use after:
- Project conversion from Godot 3.x to 4.x
godot-organize-project- Organize files first
This skill automates the tedious parts of GDScript 1.0 → 2.0 migration while preserving exact functionality.
