add godot skill
This commit is contained in:
parent
51a66b9575
commit
fa5d2ba5fa
|
|
@ -0,0 +1,147 @@
|
||||||
|
---
|
||||||
|
name: godot-47-migration
|
||||||
|
description: "Godot 4.7 migration guide: new features (HDR, Steam Frame, AreaLight3D, VirtualJoystick, tween_await, Perfetto tracing), breaking changes, and step-by-step migration patterns for existing Godot 4.x projects. Use when upgrading a Godot project to version 4.7."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- migration
|
||||||
|
- godot-4.7
|
||||||
|
- breaking-changes
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot 4.7 Migration Guide
|
||||||
|
|
||||||
|
Complete guide for migrating Godot 4.x projects to version 4.7.
|
||||||
|
|
||||||
|
## What's New in Godot 4.7
|
||||||
|
|
||||||
|
### Rendering & Visual
|
||||||
|
- **HDR output settings** — Configure HDR display support in project settings
|
||||||
|
- **New AreaLight3D** — Enhanced area lighting with new modes and intensity profiles
|
||||||
|
- **Nearest-neighbor viewport scaling** — Pixel-perfect rendering for retro-style games
|
||||||
|
|
||||||
|
### UI & Controls
|
||||||
|
- **VirtualJoystick** — Built-in virtual joystick node replaces third-party solutions
|
||||||
|
- **PopupMenu search** — Native search in popup menus
|
||||||
|
- **Control offset transforms** — New transform system for UI animation
|
||||||
|
|
||||||
|
### Animation
|
||||||
|
- **Collapsible animation tracks** — Better organization in AnimationTrackEdit
|
||||||
|
- **tween_await()** — Await tween completion for sequential chains
|
||||||
|
|
||||||
|
### Mobile & Android
|
||||||
|
- **Perfetto tracing** — Built-in Android performance profiling
|
||||||
|
- **Android XR settings** — New project settings for extended reality
|
||||||
|
|
||||||
|
### Steam Integration
|
||||||
|
- **Steam Frame configuration** — Enhanced Steam overlay and frame support
|
||||||
|
|
||||||
|
## Step-by-Step Migration
|
||||||
|
|
||||||
|
### Step 1: Backup Your Project
|
||||||
|
```bash
|
||||||
|
cp -r MyGame MyGame_backup_4.6
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: Open in Godot 4.7 Editor
|
||||||
|
1. Launch Godot 4.7
|
||||||
|
2. Open your existing project
|
||||||
|
3. Editor will auto-migrate project.godot settings
|
||||||
|
4. Review migration warnings in the output panel
|
||||||
|
|
||||||
|
### Step 3: Update project.godot Settings
|
||||||
|
```ini
|
||||||
|
# Add HDR support if needed
|
||||||
|
[rendering]
|
||||||
|
environment/default_environment="default_world.tres"
|
||||||
|
|
||||||
|
# Add Steam Frame config if using Steam
|
||||||
|
[steam]
|
||||||
|
app_id="YOUR_APP_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 4: Review AreaLight3D Changes
|
||||||
|
- Check all AreaLight3D nodes for new property options
|
||||||
|
- Update intensity profiles if using custom values
|
||||||
|
|
||||||
|
### Step 5: Replace Custom VirtualJoysticks
|
||||||
|
If using third-party virtual joystick implementations:
|
||||||
|
1. Remove custom VirtualJoystick scripts
|
||||||
|
2. Add the built-in VirtualJoystick node to your scene
|
||||||
|
3. Update input handling code (axis names may differ)
|
||||||
|
|
||||||
|
### Step 6: Update Tween Patterns
|
||||||
|
Replace nested callbacks with `tween_await()`:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Before (4.6)
|
||||||
|
func play_sequence():
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_property($Sprite, "position:x", 100, 1.0)
|
||||||
|
tween.finished.connect(func():
|
||||||
|
var tween2 = create_tween()
|
||||||
|
tween2.tween_property($Sprite, "position:y", 50, 0.5)
|
||||||
|
)
|
||||||
|
|
||||||
|
# After (4.7)
|
||||||
|
func play_sequence():
|
||||||
|
await create_tween().tween_property($Sprite, "position:x", 100, 1.0).finished
|
||||||
|
await create_tween().tween_property($Sprite, "position:y", 50, 0.5).finished
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 7: Test All Platforms
|
||||||
|
- Windows desktop build
|
||||||
|
- Linux/X11 build
|
||||||
|
- Android build (verify Perfetto tracing works)
|
||||||
|
- HTML5 build
|
||||||
|
- Steam build (if applicable)
|
||||||
|
|
||||||
|
## Breaking Changes Checklist
|
||||||
|
|
||||||
|
- [ ] Review VirtualJoystick API changes if using custom implementations
|
||||||
|
- [ ] Update tween callback patterns to use await syntax
|
||||||
|
- [ ] Test HDR rendering pipeline impact on visual quality
|
||||||
|
- [ ] Verify Android XR settings if targeting XR devices
|
||||||
|
- [ ] Check Steam Frame configuration compatibility
|
||||||
|
|
||||||
|
## Godot 4.7 Feature Quick Reference
|
||||||
|
|
||||||
|
| Feature | Category | Skill Coverage |
|
||||||
|
|---------|----------|---------------|
|
||||||
|
| HDR output | Rendering | godot-project-setup |
|
||||||
|
| AreaLight3D | Rendering | godot-code-review |
|
||||||
|
| VirtualJoystick | Mobile | godot-brainstorming |
|
||||||
|
| tween_await() | Animation | godot-gdscript-patterns |
|
||||||
|
| Perfetto tracing | Profiling | godot-debugging |
|
||||||
|
| Steam Frame | Networking | godot-project-setup |
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools support migration:
|
||||||
|
|
||||||
|
- **`script_validate`** — Run with `suggestTweenAwait: true` to detect nested tween callbacks and suggest `await` migration
|
||||||
|
- **`project_settings_read`** — Check current HDR/Steam settings before migration
|
||||||
|
- **`project_settings_set`** — Apply 4.7 settings (e.g., `rendering/hdr`, `steam/frame/*`) directly
|
||||||
|
- **`file_search`** — Search for deprecated API patterns across the project
|
||||||
|
- **`class_db_info`** — Verify new 4.7 classes like AreaLight3D and VirtualJoystick are available
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Full Migration Guide](../../docs/migration-4.7.md)
|
||||||
|
- [Godot 4.7 Changelog](https://github.com/godotengine/godot/releases)
|
||||||
|
- [Godot 4 Migration Guide](https://godotengine.org/article/migration-guide-godot-4/)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Claude Code extension for godot-4.7-migration
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "migrating to Godot 4.7"
|
||||||
|
- "Godot version upgrade"
|
||||||
|
- "breaking changes in Godot 4.7"
|
||||||
|
- "new Godot 4.7 features"
|
||||||
|
- "HDR setup in Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-4.7-migration
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "migrating to Godot 4.7"
|
||||||
|
- "Godot version upgrade"
|
||||||
|
- "breaking changes in Godot 4.7"
|
||||||
|
- "new Godot 4.7 features"
|
||||||
|
- "HDR setup in Godot"
|
||||||
|
|
@ -0,0 +1,303 @@
|
||||||
|
---
|
||||||
|
name: godot-animation
|
||||||
|
description: "Animation systems for Godot 4.x: AnimationPlayer vs AnimationTree, state machines, blend spaces (1D/2D/directional), animation libraries, root motion, collapsible tracks, and tween_await(). Use when building character animations, cutscenes, or visual effects in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- animation
|
||||||
|
- animationplayer
|
||||||
|
- animationtree
|
||||||
|
- blend-space
|
||||||
|
- root-motion
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Animation Systems for Godot 4.x
|
||||||
|
|
||||||
|
Comprehensive patterns for character animation, state machines, and visual effects in Godot 4.x.
|
||||||
|
|
||||||
|
## AnimationPlayer vs AnimationTree
|
||||||
|
|
||||||
|
### AnimationPlayer — Simple Animations
|
||||||
|
|
||||||
|
Use AnimationPlayer for linear sequences, cutscenes, and simple property animations.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
@onready var anim_player = $AnimationPlayer
|
||||||
|
|
||||||
|
func play_attack():
|
||||||
|
anim_player.play("attack")
|
||||||
|
|
||||||
|
func play_transition(target: String, fade_time: float = 0.2):
|
||||||
|
anim_player.play(target, fade_time)
|
||||||
|
|
||||||
|
# Signal-based animation completion
|
||||||
|
func _ready():
|
||||||
|
anim_player.animation_finished.connect(_on_animation_finished)
|
||||||
|
|
||||||
|
func _on_animation_finished(anim_name: String):
|
||||||
|
if anim_name == "attack":
|
||||||
|
print("Attack animation complete")
|
||||||
|
```
|
||||||
|
|
||||||
|
### AnimationTree — State Machine Animations
|
||||||
|
|
||||||
|
Use AnimationTree for complex character animations with branching states (idle, walk, run, jump).
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
@onready var anim_tree = $AnimationTree
|
||||||
|
@onready var anim_state = anim_tree.get("state_machine")
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
anim_state.travel("Idle") # Start at Idle state
|
||||||
|
|
||||||
|
func set_animation(state_name: String):
|
||||||
|
if anim_state.has_state(state_name):
|
||||||
|
anim_state.travel(state_name)
|
||||||
|
|
||||||
|
func _physics_process(delta: float):
|
||||||
|
var speed = velocity.length()
|
||||||
|
anim_tree.set("parameters/Move/speed", speed)
|
||||||
|
anim_tree.process(delta)
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Which
|
||||||
|
|
||||||
|
| Use Case | Tool |
|
||||||
|
|----------|------|
|
||||||
|
| Cutscenes, UI animations | AnimationPlayer |
|
||||||
|
| Character movement states | AnimationTree + StateMachine |
|
||||||
|
| Simple one-shot effects | AnimationPlayer |
|
||||||
|
| Blending between movements | AnimationTree + BlendSpace2D |
|
||||||
|
| Complex combat combos | AnimationTree + AnimationNodeBlendTree |
|
||||||
|
|
||||||
|
## State Machines in AnimationTree
|
||||||
|
|
||||||
|
### Building a Character State Machine
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# In the AnimationTree, create an AnimationNodeStateMachine:
|
||||||
|
# States: Idle, Walk, Run, Jump, Fall, Attack
|
||||||
|
# Transitions: Idle <-> Walk, Idle <-> Run, Walk -> Run, etc.
|
||||||
|
|
||||||
|
@onready var anim_tree = $AnimationTree
|
||||||
|
@onready var state_machine = anim_tree.get("state_machine")
|
||||||
|
|
||||||
|
func _physics_process(delta: float):
|
||||||
|
var speed = velocity.length()
|
||||||
|
var new_state = "Idle"
|
||||||
|
|
||||||
|
if not is_on_floor():
|
||||||
|
new_state = "Fall" if velocity.y < 0 else "Jump"
|
||||||
|
elif speed > 0.1:
|
||||||
|
new_state = "Run" if speed > run_threshold else "Walk"
|
||||||
|
|
||||||
|
# Only transition if the target state exists
|
||||||
|
if state_machine.has_state(new_state):
|
||||||
|
state_machine.travel(new_state)
|
||||||
|
|
||||||
|
anim_tree.set("parameters/Move/speed", speed)
|
||||||
|
anim_tree.process(delta)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transition Conditions
|
||||||
|
|
||||||
|
Set transition conditions in the AnimationTree editor:
|
||||||
|
- **Blend**: Use blend amounts based on parameters (e.g., speed)
|
||||||
|
- **One Shot**: Play once then return (for attacks, interactions)
|
||||||
|
- **Transition Rules**: Define when transitions are allowed
|
||||||
|
|
||||||
|
## Blend Spaces
|
||||||
|
|
||||||
|
### 1D Blend Space
|
||||||
|
|
||||||
|
Use for animations ordered along a single axis (e.g., walk speed).
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# AnimationTree setup:
|
||||||
|
# BlendSpace1D: Walk -> Run -> Sprint
|
||||||
|
# Parameter: "speed" (float)
|
||||||
|
|
||||||
|
anim_tree.set("parameters/BlendSpace1D/speed", current_speed)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2D Blend Space
|
||||||
|
|
||||||
|
Use for directional movement blending.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# AnimationTree setup:
|
||||||
|
# BlendSpace2D: X = speed, Y = direction blend
|
||||||
|
# Parameters: "speed" and "direction"
|
||||||
|
|
||||||
|
anim_tree.set("parameters/BlendSpace2D/speed", velocity.length())
|
||||||
|
anim_tree.set("parameters/BlendSpace2D/direction", velocity.angle())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Directional Blend Space (Godot 4.x)
|
||||||
|
|
||||||
|
Use a directional blend for 8-directional character animations.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Set up AnimationNodeBlendDBone2D for directional blending
|
||||||
|
@onready var anim_tree = $AnimationTree
|
||||||
|
|
||||||
|
func _physics_process(delta: float):
|
||||||
|
var dir = velocity.normalized()
|
||||||
|
anim_tree.set("parameters/Directional/speed", velocity.length())
|
||||||
|
anim_tree.set("parameters/Directional/direction", dir)
|
||||||
|
anim_tree.process(delta)
|
||||||
|
```
|
||||||
|
## Animation Libraries and Blending Tracks
|
||||||
|
|
||||||
|
### Animation Libraries
|
||||||
|
|
||||||
|
Group related animations into libraries for organization.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Access library animations by name
|
||||||
|
@onready var anim_player = $AnimationPlayer
|
||||||
|
|
||||||
|
func play_animation(anim_name: String):
|
||||||
|
# Animations are organized by library in the editor
|
||||||
|
anim_player.play(anim_name)
|
||||||
|
|
||||||
|
# List available animations
|
||||||
|
func get_available_animations() -> Array:
|
||||||
|
return anim_player.get_animation_list()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Blending Tracks
|
||||||
|
|
||||||
|
Blend between multiple animation tracks for smooth transitions.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Set blend amount in AnimationTree
|
||||||
|
anim_tree.set("parameters/Idle/blend_amount", 1.0)
|
||||||
|
|
||||||
|
# Blend between two animations
|
||||||
|
anim_player.play("idle", -1.0, 1.0) # loop, 1s fade-in
|
||||||
|
anim_player.play("walk", -1.0, 0.5) # loop, 0.5s fade-in
|
||||||
|
|
||||||
|
# Crossfade by playing both simultaneously
|
||||||
|
# The AnimationTree blend space handles the mixing
|
||||||
|
```
|
||||||
|
## Root Motion for Character Movement
|
||||||
|
|
||||||
|
### Using Root Motion in AnimationTree
|
||||||
|
|
||||||
|
Root motion applies bone animation directly to the character transform.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name RootMotionCharacter extends CharacterBody3D
|
||||||
|
|
||||||
|
@onready var anim_tree = $AnimationTree
|
||||||
|
@onready var anim_state = anim_tree.get("state_machine")
|
||||||
|
|
||||||
|
@export var root_motion_enabled: bool = true
|
||||||
|
|
||||||
|
func _physics_process(delta: float):
|
||||||
|
# Calculate root motion displacement
|
||||||
|
var prev_transform = global_transform
|
||||||
|
|
||||||
|
# Process animation tree (applies root motion)
|
||||||
|
anim_tree.process(delta)
|
||||||
|
|
||||||
|
if root_motion_enabled:
|
||||||
|
var root_delta = global_transform.xform_inv(prev_transform)
|
||||||
|
velocity = Vector3(root_delta.origin.x / delta, 0, root_delta.origin.z / delta)
|
||||||
|
else:
|
||||||
|
# Use scripted movement instead
|
||||||
|
velocity = transform.basis.z * speed
|
||||||
|
|
||||||
|
move_and_slide()
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Root Motion
|
||||||
|
- Platformer jump arcs (precise landing positions)
|
||||||
|
- Attack animations with fixed reach distances
|
||||||
|
- Cutscene character movements
|
||||||
|
|
||||||
|
### When to Avoid Root Motion
|
||||||
|
- Multiplayer games (server authority conflicts)
|
||||||
|
- Characters that need precise position control
|
||||||
|
- Games with custom physics responses
|
||||||
|
## Godot 4.7 Animation Features
|
||||||
|
|
||||||
|
### Collapsible Animation Tracks
|
||||||
|
|
||||||
|
Godot 4.7 introduces collapsible animation tracks for better organization in the AnimationPlayer editor.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# No code change needed — this is an editor feature
|
||||||
|
# In the AnimationPlayer inspector, click track headers to collapse/expand
|
||||||
|
# Group related properties under collapsible sections
|
||||||
|
```
|
||||||
|
|
||||||
|
### tween_await() for Sequential Animations
|
||||||
|
|
||||||
|
Use await with tweens for clean sequential animation chains.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func play_combo_animation():
|
||||||
|
# Chain animations without nested callbacks
|
||||||
|
await create_tween().tween_property($Sprite, "offset:x", 10.0, 0.1).finished
|
||||||
|
await create_tween().tween_property($Sprite, "offset:x", 0.0, 0.1).finished
|
||||||
|
print("Combo animation complete")
|
||||||
|
|
||||||
|
# Or with AnimationPlayer
|
||||||
|
func play_with_wait():
|
||||||
|
var tween = create_tween()
|
||||||
|
anim_player.play("attack")
|
||||||
|
await tween.tween_interval(0.5)
|
||||||
|
anim_player.play("recovery")
|
||||||
|
```
|
||||||
|
## Animation Performance Tips
|
||||||
|
|
||||||
|
- Use **AnimationTree** with state machines instead of multiple AnimationPlayers
|
||||||
|
- Keep animation libraries small and focused
|
||||||
|
- Use **AnimationNodeSync** to synchronize multiple character animations
|
||||||
|
- Preload animation resources with preload() for always-needed scenes
|
||||||
|
- Avoid animating properties that can be set directly (e.g., position vs tween)
|
||||||
|
- Use `animation_finished` signal instead of polling for completion
|
||||||
|
|
||||||
|
## Quick Reference: Animation Methods
|
||||||
|
|
||||||
|
| Method | Use Case |
|
||||||
|
|--------|----------|
|
||||||
|
| `anim_player.play(name)` | Start an animation |
|
||||||
|
| `anim_player.play(name, fade_time)` | Crossfade to animation |
|
||||||
|
| `anim_tree.process(delta)` | Update state machine |
|
||||||
|
| `anim_tree.set("params/...", value)` | Set blend parameters |
|
||||||
|
| `state_machine.travel(name)` | Switch to a state |
|
||||||
|
| `create_tween()` | Create animation tweens |
|
||||||
|
| `await tween.finished` | Wait for tween completion |
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools enhance animation debugging:
|
||||||
|
|
||||||
|
- **`animation_play`** — Play any animation from the project without opening the editor
|
||||||
|
- **`animation_list`** — List all available animations and their libraries
|
||||||
|
- **`animation_state_get`** — Query current AnimationTree state machine status
|
||||||
|
- **`animation_preview_frame`** — Preview a specific frame of any animation
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [AnimationPlayer Documentation](references/animationplayer-guide.md)
|
||||||
|
- [AnimationTree State Machine Patterns](references/animationtree-states.md)
|
||||||
|
- [Blend Space Design Guide](references/blend-spaces.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-animation
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "animation systems in Godot"
|
||||||
|
- "Godot AnimationTree and state machines"
|
||||||
|
- "Godot blend spaces"
|
||||||
|
- "Godot skeleton IK"
|
||||||
|
- "Godot sprite animation"
|
||||||
|
- "Godot animation blending"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-animation
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "animation systems in Godot"
|
||||||
|
- "Godot AnimationTree and state machines"
|
||||||
|
- "Godot blend spaces"
|
||||||
|
- "Godot skeleton IK"
|
||||||
|
- "Godot sprite animation"
|
||||||
|
- "Godot animation blending"
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
---
|
||||||
|
name: godot-architecture
|
||||||
|
description: "Game architecture patterns for Godot 4.x: ECS-like composition, scene organization, dependency injection via signals, service locator vs direct reference, and scalable project structure. Use when designing or refactoring a Godot game's overall architecture."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- architecture
|
||||||
|
- ecs
|
||||||
|
- composition
|
||||||
|
- service-locator
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot Game Architecture Patterns
|
||||||
|
|
||||||
|
Architectural approaches for building scalable, maintainable Godot games.
|
||||||
|
|
||||||
|
## Scene Composition Over Inheritance
|
||||||
|
|
||||||
|
Godot's scene system naturally supports composition. Build complex behavior by combining small, focused scenes rather than deep inheritance chains.
|
||||||
|
|
||||||
|
### Component-Based Composition
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Player.tscn hierarchy:
|
||||||
|
# CharacterBody2D (root — movement logic)
|
||||||
|
# Sprite2D (visual)
|
||||||
|
# CollisionShape2D (physics body)
|
||||||
|
# HealthComponent (Node — health logic, emits signals)
|
||||||
|
# InventoryComponent (Node — item management)
|
||||||
|
# ExperienceComponent (Node — XP/leveling)
|
||||||
|
|
||||||
|
# Each component is a standalone .tscn with its own script.
|
||||||
|
# The player scene composes them without inheritance coupling.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Component Script Pattern
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name HealthComponent extends Node
|
||||||
|
|
||||||
|
signal health_changed(current: int, maximum: int)
|
||||||
|
signal died()
|
||||||
|
|
||||||
|
@export var max_health: int = 100
|
||||||
|
var current_health: int = 100:
|
||||||
|
set(value):
|
||||||
|
current_health = clampi(value, 0, max_health)
|
||||||
|
health_changed.emit(current_health, max_health)
|
||||||
|
if current_health <= 0:
|
||||||
|
died.emit()
|
||||||
|
|
||||||
|
func take_damage(amount: int) -> void:
|
||||||
|
current_health -= amount
|
||||||
|
|
||||||
|
func heal(amount: int) -> void:
|
||||||
|
current_health += amount
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency Management Patterns
|
||||||
|
|
||||||
|
### Direct Reference (Simple Cases)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Good for tightly-coupled parent-child relationships
|
||||||
|
@onready var health_component = $HealthComponent as HealthComponent
|
||||||
|
@onready var animation_player = $AnimationPlayer as AnimationPlayer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signal-Based Decoupling (Recommended)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Components communicate via signals — no direct references needed
|
||||||
|
func _ready():
|
||||||
|
$HealthComponent.died.connect(_on_player_died)
|
||||||
|
$InventoryComponent.item_collected.connect(_on_item_collected)
|
||||||
|
|
||||||
|
func _on_player_died():
|
||||||
|
get_tree().reload_current_scene()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Service Locator (Autoload Singletons)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# GameManager.gd — added as autoload in Project Settings
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
var current_level: int = 0
|
||||||
|
var player_score: int = 0
|
||||||
|
var is_paused: bool = false
|
||||||
|
|
||||||
|
signal game_started()
|
||||||
|
signal game_paused(paused: bool)
|
||||||
|
signal level_completed(level_id: int, score: int)
|
||||||
|
|
||||||
|
# Access from anywhere: GameManager.current_level
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Each Approach
|
||||||
|
|
||||||
|
| Pattern | Use When | Avoid When |
|
||||||
|
|---------|----------|------------|
|
||||||
|
| Direct reference (`$Path`) | Parent-child, same scene | Cross-scene communication |
|
||||||
|
| Signals | Decoupled components, event-driven flow | Simple data access |
|
||||||
|
| Autoload singleton | Global state (score, settings) | Per-instance data |
|
||||||
|
| Resource-based | Data that needs saving/loading | Runtime-only behavior |
|
||||||
|
|
||||||
|
## ECS-Like Patterns in Godot
|
||||||
|
|
||||||
|
Godot isn't a true ECS engine, but you can approximate entity-component systems:
|
||||||
|
|
||||||
|
### Entity Registry Pattern
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name EntityRegistry extends Node
|
||||||
|
|
||||||
|
# Tracks all active entities by type
|
||||||
|
var _entities: Dictionary = {}
|
||||||
|
|
||||||
|
func register(entity_id: String, entity: Node) -> void:
|
||||||
|
if not _entities.has(entity_id):
|
||||||
|
_entities[entity_id] = []
|
||||||
|
_entities[entity_id].append(entity)
|
||||||
|
|
||||||
|
func unregister(entity_id: String, entity: Node) -> void:
|
||||||
|
if _entities.has(entity_id):
|
||||||
|
_entities[entity_id].erase(entity)
|
||||||
|
|
||||||
|
func get_all(entity_id: String) -> Array:
|
||||||
|
return _entities.get(entity_id, [])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data-Driven with Resources
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# EntityData.tres — defines entity properties without code
|
||||||
|
class_name EntityData extends Resource
|
||||||
|
|
||||||
|
@export var entity_name: String = ""
|
||||||
|
@export var health: int = 100
|
||||||
|
@export var speed: float = 100.0
|
||||||
|
@export var components: Array[String] = [] # Component scene paths
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure Patterns
|
||||||
|
|
||||||
|
### Feature-Based Organization
|
||||||
|
|
||||||
|
```
|
||||||
|
res://
|
||||||
|
├── core/ # Engine-level systems (autoloads, event bus)
|
||||||
|
│ ├── GameManager.gd
|
||||||
|
│ └── EventBus.gd
|
||||||
|
├── features/ # Game features as self-contained modules
|
||||||
|
│ ├── combat/
|
||||||
|
│ │ ├── scripts/
|
||||||
|
│ │ ├── scenes/
|
||||||
|
│ │ └── resources/
|
||||||
|
│ ├── inventory/
|
||||||
|
│ │ ├── scripts/
|
||||||
|
│ │ ├── scenes/
|
||||||
|
│ │ └── resources/
|
||||||
|
│ └── dialogue/
|
||||||
|
├── levels/ # Level scenes and data
|
||||||
|
├── ui/ # UI components and screens
|
||||||
|
└── assets/ # Art, audio, fonts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Module Pattern for Large Projects
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# FeatureModule.gd — reusable feature registration pattern
|
||||||
|
class_name FeatureModule extends Node
|
||||||
|
|
||||||
|
func initialize() -> void:
|
||||||
|
pass # Override in subclasses
|
||||||
|
|
||||||
|
func cleanup() -> void:
|
||||||
|
pass # Override in subclasses
|
||||||
|
|
||||||
|
# Usage: CombatModule.gd extends FeatureModule
|
||||||
|
# - Registers signals with EventBus
|
||||||
|
# - Spawns combat-related autoloads if needed
|
||||||
|
# - Connects to game lifecycle events
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 Architecture Notes
|
||||||
|
|
||||||
|
- **tween_await()** — Use for sequential animations without callback chains in component scripts
|
||||||
|
- **DrawableTexture2D** — Procedural rendering for debug visualization of entity bounds
|
||||||
|
- **Perfetto tracing** — Profile architecture decisions with CPU/GPU trace markers
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`scene_tree`** — Inspect scene hierarchy to audit composition patterns
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify architecture scripts
|
||||||
|
- **`class_db_info`** — Query available methods on Godot classes for integration points
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Composition Patterns](references/composition-patterns.md)
|
||||||
|
- [Service Locator vs Signals](references/service-locator-vs-signals.md)
|
||||||
|
- [ECS Approximation Guide](references/ecs-approximation.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-architecture
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "designing Godot game architecture"
|
||||||
|
- "Godot scene composition patterns"
|
||||||
|
- "ECS patterns in Godot"
|
||||||
|
- "dependency injection Godot"
|
||||||
|
- "service locator vs signals"
|
||||||
|
- "Godot project structure"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-architecture
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "designing Godot game architecture"
|
||||||
|
- "Godot scene composition patterns"
|
||||||
|
- "ECS patterns in Godot"
|
||||||
|
- "dependency injection Godot"
|
||||||
|
- "service locator vs signals"
|
||||||
|
- "Godot project structure"
|
||||||
|
|
@ -0,0 +1,298 @@
|
||||||
|
---
|
||||||
|
name: godot-audio
|
||||||
|
description: "Audio patterns for Godot 4.x: AudioStreamPlayer hierarchy, bus mixing, spatial audio (2D/3D), randomization, dynamic music, and AudioListener positioning. Use when implementing sound effects, music systems, or audio mixing in Godot games."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- audio
|
||||||
|
- sound
|
||||||
|
- music
|
||||||
|
- spatial-audio
|
||||||
|
- bus-mixing
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Audio in Godot
|
||||||
|
|
||||||
|
Patterns for game audio implementation.
|
||||||
|
|
||||||
|
## AudioStreamPlayer Hierarchy
|
||||||
|
|
||||||
|
Godot provides separate nodes for 2D and 3D audio:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# 2D audio — attached near the sound source
|
||||||
|
# AudioStreamPlayer2D (auto-pans based on position relative to AudioListener2D)
|
||||||
|
var sfx_player = AudioStreamPlayer2D.new()
|
||||||
|
sfx_player.stream = preload("res://sounds/footstep.ogg")
|
||||||
|
add_child(sfx_player)
|
||||||
|
sfx_player.play()
|
||||||
|
|
||||||
|
# 3D audio — auto-attenuates and pans based on distance to AudioListener3D
|
||||||
|
var sfx_player_3d = AudioStreamPlayer3D.new()
|
||||||
|
sfx_player_3d.stream = preload("res://sounds/explosion.ogg")
|
||||||
|
sfx_player_3d.unit_size = 1.0 # Meters per audio unit
|
||||||
|
add_child(sfx_player_3d)
|
||||||
|
sfx_player_3d.play()
|
||||||
|
```
|
||||||
|
|
||||||
|
### AudioListener Placement
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Camera typically has AudioListener3D as a child
|
||||||
|
# The camera's position determines where the "ears" are in 3D space
|
||||||
|
$Camera3D.add_child(AudioListener3D.new())
|
||||||
|
|
||||||
|
# For 2D, attach to the main camera or viewport center
|
||||||
|
$Camera2D.add_child(AudioListener2D.new())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bus Mixing
|
||||||
|
|
||||||
|
Route audio through buses for independent volume/effects control:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Configure buses via code (or use Project Settings → Audio)
|
||||||
|
func setup_audio_buses() -> void:
|
||||||
|
var bus_count = AudioServer.bus_count
|
||||||
|
|
||||||
|
# Find bus index by name
|
||||||
|
var music_bus = AudioServer.get_bus_index("Music")
|
||||||
|
var sfx_bus = AudioServer.get_bus_index("SFX")
|
||||||
|
var ambient_bus = AudioServer.get_bus_index("Ambient")
|
||||||
|
|
||||||
|
# Set volume (linear, not dB)
|
||||||
|
AudioServer.set_bus_volume_db(music_bus, linear_to_db(0.7))
|
||||||
|
AudioServer.set_bus_volume_db(sfx_bus, linear_to_db(1.0))
|
||||||
|
AudioServer.set_bus_volume_db(ambient_bus, linear_to_db(0.5))
|
||||||
|
|
||||||
|
# Mute individual buses
|
||||||
|
AudioServer.set_bus_mute(sfx_bus, false)
|
||||||
|
|
||||||
|
# Solo for debugging
|
||||||
|
AudioServer.set_bus_solo(music_bus, true)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Per-Player Bus Assignment
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Assign a player node to a specific bus
|
||||||
|
$AudioStreamPlayer.bus = "SFX" # By name
|
||||||
|
# Or by index:
|
||||||
|
$AudioStreamPlayer.bus = AudioServer.get_bus_index("SFX")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spatial Audio (3D)
|
||||||
|
|
||||||
|
`AudioStreamPlayer3D` automatically handles distance attenuation and stereo panning:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name EnemyExplosion extends Node3D
|
||||||
|
|
||||||
|
var explosion_player: AudioStreamPlayer3D
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
explosion_player = AudioStreamPlayer3D.new()
|
||||||
|
explosion_player.stream = preload("res://sounds/explosion.ogg")
|
||||||
|
explosion_player.unit_size = 1.0 # 1 unit = 1 meter
|
||||||
|
explosion_player.max_distance = 50.0
|
||||||
|
explosion_player.attenuation_model = AudioStreamPlayer3D.ATTENUATION_INVERSE_DISTANCE
|
||||||
|
add_child(explosion_player)
|
||||||
|
|
||||||
|
func explode() -> void:
|
||||||
|
explosion_player.play()
|
||||||
|
# Visual effects here...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spatial Audio Models
|
||||||
|
|
||||||
|
| Model | Use Case |
|
||||||
|
|-------|----------|
|
||||||
|
| `ATTENUATION_DISABLED` | UI sounds, non-spatial audio in 3D scenes |
|
||||||
|
| `ATTENUATION_INVERSE_DISTANCE` | Default — realistic falloff |
|
||||||
|
| `ATTENUATION_LINEAR_DISTANCE` | Linear volume drop with distance |
|
||||||
|
|
||||||
|
## Sound Effect Randomization
|
||||||
|
|
||||||
|
Avoid the "machine gun" effect by randomizing pitch and playback:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name SFXPool extends Node
|
||||||
|
|
||||||
|
@export var sounds: Array[AudioStream] = []
|
||||||
|
var _players: Array[AudioStreamPlayer] = []
|
||||||
|
@export var pool_size: int = 8
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
for i in range(pool_size):
|
||||||
|
var player = AudioStreamPlayer.new()
|
||||||
|
player.bus = "SFX"
|
||||||
|
add_child(player)
|
||||||
|
_players.append(player)
|
||||||
|
|
||||||
|
func play_random() -> void:
|
||||||
|
if sounds.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Find first available player
|
||||||
|
for player in _players:
|
||||||
|
if not player.playing:
|
||||||
|
player.stream = sounds[randi() % sounds.size()]
|
||||||
|
# Randomize pitch slightly (0.9 to 1.1)
|
||||||
|
player.pitch_scale = randf_range(0.9, 1.1)
|
||||||
|
player.play()
|
||||||
|
return
|
||||||
|
|
||||||
|
# All busy — steal the first one
|
||||||
|
_players[0].stream = sounds[randi() % sounds.size()]
|
||||||
|
_players[0].pitch_scale = randf_range(0.9, 1.1)
|
||||||
|
_players[0].play()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Music System
|
||||||
|
|
||||||
|
Crossfade between music tracks based on game state:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name MusicManager extends Node
|
||||||
|
|
||||||
|
var _current_player: AudioStreamPlayer = null
|
||||||
|
var _next_player: AudioStreamPlayer = null
|
||||||
|
const FADE_DURATION := 1.5
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_current_player = AudioStreamPlayer.new()
|
||||||
|
_current_player.bus = "Music"
|
||||||
|
add_child(_current_player)
|
||||||
|
|
||||||
|
_next_player = AudioStreamPlayer.new()
|
||||||
|
_next_player.bus = "Music"
|
||||||
|
add_child(_next_player)
|
||||||
|
|
||||||
|
func play(track: AudioStream, fade_in: bool = true) -> void:
|
||||||
|
if _current_player.stream == track and _current_player.playing:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Start next player at silent
|
||||||
|
_next_player.stream = track
|
||||||
|
_next_player.volume_db = -80.0 # Effectively silent
|
||||||
|
_next_player.play()
|
||||||
|
|
||||||
|
# Crossfade
|
||||||
|
var tween = create_tween()
|
||||||
|
if _current_player.playing:
|
||||||
|
tween.tween_method(_set_current_volume, 0.0, -80.0, FADE_DURATION)
|
||||||
|
tween.tween_method(_set_next_volume, -80.0, 0.0, FADE_DURATION)
|
||||||
|
|
||||||
|
# Swap roles after fade
|
||||||
|
await tween.finished
|
||||||
|
var temp = _current_player
|
||||||
|
_current_player = _next_player
|
||||||
|
_next_player = temp
|
||||||
|
_next_player.stop()
|
||||||
|
_next_player.volume_db = -80.0
|
||||||
|
|
||||||
|
func _set_current_volume(vol: float) -> void:
|
||||||
|
if _current_player:
|
||||||
|
_current_player.volume_db = vol
|
||||||
|
|
||||||
|
func _set_next_volume(vol: float) -> void:
|
||||||
|
if _next_player:
|
||||||
|
_next_player.volume_db = vol
|
||||||
|
|
||||||
|
func stop(fade_out: bool = true) -> void:
|
||||||
|
if not _current_player or not _current_player.playing:
|
||||||
|
return
|
||||||
|
if fade_out:
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_method(_set_current_volume, _current_player.volume_db, -80.0, FADE_DURATION)
|
||||||
|
await tween.finished
|
||||||
|
_current_player.stop()
|
||||||
|
else:
|
||||||
|
_current_player.stop()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessibility Patterns
|
||||||
|
|
||||||
|
### Subtitle/SRT Sync
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name DialogueAudio extends AudioStreamPlayer
|
||||||
|
|
||||||
|
signal subtitle_shown(text: String)
|
||||||
|
signal subtitle_hidden()
|
||||||
|
|
||||||
|
@export var subtitles: Array[SubtitleCue] = []
|
||||||
|
var _active_cue_index: int = -1
|
||||||
|
|
||||||
|
class_name SubtitleCue extends RefCounted
|
||||||
|
var time: float = 0.0
|
||||||
|
var text: String = ""
|
||||||
|
|
||||||
|
func play() -> void:
|
||||||
|
super.play()
|
||||||
|
_active_cue_index = -1
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if not playing:
|
||||||
|
return
|
||||||
|
var elapsed = get_playback_position()
|
||||||
|
while _active_cue_index + 1 < subtitles.size():
|
||||||
|
var cue = subtitles[_active_cue_index + 1]
|
||||||
|
if elapsed >= cue.time:
|
||||||
|
_active_cue_index += 1
|
||||||
|
subtitle_shown.emit(cue.text)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
```
|
||||||
|
|
||||||
|
### Audio Cues for Visually Impaired Players
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Emit directional audio cues for important events
|
||||||
|
func play_directional_cue(direction: Vector3, sound: AudioStream) -> void:
|
||||||
|
var player = AudioStreamPlayer3D.new()
|
||||||
|
player.stream = sound
|
||||||
|
player.unit_size = 1.0
|
||||||
|
player.max_distance = 20.0
|
||||||
|
# Position relative to listener
|
||||||
|
player.global_position = $AudioListener3D.global_position + direction.normalized() * 2.0
|
||||||
|
add_child(player)
|
||||||
|
player.play()
|
||||||
|
|
||||||
|
# Auto-cleanup after sound ends
|
||||||
|
player.finished.connect(func(): player.queue_free())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 Audio Notes
|
||||||
|
|
||||||
|
- **AudioServer** — Improved bus effect chain for real-time DSP processing
|
||||||
|
- **Vorbis/Opus** — Opus codec support for higher quality at lower bitrates
|
||||||
|
- **Spatial audio** — Enhanced HRTF filtering for headphone-based 3D audio
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify audio scripts
|
||||||
|
- **`project_info`** — Check configured audio buses and settings
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Audio Bus Configuration](references/audio-buses.md)
|
||||||
|
- [Spatial Audio Deep Dive](references/spatial-audio.md)
|
||||||
|
- [Dynamic Music Patterns](references/dynamic-music.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-audio
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "implementing audio in Godot"
|
||||||
|
- "Godot sound effects"
|
||||||
|
- "audio bus mixing Godot"
|
||||||
|
- "spatial audio Godot"
|
||||||
|
- "dynamic music Godot"
|
||||||
|
- "sound pool Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-audio
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "implementing audio in Godot"
|
||||||
|
- "Godot sound effects"
|
||||||
|
- "audio bus mixing Godot"
|
||||||
|
- "spatial audio Godot"
|
||||||
|
- "dynamic music Godot"
|
||||||
|
- "sound pool Godot"
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
---
|
||||||
|
name: godot-brainstorming
|
||||||
|
description: "Architecture decision patterns for Godot game development across genres (RPG, platformer, RTS, puzzle). Covers scene organization, state machines, network architecture, and component composition. Use when designing a Godot game's architecture or making Godot-specific technical decisions."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- architecture
|
||||||
|
- state-machines
|
||||||
|
- multiplayer
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot Brainstorming & Architecture
|
||||||
|
|
||||||
|
Help design game architecture using Godot's node-based composition model.
|
||||||
|
|
||||||
|
## Scene Organization Strategies
|
||||||
|
|
||||||
|
### Composition Over Inheritance
|
||||||
|
|
||||||
|
Godot's strength is scene composition. Prefer composing small, focused scenes over deep inheritance hierarchies.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Instead of: PlayerCharacter extends CharacterBody2D with all abilities
|
||||||
|
# Use: Compose a player from reusable components
|
||||||
|
# - BaseCharacter (CharacterBody2D) — movement, health
|
||||||
|
# - InventoryComponent (Node) — inventory logic
|
||||||
|
# - AbilitySystem (Node) — ability management
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scene Hierarchy Patterns
|
||||||
|
|
||||||
|
| Pattern | When to Use | Example |
|
||||||
|
|---------|-------------|---------|
|
||||||
|
| Single root scene | Entire game/level | Main.tscn, Level1.tscn |
|
||||||
|
| Component scenes | Reusable behaviors | HealthBar.tscn, ProgressBar.tscn |
|
||||||
|
| Entity templates | Instantiable objects | Enemy.tscn, Collectible.tscn |
|
||||||
|
| UI compositions | Complex screens | MainMenu.tscn, SettingsPanel.tscn |
|
||||||
|
|
||||||
|
## State Machine Patterns
|
||||||
|
|
||||||
|
### AnimationTree State Machine
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
@export var animation_tree: AnimationTree
|
||||||
|
@onready var state_machine: AnimationNodeStateMachinePlayback = \
|
||||||
|
animation_tree.get("parameters/playback")
|
||||||
|
|
||||||
|
func change_state(new_state: String):
|
||||||
|
state_machine.travel(new_state)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom State Machine
|
||||||
|
|
||||||
|
For complex AI or game logic:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name GameState extends RefCounted
|
||||||
|
|
||||||
|
var current_state: String = "menu"
|
||||||
|
|
||||||
|
func transition_to(new_state: String):
|
||||||
|
var prev = current_state
|
||||||
|
current_state = new_state
|
||||||
|
on_transition(prev, new_state)
|
||||||
|
|
||||||
|
func on_transition(from: String, to: String):
|
||||||
|
match to:
|
||||||
|
"playing": handle_playing()
|
||||||
|
"paused": handle_paused()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Network Architecture Decisions
|
||||||
|
|
||||||
|
### Authoritative Server Model
|
||||||
|
- Best for competitive multiplayer
|
||||||
|
- Server validates all actions
|
||||||
|
- Client sends inputs, server simulates
|
||||||
|
|
||||||
|
### Peer-to-Peer Model
|
||||||
|
- Best for casual/co-op games
|
||||||
|
- Lower latency for local interactions
|
||||||
|
- Higher cheating risk
|
||||||
|
|
||||||
|
### Godot 4.7: Steam Frame Integration
|
||||||
|
For multiplayer with Steam, use the new Steam Frame configuration for lobby management and overlay features.
|
||||||
|
|
||||||
|
## Genre-Specific Patterns
|
||||||
|
|
||||||
|
### RPG Architecture
|
||||||
|
- Entity-component system for character stats
|
||||||
|
- Dialogue tree system (use `RichTextLabel` + custom parser)
|
||||||
|
- Save/load system using `JSON` or `Resource` serialization
|
||||||
|
|
||||||
|
### Platformer Architecture
|
||||||
|
- Level streaming for large worlds
|
||||||
|
- Input buffering for responsive controls
|
||||||
|
- TileMapLayer for layered level design
|
||||||
|
|
||||||
|
### RTS Architecture
|
||||||
|
- Pathfinding with A* on NavigationMesh
|
||||||
|
- Unit grouping and selection system
|
||||||
|
- Command queue pattern for input handling
|
||||||
|
|
||||||
|
## Godot 4.7 Brainstorming Considerations
|
||||||
|
|
||||||
|
- **tween_await()** — Use for sequential animation chains instead of nested callbacks
|
||||||
|
- **VirtualJoystick** — Built-in mobile controls simplify mobile game development
|
||||||
|
- **Control offset transforms** — New UI animation system enables complex transitions without custom shaders
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect your live project:
|
||||||
|
|
||||||
|
- **`scene_tree`** — Inspect current scene hierarchy for architecture decisions
|
||||||
|
- **`class_db_info`** — Query class capabilities to choose the right node type
|
||||||
|
- **`map_project`** — Generate a project graph to visualize architecture
|
||||||
|
- **`project_settings_read`** — Check current project configuration during brainstorming
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Scene Composition Patterns](references/scene-composition.md)
|
||||||
|
- [State Machine Implementations](references/state-machines.md)
|
||||||
|
- [Network Architecture Guide](references/network-architecture.md)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Claude Code extension for godot-brainstorming
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "designing Godot game architecture"
|
||||||
|
- "Godot scene organization"
|
||||||
|
- "Godot state machine patterns"
|
||||||
|
- "Godot multiplayer network architecture"
|
||||||
|
- "Godot game design decisions"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-brainstorming
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "designing Godot game architecture"
|
||||||
|
- "Godot scene organization"
|
||||||
|
- "Godot state machine patterns"
|
||||||
|
- "Godot multiplayer network architecture"
|
||||||
|
- "Godot game design decisions"
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
---
|
||||||
|
name: godot-code-review
|
||||||
|
description: "Reviews Godot projects for common pitfalls, performance issues, memory leaks, and anti-patterns. Provides a performance checklist, code style guidelines, and memory management review. Use when reviewing Godot GDScript or C# code."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- code-review
|
||||||
|
- performance
|
||||||
|
- anti-patterns
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot Code Review
|
||||||
|
|
||||||
|
Systematic review patterns for Godot game development projects.
|
||||||
|
|
||||||
|
## Common Pitfalls & Anti-Patterns
|
||||||
|
|
||||||
|
### _process() Abuse
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# BAD: Using _process for everything
|
||||||
|
func _process(delta):
|
||||||
|
# Movement that could be in _physics_process
|
||||||
|
position += velocity * delta
|
||||||
|
# Animation updates
|
||||||
|
update_animation()
|
||||||
|
|
||||||
|
# GOOD: Use the right callback
|
||||||
|
func _physics_process(delta):
|
||||||
|
# Physics-related movement
|
||||||
|
position += velocity * delta
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
# Non-physics updates (UI, animations)
|
||||||
|
update_animation()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hardcoded Paths
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# BAD
|
||||||
|
var sprite = get_node("Character/Sprite2D")
|
||||||
|
|
||||||
|
# GOOD
|
||||||
|
@onready var sprite = $Sprite2D as Sprite2D
|
||||||
|
```
|
||||||
|
|
||||||
|
### Signal Leaks
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# BAD: Forgetting to disconnect signals
|
||||||
|
func _ready():
|
||||||
|
EventBus.some_signal.connect(on_some_signal)
|
||||||
|
# If this node is freed, the callback still holds a reference
|
||||||
|
|
||||||
|
# GOOD: Auto-disconnect when using method references
|
||||||
|
func _ready():
|
||||||
|
EventBus.some_signal.connect(on_some_signal) # Godot auto-handles method refs
|
||||||
|
|
||||||
|
# GOOD: Manual disconnect for lambdas
|
||||||
|
func _ready():
|
||||||
|
var callback = func(): handle_event()
|
||||||
|
EventBus.some_signal.connect(callback)
|
||||||
|
|
||||||
|
func _exit_tree():
|
||||||
|
EventBus.some_signal.disconnect(callback)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Review Checklist
|
||||||
|
|
||||||
|
### Callback Selection
|
||||||
|
|
||||||
|
| Use Case | Correct Callback | Why |
|
||||||
|
|----------|-----------------|-----|
|
||||||
|
| Physics movement | `_physics_process` | Fixed timestep, syncs with physics server |
|
||||||
|
| Non-physics updates | `_process` | Frame-rate dependent, for UI/rendering |
|
||||||
|
| One-time setup | `_ready` | Called once when node enters tree |
|
||||||
|
| Frame before ready | `_enter_tree` | Called before `_ready`, even if tree paused |
|
||||||
|
| Cleanup | `_exit_tree` | Called when node is removed from tree |
|
||||||
|
| Physics tick (input) | `_physics_input_event` | For character body input handling |
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
|
||||||
|
- [ ] All `queue_free()` calls have corresponding signal disconnections
|
||||||
|
- [ ] No circular references between nodes
|
||||||
|
- [ ] Large resources (textures, audio) unloaded when no longer needed
|
||||||
|
- [ ] Scene instances properly freed after use
|
||||||
|
- [ ] Autoload nodes don't hold unnecessary references
|
||||||
|
|
||||||
|
### Code Style Guidelines
|
||||||
|
|
||||||
|
#### GDScript Style
|
||||||
|
|
||||||
|
- Use 4-space indentation (not tabs)
|
||||||
|
- Follow [GDScript Style Guide](https://godotengine.org/doc/en/latest/tutorials/scripting/gdscript/gdscript_styleguide.html)
|
||||||
|
- Use `@export` for inspector-exposed variables
|
||||||
|
- Use `@onready` for node paths
|
||||||
|
- Type annotations on function parameters and return values
|
||||||
|
- Use `const` for compile-time constants
|
||||||
|
|
||||||
|
#### C# Style
|
||||||
|
|
||||||
|
- Use PascalCase for class names, camelCase for members
|
||||||
|
- Use Godot's `[Export]` attributes for inspector properties
|
||||||
|
- Implement `_ExitTree()` for cleanup of event subscriptions
|
||||||
|
- Prefer structs for small value types to avoid GC pressure
|
||||||
|
|
||||||
|
### Memory Management Patterns
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Proper node lifecycle
|
||||||
|
func spawn_enemy():
|
||||||
|
var enemy = enemy_scene.instantiate()
|
||||||
|
add_child(enemy)
|
||||||
|
# Enemy manages its own cleanup via queue_free() on death
|
||||||
|
|
||||||
|
func load_level(level_path: String):
|
||||||
|
# Unload previous level
|
||||||
|
if current_level:
|
||||||
|
current_level.queue_free()
|
||||||
|
current_level = null
|
||||||
|
|
||||||
|
# Load new level
|
||||||
|
var scene = load(level_path)
|
||||||
|
current_level = scene.instantiate()
|
||||||
|
add_child(current_level)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 Code Review Notes
|
||||||
|
|
||||||
|
- Check for **HDR rendering** configuration in project settings
|
||||||
|
- Review **AreaLight3D** usage for new 4.7 features (modes, intensity profiles)
|
||||||
|
- Verify **VirtualJoystick** is used instead of custom implementations on mobile
|
||||||
|
- Check that **tween_await()** replaces nested callback patterns where applicable
|
||||||
|
|
||||||
|
## Quick Review Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Find all _process calls that might be physics-related
|
||||||
|
grep -rn "_process" --include="*.gd" scripts/
|
||||||
|
|
||||||
|
# Find hardcoded get_node calls (should use @onready)
|
||||||
|
grep -rn "get_node(" --include="*.gd" scripts/
|
||||||
|
|
||||||
|
# Find potential signal leaks (connect without disconnect pattern)
|
||||||
|
grep -rn "\.connect(" --include="*.gd" scripts/
|
||||||
|
|
||||||
|
# Find queue_free usage to verify cleanup
|
||||||
|
grep -rn "queue_free" --include="*.gd" scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools enhance code review:
|
||||||
|
|
||||||
|
- **`script_read`** — Read any script file for review without opening the editor
|
||||||
|
- **`script_validate`** — Run syntax validation with `checkDeprecated: true` and `suggestTweenAwait: true`
|
||||||
|
- **`file_search`** — Search across all project files for patterns (e.g., `queue_free`, signal leaks)
|
||||||
|
- **`file_browse`** — List project directories to verify structure
|
||||||
|
- **`class_db_info`** — Verify methods/properties exist before flagging issues
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [GDScript Style Guide](references/gdscript-styleguide.md)
|
||||||
|
- [Performance Optimization Guide](references/performance-checklist.md)
|
||||||
|
- [Memory Management Patterns](references/memory-patterns.md)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Claude Code extension for godot-code-review
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "reviewing Godot code"
|
||||||
|
- "Godot performance checklist"
|
||||||
|
- "Godot memory leak detection"
|
||||||
|
- "Godot anti-patterns"
|
||||||
|
- "Godot code style review"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-code-review
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "reviewing Godot code"
|
||||||
|
- "Godot performance checklist"
|
||||||
|
- "Godot memory leak detection"
|
||||||
|
- "Godot anti-patterns"
|
||||||
|
- "Godot code style review"
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
---
|
||||||
|
name: godot-csharp-patterns
|
||||||
|
description: "C# Mono patterns for Godot 4.x: Export attributes, signal handling, performance optimization, GDExtension interop, and best practices for first-class C# support. Use when writing C# code in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- csharp
|
||||||
|
- mono
|
||||||
|
- performance
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# C# Patterns for Godot 4.x
|
||||||
|
|
||||||
|
Best practices for Godot's first-class C# (Mono) support.
|
||||||
|
|
||||||
|
## Export Attributes
|
||||||
|
|
||||||
|
### Inspector Properties
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
public partial class Player : CharacterBody2D
|
||||||
|
{
|
||||||
|
// Basic export
|
||||||
|
[Export] public float Speed { get; set; } = 200f;
|
||||||
|
|
||||||
|
// With range
|
||||||
|
[Export(PropertyHint.Range, "0,100,0.5")]
|
||||||
|
public float JumpForce { get; set; } = 400f;
|
||||||
|
|
||||||
|
// File path
|
||||||
|
[Export(FileFilter = "*.png,*.svg")]
|
||||||
|
public string IconPath { get; set; } = "";
|
||||||
|
|
||||||
|
// Resource export
|
||||||
|
[Export] public PackedScene BulletScene { get; set; }
|
||||||
|
|
||||||
|
// Enum with hint
|
||||||
|
[Export] public MovementMode MoveMode { get; set; }
|
||||||
|
|
||||||
|
public enum MovementMode
|
||||||
|
{
|
||||||
|
Walk,
|
||||||
|
Run,
|
||||||
|
Sprint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Export Categories
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[ExportGroup("Combat")]
|
||||||
|
[Export] public int AttackDamage { get; set; } = 10;
|
||||||
|
[Export] public float AttackCooldown { get; set; } = 0.5f;
|
||||||
|
|
||||||
|
[ExportGroup("Movement")]
|
||||||
|
[Export] public float MoveSpeed { get; set; } = 200f;
|
||||||
|
[Export] public float JumpForce { get; set; } = 400f;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Signal Handling
|
||||||
|
|
||||||
|
### C# Event Syntax vs Godot Signals
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Godot signal declaration in GDScript (autoload)
|
||||||
|
// class_name EventBus extends Node
|
||||||
|
// signal player_died(Vector2 position)
|
||||||
|
|
||||||
|
// C# subscriber
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
// Method reference (recommended — auto-disconnect on free)
|
||||||
|
EventBus.PlayerDied += OnPlayerDied;
|
||||||
|
|
||||||
|
// Lambda (requires manual disconnect)
|
||||||
|
EventBus.PlayerDied += (pos) => GD.Print($"Died at {pos}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
// Clean up lambda subscriptions
|
||||||
|
EventBus.PlayerDied -= OnPlayerDied;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerDied(Vector2 position)
|
||||||
|
{
|
||||||
|
GD.Print($"Player died at {position}");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Godot.Signal for C#-Side Signals
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class Enemy : CharacterBody2D
|
||||||
|
{
|
||||||
|
[Signal] public delegate void HealthChangedEventHandler(int newHealth);
|
||||||
|
[Signal] public delegate void DefeatedEventHandler();
|
||||||
|
|
||||||
|
private int health = 100;
|
||||||
|
|
||||||
|
public void TakeDamage(int amount)
|
||||||
|
{
|
||||||
|
health -= amount;
|
||||||
|
EmitSignal(SignalName.HealthChanged, health);
|
||||||
|
|
||||||
|
if (health <= 0)
|
||||||
|
{
|
||||||
|
EmitSignal(SignalName.Defeated);
|
||||||
|
QueueFree();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Considerations
|
||||||
|
|
||||||
|
### Avoiding GC Pressure
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Bad: Creates garbage every frame
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
var text = $"Score: {score}"; // String allocation
|
||||||
|
label.Text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good: Reuse objects, avoid allocations
|
||||||
|
private readonly StringBuilder _sb = new();
|
||||||
|
|
||||||
|
public override void _Process(double delta)
|
||||||
|
{
|
||||||
|
_sb.Clear();
|
||||||
|
_sb.Append("Score: ");
|
||||||
|
_sb.Append(score);
|
||||||
|
label.Text = _sb.ToString();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Structs vs Classes
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Use structs for small, short-lived data
|
||||||
|
public struct Vector2Int
|
||||||
|
{
|
||||||
|
public int X { get; }
|
||||||
|
public int Y { get; }
|
||||||
|
public Vector2Int(int x, int y) => (X, Y) = (x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use classes for objects with identity/lifetime
|
||||||
|
public class PlayerData : Resource
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int Level { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instance Pooling
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public partial class BulletPool : Node
|
||||||
|
{
|
||||||
|
private readonly Queue<Bullet> _pool = new();
|
||||||
|
private readonly PackedScene _bulletScene;
|
||||||
|
|
||||||
|
public BulletPool(PackedScene bulletScene) => _bulletScene = bulletScene;
|
||||||
|
|
||||||
|
public Bullet Get()
|
||||||
|
{
|
||||||
|
if (_pool.TryDequeue(out var bullet))
|
||||||
|
{
|
||||||
|
bullet.Visible = true;
|
||||||
|
return bullet;
|
||||||
|
}
|
||||||
|
return _bulletScene.Instantiate<Bullet>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Return(Bullet bullet)
|
||||||
|
{
|
||||||
|
bullet.Visible = false;
|
||||||
|
bullet.QueueFree();
|
||||||
|
_pool.Enqueue(bullet);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## GDExtension Interop
|
||||||
|
|
||||||
|
### Calling GDScript from C#
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// GDScript (autoload "GameData")
|
||||||
|
// class_name GameData extends Node
|
||||||
|
// var high_score: int = 0
|
||||||
|
|
||||||
|
// C# access
|
||||||
|
var gameData = Engine.GetSingleton("GameData") as GodotObject;
|
||||||
|
var highScore = gameData.Get("high_score");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Exposing C# to GDScript via GDExtension
|
||||||
|
|
||||||
|
Create a `.gdextension` file to expose C# classes to GDScript projects.
|
||||||
|
|
||||||
|
## Godot 4.7 C# Notes
|
||||||
|
|
||||||
|
- C# fully supports new 4.7 features including VirtualJoystick and tween_await()
|
||||||
|
- Use `await` with tweens for async animation sequences
|
||||||
|
- Export attributes work the same — no changes in 4.7
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`script_read`** — Read C# script files for review
|
||||||
|
- **`script_edit`** — Update C# scripts directly
|
||||||
|
- **`script_validate`** — Validate C# scripts for syntax errors
|
||||||
|
- **`scene_tree`** — Inspect scene hierarchy to verify node composition
|
||||||
|
- **`class_db_info`** — Query Godot class info for C# interop patterns
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [C# API Reference](references/csharp-api.md)
|
||||||
|
- [Performance Guide](references/performance.md)
|
||||||
|
- [GDExtension Guide](references/gdextension.md)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Claude Code extension for godot-csharp-patterns
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "writing C# code in Godot"
|
||||||
|
- "Godot Mono / C# patterns"
|
||||||
|
- "Export attributes in Godot"
|
||||||
|
- "Godot GDExtension interop"
|
||||||
|
- "Godot C# performance optimization"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-csharp-patterns
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "writing C# code in Godot"
|
||||||
|
- "Godot Mono / C# patterns"
|
||||||
|
- "Export attributes in Godot"
|
||||||
|
- "Godot GDExtension interop"
|
||||||
|
- "Godot C# performance optimization"
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
---
|
||||||
|
name: godot-debugging
|
||||||
|
description: "Debugging workflow for Godot 4.x: print vs push_error, editor output window, debugger panel, profiling with built-in profiler, network debugging, export/build troubleshooting. Use when debugging Godot games or resolving Godot-specific errors."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- debugging
|
||||||
|
- profiling
|
||||||
|
- error-handling
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot Debugging Guide
|
||||||
|
|
||||||
|
Comprehensive debugging workflow and common issue resolution for Godot 4.x.
|
||||||
|
|
||||||
|
## Output Methods
|
||||||
|
|
||||||
|
### print() vs push_error() vs push_warning()
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# print() — General output, filtered in editor output
|
||||||
|
print("Debug: position = ", position)
|
||||||
|
print("Debug: player health = ", player_health)
|
||||||
|
|
||||||
|
# printf-style formatting
|
||||||
|
printf("Player %s has %d health", player_name, health)
|
||||||
|
|
||||||
|
# push_error() — Shows in red, includes file and line number
|
||||||
|
push_error("Failed to load resource: ", resource_path)
|
||||||
|
|
||||||
|
# push_warning() — Shows in yellow, non-fatal issues
|
||||||
|
push_warning("Missing animation for state: ", state)
|
||||||
|
|
||||||
|
# load() returns null on failure — check before use
|
||||||
|
var data = load(risky_path)
|
||||||
|
if data == null:
|
||||||
|
push_error("Could not load resource at: %s" % risky_path)
|
||||||
|
|
||||||
|
# Or with ResourceLoader for error codes
|
||||||
|
var err, data = ResourceLoader.load_interactive(risky_path)
|
||||||
|
if err != OK:
|
||||||
|
push_error("Failed to load: %s (error code %d)" % [risky_path, err])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Editor Output Window
|
||||||
|
|
||||||
|
Access via **Window > Debugger** or **Debug > Start Debugging**. The output panel shows:
|
||||||
|
- Standard print output (gray)
|
||||||
|
- Warnings (yellow)
|
||||||
|
- Errors (red) with file/line info
|
||||||
|
- Custom logger messages
|
||||||
|
|
||||||
|
## Debugger Panel
|
||||||
|
|
||||||
|
### Built-in Debugger Features
|
||||||
|
|
||||||
|
1. **Scene Tree** — Visual tree of current scene with property inspection
|
||||||
|
2. **Resources** — List of loaded resources, check for leaks
|
||||||
|
3. **Profiler** — CPU and GPU usage breakdown
|
||||||
|
4. **Remote Transform** — Edit node properties in running scenes
|
||||||
|
5. **Network Profiler** — Monitor multiplayer traffic
|
||||||
|
|
||||||
|
### Remote Debugging
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Connect to remote scene tree from editor
|
||||||
|
# When running in debug mode (F6), the editor connects automatically
|
||||||
|
|
||||||
|
# Access remote nodes
|
||||||
|
func get_remote_node(path: String):
|
||||||
|
var node = get_node(path) # Works on local and remote
|
||||||
|
return node
|
||||||
|
```
|
||||||
|
|
||||||
|
## Profiling Workflow
|
||||||
|
|
||||||
|
### CPU Profiler
|
||||||
|
|
||||||
|
```
|
||||||
|
Profile > Start Profiling in the editor
|
||||||
|
Then view: Profile > CPU Usage or Profile > Frame Time Graph
|
||||||
|
```
|
||||||
|
|
||||||
|
Key metrics:
|
||||||
|
- **Frame time** — Should be under 16.67ms for 60fps
|
||||||
|
- **Physics FPS** — Should match target (usually 60)
|
||||||
|
- **Script time** — Time spent in GDScript/C# code
|
||||||
|
- **Draw calls** — Number of GPU draw calls per frame
|
||||||
|
|
||||||
|
### Memory Profiler
|
||||||
|
|
||||||
|
```
|
||||||
|
Profile > Memory Usage
|
||||||
|
Check for:
|
||||||
|
- Growing resource count over time
|
||||||
|
- Unfreed scene instances
|
||||||
|
- Large textures not being unloaded
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPU Profiler
|
||||||
|
|
||||||
|
```
|
||||||
|
Project Settings > Rendering > Debug > GPU Profiling = On
|
||||||
|
Then: Profile > GPU Usage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Error Patterns & Fixes
|
||||||
|
|
||||||
|
### "Script class 'XXX' does not exist in current project"
|
||||||
|
- **Cause:** Autoload script reference broken after rename/move
|
||||||
|
- **Fix:** Re-add the autoload in Project Settings > Autoload
|
||||||
|
|
||||||
|
### "Cannot instantiate non-imported scene: res://..."
|
||||||
|
- **Cause:** Scene file not saved or path is incorrect
|
||||||
|
- **Fix:** Open the scene in editor and save it, verify path
|
||||||
|
|
||||||
|
### "Invalid get index 'XXX' (on base: 'Node2D')"
|
||||||
|
- **Cause:** Accessing a property that doesn't exist on the node
|
||||||
|
- **Fix:** Check node type and available properties, use `has_method()` or `is()` checks
|
||||||
|
|
||||||
|
### "Failed to connect signal"
|
||||||
|
- **Cause:** Method name mismatch or signal doesn't exist
|
||||||
|
- **Fix:** Verify signal exists on source node, check method signature matches
|
||||||
|
|
||||||
|
### Export/Build Errors
|
||||||
|
|
||||||
|
| Error | Cause | Fix |
|
||||||
|
|-------|-------|-----|
|
||||||
|
| "Export failed with code -1" | Missing export preset config | Re-create export preset |
|
||||||
|
| "Android build failed" | SDK/NDK path wrong | Check Project Settings > Android |
|
||||||
|
| "HTML5 export failed" | Emscripten not installed | Install Godot HTML5 export template |
|
||||||
|
| "Windows export failed" | MSVC not installed | Install Visual Studio Build Tools |
|
||||||
|
|
||||||
|
## Network Debugging
|
||||||
|
|
||||||
|
### Multiplayer API Inspection
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Check connection status
|
||||||
|
print("Connected: ", Network.multiplayer.is_connected_to_server())
|
||||||
|
print("Peer count: ", Network.multiplayer.get_peers().size())
|
||||||
|
|
||||||
|
# Log RPC calls for debugging
|
||||||
|
func _rpc_debug_message(msg):
|
||||||
|
print("[RPC] ", msg)
|
||||||
|
|
||||||
|
# Monitor network traffic
|
||||||
|
# Use Profile > Network Profiler in the editor
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lag Compensation Debugging
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Log timing information
|
||||||
|
print("Server time: ", get_tree().get_network_unique_id())
|
||||||
|
print("Local latency: ", Network.multiplayer.get_connection_status())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 Debugging Notes
|
||||||
|
|
||||||
|
- **Perfetto tracing** — New built-in Android profiler for detailed performance analysis
|
||||||
|
- **New editor log improvements** — Better error messages with more context
|
||||||
|
- **tween_await() debugging** — Use `await` breakpoints in C# debugger for animation sequences
|
||||||
|
|
||||||
|
## Debug Utility Functions
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name DebugUtils extends Node
|
||||||
|
|
||||||
|
static func draw_rect_debug(rect: Rect2, color: Color, lifetime: float = 0.0):
|
||||||
|
# Draw debug rectangle (use with CanvasItem.draw_rect)
|
||||||
|
pass
|
||||||
|
|
||||||
|
static func log_tree(node: Node, indent: int = 0):
|
||||||
|
# Print scene tree for debugging
|
||||||
|
var prefix = " ".repeat(indent)
|
||||||
|
print(prefix + node.name, " (", node.get_class(), ")")
|
||||||
|
for child in node.get_children():
|
||||||
|
log_tree(child, indent + 1)
|
||||||
|
|
||||||
|
static func check_node_ready(node: Node):
|
||||||
|
# Verify node is ready before accessing children
|
||||||
|
if not node.is_inside_tree():
|
||||||
|
push_error("Node not in tree: ", node.name)
|
||||||
|
return node.is_inside_tree()
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools provide live debugging capabilities:
|
||||||
|
|
||||||
|
- **`debugger_output`** — Read real-time debugger output from the running scene
|
||||||
|
- **`output_log`** — Read Godot's output log with level filtering (error/warning/info)
|
||||||
|
- **`scene_run` / `scene_stop`** — Start and stop scenes for testing fixes
|
||||||
|
- **`pause_resume`** — Pause execution to inspect state at breakpoints
|
||||||
|
- **`script_validate`** — Validate scripts without leaving the agent workflow
|
||||||
|
- **`scene_tree`** — Inspect runtime scene hierarchy for missing nodes
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Debugging Guide](references/debugging-workflow.md)
|
||||||
|
- [Profiler Deep Dive](references/profiler-guide.md)
|
||||||
|
- [Network Debugging](references/network-debug.md)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Claude Code extension for godot-debugging
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "debugging Godot errors"
|
||||||
|
- "profiling Godot performance"
|
||||||
|
- "resolving Godot crashes"
|
||||||
|
- "network debugging in Godot"
|
||||||
|
- "export build troubleshooting"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-debugging
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "debugging Godot errors"
|
||||||
|
- "profiling Godot performance"
|
||||||
|
- "resolving Godot crashes"
|
||||||
|
- "network debugging in Godot"
|
||||||
|
- "export build troubleshooting"
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
---
|
||||||
|
name: godot-dialog-systems
|
||||||
|
description: "Dialogue system patterns for Godot 4.x: resource-based dialogue trees, branching narratives, typewriter text effects, voice sync, and portrait management. Use when building NPC conversations, quest dialogue, or narrative systems in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- dialogue
|
||||||
|
- narrative
|
||||||
|
- branching
|
||||||
|
- typewriter
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Dialogue Systems in Godot
|
||||||
|
|
||||||
|
Patterns for building conversation and narrative systems.
|
||||||
|
|
||||||
|
## Resource-Based Dialogue Data
|
||||||
|
|
||||||
|
Store dialogue as `Resource` files for easy editing and versioning:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name DialogueLine extends Resource
|
||||||
|
|
||||||
|
@export var speaker: String = ""
|
||||||
|
@export var text: String = ""
|
||||||
|
@export var voice_file: String = "" # Optional audio cue
|
||||||
|
@export var portrait_index: int = -1
|
||||||
|
@export var next_line_id: String = "" # Branch target
|
||||||
|
@export var choices: Array[DialogueChoice] = []
|
||||||
|
|
||||||
|
class_name DialogueChoice extends Resource
|
||||||
|
|
||||||
|
@export var choice_text: String = ""
|
||||||
|
@export var target_line_id: String = ""
|
||||||
|
@export var requires_flag: String = "" # Optional quest flag check
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dialogue Script Resource
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name DialogueScript extends Resource
|
||||||
|
|
||||||
|
@export var script_name: String = ""
|
||||||
|
@export var start_line_id: String = ""
|
||||||
|
@export var lines: Dictionary = {} # id -> DialogueLine
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dialogue Manager
|
||||||
|
|
||||||
|
Central system for playing dialogue sequences:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name DialogueManager extends Node
|
||||||
|
|
||||||
|
signal dialogue_started(speaker: String, text: String)
|
||||||
|
signal dialogue_ended()
|
||||||
|
signal choice_presented(choices: Array[DialogueChoice])
|
||||||
|
signal typewriter_progress(chars_shown: int, total_chars: int)
|
||||||
|
|
||||||
|
var current_script: DialogueScript = null
|
||||||
|
var current_line: DialogueLine = null
|
||||||
|
var is_playing: bool = false
|
||||||
|
var flags: Dictionary = {} # Quest/state flags
|
||||||
|
|
||||||
|
func play(script: DialogueScript) -> void:
|
||||||
|
current_script = script
|
||||||
|
is_playing = true
|
||||||
|
_show_line(script.start_line_id)
|
||||||
|
|
||||||
|
func _show_line(line_id: String) -> void:
|
||||||
|
if not current_script.lines.has(line_id):
|
||||||
|
push_error("DialogueManager: Line '%s' not found" % line_id)
|
||||||
|
end_dialogue()
|
||||||
|
return
|
||||||
|
|
||||||
|
current_line = current_script.lines[line_id] as DialogueLine
|
||||||
|
dialogue_started.emit(current_line.speaker, current_line.text)
|
||||||
|
|
||||||
|
if current_line.choices.size() > 0:
|
||||||
|
choice_presented.emit(current_line.choices)
|
||||||
|
else:
|
||||||
|
# Auto-advance after typewriter
|
||||||
|
_auto_advance()
|
||||||
|
|
||||||
|
func advance() -> void:
|
||||||
|
if not current_line:
|
||||||
|
return
|
||||||
|
if current_line.next_line_id.is_empty():
|
||||||
|
end_dialogue()
|
||||||
|
else:
|
||||||
|
_show_line(current_line.next_line_id)
|
||||||
|
|
||||||
|
func select_choice(choice_index: int) -> void:
|
||||||
|
if not current_line or choice_index >= current_line.choices.size():
|
||||||
|
return
|
||||||
|
var choice = current_line.choices[choice_index]
|
||||||
|
|
||||||
|
# Check flag requirement
|
||||||
|
if not choice.requires_flag.is_empty():
|
||||||
|
if not flags.get(choice.requires_flag, false):
|
||||||
|
return # Player hasn't met requirement
|
||||||
|
|
||||||
|
_show_line(choice.target_line_id)
|
||||||
|
|
||||||
|
func end_dialogue() -> void:
|
||||||
|
is_playing = false
|
||||||
|
current_script = null
|
||||||
|
current_line = null
|
||||||
|
dialogue_ended.emit()
|
||||||
|
|
||||||
|
func set_flag(flag: String, value: bool = true) -> void:
|
||||||
|
flags[flag] = value
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typewriter Text Effect
|
||||||
|
|
||||||
|
Animate text appearing character-by-character:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name DialogueText extends RichTextLabel
|
||||||
|
|
||||||
|
signal typing_complete()
|
||||||
|
|
||||||
|
@export var type_speed: float = 0.03 # Seconds per character
|
||||||
|
var _type_timer: float = 0.0
|
||||||
|
var _full_text: String = ""
|
||||||
|
var _char_index: int = 0
|
||||||
|
var _is_typing: bool = false
|
||||||
|
var _waiting_for_input: bool = false
|
||||||
|
|
||||||
|
func start_typing(text: String) -> void:
|
||||||
|
_full_text = text
|
||||||
|
_char_index = 0
|
||||||
|
_is_typing = true
|
||||||
|
_waiting_for_input = false
|
||||||
|
text = ""
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not _is_typing:
|
||||||
|
return
|
||||||
|
|
||||||
|
if _waiting_for_input:
|
||||||
|
return # Wait for player to click to advance
|
||||||
|
|
||||||
|
_type_timer += delta
|
||||||
|
if _type_timer >= type_speed and _char_index < _full_text.length():
|
||||||
|
_type_timer = 0.0
|
||||||
|
_char_index += 1
|
||||||
|
text = _full_text.substr(0, _char_index)
|
||||||
|
DialogueManager.typewriter_progress.emit(_char_index, _full_text.length())
|
||||||
|
|
||||||
|
if _char_index >= _full_text.length():
|
||||||
|
_is_typing = false
|
||||||
|
typing_complete.emit()
|
||||||
|
|
||||||
|
# Skip to end on input
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed:
|
||||||
|
if _is_typing:
|
||||||
|
# Show remaining text instantly
|
||||||
|
text = _full_text
|
||||||
|
_char_index = _full_text.length()
|
||||||
|
_is_typing = false
|
||||||
|
typing_complete.emit()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Branching Dialogue Pattern
|
||||||
|
|
||||||
|
Handle conditional branches based on game state:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# In dialogue script editor or code:
|
||||||
|
# Line "greeting":
|
||||||
|
# text: "Hello there, {0}!"
|
||||||
|
# choices:
|
||||||
|
# - text: "Hi!" → target: "friendly_chat"
|
||||||
|
# - text: "Who are you?" → target: "hostile_chat"
|
||||||
|
# - text: "I need help." → target: "quest_offer" (requires: met_quest_giver)
|
||||||
|
|
||||||
|
func _resolve_branch(line_id: String) -> String:
|
||||||
|
var line = current_script.lines[line_id] as DialogueLine
|
||||||
|
if line.choices.size() == 0:
|
||||||
|
return line.next_line_id
|
||||||
|
|
||||||
|
# Filter choices by flags
|
||||||
|
var valid_choices: Array[DialogueChoice] = []
|
||||||
|
for choice in line.choices:
|
||||||
|
if choice.requires_flag.is_empty():
|
||||||
|
valid_choices.append(choice)
|
||||||
|
elif flags.get(choice.requires_flag, false):
|
||||||
|
valid_choices.append(choice)
|
||||||
|
|
||||||
|
# If no valid choices, use default next_line_id
|
||||||
|
if valid_choices.size() == 0:
|
||||||
|
return line.next_line_id
|
||||||
|
|
||||||
|
return "" # Wait for player choice
|
||||||
|
```
|
||||||
|
|
||||||
|
## Portrait Management
|
||||||
|
|
||||||
|
Display character portraits during dialogue:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name DialoguePortrait extends Container
|
||||||
|
|
||||||
|
@export var portrait_frames: Array[Texture2D] = []
|
||||||
|
|
||||||
|
func show_portrait(index: int) -> void:
|
||||||
|
if index < 0 or index >= portrait_frames.size():
|
||||||
|
return
|
||||||
|
# Update portrait sprite with frame[index]
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Animate portrait reactions (idle, talking, surprised)
|
||||||
|
enum PortraitState { IDLE, TALKING, SURPRISED }
|
||||||
|
var current_state: PortraitState = PortraitState.IDLE
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 Dialogue Notes
|
||||||
|
|
||||||
|
- **tween_await()** — Animate dialogue panel entrance/exit smoothly: `await get_tree().create_tween().tween_property(panel, "modulate:a", 1.0).finished`
|
||||||
|
- **RichTextLabel** — Improved bbcode for dialogue formatting (`[wave]`, `[shake]`, `[carousel]`)
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify dialogue scripts
|
||||||
|
- **`scene_tree`** — Inspect dialogue UI node hierarchy
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Dialogue Resource Design](references/dialogue-resources.md)
|
||||||
|
- [Branching Narrative Patterns](references/branching-narrative.md)
|
||||||
|
- [Typewriter Effect Variants](references/typewriter-effects.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-dialog-systems
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "building dialogue systems in Godot"
|
||||||
|
- "Godot branching narrative"
|
||||||
|
- "NPC conversation Godot"
|
||||||
|
- "typewriter text effect Godot"
|
||||||
|
- "resource-based dialogue Godot"
|
||||||
|
- "voice sync Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-dialog-systems
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "building dialogue systems in Godot"
|
||||||
|
- "Godot branching narrative"
|
||||||
|
- "NPC conversation Godot"
|
||||||
|
- "typewriter text effect Godot"
|
||||||
|
- "resource-based dialogue Godot"
|
||||||
|
- "voice sync Godot"
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
---
|
||||||
|
name: godot-gdscript-patterns
|
||||||
|
description: "GDScript best practices for Godot 4.x: node composition, typed signals, scene instantiation, resource management, singletons, event buses, and common design patterns. Use when writing or reviewing GDScript code in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- gdscript
|
||||||
|
- design-patterns
|
||||||
|
- signals
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# GDScript Patterns for Godot 4.x
|
||||||
|
|
||||||
|
Best practices and patterns for writing clean, maintainable GDScript code.
|
||||||
|
|
||||||
|
## Node Hierarchy Best Practices
|
||||||
|
|
||||||
|
### Composition Over Inheritance
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Good: Small, focused scenes composed together
|
||||||
|
# Player.tscn contains:
|
||||||
|
# - CharacterBody2D (root)
|
||||||
|
# - Sprite2D (visual)
|
||||||
|
# - CollisionShape2D (physics)
|
||||||
|
# - AnimatedSprite2D (animation)
|
||||||
|
# - HealthComponent (Node — logic)
|
||||||
|
# - InventoryComponent (Node — logic)
|
||||||
|
|
||||||
|
# Avoid: Deep inheritance chains
|
||||||
|
# Player extends CharacterBody2D extends Node2D...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Scene Instantiation Patterns
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# preload() — Compile-time, for always-needed resources
|
||||||
|
@onready var bullet_scene = preload("res://scenes/entities/bullet.tscn")
|
||||||
|
|
||||||
|
# load() — Runtime, for conditional loading
|
||||||
|
var dialog_scene = load("res://scenes/ui/dialog.tscn")
|
||||||
|
|
||||||
|
# instantiate() — Create instance from scene
|
||||||
|
func spawn_bullet():
|
||||||
|
var bullet = bullet_scene.instantiate()
|
||||||
|
add_child(bullet)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Typed Signals (Godot 4.x)
|
||||||
|
|
||||||
|
### Declaring Typed Signals
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Godot 4.x typed signal syntax
|
||||||
|
signal health_changed(new_health: int, max_health: int)
|
||||||
|
signal player_died(position: Vector2)
|
||||||
|
signal item_collected(item_data: Resource)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Connecting Signals
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Auto-connect in editor (rename method to on_NodeName_signal)
|
||||||
|
func _on_health_changed(new_health: int, _max_health: int):
|
||||||
|
update_health_bar(new_health)
|
||||||
|
|
||||||
|
# Manual connection
|
||||||
|
func _ready():
|
||||||
|
enemy.health_depleted.connect(_on_enemy_defeated)
|
||||||
|
|
||||||
|
# Callable syntax (Godot 4.x)
|
||||||
|
button.pressed.connect(func: do_something())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Scene Loading Patterns
|
||||||
|
|
||||||
|
### Resource Loading
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Preload for compile-time constants
|
||||||
|
const PlayerScene = preload("res://scenes/entities/player.tscn")
|
||||||
|
|
||||||
|
# Load for runtime/conditional loading
|
||||||
|
func load_level(level_name: String):
|
||||||
|
var path = "res://levels/%s.tscn" % level_name
|
||||||
|
var scene = load(path)
|
||||||
|
if scene:
|
||||||
|
current_level = scene.instantiate()
|
||||||
|
add_child(current_level)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Resource Management
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Use Resource for game data
|
||||||
|
@export_file("*.tres") var player_data: String
|
||||||
|
var data: PlayerData = load(player_data) as PlayerData
|
||||||
|
|
||||||
|
# Scene packs for level data
|
||||||
|
var level_pack = preload("res://resources/data/level_pack.tscn")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Patterns
|
||||||
|
|
||||||
|
### Singleton (Autoload) Pattern
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Add to Project Settings > Autoload as "GameData"
|
||||||
|
class_name GameData extends Node
|
||||||
|
|
||||||
|
var player_score: int = 0
|
||||||
|
var high_score: int = 0
|
||||||
|
|
||||||
|
func add_score(points: int):
|
||||||
|
player_score += points
|
||||||
|
if player_score > high_score:
|
||||||
|
high_score = player_score
|
||||||
|
```
|
||||||
|
|
||||||
|
### Event Bus Pattern
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Add to Project Settings > Autoload as "EventBus"
|
||||||
|
class_name EventBus extends Node
|
||||||
|
|
||||||
|
signal game_started()
|
||||||
|
signal level_completed(level_id: int)
|
||||||
|
signal player_died()
|
||||||
|
signal game_paused(paused: bool)
|
||||||
|
|
||||||
|
# Usage in any script
|
||||||
|
func _ready():
|
||||||
|
EventBus.level_completed.connect(on_level_completed)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Node Path Best Practices
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Good: Use @onready for scene tree paths
|
||||||
|
@onready var health_bar = $UI/HealthBar as ProgressBar
|
||||||
|
@onready var sprite = $Sprite2D as Sprite2D
|
||||||
|
|
||||||
|
# Avoid: Hardcoded paths
|
||||||
|
var health_bar = get_node("UI/HealthBar") # Fragile to hierarchy changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 GDScript Notes
|
||||||
|
|
||||||
|
- **DrawableTexture2D** — Create dynamic textures at runtime for procedural rendering
|
||||||
|
- **tween_await()** — Use await with tweens for sequential animations
|
||||||
|
- **Typed signals** are now the standard — prefer typed over untyped
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`script_read`** — Read any script file from the project
|
||||||
|
- **`script_edit`** — Write updated script contents directly to disk
|
||||||
|
- **`script_validate`** — Validate GDScript syntax with `suggestTweenAwait: true` for Godot 4.7 migration
|
||||||
|
- **`scene_tree`** — Inspect the current scene hierarchy
|
||||||
|
- **`class_db_info`** — Query available methods/properties on any Godot class
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [GDScript Style Guide](references/gdscript-style.md)
|
||||||
|
- [Signal Patterns Deep Dive](references/signals.md)
|
||||||
|
- [Resource Management](references/resource-management.md)
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Claude Code extension for godot-gdscript-patterns
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "writing GDScript code"
|
||||||
|
- "Godot signals and events"
|
||||||
|
- "node composition in Godot"
|
||||||
|
- "GDScript design patterns"
|
||||||
|
- "resource management in Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-gdscript-patterns
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "writing GDScript code"
|
||||||
|
- "Godot signals and events"
|
||||||
|
- "node composition in Godot"
|
||||||
|
- "GDScript design patterns"
|
||||||
|
- "resource management in Godot"
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
---
|
||||||
|
name: godot-headless-workflow
|
||||||
|
description: "AI-agent development loop for Godot 4.x without opening the editor: headless validation (--check-only), project import, gdUnit4 testing, safe hand-editing of text scenes (.tscn), CLI export pipeline for Android/iOS, mobile checklist, and RevenueCat plugin integration. Use when building or modifying a Godot game as a coding agent, running Godot in CI, validating GDScript changes, exporting mobile builds, or integrating in-app purchases."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- headless
|
||||||
|
- ci
|
||||||
|
- ai-agents
|
||||||
|
- testing
|
||||||
|
- export
|
||||||
|
- revenuecat
|
||||||
|
created: 2026-08-24
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot Headless Workflow (Agent Development Loop)
|
||||||
|
|
||||||
|
The complete edit→validate→test→run→export loop for Godot projects **without ever opening
|
||||||
|
the editor**. Every change must pass the headless gate before you claim it works.
|
||||||
|
|
||||||
|
## Golden Rules
|
||||||
|
|
||||||
|
1. **Never trust an edit you haven't validated headlessly.** A script that looks right but
|
||||||
|
was never parsed is not done.
|
||||||
|
2. **GDScript only** unless the project already uses C# (see `godot-csharp-patterns`). Pin the
|
||||||
|
engine version in the project's `AGENTS.md` (e.g., `Godot 4.7.2 stable, GDScript`).
|
||||||
|
3. **Scenes are text** (`.tscn`/`.tres`) — editable by hand *with care*, or better, generated
|
||||||
|
by tool scripts. See §Text Asset Safety.
|
||||||
|
4. **Never touch `.godot/`** — it's a regenerable cache.
|
||||||
|
5. **The editor GUI is optional.** Use it (or the package's MCP bridge) only for visual
|
||||||
|
layout iteration; logic, tests, and builds are all CLI work.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --version # e.g. 4.7.stable.official — confirm once per machine/session
|
||||||
|
```
|
||||||
|
|
||||||
|
The Godot binary must be on PATH **in the shell you run commands from** (Git Bash on Windows,
|
||||||
|
native bash on Linux). Export templates are needed only for §CLI Export:
|
||||||
|
|
||||||
|
| Host | Template install path |
|
||||||
|
|---|---|
|
||||||
|
| Windows | `%APPDATA%\Godot\export_templates\<engine-version>\` |
|
||||||
|
| Linux | `~/.local/share/godot/export_templates/<engine-version>/` |
|
||||||
|
| macOS | `~/Library/Application Support/Godot/export_templates/<engine-version>/` |
|
||||||
|
|
||||||
|
## Cross-Platform (Windows ↔ Ubuntu hosts)
|
||||||
|
|
||||||
|
All Godot CLI flags, gdUnit4 runners (`runtest.cmd` / `runtest.sh`), `res://` paths, and
|
||||||
|
export behavior are **identical across hosts** — projects stay fully portable. Watch only:
|
||||||
|
|
||||||
|
1. **Line endings kill `.sh` scripts**: if `verify.sh` is checked out with CRLF on Linux it
|
||||||
|
fails with `$'\r': command not found`. Add to every game repo's `.gitattributes`:
|
||||||
|
`*.sh text eol=lf` (and commit it from day one).
|
||||||
|
2. **Runner extension differs by host** — `runtest.cmd` on Windows, `runtest.sh` elsewhere;
|
||||||
|
detect or parameterize instead of hardcoding one.
|
||||||
|
3. **Don't mix toolchain worlds**: run Godot + Android SDK/adb **natively** per OS. Building
|
||||||
|
inside WSL while your emulator/adb runs on Windows causes device-detection pain.
|
||||||
|
On Ubuntu, USB debugging needs udev rules for your phone vendor before `adb devices` lists it.
|
||||||
|
4. **Signing env vars are the same** on both hosts — just use absolute paths appropriate
|
||||||
|
per filesystem in `GODOT_ANDROID_KEYSTORE_PATH`.
|
||||||
|
|
||||||
|
## The Core Loop
|
||||||
|
|
||||||
|
```
|
||||||
|
edit (.gd / .tscn / project settings)
|
||||||
|
→ full import (after adding/removing assets or scenes)
|
||||||
|
→ parse check (every changed script)
|
||||||
|
→ unit tests (gdUnit4, see references/testing.md)
|
||||||
|
→ smoke run (boot the game headlessly for N frames)
|
||||||
|
→ commit (only when everything above exits 0)
|
||||||
|
→ export (when feature-complete; see references/exporting.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 1 — Full import (required after asset/scene changes)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --path . --import
|
||||||
|
```
|
||||||
|
|
||||||
|
Imports all resources, generates `.godot/` cache, resolves UIDs, and reports load errors.
|
||||||
|
Run this before anything else in a fresh clone, and whenever you added textures/scenes/fonts.
|
||||||
|
Exit code ≠ 0 means broken resources — fix before continuing.
|
||||||
|
|
||||||
|
### Step 2 — Parse-check changed scripts
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --check-only --script res://scripts/player.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
Syntax + basic type checking without executing. Fast enough to run per-edit. Note: it does
|
||||||
|
NOT catch broken node paths, missing autoloads, or runtime type errors — those need steps 3–4.
|
||||||
|
|
||||||
|
### Step 3 — Unit tests (gdUnit4)
|
||||||
|
|
||||||
|
Standardize every project on [gdUnit4](https://github.com/MikeSchulze/gdUnit4). Tests live in
|
||||||
|
`res://tests/`; suites extend `GdUnitTestSuite`. Run from the project root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows
|
||||||
|
addons/gdUnit4/runtest.cmd -a res://tests --continue
|
||||||
|
# Linux/macOS
|
||||||
|
./addons/gdUnit4/runtest.sh -a res://tests --continue
|
||||||
|
```
|
||||||
|
|
||||||
|
Non-zero exit = failures (CI-safe). HTML/XML reports land in `res://reports/`.
|
||||||
|
Full patterns — assertions, async tests, parametrized cases — in `references/testing.md`.
|
||||||
|
|
||||||
|
### Step 4 — Smoke run
|
||||||
|
|
||||||
|
Boot the real game headlessly and let it tick a bounded number of frames:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --path . --quit-after 300 # ~300 frames of main scene
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime errors (bad autoload order, missing nodes, signal typos) print here and fail loudly.
|
||||||
|
For deeper checks (instantiate specific scenes, assert state), use a SceneTree driver script:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tools/smoke_test.gd — instantiate target scenes, tick frames, assert they survive
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var targets := ["res://scenes/main.tscn"]
|
||||||
|
var errors := 0
|
||||||
|
for path in targets:
|
||||||
|
var ps: PackedScene = load(path)
|
||||||
|
if ps == null:
|
||||||
|
push_error("FAILED TO LOAD: " + path)
|
||||||
|
errors += 1
|
||||||
|
continue
|
||||||
|
var node := ps.instantiate()
|
||||||
|
root.add_child(node)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
node.queue_free()
|
||||||
|
await process_frame
|
||||||
|
print("SMOKE RESULT: ", "FAIL" if errors > 0 else "PASS")
|
||||||
|
quit(1 if errors > 0 else 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --path . -s res://tools/smoke_test.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
### Copy-paste verification gate (put this in every game repo)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify.sh — the agent gate. Run after EVERY batch of edits. All steps must exit 0.
|
||||||
|
set -e
|
||||||
|
godot --headless --path . --import
|
||||||
|
for f in $(git diff --name-only HEAD | grep '\.gd$'); do
|
||||||
|
godot --headless --check-only --script "res://${f#*/}" || exit 1
|
||||||
|
done
|
||||||
|
addons/gdUnit4/runtest.cmd -a res://tests --continue # Linux/macOS: runtest.sh
|
||||||
|
godot --headless --path . --quit-after 300
|
||||||
|
echo "ALL CHECKS PASSED"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Text Asset Safety (.tscn / .tres)
|
||||||
|
|
||||||
|
Scenes and resources are plain text — readable, diffable, mergeable. Rules:
|
||||||
|
|
||||||
|
| Do | Don't |
|
||||||
|
|---|---|
|
||||||
|
| Read `.tscn` to learn structure before editing scripts | Invent `uid://` values |
|
||||||
|
| Edit node properties, positions, exported values in place | Delete or rename `.uid` sidecar files |
|
||||||
|
| Add `[node]` blocks mirroring existing syntax exactly | Reference resources by UID you haven't seen |
|
||||||
|
| Prefer generating scenes via a tool script (below) | Hand-write complex nested scenes from scratch |
|
||||||
|
| Let Godot regenerate paths after moving files (`--import`) | Edit anything under `.godot/` |
|
||||||
|
|
||||||
|
Since Godot 4.4, scripts carry `.uid` sidecars and `ext_resource` entries may point at
|
||||||
|
`uid://…`. If both `uid` and `path` exist, either resolving is fine — **keep the `path`
|
||||||
|
attribute when hand-editing** and never fabricate UIDs.
|
||||||
|
|
||||||
|
When structural changes get risky (new node trees, wiring signals between nodes), generate
|
||||||
|
the scene programmatically instead:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tools/build_scene.gd — run: godot --headless --path . -s res://tools/build_scene.gd
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
func _init() -> void:
|
||||||
|
call_deferred("_build")
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
var root_node := Node2D.new()
|
||||||
|
root_node.name = "Level1"
|
||||||
|
var sprite := Sprite2D.new()
|
||||||
|
sprite.name = "Player"
|
||||||
|
sprite.position = Vector2(640, 360)
|
||||||
|
root_node.add_child(sprite)
|
||||||
|
|
||||||
|
var packed := PackedScene.new()
|
||||||
|
packed.pack(root_node)
|
||||||
|
ResourceSaver.save(packed, "res://scenes/levels/level_1.tscn")
|
||||||
|
print("SCENE SAVED")
|
||||||
|
quit(0)
|
||||||
|
```
|
||||||
|
|
||||||
|
This is deterministic, reviewable, and can't corrupt unrelated scene sections.
|
||||||
|
|
||||||
|
## CLI Export
|
||||||
|
|
||||||
|
One-time setup: create presets (editor UI or by writing `export_presets.cfg` — see
|
||||||
|
`references/exporting.md` for working Android/iOS preset skeletons).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows/Linux desktop
|
||||||
|
godot --headless --path . --export-release "Windows Desktop" builds/win/game.exe
|
||||||
|
|
||||||
|
# Android (AAB for Play Store; APK for sideload/Galaxy Store)
|
||||||
|
godot --headless --path . --export-release "Android" builds/android/game.aab
|
||||||
|
|
||||||
|
# iOS — produces an Xcode project FOLDER, archived later on macOS
|
||||||
|
godot --headless --path . --export-release "iOS" builds/ios/
|
||||||
|
```
|
||||||
|
|
||||||
|
Release Android exports read signing config from env vars:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GODOT_ANDROID_KEYSTORE_PATH=/path/to/release.keystore
|
||||||
|
export GODOT_ANDROID_KEYSTORE_USER=alias
|
||||||
|
export GODOT_ANDROID_KEYSTORE_PASSWORD=secret
|
||||||
|
```
|
||||||
|
|
||||||
|
Details, debugging exports (`--export-debug`), and the store-submission checklist live in
|
||||||
|
`references/exporting.md`.
|
||||||
|
|
||||||
|
## Mobile Quick Checklist
|
||||||
|
|
||||||
|
- Stretch mode `canvas_items` + portrait/landscape locked in `project.godot`
|
||||||
|
- Touch input: enable "Emulate Mouse From Touch"; consider Godot 4.7's `VirtualJoystick`
|
||||||
|
- Respect notches/safe areas (get visible rect from viewport, anchor UI inside it)
|
||||||
|
- Budget: mid-range phone should hold 60 fps — profile with `godot-performance` skill before shipping
|
||||||
|
- Min versions when using the RevenueCat plugin below: iOS 15+, Android SDK 24+
|
||||||
|
|
||||||
|
## In-App Purchases (RevenueCat)
|
||||||
|
|
||||||
|
Use the community **Godotx RevenueCat** plugin (MIT; wraps the official RC iOS/Android SDKs;
|
||||||
|
built against Godot 4.7-stable). Condensed flow — full playbook in `references/revenuecat.md`:
|
||||||
|
|
||||||
|
1. Install via AssetLib ("Godotx RevenueCat") → gives `addons/`, `ios/plugins/`, `android/plugins/`
|
||||||
|
2. Enable the plugin **per export preset**; Android additionally needs **Gradle Build ON**
|
||||||
|
3. Create the app + IAP products in Play Console / App Store Connect first, mirror them in
|
||||||
|
the RevenueCat dashboard, note the platform-specific public API keys
|
||||||
|
4. Init at startup with the platform key → fetch offerings → paywall → purchase → entitlement check
|
||||||
|
5. Test a real sandbox purchase on device **before** building game systems on top of it
|
||||||
|
|
||||||
|
⚠️ Verify exact method names against the addon's own README/API reference at integration
|
||||||
|
time — don't guess API surface (see failure modes below).
|
||||||
|
|
||||||
|
## Agent Failure Modes (catch these in review)
|
||||||
|
|
||||||
|
| Failure mode | Symptom | Gate |
|
||||||
|
|---|---|---|
|
||||||
|
| **Godot 3 API bleed** | `onready var`, `yield(`, `.instance()`, `tool` alone, `export var`, string-form `connect("x", self, "y")` | `grep -rnE "onready \|yield\(|\.instance\(\)|^tool\b|^export \|connect\(\"" scripts/` → must be empty |
|
||||||
|
| **Unity API bleed (C# projects)** | `UnityEngine`, `MonoBehaviour`, `GetComponent` | Only relevant in .NET projects; prefer GDScript to avoid entirely |
|
||||||
|
| **Invented UIDs / broken refs** | Load errors at import | Run `--import` gate after every scene edit |
|
||||||
|
| **Autoload order bugs** | Singleton accessed before init in `_init()` | Move access to `_ready()`; smoke run catches it |
|
||||||
|
| **Wrong callback** | Game logic in `_process()` that needs physics ticks | Use `_physics_process()` for movement/forces (`godot-physics` skill) |
|
||||||
|
| **Signal leaks** | Connections to freed nodes; crashes on scene change | Connect in `_ready()`, disconnect or use `CONNECT_ONE_SHOT`; review with `godot-code-review` skill |
|
||||||
|
| **Guessed external APIs** | Hallucinated plugin/engine methods | Only cite APIs found in the project's addons/docs or verified via `--check-only` passing |
|
||||||
|
|
||||||
|
## Editor vs Headless — When To Use Which
|
||||||
|
|
||||||
|
| Task | Tool |
|
||||||
|
|---|---|
|
||||||
|
| Script edits, tests, exports, CI, refactors | **Headless CLI (this skill)** |
|
||||||
|
| Visual layout polish, animation curves, theme tweaking | Editor GUI (human) or the package's `mcp-bridge/` MCP server for agent-driven editor control |
|
||||||
|
| Import cache issues | Always `--import` headless, never hand-fix `.godot/` |
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
| Need | Skill |
|
||||||
|
|---|---|
|
||||||
|
| Scaffold the project | `godot-project-setup` |
|
||||||
|
| GDScript style & architecture decisions | `godot-gdscript-patterns`, `godot-brainstorming` |
|
||||||
|
| Review before merging features | `godot-code-review` |
|
||||||
|
| Diagnose failing smoke runs | `godot-debugging` |
|
||||||
|
| Ship fast on phones | `godot-performance` |
|
||||||
|
| Input handling, save systems, audio, shaders | `godot-input`, `godot-save-systems`, `godot-audio`, `godot-shaders-vfx` |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- `references/testing.md` — gdUnit4 deep dive: suites, assertions, async, CI integration
|
||||||
|
- `references/exporting.md` — export presets, Android/iOS pipelines, store submission checklist
|
||||||
|
- `references/revenuecat.md` — RevenueCat plugin integration playbook for mobile games
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-headless-workflow
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "building or modifying a Godot game from a coding agent"
|
||||||
|
- "validating GDScript changes without opening the editor"
|
||||||
|
- "running Godot tests in CI"
|
||||||
|
- "exporting Android/iOS builds from the command line"
|
||||||
|
- "integrating RevenueCat purchases in a Godot game"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- bash
|
||||||
|
- edit
|
||||||
|
- write
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-headless-workflow
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: false
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "building or modifying a Godot game from a coding agent"
|
||||||
|
- "validating GDScript changes without opening the editor"
|
||||||
|
- "running Godot tests in CI"
|
||||||
|
- "exporting Android/iOS builds from the command line"
|
||||||
|
- "integrating RevenueCat purchases in a Godot game"
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
# CLI Export Pipeline (Android / iOS / Desktop)
|
||||||
|
|
||||||
|
Export from CI or an agent shell with zero editor interaction, once presets exist.
|
||||||
|
|
||||||
|
## One-Time Setup Per Machine
|
||||||
|
|
||||||
|
1. Export templates matching the engine version must be installed — per host:
|
||||||
|
- Windows: `%APPDATA%\Godot\export_templates\<version>\`
|
||||||
|
- Linux: `~/.local/share/godot/export_templates/<version>/`
|
||||||
|
- macOS: `~/Library/Application Support/Godot/export_templates/<version>/`
|
||||||
|
Install via any editor's Manage Export Templates UI once, or copy manually.
|
||||||
|
2. Android toolchain (per build host): JDK 17, Android SDK with build-tools/platform-tools,
|
||||||
|
debug keystore. Typical `ANDROID_HOME`: `%LOCALAPPDATA%\Android\Sdk` (Windows),
|
||||||
|
`~/Android/Sdk` (Linux).
|
||||||
|
3. iOS builds always finish on macOS/Xcode — see iOS section. Generating the Xcode project
|
||||||
|
folder itself works fine from Windows or Linux.
|
||||||
|
|
||||||
|
## Presets (`export_presets.cfg`)
|
||||||
|
|
||||||
|
Create presets once (editor UI is fine) or write entries by hand. Minimal skeletons:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[preset.0]
|
||||||
|
name="Windows Desktop"
|
||||||
|
platform="Windows Desktop"
|
||||||
|
export_path="builds/win/game.exe"
|
||||||
|
|
||||||
|
[preset.1]
|
||||||
|
name="Android"
|
||||||
|
platform="Android"
|
||||||
|
export_path="builds/android/game.aab"
|
||||||
|
gradle_build/use_gradle_build=true # REQUIRED for native plugins (RevenueCat etc.)
|
||||||
|
package/unique_name="com.yourstudio.game"
|
||||||
|
|
||||||
|
[preset.2]
|
||||||
|
name="iOS"
|
||||||
|
platform="iOS"
|
||||||
|
export_path="builds/ios/game.xcodeproj"
|
||||||
|
```
|
||||||
|
|
||||||
|
After hand-editing presets, validate with a real export — typos fail at export time.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debug build (faster, includes debugger; use for device testing during dev)
|
||||||
|
godot --headless --path . --export-debug "Android" builds/android/debug.apk
|
||||||
|
|
||||||
|
# Release build (store submission)
|
||||||
|
godot --headless --path . --export-release "Windows Desktop" builds/win/game.exe
|
||||||
|
godot --headless --path . --export-release "Android" builds/android/game.aab # AAB = Play Store
|
||||||
|
godot --headless --path . --export-release "Android" builds/android/game.apk # APK = sideload/Galaxy Store
|
||||||
|
godot --headless --path . --export-release "iOS" builds/ios/ # Xcode project folder
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code 0 + file exists = success. On failure read the FULL log — export errors cascade
|
||||||
|
(missing template → missing SDK → signing failure).
|
||||||
|
|
||||||
|
## Android Signing
|
||||||
|
|
||||||
|
Debug keystore is automatic. For release:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -genkeypair -v -keystore release.keystore -alias game -keyalg RSA -validity 10000
|
||||||
|
|
||||||
|
export GODOT_ANDROID_KEYSTORE_PATH=/abs/path/release.keystore
|
||||||
|
export GODOT_ANDROID_KEYSTORE_USER=game
|
||||||
|
export GODOT_ANDROID_KEYSTORE_PASSWORD=secret
|
||||||
|
godot --headless --path . --export-release "Android" builds/android/game.aab
|
||||||
|
```
|
||||||
|
|
||||||
|
Guard the keystore like a password — losing it means losing app-signing continuity on Play.
|
||||||
|
Keep one shared release keystore in sync between your Windows and Linux machines (password
|
||||||
|
manager / secure transfer); signing with different keystores breaks update compatibility.
|
||||||
|
|
||||||
|
## Gradle Build Notes
|
||||||
|
|
||||||
|
Native Android plugins (RevenueCat, ads, Firebase…) require **Gradle Build enabled** in the
|
||||||
|
preset. First gradle build downloads dependencies (slow); subsequent builds cache. If gradle
|
||||||
|
fails, check: JDK version (17), `ANDROID_HOME`, and that plugin versions are mutually compatible.
|
||||||
|
|
||||||
|
## iOS Pipeline
|
||||||
|
|
||||||
|
The export produces an **Xcode project folder** (not an IPA):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
godot --headless --path . --export-release "iOS" builds/ios/
|
||||||
|
# → builds/ios/ contains .xcodeproj — open on macOS, set team/signing, Product → Archive
|
||||||
|
```
|
||||||
|
|
||||||
|
No Mac? Options: GitHub Actions `macos-*` runner, Codemagic, or borrow a Mac for a single
|
||||||
|
archive session. Set `bundle identifier`, version, and signing before archiving.
|
||||||
|
|
||||||
|
## Store Submission Checklist (Shipaton-relevant)
|
||||||
|
|
||||||
|
- [ ] Icon 1024×1024 PNG exported from the icon preset
|
||||||
|
- [ ] Screenshot 1179×2556 px **frameless** (Devpost requirement) — capture from emulator/device, not editor viewport
|
||||||
|
- [ ] `version/code` bumped every Play upload; iOS `CFBundleVersion` too
|
||||||
|
- [ ] Privacy policy URL + data-safety forms (purchases ⇒ declare IAP)
|
||||||
|
- [ ] IAP products created in store consoles BEFORE RevenueCat mirrors them
|
||||||
|
- [ ] Internal testing track first: verify purchase + restore flow on real device
|
||||||
|
- [ ] Submit days before deadline — review queues stretch unpredictably
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Error | Cause | Fix |
|
||||||
|
|---|---|---|
|
||||||
|
| `No export template found` | Version mismatch | Install templates matching exact engine version |
|
||||||
|
| Gradle: `SDK location not found` | Env | Set `ANDROID_HOME`, accept licenses |
|
||||||
|
| Plugin listed but "not enabled" | Preset toggle | Enable plugin checkbox per preset (both platforms!) |
|
||||||
|
| AAB rejected: versionCode lower | Increment | Bump `version/code` in preset |
|
||||||
|
| iOS archive fails: signing | Team/capabilities | Set development team in exported Xcode project |
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
# RevenueCat Plugin Integration Playbook (Godot Mobile Games)
|
||||||
|
|
||||||
|
For hackathon/game-jam mobile games that must monetize via RevenueCat. Based on research of
|
||||||
|
the **Godotx RevenueCat** plugin (MIT, community-maintained) — verified August 2026.
|
||||||
|
|
||||||
|
## Versions (pin these)
|
||||||
|
|
||||||
|
| Component | Version | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| Godot | 4.7-stable | plugin is built against this |
|
||||||
|
| Godotx RevenueCat plugin | v2.2.x | github.com/FunkyMuse/godot-revenuecat (also on official Asset Library, id 4493) |
|
||||||
|
| Bundled RC iOS SDK | 5.79.0 | min iOS 15.0 |
|
||||||
|
| Bundled RC Android SDK | 10.10.0 | min Android SDK 24 |
|
||||||
|
|
||||||
|
## Why this plugin
|
||||||
|
|
||||||
|
It wraps the **official** native RevenueCat SDKs and ships them as `.xcframework` (iOS) +
|
||||||
|
`.aar` (Android), managed through Godot's export-plugin pipeline — no C++/Java/Obj-C work
|
||||||
|
for us. It's thin glue: if it breaks, fork and patch it rather than replacing it.
|
||||||
|
|
||||||
|
⚠️ **No first-party RevenueCat SDK exists for Godot.** This community plugin is the
|
||||||
|
pragmatic path; budget a spike to validate it BEFORE building game systems on top.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. AssetLib in-editor → "Godotx RevenueCat" → Install & Enable
|
||||||
|
(or download release ZIP → copy `addons/`, `ios/plugins/`, `android/plugins/` into project root)
|
||||||
|
2. Commit all three folders
|
||||||
|
3. Run `godot --headless --path . --import` to regenerate cache
|
||||||
|
|
||||||
|
## Configure Export Presets
|
||||||
|
|
||||||
|
Both platforms need their plugin enabled — per preset:
|
||||||
|
|
||||||
|
- **Android preset**: enable the RevenueCat plugin AND `Gradle Build` (native plugins require gradle)
|
||||||
|
- **iOS preset**: enable the RevenueCat plugin
|
||||||
|
|
||||||
|
Verify with an actual headless export after setup (see `exporting.md`).
|
||||||
|
|
||||||
|
## Store Setup Order (do it in this sequence)
|
||||||
|
|
||||||
|
1. Create app in Google Play Console + App Store Connect
|
||||||
|
2. Create IAP products there (e.g., `premium_unlock`, `hints_10`)
|
||||||
|
3. Create matching products + entitlement (`premium`) + offering (`default`) in the
|
||||||
|
RevenueCat dashboard; note each platform's **public** API key
|
||||||
|
4. Sandbox-test purchases with internal testers before writing more game code
|
||||||
|
|
||||||
|
## Runtime Integration Skeleton
|
||||||
|
|
||||||
|
Read the addon's bundled README/API reference for exact method names — verify against the
|
||||||
|
installed version, don't guess (see SKILL.md failure modes). The flow is always:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# autoload IapManager — adapt names to the installed plugin API
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const IOS_API_KEY := "appl_xxx"
|
||||||
|
const ANDROID_API_KEY := "goog_xxx"
|
||||||
|
|
||||||
|
signal entitlements_changed(is_premium: bool)
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var key := IOS_API_KEY if OS.get_name() == "iOS" else ANDROID_API_KEY
|
||||||
|
_configure(key) # plugin configure/init call
|
||||||
|
check_entitlements()
|
||||||
|
|
||||||
|
func _configure(key: String) -> void:
|
||||||
|
pass # TODO: plugin init — see addons/godotx_revenue_cat README
|
||||||
|
|
||||||
|
func buy(product_id: String) -> void:
|
||||||
|
pass # TODO: purchase call → await result → check_entitlements()
|
||||||
|
|
||||||
|
func restore() -> void:
|
||||||
|
pass # TODO: restore purchases (App Store requires a restore button!)
|
||||||
|
|
||||||
|
func check_entitlements() -> void:
|
||||||
|
pass # TODO: fetch customer info → emit entitlements_changed
|
||||||
|
|
||||||
|
func show_paywall() -> void:
|
||||||
|
pass # TODO: present_paywall (plugin ships native paywall UI)
|
||||||
|
```
|
||||||
|
|
||||||
|
Wire `entitlements_changed` into game systems (unlock levels, remove ads, etc.).
|
||||||
|
|
||||||
|
## Hackathon-Specific Requirements
|
||||||
|
|
||||||
|
- Judges must get **a free trial OR a promo unlock** for premium features:
|
||||||
|
- Simplest: make the paywall offer a trial; or add a hidden "promo code" input redeeming
|
||||||
|
entitlement locally / via your own backend
|
||||||
|
- Demo video must show the real purchase flow on device (sandbox OK)
|
||||||
|
- Grand Prize judging uses RevenueCat-reported revenue → integrate early so revenue accrues
|
||||||
|
- Consider RevenueCat Ads tracking (AdMob ILRD → AdTracker) as an extra category entry
|
||||||
|
|
||||||
|
## Failure Playbook
|
||||||
|
|
||||||
|
| Problem | Action |
|
||||||
|
|---|---|
|
||||||
|
| Plugin init fails on export | Check plugin enabled in BOTH presets; Android needs Gradle Build ON |
|
||||||
|
| Products not loading | Store products not yet approved/available to tester account; check dashboard mirroring |
|
||||||
|
| Purchase succeeds, entitlement stale | Call customer-info refresh after purchase; don't cache entitlements across sessions without refresh |
|
||||||
|
| iOS build missing plugin symbols | Re-export; confirm `.xcframework` copied by export plugin |
|
||||||
|
| Plugin abandoned/broken | Fork repo, patch (thin native glue), or pin last-good release tag |
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- Plugin: github.com/FunkyMuse/godot-revenuecat · godot-x/revenuecat · Asset Library #4493
|
||||||
|
- Official SDKs: github.com/RevenueCat/purchases-ios · purchases-android · docs.revenuecat.com
|
||||||
|
- Engine ranking context: ship-a-ton-2026/docs/engine-research.md
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
# gdUnit4 Testing Deep Dive
|
||||||
|
|
||||||
|
Standardize every Godot project on [gdUnit4](https://github.com/MikeSchulze/gdUnit4) — the
|
||||||
|
actively maintained test framework with a headless CLI runner built for CI and agents.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. AssetLib → "gdUnit4" → Install (or add as git submodule under `addons/gdUnit4`)
|
||||||
|
2. Enable the addon in Project Settings → Plugins
|
||||||
|
3. Commit `addons/gdUnit4/` with the project so agents on fresh clones can run tests
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── unit/ # pure logic: score, economy, state machines
|
||||||
|
│ └── test_score_manager.gd
|
||||||
|
├── integration/ # scenes wired together
|
||||||
|
└── fixtures/ # test data (.tres)
|
||||||
|
reports/ # generated HTML/XML — .gitignore this
|
||||||
|
```
|
||||||
|
|
||||||
|
## Suite Conventions
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# tests/unit/test_score_manager.gd
|
||||||
|
class_name TestScoreManager
|
||||||
|
extends GdUnitTestSuite
|
||||||
|
|
||||||
|
const ScoreManager := preload("res://scripts/core/score_manager.gd")
|
||||||
|
|
||||||
|
var _sm
|
||||||
|
|
||||||
|
func before_test() -> void:
|
||||||
|
_sm = ScoreManager.new() # auto-freed after each test
|
||||||
|
|
||||||
|
func test_add_score_increments() -> void:
|
||||||
|
_sm.add(10)
|
||||||
|
assert_int(_sm.score).is_equal(10)
|
||||||
|
|
||||||
|
func test_score_never_negative() -> void:
|
||||||
|
_sm.add(-5)
|
||||||
|
assert_int(_sm.score).is_equal(0)
|
||||||
|
|
||||||
|
func test_addition_is_parameterized(a: int, b: int, expected: int) -> void:
|
||||||
|
_sm.add(a + b)
|
||||||
|
assert_int(_sm.score).is_equal(expected) # supply via test-suite args or use fuzzers
|
||||||
|
```
|
||||||
|
|
||||||
|
Assertion helpers to know: `assert_int`, `assert_float`, `assert_str`, `assert_bool`,
|
||||||
|
`assert_that(expr)`, `assert_array`, `assert_dict`, `assert_object(obj).is_instanceof(...)`,
|
||||||
|
`assert_signal(spy).is_emitted("name")`. Full list in gdUnit4 docs.
|
||||||
|
|
||||||
|
## Async & Scene Tests
|
||||||
|
|
||||||
|
Await signals/frames inside tests — essential for scene-dependent logic:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func test_enemy_dies_after_hit() -> void:
|
||||||
|
var enemy := auto_free(load("res://scenes/entities/enemy.tscn").instantiate())
|
||||||
|
scene_runner().add_child(enemy) # scene_runner() gives a managed tree context
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
enemy.take_damage(enemy.hp)
|
||||||
|
await assert_signal(enemy).is_emitted("died")
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep rendering-dependent asserts out of headless runs: no pixel/screenshot checks in CI.
|
||||||
|
|
||||||
|
## Running From CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# everything under res://tests
|
||||||
|
addons/gdUnit4/runtest.cmd -a res://tests --continue # Windows
|
||||||
|
./addons/gdUnit4/runtest.sh -a res://tests --continue # Linux/macOS
|
||||||
|
|
||||||
|
# one suite only
|
||||||
|
addons/gdUnit4/runtest.cmd -a tests/unit/test_score_manager.gd
|
||||||
|
|
||||||
|
# filter by name pattern; stop-on-first-failure for fast loops
|
||||||
|
addons/gdUnit4/runtest.cmd -a res://tests --include test_score --no-failure
|
||||||
|
```
|
||||||
|
|
||||||
|
- Exit code 0 = all passed; non-zero = failures → CI/agent gate safe
|
||||||
|
- Reports: `res://reports/` (HTML + XML/JUnit)
|
||||||
|
- Runner is headless by default when no display is available
|
||||||
|
|
||||||
|
## Agent Workflow Integration
|
||||||
|
|
||||||
|
1. Write the failing test FIRST when fixing a bug (reproduce via test, then fix)
|
||||||
|
2. Run only the touched suite while iterating (`--include`), full suite before commit
|
||||||
|
3. If a test is flaky (async timing), fix the race condition — never delete the assertion
|
||||||
|
4. New feature ⇒ new suite; refactoring ⇒ tests unchanged, code changes
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
|
||||||
|
| Pitfall | Fix |
|
||||||
|
|---|---|
|
||||||
|
| Test class missing `class_name` / wrong base class | Must be `extends GdUnitTestSuite` + `class_name Test*` |
|
||||||
|
| Asserting inside awaited coroutines after timeout | Use `await assert_signal(...)` helpers, not manual timers |
|
||||||
|
| Scene loads but `_ready` errors | Run smoke test (`SKILL.md §Smoke run`) — it surfaces these |
|
||||||
|
| Tests pass alone, fail together | Suites sharing autoload state → reset in `before_test()` |
|
||||||
|
| Adding tests without enabling addon | `--import` + check plugin enabled in project.godot |
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
---
|
||||||
|
name: godot-i18n
|
||||||
|
description: "Localization patterns for Godot 4.x: TranslationServer, CSV/PO/JSON translation formats, runtime language switching, RTL text support, font fallback chains, and pluralization. Use when implementing internationalization in Godot games."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- i18n
|
||||||
|
- localization
|
||||||
|
- translation
|
||||||
|
- rtl
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Localization in Godot
|
||||||
|
|
||||||
|
Patterns for internationalizing Godot games.
|
||||||
|
|
||||||
|
## TranslationServer Basics
|
||||||
|
|
||||||
|
Godot's built-in translation system uses `.translation` files (CSV-based):
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Mark strings for translation with _() shorthand
|
||||||
|
func _ready():
|
||||||
|
$Label.text = tr("Hello, Player!")
|
||||||
|
$Button.text = tr("Start Game")
|
||||||
|
|
||||||
|
# tr() calls TranslationServer.translate() internally
|
||||||
|
# Strings are looked up in loaded .translation resources
|
||||||
|
```
|
||||||
|
|
||||||
|
### Creating Translation Files
|
||||||
|
|
||||||
|
Use the editor: **Project → Tools → Translation** to create `.csv` files, then export as `.translation`.
|
||||||
|
|
||||||
|
Or create CSV manually:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
msgid "Hello, Player!"
|
||||||
|
msgstr "¡Hola, Jugador!"
|
||||||
|
|
||||||
|
msgid "Start Game"
|
||||||
|
msgstr "Comenzar Juego"
|
||||||
|
|
||||||
|
msgid "Score: {0}"
|
||||||
|
msgstr "Puntuación: {0}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Registering Translations
|
||||||
|
|
||||||
|
In **Project Settings → Locale → Configured Locales**, add supported languages (e.g., `es`, `fr`, `ja`).
|
||||||
|
|
||||||
|
## Runtime Language Switching
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name LanguageManager extends Node
|
||||||
|
|
||||||
|
signal language_changed(locale: String)
|
||||||
|
|
||||||
|
var available_locales: Array[String] = ["en", "es", "fr", "ja"]
|
||||||
|
var current_locale: String = "en"
|
||||||
|
|
||||||
|
func set_language(locale: String) -> void:
|
||||||
|
if not available_locales.has(locale):
|
||||||
|
push_error("LanguageManager: Unsupported locale '%s'" % locale)
|
||||||
|
return
|
||||||
|
TranslationServer.set_locale(locale)
|
||||||
|
current_locale = locale
|
||||||
|
language_changed.emit(locale)
|
||||||
|
_notify_ui_update()
|
||||||
|
|
||||||
|
func _notify_ui_update() -> void:
|
||||||
|
# Re-tr() all UI labels that need updating
|
||||||
|
# Connect to language_changed signal in UI nodes
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Save/restore preference
|
||||||
|
func _ready() -> void:
|
||||||
|
var saved = ConfigFile.new()
|
||||||
|
if saved.load("user://settings.cfg") == OK:
|
||||||
|
var locale = saved.get_value("general", "language", "en")
|
||||||
|
set_language(locale)
|
||||||
|
|
||||||
|
func _save_preference() -> void:
|
||||||
|
var config = ConfigFile.new()
|
||||||
|
config.load("user://settings.cfg")
|
||||||
|
config.set_value("general", "language", current_locale)
|
||||||
|
config.save("user://settings.cfg")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Language Selector UI
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# LanguageSelector.gd — attached to OptionButton
|
||||||
|
extends OptionButton
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
for locale in LanguageManager.available_locales:
|
||||||
|
var display_name = TranslationServer.get_locale_name(locale)
|
||||||
|
add_item(display_name, locale)
|
||||||
|
selected = LanguageManager.available_locales.find(LanguageManager.current_locale)
|
||||||
|
item_selected.connect(LanguageManager.set_language)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pluralization
|
||||||
|
|
||||||
|
Godot supports plural forms via `trn()`:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# trn(singular, plural, count)
|
||||||
|
func show_item_count(count: int):
|
||||||
|
$Label.text = trn("You have {0} item.", "You have {0} items.", count) % count
|
||||||
|
|
||||||
|
# For complex plurals (Slavic languages with 3+ forms), use custom logic
|
||||||
|
func get_plural_string(msgid_singular: String, msgid_plural: String, n: int) -> String:
|
||||||
|
# Godot handles plural forms via .po format plural rules
|
||||||
|
return trn(msgid_singular, msgid_plural, n)
|
||||||
|
```
|
||||||
|
|
||||||
|
## RTL Text Support
|
||||||
|
|
||||||
|
For Arabic, Hebrew, and other right-to-left languages:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# In Label or RichTextLabel:
|
||||||
|
$Label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
$Label.text_direction = TextServer.DIRECTION_RTL
|
||||||
|
$Label.alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
|
||||||
|
# For mixed LTR/RTL content, use RichTextLabel with bbcode
|
||||||
|
$RichTextLabel.text = "[rtl]مرحبا[/rtl] [ltr]Hello[/ltr]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### RTL-Aware UI Layout
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Container layouts flip automatically with RTL
|
||||||
|
# Use Control.layout_mode for proper RTL support:
|
||||||
|
$PanelContainer.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
|
||||||
|
# For manual positioning, mirror anchors:
|
||||||
|
func mirror_for_rtl(control: Control) -> void:
|
||||||
|
if TranslationServer.get_locale() in ["ar", "he", "fa"]:
|
||||||
|
var left = control.anchor_left
|
||||||
|
control.anchor_left = control.anchor_right
|
||||||
|
control.anchor_right = left
|
||||||
|
```
|
||||||
|
|
||||||
|
## Font Fallback Chains
|
||||||
|
|
||||||
|
Handle characters not available in the primary font:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Theme approach (recommended):
|
||||||
|
# 1. Create Theme resource
|
||||||
|
# 2. Set "FontFile" for Label/LineEdit
|
||||||
|
# 3. Add fallback fonts via DynamicFont
|
||||||
|
|
||||||
|
# Code approach:
|
||||||
|
var dynamic_font = DynamicFont.new()
|
||||||
|
dynamic_font.font_path = "res://fonts/primary.ttf"
|
||||||
|
dynamic_font.fallback_fonts.append(DynamicFontLoading.new())
|
||||||
|
$Label.add_theme_font_override("font", dynamic_font)
|
||||||
|
|
||||||
|
# For CJK + Latin support, chain fonts:
|
||||||
|
# Primary: Noto Sans (Latin) → Fallback: Noto Sans CJK
|
||||||
|
```
|
||||||
|
|
||||||
|
## Translation Best Practices
|
||||||
|
|
||||||
|
### Use Context Keys
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# When the same word has different meanings:
|
||||||
|
tr("Attack [weapon]") # As a verb
|
||||||
|
tr("Attack [noun]") # As a noun/stat name
|
||||||
|
|
||||||
|
# In .po files, msgctxt provides context
|
||||||
|
# msgctxt "weapon" / msgid "Attack" / msgstr "Atacar"
|
||||||
|
# msgctxt "noun" / msgid "Attack" / msgstr "Ataque"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Avoid String Concatenation for Translatable Text
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Bad — translators can't reorder
|
||||||
|
tr("You found ") + item_name + tr(" in the cave!")
|
||||||
|
|
||||||
|
# Good — translators control word order
|
||||||
|
tr("You found {0} in the cave!") % item_name
|
||||||
|
|
||||||
|
# Better — named placeholders for clarity
|
||||||
|
tr("You found {item} in {location}!") % {"item": item_name, "location": location_name}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Externalize All UI Strings
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Hardcoded strings make translation impossible
|
||||||
|
$Label.text = "Score: 100" # BAD
|
||||||
|
|
||||||
|
# Use tr() for everything visible to players
|
||||||
|
$Label.text = tr("Score: {0}") % score # GOOD
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 i18n Notes
|
||||||
|
|
||||||
|
- **TextServer** API improved for complex script handling
|
||||||
|
- **RichTextLabel** supports better RTL paragraph alignment
|
||||||
|
- Translation import pipeline handles `.po` files natively (no conversion needed)
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`project_info`** — Check configured locales and translation file paths
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify localization scripts
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [TranslationServer Deep Dive](references/translation-server.md)
|
||||||
|
- [RTL Layout Patterns](references/rtl-layout.md)
|
||||||
|
- [Font Fallback Strategies](references/font-fallbacks.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-i18n
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "localizing Godot games"
|
||||||
|
- "Godot translation system"
|
||||||
|
- "i18n Godot patterns"
|
||||||
|
- "runtime language switching Godot"
|
||||||
|
- "RTL text Godot"
|
||||||
|
- "font fallback Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-i18n
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "localizing Godot games"
|
||||||
|
- "Godot translation system"
|
||||||
|
- "i18n Godot patterns"
|
||||||
|
- "runtime language switching Godot"
|
||||||
|
- "RTL text Godot"
|
||||||
|
- "font fallback Godot"
|
||||||
|
|
@ -0,0 +1,375 @@
|
||||||
|
---
|
||||||
|
name: godot-input
|
||||||
|
description: "Input handling patterns for Godot 4.x: Input map configuration, action polling vs events, gamepad/virtual joystick support, input remapping UI, dead zones, and touch/multi-touch. Use when implementing player controls, controller support, or input customization in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- input
|
||||||
|
- controls
|
||||||
|
- gamepad
|
||||||
|
- touch
|
||||||
|
- remapping
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Input Handling in Godot
|
||||||
|
|
||||||
|
Patterns for robust, accessible input systems.
|
||||||
|
|
||||||
|
## Input Map Configuration
|
||||||
|
|
||||||
|
Define actions in **Project Settings → Input Map** or via code:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Register actions at runtime (or use editor)
|
||||||
|
InputMap.add_action("move_left")
|
||||||
|
InputMap.action_add_event("move_left", InputEventKey.new())
|
||||||
|
|
||||||
|
var left_event = InputEventKey.new()
|
||||||
|
left_event.keycode = KEY_A
|
||||||
|
InputMap.action_add_event("move_left", left_event)
|
||||||
|
|
||||||
|
# Add gamepad button
|
||||||
|
var dpad_event = InputEventJoypadButton.new()
|
||||||
|
dpad_event.button_index = JOY_BUTTON_DPAD_LEFT
|
||||||
|
dpad_event.pressed = true
|
||||||
|
InputMap.action_add_event("move_left", dpad_event)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Action Naming
|
||||||
|
|
||||||
|
```
|
||||||
|
move_left, move_right, move_up, move_down
|
||||||
|
jump, crouch, interact, attack
|
||||||
|
inventory, pause, menu_confirm, menu_cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
## Polling vs Events
|
||||||
|
|
||||||
|
Two approaches to reading input:
|
||||||
|
|
||||||
|
### Polling (Continuous — `_physics_process`)
|
||||||
|
|
||||||
|
For movement and analog input:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name PlayerController extends CharacterBody2D
|
||||||
|
|
||||||
|
@export var speed: float = 200.0
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
# Input.get_action_strength returns 0.0 to 1.0
|
||||||
|
# Automatically combines keyboard + gamepad inputs
|
||||||
|
var input_x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
|
||||||
|
var input_y = Input.get_action_strength("move_down") - Input.get_action_strength("move_up")
|
||||||
|
|
||||||
|
var direction = Vector2(input_x, input_y).normalized()
|
||||||
|
velocity = direction * speed
|
||||||
|
|
||||||
|
move_and_slide()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Events (Discrete — `_unhandled_input`)
|
||||||
|
|
||||||
|
For camera rotation, menu navigation, one-shot actions:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("interact"):
|
||||||
|
interact_with_target()
|
||||||
|
elif event.is_action_pressed("pause"):
|
||||||
|
toggle_pause()
|
||||||
|
|
||||||
|
# Use _unhandled_input so UI can consume input first
|
||||||
|
# Use _input if this node should always receive input
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Each
|
||||||
|
|
||||||
|
| Approach | Use For | Method |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| Polling | Movement, analog sticks, continuous actions | `_physics_process` / `_process` |
|
||||||
|
| Events | Camera rotation, one-shot actions, menu nav | `_unhandled_input` |
|
||||||
|
| Both | Games with movement + discrete actions | Combined |
|
||||||
|
|
||||||
|
## Gamepad Support
|
||||||
|
|
||||||
|
### Detecting Connected Controllers
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func _ready() -> void:
|
||||||
|
var connected = Input.get_connected_joypads()
|
||||||
|
if connected.size() > 0:
|
||||||
|
print("Gamepad connected: ", connected[0])
|
||||||
|
# Enable rumble, vibrate, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rumble / Haptic Feedback
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Simple rumble on action
|
||||||
|
func play_rumble(strength: float = 0.5, duration: float = 0.1) -> void:
|
||||||
|
Input.start_joy_vibration(0, strength, strength, duration)
|
||||||
|
|
||||||
|
# Dual-motor (separate weak/strong rumble)
|
||||||
|
Input.start_joy_vibration(0, 0.3, 0.8, 0.2) # weak, strong, duration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dead Zone Configuration
|
||||||
|
|
||||||
|
Prevent drift from analog sticks:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Set global dead zone (Project Settings → Input Devices → Keyboard/Mouse → Joystick Dead Zone)
|
||||||
|
# Or per-action in code:
|
||||||
|
Input.set_deadzone_value(0.15) # Default is typically 0.15-0.25
|
||||||
|
|
||||||
|
# Custom dead zone for specific input reading
|
||||||
|
func get_analog_input(action_name: String, dead_zone: float = 0.15) -> float:
|
||||||
|
var value = Input.get_action_strength(action_name)
|
||||||
|
if value < dead_zone:
|
||||||
|
return 0.0
|
||||||
|
# Remap from [dead_zone, 1.0] to [0.0, 1.0]
|
||||||
|
return (value - dead_zone) / (1.0 - dead_zone)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Input Remapping UI
|
||||||
|
|
||||||
|
Let players customize controls:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name InputRemapButton extends Button
|
||||||
|
|
||||||
|
var current_action: String = ""
|
||||||
|
var waiting_for_input: bool = false
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
pressed.connect(_on_button_pressed)
|
||||||
|
|
||||||
|
func set_action(action: String) -> void:
|
||||||
|
current_action = action
|
||||||
|
update_display()
|
||||||
|
|
||||||
|
func _on_button_pressed() -> void:
|
||||||
|
if waiting_for_input:
|
||||||
|
return
|
||||||
|
waiting_for_input = true
|
||||||
|
text = "Press a key..."
|
||||||
|
# Grab focus so input events come here
|
||||||
|
grab_focus()
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not waiting_for_input:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event is InputEventKey or event is InputEventJoypadButton:
|
||||||
|
# Clear old bindings for this action
|
||||||
|
var old_events = InputMap.action_get_events(current_action)
|
||||||
|
for old_event in old_events:
|
||||||
|
InputMap.action_remove_event(current_action, old_event)
|
||||||
|
|
||||||
|
# Add new binding
|
||||||
|
InputMap.action_add_event(current_action, event.duplicate())
|
||||||
|
waiting_for_input = false
|
||||||
|
update_display()
|
||||||
|
get_viewport().set_input_as_handled() # Prevent game from receiving it
|
||||||
|
|
||||||
|
func update_display() -> void:
|
||||||
|
if not waiting_for_input:
|
||||||
|
var events = InputMap.action_get_events(current_action)
|
||||||
|
if events.size() > 0:
|
||||||
|
text = _event_to_string(events[0])
|
||||||
|
else:
|
||||||
|
text = "Not assigned"
|
||||||
|
|
||||||
|
func _event_to_string(event: InputEvent) -> String:
|
||||||
|
if event is InputEventKey:
|
||||||
|
return OS.get_keycode_string(event.keycode)
|
||||||
|
elif event is InputEventJoypadButton:
|
||||||
|
return "Gamepad Button %d" % event.button_index
|
||||||
|
return "Unknown"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Saving Remapped Controls
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name InputSettings extends Node
|
||||||
|
|
||||||
|
const SETTINGS_PATH := "user://input_settings.cfg"
|
||||||
|
|
||||||
|
func save_bindings() -> void:
|
||||||
|
var config = ConfigFile.new()
|
||||||
|
var actions = InputMap.get_actions()
|
||||||
|
|
||||||
|
for action in actions:
|
||||||
|
var events = InputMap.action_get_events(action)
|
||||||
|
var event_strings = []
|
||||||
|
for event in events:
|
||||||
|
event_strings.append(_event_to_string(event))
|
||||||
|
config.set_value("input", action, ",".join(event_strings))
|
||||||
|
|
||||||
|
config.save(SETTINGS_PATH)
|
||||||
|
|
||||||
|
func load_bindings() -> Error:
|
||||||
|
var config = ConfigFile.new()
|
||||||
|
if config.load(SETTINGS_PATH) != OK:
|
||||||
|
return Error.ERR_FILE_NOT_FOUND
|
||||||
|
|
||||||
|
var actions = InputMap.get_actions()
|
||||||
|
for action in actions:
|
||||||
|
var event_strings = config.get_value("input", action, "") as String
|
||||||
|
if event_strings.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Clear and re-add
|
||||||
|
var old_events = InputMap.action_get_events(action)
|
||||||
|
for old_event in old_events:
|
||||||
|
InputMap.action_remove_event(action, old_event)
|
||||||
|
|
||||||
|
for str in event_strings.split(","):
|
||||||
|
var event = _string_to_event(str)
|
||||||
|
if event:
|
||||||
|
InputMap.action_add_event(action, event)
|
||||||
|
|
||||||
|
return Error.OK
|
||||||
|
|
||||||
|
func _event_to_string(event: InputEvent) -> String:
|
||||||
|
if event is InputEventKey:
|
||||||
|
return "key:%d" % event.keycode
|
||||||
|
elif event is InputEventJoypadButton:
|
||||||
|
return "joypad:%d" % event.button_index
|
||||||
|
return ""
|
||||||
|
|
||||||
|
func _string_to_event(str: String) -> InputEvent:
|
||||||
|
var parts = str.split(":")
|
||||||
|
if parts[0] == "key":
|
||||||
|
var event = InputEventKey.new()
|
||||||
|
event.keycode = int(parts[1])
|
||||||
|
return event
|
||||||
|
elif parts[0] == "joypad":
|
||||||
|
var event = InputEventJoypadButton.new()
|
||||||
|
event.button_index = int(parts[1])
|
||||||
|
event.pressed = true
|
||||||
|
return event
|
||||||
|
return null
|
||||||
|
```
|
||||||
|
|
||||||
|
## Touch and Multi-Touch
|
||||||
|
|
||||||
|
### Virtual Joystick (Godot 4.7+)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Godot 4.7 includes VirtualJoystick control node
|
||||||
|
# Place in scene, configure via inspector:
|
||||||
|
# - Base texture, knob texture
|
||||||
|
# - Dead zone value
|
||||||
|
# - Action names for directional output
|
||||||
|
|
||||||
|
# Read virtual joystick input:
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
var joystick = $VirtualJoystick as VirtualJoystick
|
||||||
|
if joystick and joystick.is_active():
|
||||||
|
var direction = joystick.get_vector() # Vector2 (-1 to 1)
|
||||||
|
velocity = direction * speed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-Touch Gestures
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name TouchHandler extends Node2D
|
||||||
|
|
||||||
|
var _touch_points: Dictionary = {} # index -> Vector2
|
||||||
|
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventScreenDrag:
|
||||||
|
_handle_drag(event)
|
||||||
|
elif event is InputEventScreenTouch:
|
||||||
|
if event.pressed:
|
||||||
|
_touch_points[event.index] = event.position
|
||||||
|
else:
|
||||||
|
_touch_points.erase(event.index)
|
||||||
|
|
||||||
|
func _handle_drag(event: InputEventScreenDrag) -> void:
|
||||||
|
if _touch_points.has(event.index):
|
||||||
|
var delta = event.position - _touch_points[event.index]
|
||||||
|
_touch_points[event.index] = event.position
|
||||||
|
# Use delta for movement, camera pan, etc.
|
||||||
|
|
||||||
|
# Pinch-to-zoom with two fingers
|
||||||
|
func get_pinch_scale() -> float:
|
||||||
|
var indices = _touch_points.keys()
|
||||||
|
if indices.size() < 2:
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
var idx_a = indices[0]
|
||||||
|
var idx_b = indices[1]
|
||||||
|
var current_dist = _touch_points[idx_a].distance_to(_touch_points[idx_b])
|
||||||
|
# Compare to previous frame distance for zoom delta
|
||||||
|
return current_dist / maxf(1.0, _get_previous_distance())
|
||||||
|
|
||||||
|
var _previous_distance: float = 0.0
|
||||||
|
func _get_previous_distance() -> float:
|
||||||
|
return _previous_distance
|
||||||
|
```
|
||||||
|
|
||||||
|
## Accessibility Patterns
|
||||||
|
|
||||||
|
### Hold vs Toggle Actions
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Support both hold and toggle for accessibility
|
||||||
|
@export var use_toggle_jump: bool = false
|
||||||
|
var jump_toggled: bool = false
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event.is_action_pressed("jump"):
|
||||||
|
if use_toggle_jump:
|
||||||
|
jump_toggled = !jump_toggled
|
||||||
|
# Movement code checks either:
|
||||||
|
# Input.is_action_pressed("jump") OR jump_toggled
|
||||||
|
```
|
||||||
|
|
||||||
|
### Sensitivity Slider
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# For camera or aim sensitivity
|
||||||
|
@export var sensitivity: float = 1.0
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseMotion:
|
||||||
|
camera.rotate_y(-event.relative.x * sensitivity * 0.002)
|
||||||
|
camera.rotate_x(-event.relative.y * sensitivity * 0.002)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 Input Notes
|
||||||
|
|
||||||
|
- **VirtualJoystick** — New built-in control node for touch-based analog input
|
||||||
|
- **InputActionEvent** — Enhanced event structure for complex action queries
|
||||||
|
- **Gamepad** — Improved auto-detection and button mapping consistency
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`project_info`** — Check configured input actions and settings
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify input scripts
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Input Map Best Practices](references/input-map.md)
|
||||||
|
- [Gamepad Support Guide](references/gamepad-support.md)
|
||||||
|
- [Touch Input Patterns](references/touch-input.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-input
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "implementing input in Godot"
|
||||||
|
- "Godot gamepad support"
|
||||||
|
- "input remapping Godot"
|
||||||
|
- "touch controls Godot"
|
||||||
|
- "virtual joystick Godot"
|
||||||
|
- "multi-touch Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-input
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "implementing input in Godot"
|
||||||
|
- "Godot gamepad support"
|
||||||
|
- "input remapping Godot"
|
||||||
|
- "touch controls Godot"
|
||||||
|
- "virtual joystick Godot"
|
||||||
|
- "multi-touch Godot"
|
||||||
|
|
@ -0,0 +1,314 @@
|
||||||
|
---
|
||||||
|
name: godot-inventory-economy
|
||||||
|
description: "Inventory and economy patterns for Godot 4.x: resource-based item definitions, stackable/unique items, drag-drop UI, equip systems, currency management, and save integration. Use when building item systems, shops, or economies in Godot games."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- inventory
|
||||||
|
- economy
|
||||||
|
- items
|
||||||
|
- equip
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Inventory and Economy Systems in Godot
|
||||||
|
|
||||||
|
Patterns for item management, equipping, and game economies.
|
||||||
|
|
||||||
|
## Resource-Based Item Definitions
|
||||||
|
|
||||||
|
Define all items as `Resource` files — no code changes needed for new items:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ItemData extends Resource
|
||||||
|
|
||||||
|
enum ItemType { CONSUMABLE, EQUIPMENT, KEY_ITEM, MATERIAL }
|
||||||
|
enum EquipmentSlot { HEAD, CHEST, HANDS, FEET, WEAPON, ACCESSORY }
|
||||||
|
|
||||||
|
@export var item_id: String = ""
|
||||||
|
@export var display_name: String = ""
|
||||||
|
@export var description: String = ""
|
||||||
|
@export var icon: Texture2D = null
|
||||||
|
@export var item_type: ItemType = ItemType.MATERIAL
|
||||||
|
@export var stack_size: int = 1 # Max per stack, 0 = unique
|
||||||
|
@export var value: int = 0 # Gold/sell value
|
||||||
|
@export var equipment_slot: EquipmentSlot = EquipmentSlot.WEAPON
|
||||||
|
|
||||||
|
# Stat modifiers (for equipment)
|
||||||
|
@export var stat_modifiers: Dictionary = {} # {"speed": 10, "defense": 5}
|
||||||
|
|
||||||
|
# Consumable effects
|
||||||
|
@export var heal_amount: int = 0
|
||||||
|
@export var effect_script: Script = null # Optional custom effect
|
||||||
|
```
|
||||||
|
|
||||||
|
### Item Instance (Runtime)
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name InventoryItem extends Resource
|
||||||
|
|
||||||
|
@export var item_data: ItemData
|
||||||
|
@export var quantity: int = 1
|
||||||
|
@export var durability: float = 1.0 # 0.0 to 1.0, -1 = unbreakable
|
||||||
|
@export var is_equipped: bool = false
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inventory Manager
|
||||||
|
|
||||||
|
Core inventory system with stack management:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name Inventory extends Node
|
||||||
|
|
||||||
|
signal item_added(item: InventoryItem)
|
||||||
|
signal item_removed(item: InventoryItem)
|
||||||
|
signal item_equipped(item: InventoryItem, slot: ItemData.EquipmentSlot)
|
||||||
|
signal item_unequipped(slot: ItemData.EquipmentSlot)
|
||||||
|
signal inventory_changed()
|
||||||
|
|
||||||
|
var items: Array[InventoryItem] = []
|
||||||
|
var equipped: Dictionary = {} # slot -> InventoryItem
|
||||||
|
@export var max_slots: int = 20
|
||||||
|
|
||||||
|
func add_item(data: ItemData, quantity: int = 1) -> bool:
|
||||||
|
if items.size() >= max_slots:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Try to stack with existing item
|
||||||
|
for inv_item in items:
|
||||||
|
if inv_item.item_data == data and data.stack_size > 1:
|
||||||
|
var remaining = data.stack_size - inv_item.quantity
|
||||||
|
if remaining > 0:
|
||||||
|
var add_amount = min(quantity, remaining)
|
||||||
|
inv_item.quantity += add_amount
|
||||||
|
quantity -= add_amount
|
||||||
|
item_added.emit(inv_item)
|
||||||
|
inventory_changed.emit()
|
||||||
|
if quantity <= 0:
|
||||||
|
return true
|
||||||
|
|
||||||
|
# Create new stack
|
||||||
|
while quantity > 0:
|
||||||
|
var new_item = InventoryItem.new()
|
||||||
|
new_item.item_data = data
|
||||||
|
var add_amount = min(quantity, data.stack_size if data.stack_size > 0 else 1)
|
||||||
|
new_item.quantity = add_amount
|
||||||
|
new_item.durability = data.stack_size > 1 ? 1.0 : -1.0
|
||||||
|
items.append(new_item)
|
||||||
|
item_added.emit(new_item)
|
||||||
|
quantity -= add_amount
|
||||||
|
|
||||||
|
inventory_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
func remove_item(index: int, quantity: int = 1) -> bool:
|
||||||
|
if index < 0 or index >= items.size():
|
||||||
|
return false
|
||||||
|
|
||||||
|
var inv_item = items[index]
|
||||||
|
if inv_item.quantity <= quantity:
|
||||||
|
item_removed.emit(inv_item)
|
||||||
|
items.remove_at(index)
|
||||||
|
else:
|
||||||
|
inv_item.quantity -= quantity
|
||||||
|
item_removed.emit(inv_item)
|
||||||
|
|
||||||
|
inventory_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
func equip(item_index: int) -> bool:
|
||||||
|
var inv_item = items[item_index]
|
||||||
|
var slot = inv_item.item_data.equipment_slot
|
||||||
|
|
||||||
|
# Unequip current item in slot
|
||||||
|
if equipped.has(slot):
|
||||||
|
unequip(slot)
|
||||||
|
|
||||||
|
inv_item.is_equipped = true
|
||||||
|
equipped[slot] = inv_item
|
||||||
|
item_equipped.emit(inv_item, slot)
|
||||||
|
return true
|
||||||
|
|
||||||
|
func unequip(slot: ItemData.EquipmentSlot) -> bool:
|
||||||
|
if not equipped.has(slot):
|
||||||
|
return false
|
||||||
|
|
||||||
|
var inv_item = equipped[slot]
|
||||||
|
inv_item.is_equipped = false
|
||||||
|
equipped.erase(slot)
|
||||||
|
item_unequipped.emit(slot)
|
||||||
|
return true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Drag-and-Drop Inventory UI
|
||||||
|
|
||||||
|
Grid-based inventory with drag support:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name InventorySlot extends Container
|
||||||
|
|
||||||
|
signal slot_clicked(index: int)
|
||||||
|
signal drag_started(item: InventoryItem, index: int)
|
||||||
|
signal drop_requested(from_index: int, to_index: int)
|
||||||
|
|
||||||
|
var item: InventoryItem = null
|
||||||
|
var slot_index: int = 0
|
||||||
|
|
||||||
|
func _gui_input(event: InputEvent) -> void:
|
||||||
|
if event is InputEventMouseButton and event.pressed:
|
||||||
|
if event.button_index == MOUSE_BUTTON_LEFT and item:
|
||||||
|
drag_started.emit(item, slot_index)
|
||||||
|
|
||||||
|
# Handle dropping another item into this slot
|
||||||
|
func accept_drop(from_slot: InventorySlot) -> bool:
|
||||||
|
if not item:
|
||||||
|
# Empty slot — move item here
|
||||||
|
drop_requested.emit(from_slot.slot_index, slot_index)
|
||||||
|
return true
|
||||||
|
elif from_slot.item and not item:
|
||||||
|
drop_requested.emit(from_slot.slot_index, slot_index)
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
func update_display() -> void:
|
||||||
|
# Update child controls (icon, quantity label) based on item
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inventory Grid
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name InventoryGrid extends GridContainer
|
||||||
|
|
||||||
|
@export var inventory: Inventory
|
||||||
|
@export var slot_scene: PackedScene
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
for i in range(inventory.max_slots):
|
||||||
|
var slot = slot_scene.instantiate() as InventorySlot
|
||||||
|
slot.slot_index = i
|
||||||
|
slot.drag_started.connect(_on_drag_started)
|
||||||
|
slot.drop_requested.connect(_on_drop_requested)
|
||||||
|
add_child(slot)
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
func refresh() -> void:
|
||||||
|
for child in get_children():
|
||||||
|
if child is InventorySlot:
|
||||||
|
var slot = child as InventorySlot
|
||||||
|
if slot.slot_index < inventory.items.size():
|
||||||
|
slot.item = inventory.items[slot.slot_index]
|
||||||
|
else:
|
||||||
|
slot.item = null
|
||||||
|
slot.update_display()
|
||||||
|
|
||||||
|
func _on_drop_requested(from: int, to: int) -> void:
|
||||||
|
# Swap items between slots
|
||||||
|
if from >= inventory.items.size() or to >= inventory.items.size():
|
||||||
|
return
|
||||||
|
var temp = inventory.items[from]
|
||||||
|
inventory.items[from] = inventory.items[to]
|
||||||
|
inventory.items[to] = temp
|
||||||
|
refresh()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Currency and Shop System
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name EconomyManager extends Node
|
||||||
|
|
||||||
|
signal gold_changed(amount: int)
|
||||||
|
signal item_purchased(item: ItemData, cost: int)
|
||||||
|
signal item_sold(item: ItemData, price: int)
|
||||||
|
|
||||||
|
var gold: int = 0
|
||||||
|
|
||||||
|
func add_gold(amount: int) -> void:
|
||||||
|
gold += amount
|
||||||
|
gold_changed.emit(gold)
|
||||||
|
|
||||||
|
func spend_gold(amount: int) -> bool:
|
||||||
|
if gold < amount:
|
||||||
|
return false
|
||||||
|
gold -= amount
|
||||||
|
gold_changed.emit(gold)
|
||||||
|
return true
|
||||||
|
|
||||||
|
# Shop transaction
|
||||||
|
func buy_item(inventory: Inventory, item_data: ItemData, price: int) -> bool:
|
||||||
|
if not spend_gold(price):
|
||||||
|
return false
|
||||||
|
inventory.add_item(item_data)
|
||||||
|
item_purchased.emit(item_data, price)
|
||||||
|
return true
|
||||||
|
|
||||||
|
func sell_item(inventory: Inventory, item_index: int, price: int) -> bool:
|
||||||
|
var inv_item = inventory.items[item_index]
|
||||||
|
if not inv_item:
|
||||||
|
return false
|
||||||
|
inventory.remove_item(item_index, 1)
|
||||||
|
add_gold(price)
|
||||||
|
item_sold.emit(inv_item.item_data, price)
|
||||||
|
return true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Save Integration
|
||||||
|
|
||||||
|
Serialize inventory for save systems:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func serialize_inventory() -> Dictionary:
|
||||||
|
var result = []
|
||||||
|
for inv_item in items:
|
||||||
|
result.append({
|
||||||
|
"item_id": inv_item.item_data.item_id,
|
||||||
|
"quantity": inv_item.quantity,
|
||||||
|
"durability": inv_item.durability,
|
||||||
|
"equipped": inv_item.is_equipped
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
func deserialize_inventory(data: Array) -> void:
|
||||||
|
items.clear()
|
||||||
|
for entry in data:
|
||||||
|
var item_data = load("res://items/" + entry["item_id"] + ".tres") as ItemData
|
||||||
|
if item_data:
|
||||||
|
var inv_item = InventoryItem.new()
|
||||||
|
inv_item.item_data = item_data
|
||||||
|
inv_item.quantity = entry["quantity"]
|
||||||
|
inv_item.durability = entry.get("durability", 1.0)
|
||||||
|
items.append(inv_item)
|
||||||
|
inventory_changed.emit()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 Economy Notes
|
||||||
|
|
||||||
|
- **ResourceCache** — Preload item resources at game start for instant UI updates
|
||||||
|
- **tween_await()** — Animate gold counter changes or item pickup effects
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify inventory scripts
|
||||||
|
- **`scene_tree`** — Inspect inventory UI node hierarchy
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Item Resource Design](references/item-resources.md)
|
||||||
|
- [Inventory UI Patterns](references/inventory-ui.md)
|
||||||
|
- [Economy Balance Guide](references/economy-balance.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-inventory-economy
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "building inventory systems in Godot"
|
||||||
|
- "Godot item management"
|
||||||
|
- "game economy Godot"
|
||||||
|
- "drag-drop inventory Godot"
|
||||||
|
- "equip system Godot"
|
||||||
|
- "shop system Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-inventory-economy
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "building inventory systems in Godot"
|
||||||
|
- "Godot item management"
|
||||||
|
- "game economy Godot"
|
||||||
|
- "drag-drop inventory Godot"
|
||||||
|
- "equip system Godot"
|
||||||
|
- "shop system Godot"
|
||||||
|
|
@ -0,0 +1,360 @@
|
||||||
|
---
|
||||||
|
name: godot-networking
|
||||||
|
description: "Networking and multiplayer for Godot 4.x: Multiplayer API with RPC system, authority models (server-authoritative vs client-predictive), replication patterns (rpc(), rset(), rset_group()), connection management, lag compensation, and Steam Frame integration. Use when building multiplayer games, networked simulations, or online features in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- networking
|
||||||
|
- multiplayer
|
||||||
|
- rpc
|
||||||
|
- replication
|
||||||
|
- steam-frame
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Networking and Multiplayer for Godot 4.x
|
||||||
|
|
||||||
|
Comprehensive patterns for multiplayer, RPC communication, and network synchronization in Godot 4.x.
|
||||||
|
|
||||||
|
## Multiplayer API (Godot 4.x RPC System)
|
||||||
|
|
||||||
|
### Setting Up Multiplayer
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# In _ready(), set the peer and peer ID
|
||||||
|
func _ready():
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
peer.create_client("127.0.0.1", 9000)
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
multiplayer.server_id = 1 # Server is always ID 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### RPC Methods
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Remote: Call only from non-owner
|
||||||
|
@rpc("reliable", "call_remote")
|
||||||
|
func rpc_move_player(position: Vector3):
|
||||||
|
global_position = position
|
||||||
|
|
||||||
|
# Reliable RPC (ordered, guaranteed delivery)
|
||||||
|
@rpc("reliable", "authority", "call_remote")
|
||||||
|
func rpc_take_damage(amount: int, source_id: int):
|
||||||
|
health -= amount
|
||||||
|
if health <= 0:
|
||||||
|
rpc("rpc_die", source_id)
|
||||||
|
|
||||||
|
# Unreliable RPC (faster, may drop packets — good for position updates)
|
||||||
|
@rpc("unreliable", "authority", "call_remote")
|
||||||
|
func rpc_update_position(position: Vector3, rotation: float):
|
||||||
|
global_position = position
|
||||||
|
rotation_degrees = rotation
|
||||||
|
```
|
||||||
|
|
||||||
|
### RPC Reliability Options
|
||||||
|
|
||||||
|
| Mode | Use Case |
|
||||||
|
|------|----------|
|
||||||
|
| `"reliable"` | Damage, state changes, game events |
|
||||||
|
| `"unreliable"` | Position updates, player movement |
|
||||||
|
| `"unreliable_ordered"` | Ordered but fast (chat messages) |
|
||||||
|
| `"call_local"` | Only on the node owner |
|
||||||
|
| `"call_remote"` | On all non-owner nodes |
|
||||||
|
| `"call_authority"` | Only on the authority node |
|
||||||
|
## Authority Models
|
||||||
|
|
||||||
|
### Server-Authoritative Model
|
||||||
|
|
||||||
|
The server has final say on all game state. Clients send inputs, server validates and broadcasts results.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ServerAuthPlayer extends CharacterBody3D
|
||||||
|
|
||||||
|
@export var player_id: int = 0
|
||||||
|
var _server := false
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
if multiplayer.is_server():
|
||||||
|
_server = true
|
||||||
|
set_multiplayer_authority(1)
|
||||||
|
else:
|
||||||
|
# Client-owned players are predicted locally
|
||||||
|
set_multiplayer_authority(multiplayer.get_unique_id())
|
||||||
|
|
||||||
|
# Client sends input to server
|
||||||
|
func _input(event: InputEvent) -> void:
|
||||||
|
if not multiplayer.is_server():
|
||||||
|
rpc("rpc_request_move", event)
|
||||||
|
|
||||||
|
# Server receives and validates input
|
||||||
|
@rpc("authority")
|
||||||
|
func rpc_request_move(event: InputEvent):
|
||||||
|
# Validate the input (check timing, etc.)
|
||||||
|
_process_input(event)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client-Predictive Model
|
||||||
|
|
||||||
|
Clients predict their own actions locally and correct on server acknowledgment.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ClientPredictivePlayer extends CharacterBody3D
|
||||||
|
|
||||||
|
@export var prediction_enabled: bool = true
|
||||||
|
var _input_buffer := []
|
||||||
|
var _last_confirmed_input: int = 0
|
||||||
|
|
||||||
|
func _physics_process(delta: float):
|
||||||
|
# Apply buffered inputs up to current state
|
||||||
|
for input_data in _input_buffer:
|
||||||
|
if input_data.sequence > _last_confirmed_input:
|
||||||
|
_apply_input(input_data)
|
||||||
|
|
||||||
|
# Send position to server
|
||||||
|
rpc("rpc_update_position", global_position, rotation_degrees)
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to Use Which
|
||||||
|
|
||||||
|
| Model | Best For |
|
||||||
|
|-------|----------|
|
||||||
|
| Server-authoritative | Competitive games, anti-cheat priority |
|
||||||
|
| Client-predictive | Fast-paced action, smooth feel priority |
|
||||||
|
| Hybrid (recommended) | Most multiplayer games |
|
||||||
|
## Replication Patterns
|
||||||
|
|
||||||
|
### rpc() — Remote Procedure Call
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Call a method on all other nodes
|
||||||
|
rpc("rpc_on_enemy_hit", damage, target_id)
|
||||||
|
|
||||||
|
# With parameters
|
||||||
|
@rpc("reliable")
|
||||||
|
func rpc_spawn_pickup(pickup_type: String, position: Vector3):
|
||||||
|
var pickup = preload("res://scenes/pickups/" + pickup_type + ".tscn").instantiate()
|
||||||
|
pickup.global_position = position
|
||||||
|
get_tree().root.add_child(pickup)
|
||||||
|
```
|
||||||
|
|
||||||
|
### rset() — Remote Set
|
||||||
|
|
||||||
|
Set a property on remote nodes.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Set health bar on all clients
|
||||||
|
rset("health", new_health)
|
||||||
|
rset("max_health", max_health)
|
||||||
|
rset("visible", true)
|
||||||
|
|
||||||
|
# Bulk set with rset_group()
|
||||||
|
rset_group("players", "set_state", state_name)
|
||||||
|
```
|
||||||
|
|
||||||
|
### rset_group() — Broadcast to Group
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Add nodes to a group for batch updates
|
||||||
|
func _ready():
|
||||||
|
multiplayer.add_to_group("players")
|
||||||
|
|
||||||
|
# Update all players at once
|
||||||
|
func broadcast_position():
|
||||||
|
rset_group("players", "sync_position", global_position, rotation_degrees)
|
||||||
|
|
||||||
|
# Server-only group
|
||||||
|
func _ready():
|
||||||
|
if multiplayer.is_server():
|
||||||
|
add_to_group("server_only")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Networked Autoload
|
||||||
|
|
||||||
|
Use autoloads for network-wide state.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# NetworkManager.gd (Autoload)
|
||||||
|
class_name NetworkManager extends Node
|
||||||
|
|
||||||
|
signal player_connected(player_id: int)
|
||||||
|
signal player_disconnected(player_id: int)
|
||||||
|
signal game_started()
|
||||||
|
|
||||||
|
var players := {}
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||||
|
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||||
|
|
||||||
|
func _on_peer_connected(id: int):
|
||||||
|
players[id] = PlayerInfo.new(id)
|
||||||
|
player_connected.emit(id)
|
||||||
|
|
||||||
|
func _on_peer_disconnected(id: int):
|
||||||
|
players.erase(id)
|
||||||
|
player_disconnected.emit(id)
|
||||||
|
```
|
||||||
|
## Connection Management
|
||||||
|
|
||||||
|
### Server Setup
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name MultiplayerServer extends Node
|
||||||
|
|
||||||
|
const PORT = 9000
|
||||||
|
const MAX_PLAYERS = 16
|
||||||
|
var server: HTTPServer
|
||||||
|
|
||||||
|
func start_server():
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
peer.create_server(PORT, MAX_PLAYERS)
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
print("Server started on port ", PORT)
|
||||||
|
|
||||||
|
func stop_server():
|
||||||
|
if multiplayer.multiplayer_peer:
|
||||||
|
multiplayer.multiplayer_peer.disconnect_from_server()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client Connection
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name MultiplayerClient extends Node
|
||||||
|
|
||||||
|
signal connected
|
||||||
|
signal connection_failed
|
||||||
|
|
||||||
|
func connect_to_server(host: String, port: int):
|
||||||
|
var peer = ENetMultiplayerPeer.new()
|
||||||
|
var error = peer.create_client(host, port)
|
||||||
|
if error != OK:
|
||||||
|
connection_failed.emit()
|
||||||
|
return
|
||||||
|
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
# Wait for server to assign unique ID
|
||||||
|
await get_tree().create_timer(0.5).timeout
|
||||||
|
if multiplayer.is_connected_to_server():
|
||||||
|
connected.emit()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lag Compensation
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Store player positions for rewind capability
|
||||||
|
class_name PositionHistory extends Node
|
||||||
|
|
||||||
|
@export var history_length: int = 3.0
|
||||||
|
var _history := []
|
||||||
|
|
||||||
|
func _physics_process(delta: float):
|
||||||
|
_history.push_back({
|
||||||
|
"position": global_position,
|
||||||
|
"rotation": rotation_degrees,
|
||||||
|
"time": Time.get_ticks_msec()
|
||||||
|
})
|
||||||
|
# Prune old entries
|
||||||
|
var cutoff = Time.get_ticks_msec() - int(history_length * 1000)
|
||||||
|
while _history.size() > 0 and _history[0].time < cutoff:
|
||||||
|
_history.pop_front()
|
||||||
|
|
||||||
|
func get_position_at(time_ms: int) -> Variant:
|
||||||
|
for entry in _history:
|
||||||
|
if entry.time >= time_ms:
|
||||||
|
return entry
|
||||||
|
return _history.back()
|
||||||
|
```
|
||||||
|
## Steam Frame Integration (Godot 4.7+)
|
||||||
|
|
||||||
|
### Using Steam Networking Sockets
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Requires Steamworks SDK and godot-steam plugin
|
||||||
|
func connect_steam_server(app_id: int, steam_id: String):
|
||||||
|
var peer = SteamMultiplayerPeer.new()
|
||||||
|
peer.create_client(steam_id)
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frame-Synced Updates
|
||||||
|
|
||||||
|
For competitive games requiring frame-accurate sync:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
@export var tick_rate: int = 60
|
||||||
|
var _tick_counter: int = 0
|
||||||
|
|
||||||
|
func _physics_process(delta: float):
|
||||||
|
_tick_counter += 1
|
||||||
|
if _tick_counter % (int(60 / tick_rate)) == 0:
|
||||||
|
rpc("rpc_tick_update", _tick_counter, get_state_snapshot())
|
||||||
|
```
|
||||||
|
|
||||||
|
### Steam Lobby Integration
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func create_lobby(max_players: int = 4):
|
||||||
|
# Create lobby via Steamworks API
|
||||||
|
var lobby_id = Steam.matchmaking.create_lobby(
|
||||||
|
max_players,
|
||||||
|
Steam.LobbyType.LobbyPrivate
|
||||||
|
)
|
||||||
|
return lobby_id
|
||||||
|
|
||||||
|
func join_lobby(lobby_id: String):
|
||||||
|
Steam.matchmaking.join_lobby(lobby_id)
|
||||||
|
```
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Do
|
||||||
|
- Use `@rpc("unreliable")` for frequent position updates — saves bandwidth
|
||||||
|
- Validate all client input on the server before applying
|
||||||
|
- Keep RPC payloads small — serialize only what changes
|
||||||
|
- Use `multiplayer.is_server()` guards to prevent client-side authority bugs
|
||||||
|
- Implement a heartbeat/ping system for connection quality monitoring
|
||||||
|
|
||||||
|
### Don't
|
||||||
|
- Don't call `rpc()` in `_process` without throttling — floods the network
|
||||||
|
- Don't trust client-reported health/damage — always recalculate server-side
|
||||||
|
- Don't send full state snapshots every frame — use delta compression
|
||||||
|
- Don't forget `set_multiplayer_authority()` when spawning remote nodes
|
||||||
|
- Don't mix reliable and unreliable RPCs for the same logical update
|
||||||
|
|
||||||
|
### Debugging Tips
|
||||||
|
```
|
||||||
|
# Enable network debugging in Project Settings:
|
||||||
|
network/enable_packet_logging = true
|
||||||
|
network/max_packet_size = 1400
|
||||||
|
|
||||||
|
# Print all RPC traffic:
|
||||||
|
multiplayer.peer_connected.connect(func(id): print("Peer connected: ", id))
|
||||||
|
multiplayer.peer_disconnected.connect(func(id): print("Peer disconnected: ", id))
|
||||||
|
```
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools enhance networking debugging:
|
||||||
|
|
||||||
|
- **`network_peer_list`** — List connected peers and their unique IDs
|
||||||
|
- **`rpc_trace`** — Log all RPC calls with sender/receiver info for debugging
|
||||||
|
- **`network_stats`** — View bandwidth usage, packet loss, and latency metrics
|
||||||
|
- **`lobby_inspect`** — Query Steam lobby state and player list
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Multiplayer API Documentation](references/multiplayer-api.md)
|
||||||
|
- [RPC Patterns Deep Dive](references/rpc-patterns.md)
|
||||||
|
- [Authority Models Guide](references/authority-models.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-networking
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "networking and multiplayer in Godot"
|
||||||
|
- "Godot RPC system and replication"
|
||||||
|
- "Godot server-authoritative games"
|
||||||
|
- "Godot client prediction"
|
||||||
|
- "Godot lag compensation"
|
||||||
|
- "Godot Steam networking"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-networking
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "networking and multiplayer in Godot"
|
||||||
|
- "Godot RPC system and replication"
|
||||||
|
- "Godot server-authoritative games"
|
||||||
|
- "Godot client prediction"
|
||||||
|
- "Godot lag compensation"
|
||||||
|
- "Godot Steam networking"
|
||||||
|
|
@ -0,0 +1,329 @@
|
||||||
|
---
|
||||||
|
name: godot-performance
|
||||||
|
description: "Performance optimization for Godot 4.x: profiling workflow (built-in profiler, CPU/GPU usage), memory management with ResourceCache and instance pooling, instancing optimization with packed scenes and LOD, render passes, draw calls, mobile-specific optimizations, Perfetto tracing, and nearest-neighbor viewport scaling. Use when debugging performance, optimizing games, or targeting low-end hardware."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- performance
|
||||||
|
- profiling
|
||||||
|
- optimization
|
||||||
|
- memory
|
||||||
|
- mobile
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Performance Optimization for Godot 4.x
|
||||||
|
|
||||||
|
Systematic approach to profiling, optimizing, and debugging performance in Godot 4.x.
|
||||||
|
|
||||||
|
## Profiling Workflow
|
||||||
|
|
||||||
|
### Built-in Profiler
|
||||||
|
|
||||||
|
Access the profiler via Debug > Start Profiling or press Ctrl+Shift+P.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Enable profiling in your project settings:
|
||||||
|
# Project Settings > Debug > Profile Every Frame = true
|
||||||
|
|
||||||
|
# Use time measure for specific code sections
|
||||||
|
var start_time = Time.get_ticks_usec()
|
||||||
|
# ... expensive operation ...
|
||||||
|
var elapsed = Time.get_ticks_usec() - start_time
|
||||||
|
print("Operation took: ", elapsed, " us")
|
||||||
|
```
|
||||||
|
|
||||||
|
### CPU/GPU Usage Monitoring
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Display FPS and frame time in debug overlay
|
||||||
|
# Project Settings > Debug > Show FPS = true
|
||||||
|
|
||||||
|
# Monitor specific metrics
|
||||||
|
func get_performance_stats():
|
||||||
|
var stats = {
|
||||||
|
fps: Performance.get_monitor(Performance.TIME_FPS),
|
||||||
|
frame_time: Performance.get_monitor(Performance.TIME_FRAME_DURATION),
|
||||||
|
draw_calls: Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME),
|
||||||
|
active_objects: Performance.get_monitor(Performance.OBJECT_COUNT),
|
||||||
|
mem_alloc: Performance.get_monitor(Performance.MEMORY_STATIC),
|
||||||
|
mem_pool: Performance.get_monitor(Performance.MEMORY_STATIC_SPECIALIZATION)
|
||||||
|
}
|
||||||
|
return stats
|
||||||
|
```
|
||||||
|
|
||||||
|
### Profiling Checklist
|
||||||
|
|
||||||
|
1. **Start with a clean build** — Profile the exported release build, not the debug version
|
||||||
|
2. **Enable all profiler categories** — CPU, GPU, Physics, Rendering, Audio
|
||||||
|
3. **Record a representative gameplay segment** — 30-60 seconds of typical play
|
||||||
|
4. **Identify hotspots** — Look for functions consuming >5% of frame time
|
||||||
|
5. **Profile before and after changes** — Measure impact of each optimization
|
||||||
|
|
||||||
|
## Memory Management
|
||||||
|
|
||||||
|
### ResourceCache Pattern
|
||||||
|
|
||||||
|
Preload and cache frequently-used resources.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ResourceCache extends Node
|
||||||
|
|
||||||
|
static var _cache := {}
|
||||||
|
|
||||||
|
static func preload_resource(path: String) -> Variant:
|
||||||
|
if not _cache.has(path):
|
||||||
|
var res = load(path)
|
||||||
|
if res:
|
||||||
|
_cache[path] = res
|
||||||
|
else:
|
||||||
|
push_warning("Failed to load resource: " + path)
|
||||||
|
return _cache[path]
|
||||||
|
|
||||||
|
static func get_scene(path: String) -> PackedScene:
|
||||||
|
return preload_resource(path) as PackedScene
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
var bullet_scene = ResourceCache.get_scene("res://scenes/entities/bullet.tscn")
|
||||||
|
var bullet = bullet_scene.instantiate()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instance Pooling Pattern
|
||||||
|
|
||||||
|
Reuse objects instead of creating/destroying them.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ObjectPool<T> extends Node where T is RefCounted
|
||||||
|
|
||||||
|
@export var pool_size: int = 20
|
||||||
|
var _pool: Array[T] = []
|
||||||
|
var _active: Array[T] = []
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
for i in range(pool_size):
|
||||||
|
var instance = create_instance()
|
||||||
|
instance.set_process_mode(PROCESS_MODE_ALWAYS)
|
||||||
|
add_child(instance as Node)
|
||||||
|
_pool.append(instance as T)
|
||||||
|
|
||||||
|
func get_free() -> T:
|
||||||
|
if _pool.size() > 0:
|
||||||
|
var obj = _pool.pop_back()
|
||||||
|
obj.set_active(true)
|
||||||
|
_active.append(obj)
|
||||||
|
return obj
|
||||||
|
return null
|
||||||
|
|
||||||
|
func release(obj: T) -> void:
|
||||||
|
obj.set_active(false)
|
||||||
|
_active.erase(obj)
|
||||||
|
_pool.append(obj)
|
||||||
|
```
|
||||||
|
## Instancing Optimization
|
||||||
|
|
||||||
|
### Packed Scene Best Practices
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# preload() for always-needed scenes (compile-time)
|
||||||
|
const BulletScene = preload("res://scenes/entities/bullet.tscn")
|
||||||
|
|
||||||
|
func spawn_bullet():
|
||||||
|
var bullet = BulletScene.instantiate()
|
||||||
|
add_child(bullet)
|
||||||
|
|
||||||
|
# load() for conditional scenes (runtime)
|
||||||
|
func load_level(level_name: String):
|
||||||
|
var path = "res://levels/" + level_name + ".tscn"
|
||||||
|
var scene = load(path) # Cached by Godot internally
|
||||||
|
if scene:
|
||||||
|
current_level = scene.instantiate()
|
||||||
|
add_child(current_level)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Level of Detail (LOD)
|
||||||
|
|
||||||
|
Use LOD to reduce rendering cost for distant objects.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name LODObject extends Node3D
|
||||||
|
|
||||||
|
@export var lod_distances: PackedFloat32Array = [10.0, 30.0, 60.0]
|
||||||
|
@onready var lod_nodes = [$HighDetail, $MediumDetail, $LowDetail]
|
||||||
|
|
||||||
|
func _process():
|
||||||
|
var dist = global_position.distance_to(get_viewport().get_camera3D().global_position)
|
||||||
|
for i in range(lod_nodes.size()):
|
||||||
|
lod_nodes[i].visible = dist < lod_distances[i]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Instanced Rendering
|
||||||
|
|
||||||
|
Use `MultiMeshInstance2D/3D` for many similar objects.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Draw 1000 identical trees with a single draw call
|
||||||
|
var mesh = preload("res://models/tree.tscn")
|
||||||
|
var multi_mesh = MultiMesh.new()
|
||||||
|
multi_mesh.mesh = mesh.resource
|
||||||
|
multi_mesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||||
|
multi_mesh.instance_count = 1000
|
||||||
|
|
||||||
|
for i in range(1000):
|
||||||
|
var x = randf() * 200 - 100
|
||||||
|
var z = randf() * 200 - 100
|
||||||
|
multi_mesh.set_instance_transform(i, Transform3D(Basis(), Vector3(x, 0, z)))
|
||||||
|
|
||||||
|
$MultiMeshInstance3D.multi_mesh = multi_mesh
|
||||||
|
```
|
||||||
|
## Render Passes and Draw Calls
|
||||||
|
|
||||||
|
### Minimizing Draw Calls
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# BAD: Many small sprites (many draw calls)
|
||||||
|
for i in range(100):
|
||||||
|
var sprite = Sprite2D.new()
|
||||||
|
sprite.texture = grass_texture
|
||||||
|
add_child(sprite)
|
||||||
|
|
||||||
|
# GOOD: Use TileMap or MultiMesh (single draw call)
|
||||||
|
# TileMap handles batching automatically
|
||||||
|
# Or use a custom mesh with instanced rendering
|
||||||
|
```
|
||||||
|
|
||||||
|
### Render Pass Optimization
|
||||||
|
|
||||||
|
- **Forward+** renderer: Better for many lights, use for 3D games
|
||||||
|
- **Mobile** renderer: Fewer passes, better for low-end devices
|
||||||
|
- **Stencil shadows**: Use shadow maps instead of volumetric shadows when possible
|
||||||
|
- **LightmapGI**: Bake static lighting to avoid real-time shadows
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Set renderer in Project Settings > Rendering > Renderer
|
||||||
|
# Forward Plus: Better quality, higher cost
|
||||||
|
# Mobile: Optimized for low-end hardware
|
||||||
|
```
|
||||||
|
## Mobile-Specific Optimizations
|
||||||
|
|
||||||
|
### Texture Compression
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Project Settings > Export > Android/iOS
|
||||||
|
# Set texture format:
|
||||||
|
# - ASTC: Best quality/size ratio (modern devices)
|
||||||
|
# - ETC2: Fallback for older Android devices
|
||||||
|
# - DXT: Desktop only
|
||||||
|
# Use compressed textures in the editor:
|
||||||
|
# Texture > Compressed = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mobile Rendering Tips
|
||||||
|
|
||||||
|
- Use **Mobile renderer** instead of Forward+ for Android/iOS
|
||||||
|
- Limit active lights per object (use light masks)
|
||||||
|
- Bake static lighting with LightmapGI
|
||||||
|
- Use smaller shadow map sizes (512x512 or 256x256)
|
||||||
|
- Disable post-processing effects when not needed
|
||||||
|
- Use `CanvasGroup` to batch UI rendering
|
||||||
|
- Set `texture_filter` to `TEXTURE_FILTER_NEAREST` for pixel art
|
||||||
|
|
||||||
|
### CPU Optimization for Mobile
|
||||||
|
|
||||||
|
- Avoid `_get_node()` in _process — use `@onready` instead
|
||||||
|
- Minimize `_instantiate()` calls — use object pooling
|
||||||
|
- Use `NavigationServer3D` for pathfinding (avoids scene tree overhead)
|
||||||
|
- Batch physics queries instead of individual raycasts
|
||||||
|
- Use `PhysicsServer3D` directly for bulk operations
|
||||||
|
|
||||||
|
## Godot 4.7 Performance Features
|
||||||
|
|
||||||
|
### Perfetto Tracing for Android (Godot 4.7)
|
||||||
|
|
||||||
|
Perfetto provides detailed profiling data for Android builds.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Enable in Project Settings > Debug
|
||||||
|
# Perfetto tracing captures:
|
||||||
|
# - Frame timing breakdown
|
||||||
|
# - GC pauses
|
||||||
|
# - Thread scheduling
|
||||||
|
# - GPU command queue
|
||||||
|
|
||||||
|
# After export, run:
|
||||||
|
# godot --perfetto-trace output.perfetto
|
||||||
|
# Then open in https://ui.perfetto.dev/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nearest-Neighbor Viewport Scaling (Godot 4.7)
|
||||||
|
|
||||||
|
Pixel-perfect scaling for retro-style games.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Project Settings > Display > Window > Stretch Mode
|
||||||
|
stretch_mode = CanvasGroup.STRETCH_MODE_SCALE
|
||||||
|
stretch_aspect = CanvasGroup.STRETCH_ASPECT_KEEP
|
||||||
|
|
||||||
|
# Godot 4.7: Use nearest-neighbor scaling for crisp pixel art
|
||||||
|
texture_filter = TextureFilter.NEAREST
|
||||||
|
|
||||||
|
# Calculate integer scale factor
|
||||||
|
func get_viewport_scale() -> int:
|
||||||
|
var target_width = 320 # Original game resolution
|
||||||
|
return max(1, int(DisplayServer.window_get_size().x / target_width))
|
||||||
|
```
|
||||||
|
## Performance Checklist
|
||||||
|
|
||||||
|
### Before Release
|
||||||
|
|
||||||
|
- [ ] Profile the release build (not debug)
|
||||||
|
- [ ] Check FPS stays above 30 (60 ideal) on target hardware
|
||||||
|
- [ ] Verify memory usage stays within device limits
|
||||||
|
- [ ] Test on lowest-spec target device
|
||||||
|
- [ ] Check for GC spikes (>1ms pauses)
|
||||||
|
- [ ] Verify texture compression is enabled
|
||||||
|
- [ ] Reduce draw calls with instancing
|
||||||
|
- [ ] Use lightmaps for static scenes
|
||||||
|
- [ ] Implement object pooling for frequently spawned entities
|
||||||
|
|
||||||
|
### Runtime Monitoring
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Add to debug HUD
|
||||||
|
class_name PerfHUD extends CanvasLayer
|
||||||
|
|
||||||
|
@onready var fps_label = $FPSLabel
|
||||||
|
@onready var mem_label = $MemLabel
|
||||||
|
|
||||||
|
func _process(delta):
|
||||||
|
fps_label.text = "FPS: " + str(Performance.get_monitor(Performance.TIME_FPS))
|
||||||
|
mem_label.text = "Mem: " + str(Performance.get_monitor(Performance.MEMORY_STATIC) / 1024 / 1024) + " MB"
|
||||||
|
```
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools enhance performance debugging:
|
||||||
|
|
||||||
|
- **`profiler_start`** — Start a profiling session from the editor
|
||||||
|
- **`profiler_stop`** — Stop and display results
|
||||||
|
- **`memory_stats`** — View current memory allocation breakdown
|
||||||
|
- **`render_stats`** — Query draw calls, triangles, and active textures
|
||||||
|
- **`perfetto_export`** — Export Perfetto trace for Android debugging
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Profiler Documentation](references/profiler-guide.md)
|
||||||
|
- [Memory Management Patterns](references/memory-patterns.md)
|
||||||
|
- [Mobile Optimization Checklist](references/mobile-checklist.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-performance
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "performance optimization in Godot"
|
||||||
|
- "Godot profiling and benchmarking"
|
||||||
|
- "Godot draw call reduction"
|
||||||
|
- "Godot instancing optimization"
|
||||||
|
- "Godot memory management"
|
||||||
|
- "Godot mobile performance"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-performance
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "performance optimization in Godot"
|
||||||
|
- "Godot profiling and benchmarking"
|
||||||
|
- "Godot draw call reduction"
|
||||||
|
- "Godot instancing optimization"
|
||||||
|
- "Godot memory management"
|
||||||
|
- "Godot mobile performance"
|
||||||
|
|
@ -0,0 +1,320 @@
|
||||||
|
---
|
||||||
|
name: godot-physics
|
||||||
|
description: "Physics systems for Godot 4.x: collision layers/masks, rigid body dynamics, character controllers (CharacterBody2D/3D), area nodes for triggers, physics interpolation, bulk queries, and performance optimization. Use when implementing physics, collisions, movement, or triggers in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- physics
|
||||||
|
- collision
|
||||||
|
- character-body
|
||||||
|
- rigid-body
|
||||||
|
- area
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Physics Systems for Godot 4.x
|
||||||
|
|
||||||
|
Comprehensive patterns for physics, collisions, and movement in Godot 4.x.
|
||||||
|
|
||||||
|
## Collision Layers and Masks (Godot 4.x Typed System)
|
||||||
|
|
||||||
|
Godot 4.x uses bit-based collision layer/mask system for efficient spatial queries.
|
||||||
|
|
||||||
|
### Configuring Collision Layers
|
||||||
|
|
||||||
|
Set layers in the editor via Inspector > CollisionShape3D > Collision Layer (bits 1-32).
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Recommended layer layout
|
||||||
|
const LAYER_PLAYER = 0x1
|
||||||
|
const LAYER_ENEMIES = 0x2
|
||||||
|
const LAYER_PROJECTILES = 0x4
|
||||||
|
const LAYER_ENVIRONMENT = 0x8
|
||||||
|
const LAYER_PICKUPS = 0x10
|
||||||
|
const LAYER_TRIGGERS = 0x20
|
||||||
|
|
||||||
|
collision_layer = LAYER_PLAYER
|
||||||
|
collision_mask = LAYER_ENVIRONMENT | LAYER_PICKUPS
|
||||||
|
```
|
||||||
|
|
||||||
|
### Layer Design Best Practices
|
||||||
|
|
||||||
|
| Layer | Bit | Use Case |
|
||||||
|
|-------|-----|----------|
|
||||||
|
| 1 | `0x1` | Player characters |
|
||||||
|
| 2 | `0x2` | Enemies and NPCs |
|
||||||
|
| 4 | `0x4` | Projectiles and weapons |
|
||||||
|
| 8 | `0x8` | Environment / terrain |
|
||||||
|
| 16 | `0x10` | Pickups and collectibles |
|
||||||
|
| 32 | `0x20` | Triggers (Area-only) |
|
||||||
|
|
||||||
|
### Typed Collision Queries
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var ray_query := PhysicsRayQueryParameters3D.new()
|
||||||
|
ray_query.from = camera.global_position
|
||||||
|
ray_query.to = global_position + forward * range
|
||||||
|
ray_query.collision_mask = LAYER_ENVIRONMENT
|
||||||
|
ray_query.exclude = [self]
|
||||||
|
|
||||||
|
var result = get_world_3d().direct_space.intersect_ray(ray_query)
|
||||||
|
if result:
|
||||||
|
var collider = result.get("collider")
|
||||||
|
print("Hit: ", collider.get_name())
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rigid Body Dynamics
|
||||||
|
|
||||||
|
### Basic Rigid Body Setup
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name PlayerRigidBody extends CharacterBody3D
|
||||||
|
|
||||||
|
@export var gravity_scale: float = 1.0
|
||||||
|
@export var jump_force: float = 8.0
|
||||||
|
@export var move_speed: float = 5.0
|
||||||
|
|
||||||
|
var velocity := Vector3.ZERO
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
velocity.y -= 9.8 * gravity_scale * delta
|
||||||
|
var input_dir := Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
||||||
|
velocity.x = input_dir.x * move_speed
|
||||||
|
velocity.z = input_dir.y * move_speed
|
||||||
|
move_and_slide()
|
||||||
|
if is_on_floor() and Input.is_action_just_pressed("jump"):
|
||||||
|
velocity.y = jump_force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Physics Material
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var bouncy_material := PhysicsMaterial.new()
|
||||||
|
bouncy_material.bounce = 0.8
|
||||||
|
bouncy_material.friction = 0.2
|
||||||
|
collision_shape.shape.material = bouncy_material
|
||||||
|
```
|
||||||
|
|
||||||
|
### Rigid Body with Forces
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name Projectile extends RigidBody3D
|
||||||
|
|
||||||
|
@export var lifetime: float = 3.0
|
||||||
|
@export var damage: int = 10
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
linear_velocity = transform.basis.z * 20.0
|
||||||
|
get_tree().create_timer(lifetime).timeout.connect(queue_free)
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node3D) -> void:
|
||||||
|
if body is Enemy:
|
||||||
|
body.take_damage(damage)
|
||||||
|
queue_free()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Character Controllers
|
||||||
|
|
||||||
|
### CharacterBody3D Movement Pattern
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name ThirdPersonController extends CharacterBody3D
|
||||||
|
|
||||||
|
@export var walk_speed: float = 4.0
|
||||||
|
@export var run_speed: float = 8.0
|
||||||
|
@export var jump_velocity: float = 4.5
|
||||||
|
@export var gravity_amount: float = 9.8
|
||||||
|
@export var sprint_factor: float = 2.0
|
||||||
|
|
||||||
|
var _is_sprinting := false
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity.y -= gravity_amount * delta
|
||||||
|
if is_on_floor() and Input.is_action_just_pressed("jump"):
|
||||||
|
velocity.y = jump_velocity
|
||||||
|
var input_dir := Vector2.ZERO
|
||||||
|
input_dir = Vector2(
|
||||||
|
Input.get_axis("move_left", "move_right"),
|
||||||
|
Input.get_axis("move_up", "move_down")
|
||||||
|
)
|
||||||
|
_is_sprinting = Input.is_action_pressed("sprint")
|
||||||
|
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
|
||||||
|
if direction:
|
||||||
|
var spd = run_speed * sprint_factor if _is_sprinting else walk_speed
|
||||||
|
velocity.x = direction.x * spd
|
||||||
|
velocity.z = direction.z * spd
|
||||||
|
else:
|
||||||
|
velocity.x = move_toward(velocity.x, 0, walk_speed)
|
||||||
|
velocity.z = move_toward(velocity.z, 0, walk_speed)
|
||||||
|
move_and_slide()
|
||||||
|
```
|
||||||
|
|
||||||
|
### CharacterBody2D Platformer Pattern
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name PlatformerCharacter extends CharacterBody2D
|
||||||
|
|
||||||
|
@export var speed: float = 200.0
|
||||||
|
@export var jump_force: float = -450.0
|
||||||
|
@export var gravity: float = 1200.0
|
||||||
|
@export var max_fall_speed: float = 700.0
|
||||||
|
@export var acceleration: float = 800.0
|
||||||
|
@export var friction: float = 600.0
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
if not is_on_floor():
|
||||||
|
velocity.y += gravity * delta
|
||||||
|
velocity.y = min(velocity.y, max_fall_speed)
|
||||||
|
if is_on_floor() and Input.is_action_just_pressed("jump"):
|
||||||
|
velocity.y = jump_force
|
||||||
|
var move_input := Input.get_axis("move_left", "move_right")
|
||||||
|
if move_input:
|
||||||
|
velocity.x = move_toward(velocity.x, move_input * speed, acceleration * delta)
|
||||||
|
else:
|
||||||
|
velocity.x = move_toward(velocity.x, 0, friction * delta)
|
||||||
|
move_and_slide()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Area Nodes for Triggers and Detection
|
||||||
|
|
||||||
|
### Basic Pickup Zone
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name PickupZone extends Area3D
|
||||||
|
|
||||||
|
@export var pickup_scene: PackedScene
|
||||||
|
@export var min_items: int = 1
|
||||||
|
@export var max_items: int = 3
|
||||||
|
var _has_been_activated := false
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
body_entered.connect(_on_body_entered)
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node3D) -> void:
|
||||||
|
if _has_been_activated: return
|
||||||
|
if body is CharacterBody3D:
|
||||||
|
_spawn_items()
|
||||||
|
_has_been_activated = true
|
||||||
|
queue_free()
|
||||||
|
|
||||||
|
func _spawn_items() -> void:
|
||||||
|
var count = randi() % (max_items - min_items + 1) + min_items
|
||||||
|
for i in range(count):
|
||||||
|
var item = pickup_scene.instantiate()
|
||||||
|
item.position = global_position + Vector3(randf() * 2 - 1, 0.5, randf() * 2 - 1)
|
||||||
|
get_tree().root.add_child(item)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Enemy Detection Sensor
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name EnemySensor extends Area3D
|
||||||
|
|
||||||
|
signal player_detected(player: CharacterBody3D)
|
||||||
|
signal player_lost(player: CharacterBody3D)
|
||||||
|
|
||||||
|
@export var detection_radius: float = 10.0
|
||||||
|
@export var detection_layers: int = 0x1
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
body_entered.connect(_on_body_entered)
|
||||||
|
body_exited.connect(_on_body_exited)
|
||||||
|
var sphere := SphereShape3D.new()
|
||||||
|
sphere.radius = detection_radius
|
||||||
|
$CollisionShape3D.shape = sphere
|
||||||
|
collision_mask = detection_layers
|
||||||
|
|
||||||
|
func _on_body_entered(body: Node3D) -> void:
|
||||||
|
if body is CharacterBody3D:
|
||||||
|
player_detected.emit(body)
|
||||||
|
|
||||||
|
func _on_body_exited(body: Node3D) -> void:
|
||||||
|
if body is CharacterBody3D:
|
||||||
|
player_lost.emit(body)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Physics Interpolation (Godot 4.x Feature)
|
||||||
|
|
||||||
|
Godot 4.x supports physics interpolation for smooth rendering between physics ticks.
|
||||||
|
|
||||||
|
### Enabling Physics Interpolation
|
||||||
|
|
||||||
|
Enable in Project Settings > Rendering > Other > Physics Interpolation set to "Interpolate". The engine automatically interpolates transforms between physics ticks when rendering. No extra code needed for standard nodes.
|
||||||
|
|
||||||
|
For custom rendering, use the built-in interpolation factor available through the rendering pipeline.
|
||||||
|
|
||||||
|
## Performance: Bulk Physics Queries
|
||||||
|
|
||||||
|
### Space State Batched Queries
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func get_all_collisions_in_area(origin: Vector3, radius: float, mask: int) -> Array:
|
||||||
|
var query := PhysicsShapeQueryParameters3D.new()
|
||||||
|
var sphere := SphereShape3D.new()
|
||||||
|
sphere.radius = radius
|
||||||
|
query.shape = sphere
|
||||||
|
query.transform = Transform3D.IDENTITY.translated(origin)
|
||||||
|
query.collision_mask = mask
|
||||||
|
query.exclude = [self]
|
||||||
|
var space = get_world_3d().direct_space
|
||||||
|
return space.intersect_shape(query)
|
||||||
|
|
||||||
|
func aoe_attack(center: Vector3, radius: float, damage: int) -> void:
|
||||||
|
var hits := get_all_collisions_in_area(center, radius, LAYER_ENEMIES | LAYER_PROJECTILES)
|
||||||
|
for hit in hits:
|
||||||
|
var collider = hit.get("collider")
|
||||||
|
if collider is Enemy:
|
||||||
|
collider.take_damage(damage)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Collision Layer Optimization
|
||||||
|
|
||||||
|
- Only check layers you need — avoid `collision_mask = 0xFFFFFFFF`
|
||||||
|
- Use `NavigationServer3D` for pathfinding queries
|
||||||
|
- Use small-radius `Area3D` nodes for proximity checks
|
||||||
|
- Avoid querying the entire world every frame
|
||||||
|
|
||||||
|
## Godot 4.7 Physics Notes
|
||||||
|
|
||||||
|
- Physics interpolation is more robust with improved accuracy
|
||||||
|
- Use `AreaLight3D` for dynamic light casting on visible surfaces
|
||||||
|
- Test physics performance with Perfetto tracing on mobile targets
|
||||||
|
|
||||||
|
## Quick Reference: Movement Methods
|
||||||
|
|
||||||
|
| Method | Use Case | Returns |
|
||||||
|
|--------|----------|---------|
|
||||||
|
| `move_and_slide()` | Standard character movement | SlideFlags |
|
||||||
|
| `move_and_collide()` | When you need collision info | CollisionData or null |
|
||||||
|
| `move_and_snap()` | Platformer with snap-to-floor | SlideFlags |
|
||||||
|
| `move_teleport()` | Teleportation (no sliding) | CollisionData or null |
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools enhance physics debugging:
|
||||||
|
|
||||||
|
- **`physics_debug_draw`** — Toggle collision shape visualization in editor
|
||||||
|
- **`physics_query_shape`** — Run shape queries from any position without placing nodes
|
||||||
|
- **`rigid_body_apply_force`** — Apply forces to rigid bodies for testing
|
||||||
|
- **`physics_stats`** — View active physics bodies, sleep count, and frame cost
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [CharacterBody3D Documentation](references/characterbody3d-guide.md)
|
||||||
|
- [Collision Layer Design Patterns](references/collision-layers.md)
|
||||||
|
- [Physics Material Reference](references/physics-materials.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-physics
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "physics systems in Godot"
|
||||||
|
- "Godot collision layers and masks"
|
||||||
|
- "Godot rigid body dynamics"
|
||||||
|
- "Godot character physics movement"
|
||||||
|
- "Godot physics materials"
|
||||||
|
- "Godot area detection"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-physics
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "physics systems in Godot"
|
||||||
|
- "Godot collision layers and masks"
|
||||||
|
- "Godot rigid body dynamics"
|
||||||
|
- "Godot character physics movement"
|
||||||
|
- "Godot physics materials"
|
||||||
|
- "Godot area detection"
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
---
|
||||||
|
name: godot-project-setup
|
||||||
|
description: "Scaffolds a new Godot 4.x project with proper folder structure, .gdignore, export presets, .gitignore, and GDScript/C# Mono initialization. Use when creating a new Godot game or app."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- project-setup
|
||||||
|
- scaffolding
|
||||||
|
- export-presets
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# Godot Project Setup
|
||||||
|
|
||||||
|
Scaffold a complete, production-ready Godot 4.x project with industry-standard organization.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create a new project
|
||||||
|
./scripts/new-godot-project.sh MyGame /path/to/output
|
||||||
|
```
|
||||||
|
|
||||||
|
## Standard Directory Structure
|
||||||
|
|
||||||
|
After scaffolding, your project will have:
|
||||||
|
|
||||||
|
```
|
||||||
|
MyGame/
|
||||||
|
├── .gdignore # Exclude editor cache from export
|
||||||
|
├── .gitignore # Version control ignores
|
||||||
|
├── project.godot # Project configuration
|
||||||
|
├── scenes/ # Scene files (.tscn)
|
||||||
|
│ ├── ui/ # UI scene compositions
|
||||||
|
│ ├── levels/ # Level/world scenes
|
||||||
|
│ └── entities/ # Character/entity scenes
|
||||||
|
├── scripts/ # Script files (.gd, .cs)
|
||||||
|
│ ├── core/ # Game systems (manager, event bus)
|
||||||
|
│ ├── entities/ # Entity behaviors
|
||||||
|
│ ├── ui/ # UI controllers
|
||||||
|
│ └── levels/ # Level-specific logic
|
||||||
|
├── resources/ # Resource files (.tres, .res)
|
||||||
|
│ ├── data/ # Game data (items, skills, etc.)
|
||||||
|
│ └── config/ # Configuration resources
|
||||||
|
├── assets/ # Raw assets (do not edit in Godot)
|
||||||
|
│ ├── textures/
|
||||||
|
│ ├── audio/
|
||||||
|
│ ├── fonts/
|
||||||
|
│ └── models/
|
||||||
|
├── levels/ # Level data and configurations
|
||||||
|
└── export_presets.cfg # Export presets (auto-generated)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project.godot Configuration
|
||||||
|
|
||||||
|
Key settings for a clean project:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[application]
|
||||||
|
|
||||||
|
config/icon="res://assets/icons/icon.png"
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/width=1280
|
||||||
|
window/size/height=720
|
||||||
|
window/stretch/mode="canvas_items"
|
||||||
|
|
||||||
|
[rendering]
|
||||||
|
|
||||||
|
renderer/rendering_method="forward_plus"
|
||||||
|
```
|
||||||
|
|
||||||
|
## .gdignore Setup
|
||||||
|
|
||||||
|
Create `.gdignore` in directories that Godot should not import:
|
||||||
|
|
||||||
|
```
|
||||||
|
# In assets/ directory
|
||||||
|
# Godot ignores this directory for resource imports
|
||||||
|
```
|
||||||
|
|
||||||
|
## GDScript Project Initialization
|
||||||
|
|
||||||
|
Standard project.godot autoload setup:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Add to Project Settings > Autoload
|
||||||
|
# Name: GameManager, Path: res://scripts/core/game_manager.gd
|
||||||
|
# Name: EventBus, Path: res://scripts/core/event_bus.gd
|
||||||
|
# Name: AudioManager, Path: res://scripts/core/audio_manager.gd
|
||||||
|
```
|
||||||
|
|
||||||
|
## C# Mono Project Initialization
|
||||||
|
|
||||||
|
For C# projects, the scaffolded project includes:
|
||||||
|
|
||||||
|
- `.csproj` file configured for Godot 4.x
|
||||||
|
- `global.json` for .NET version pinning
|
||||||
|
- Standard namespace structure matching directory layout
|
||||||
|
- `[Export]` attribute patterns in template scripts
|
||||||
|
|
||||||
|
## Godot 4.7 Specific Setup
|
||||||
|
|
||||||
|
When creating projects with Godot 4.7 features:
|
||||||
|
|
||||||
|
1. **HDR Settings** — Enable in project.godot:
|
||||||
|
```ini
|
||||||
|
[rendering]
|
||||||
|
environment/default_environment="default_world.tres"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Steam Frame Configuration** — Add to project.godot:
|
||||||
|
```ini
|
||||||
|
[steam]
|
||||||
|
app_id="YOUR_APP_ID"
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **VirtualJoystick** — Use built-in node for mobile projects instead of third-party solutions
|
||||||
|
|
||||||
|
4. **Export Presets** — Configure for:
|
||||||
|
- Windows Desktop (64-bit)
|
||||||
|
- Linux/X11 (64-bit)
|
||||||
|
- Android (API 31+)
|
||||||
|
- HTML5 (Web)
|
||||||
|
|
||||||
|
## Export Presets
|
||||||
|
|
||||||
|
Common export configurations:
|
||||||
|
|
||||||
|
| Target | Settings | Notes |
|
||||||
|
|--------|----------|-------|
|
||||||
|
| Windows | Desktop, 64-bit, D3D12 | Default for PC games |
|
||||||
|
| Linux | Desktop, 64-bit, Vulkan | Requires vulkan drivers |
|
||||||
|
| Android | Mobile, API 31+, ARM64 | Use VirtualJoystick for controls |
|
||||||
|
| HTML5 | Web, WASM | Limited to web-compatible features |
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`project_settings_read`** — Read current project.godot settings
|
||||||
|
- **`project_settings_set`** — Modify project settings (e.g., enable HDR, set Steam app ID)
|
||||||
|
- **`file_browse`** — List project directories to verify scaffold structure
|
||||||
|
- **`file_write`** — Write configuration files directly
|
||||||
|
- **`class_db_info`** — Query Godot class database for export preset node types
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Godot Project Configuration](references/project-configuration.md)
|
||||||
|
- [Export Presets Deep Dive](references/export-presets.md)
|
||||||
|
- [Mono/C# Setup Guide](references/mono-setup.md)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
# Claude Code extension for godot-project-setup
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "creating a new Godot project"
|
||||||
|
- "setting up Godot project structure"
|
||||||
|
- "Godot export presets"
|
||||||
|
- "scaffolding a Godot game"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-project-setup
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "creating a new Godot project"
|
||||||
|
- "setting up Godot project structure"
|
||||||
|
- "Godot export presets"
|
||||||
|
- "scaffolding a Godot game"
|
||||||
|
|
@ -0,0 +1,247 @@
|
||||||
|
---
|
||||||
|
name: godot-save-systems
|
||||||
|
description: "Save system patterns for Godot 4.x: resource-based saves, JSON vs binary serialization, versioning/migration strategies, cloud save integration, and corruption recovery. Use when implementing persistence in Godot games."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- save-system
|
||||||
|
- persistence
|
||||||
|
- serialization
|
||||||
|
- migration
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Save Systems in Godot
|
||||||
|
|
||||||
|
Patterns for persisting game state reliably.
|
||||||
|
|
||||||
|
## Resource-Based Saves (Recommended)
|
||||||
|
|
||||||
|
Godot's `Resource` system provides native serialization with type safety:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name SaveData extends Resource
|
||||||
|
|
||||||
|
const SAVE_VERSION := 1
|
||||||
|
|
||||||
|
@export var save_version: int = SAVE_VERSION
|
||||||
|
@export var timestamp: float = 0.0
|
||||||
|
@export var player_position: Vector2 = Vector2.ZERO
|
||||||
|
@export var player_health: int = 100
|
||||||
|
@export var inventory: Array[Resource] = []
|
||||||
|
@export var quest_flags: Dictionary = {}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Save Manager
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name SaveManager extends Node
|
||||||
|
|
||||||
|
const SAVE_PATH := "user://savegame.tres"
|
||||||
|
|
||||||
|
func save(data: SaveData) -> Error:
|
||||||
|
data.timestamp = Time.get_unix_time_from_system()
|
||||||
|
var error = ResourceSaver.save(data, SAVE_PATH)
|
||||||
|
if error != Error.OK:
|
||||||
|
push_error("SaveManager: Failed to save — %d" % error)
|
||||||
|
return error
|
||||||
|
|
||||||
|
func load() -> SaveData:
|
||||||
|
if not FileAccess.file_exists(SAVE_PATH):
|
||||||
|
return null
|
||||||
|
var data = ResourceLoader.load(SAVE_PATH) as SaveData
|
||||||
|
if not data:
|
||||||
|
push_error("SaveManager: Failed to load save file")
|
||||||
|
return null
|
||||||
|
return data
|
||||||
|
|
||||||
|
func delete() -> void:
|
||||||
|
DirAccess.remove_absolute(SAVE_PATH)
|
||||||
|
|
||||||
|
func has_save() -> bool:
|
||||||
|
return FileAccess.file_exists(SAVE_PATH)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple Save Slots
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func get_slot_path(slot_index: int) -> String:
|
||||||
|
return "user://savegame_%d.tres" % slot_index
|
||||||
|
|
||||||
|
# Save slot metadata for UI display
|
||||||
|
class_name SlotInfo extends Resource
|
||||||
|
@export var slot_index: int = 0
|
||||||
|
@export var timestamp: float = 0.0
|
||||||
|
@export var player_level: int = 1
|
||||||
|
@export var thumbnail: Texture2D = null
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSON Serialization
|
||||||
|
|
||||||
|
For human-readable saves or cross-platform data exchange:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func save_as_json(data: SaveData, path: String) -> Error:
|
||||||
|
var dict := {
|
||||||
|
"version": data.save_version,
|
||||||
|
"timestamp": data.timestamp,
|
||||||
|
"position": { "x": data.player_position.x, "y": data.player_position.y },
|
||||||
|
"health": data.player_health,
|
||||||
|
"inventory": [],
|
||||||
|
"quests": data.quest_flags
|
||||||
|
}
|
||||||
|
var json = JSON.stringify(dict)
|
||||||
|
var file = FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
if not file:
|
||||||
|
return Error.CANT_OPEN
|
||||||
|
file.store_string(json)
|
||||||
|
return Error.OK
|
||||||
|
|
||||||
|
func load_from_json(path: String) -> SaveData:
|
||||||
|
var file = FileAccess.open(path, FileAccess.READ)
|
||||||
|
if not file:
|
||||||
|
return null
|
||||||
|
var json_str = file.get_as_text()
|
||||||
|
var parse_result = JSON.parse_string(json_str)
|
||||||
|
if parse_result is Dictionary:
|
||||||
|
return _dict_to_save_data(parse_result as Dictionary)
|
||||||
|
return null
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning and Migration
|
||||||
|
|
||||||
|
Handle save format changes across game updates:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func load() -> SaveData:
|
||||||
|
var raw = ResourceLoader.load(SAVE_PATH) as SaveData
|
||||||
|
if not raw:
|
||||||
|
return null
|
||||||
|
|
||||||
|
# Migrate older save formats to current version
|
||||||
|
while raw.save_version < SaveData.SAVE_VERSION:
|
||||||
|
raw = _migrate(raw, raw.save_version)
|
||||||
|
|
||||||
|
return raw
|
||||||
|
|
||||||
|
func _migrate(data: SaveData, from_version: int) -> SaveData:
|
||||||
|
match from_version:
|
||||||
|
0:
|
||||||
|
# v0 → v1: add quest_flags field
|
||||||
|
data.quest_flags = {}
|
||||||
|
data.save_version = 1
|
||||||
|
_:
|
||||||
|
push_warning("SaveManager: Unknown migration from v%d" % from_version)
|
||||||
|
return data
|
||||||
|
```
|
||||||
|
|
||||||
|
## Corruption Recovery
|
||||||
|
|
||||||
|
Protect against corrupted save files:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func load_safe() -> SaveData:
|
||||||
|
var data = load()
|
||||||
|
if data and _validate(data):
|
||||||
|
return data
|
||||||
|
|
||||||
|
# Try backup
|
||||||
|
if FileAccess.file_exists(SAVE_PATH + ".bak"):
|
||||||
|
push_warning("SaveManager: Primary save corrupted, loading backup")
|
||||||
|
var temp = SAVE_PATH
|
||||||
|
OS.move_to_trash(SAVE_PATH) # Move corrupted file
|
||||||
|
DirAccess.copy_absolute(SAVE_PATH + ".bak", SAVE_PATH)
|
||||||
|
return load()
|
||||||
|
|
||||||
|
push_error("SaveManager: No valid save found")
|
||||||
|
return null
|
||||||
|
|
||||||
|
func _validate(data: SaveData) -> bool:
|
||||||
|
if not data:
|
||||||
|
return false
|
||||||
|
if data.save_version < 1:
|
||||||
|
return false
|
||||||
|
# Add more validation checks
|
||||||
|
return true
|
||||||
|
|
||||||
|
# Create backup on successful save
|
||||||
|
func save_with_backup(data: SaveData) -> Error:
|
||||||
|
# Backup current save
|
||||||
|
if FileAccess.file_exists(SAVE_PATH):
|
||||||
|
DirAccess.copy_absolute(SAVE_PATH, SAVE_PATH + ".bak")
|
||||||
|
return save(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cloud Save Integration Pattern
|
||||||
|
|
||||||
|
Abstract cloud provider behind an interface:
|
||||||
|
|
||||||
|
```gdot
|
||||||
|
class_name CloudSaveProvider extends RefCounted
|
||||||
|
|
||||||
|
virtual func upload(data: String, key: String) -> bool:
|
||||||
|
return false
|
||||||
|
|
||||||
|
virtual func download(key: String) -> String:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
virtual func delete(key: String) -> bool:
|
||||||
|
return false
|
||||||
|
|
||||||
|
# Usage: implement concrete providers
|
||||||
|
# class_name GooglePlaySaveProvider extends CloudSaveProvider
|
||||||
|
# class_name AppleCloudSaveProvider extends CloudSaveProvider
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Save Strategies
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name AutoSaver extends Node
|
||||||
|
|
||||||
|
@export var interval_seconds: float = 30.0
|
||||||
|
var _timer: Timer
|
||||||
|
var _save_manager: SaveManager
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_timer = Timer.new()
|
||||||
|
_timer.wait_time = interval_seconds
|
||||||
|
_timer.timeout.connect(_on_auto_save)
|
||||||
|
add_child(_timer)
|
||||||
|
_timer.start()
|
||||||
|
|
||||||
|
func _on_auto_save() -> void:
|
||||||
|
var data = _build_save_data()
|
||||||
|
_save_manager.save(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 Save Notes
|
||||||
|
|
||||||
|
- **ResourceSaver** supports `ResourceSaver.FLAG_COMPRESS` for smaller save files
|
||||||
|
- **ConfigFile** — Use for settings/preferences (separate from game saves)
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify save system scripts
|
||||||
|
- **`project_info`** — Check user:// directory paths for save location debugging
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Resource Save Patterns](references/resource-saves.md)
|
||||||
|
- [Save Migration Strategies](references/save-migration.md)
|
||||||
|
- [Cloud Save Integration](references/cloud-saves.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-save-systems
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "implementing save systems in Godot"
|
||||||
|
- "Godot persistence patterns"
|
||||||
|
- "save file serialization Godot"
|
||||||
|
- "save migration versioning"
|
||||||
|
- "cloud save Godot"
|
||||||
|
- "auto-save Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-save-systems
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "implementing save systems in Godot"
|
||||||
|
- "Godot persistence patterns"
|
||||||
|
- "save file serialization Godot"
|
||||||
|
- "save migration versioning"
|
||||||
|
- "cloud save Godot"
|
||||||
|
- "auto-save Godot"
|
||||||
|
|
@ -0,0 +1,299 @@
|
||||||
|
---
|
||||||
|
name: godot-shaders-vfx
|
||||||
|
description: "Visual effects patterns for Godot 4.x: Godot Shader Language (GSL) basics, canvas_item/vertex/spatial shaders, particle systems (GPUParticles2D/3D), shader materials, and post-processing. Use when implementing custom rendering effects, particles, or visual polish in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- shaders
|
||||||
|
- vfx
|
||||||
|
- particles
|
||||||
|
- post-processing
|
||||||
|
- gsl
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# Shaders and Visual Effects in Godot
|
||||||
|
|
||||||
|
Patterns for custom rendering and particle effects.
|
||||||
|
|
||||||
|
## Shader Types
|
||||||
|
|
||||||
|
Godot supports three shader modes:
|
||||||
|
|
||||||
|
| Mode | Node Type | Use Case |
|
||||||
|
|------|-----------|----------|
|
||||||
|
| `canvas_item` | Sprite2D, TileMap, Control | 2D sprite effects, UI shaders |
|
||||||
|
| `spatial` | MeshInstance3D, SurfaceTool | 3D material effects |
|
||||||
|
| `vertex` | Any mesh | Displacement, morphing (no fragment stage) |
|
||||||
|
|
||||||
|
## Canvas Item Shader (2D)
|
||||||
|
|
||||||
|
Basic 2D shader structure:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
// Pixelate effect for retro aesthetics
|
||||||
|
shader_type canvas_item;
|
||||||
|
|
||||||
|
uniform float pixel_size : hint_range(1.0, 32.0) = 4.0;
|
||||||
|
uniform float brightness : hint_range(0.0, 2.0) = 1.0;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
// Snap UV to pixel grid
|
||||||
|
vec2 uv = floor(UV * (1.0 / pixel_size)) * pixel_size;
|
||||||
|
vec4 color = texture(TEXTURE, uv);
|
||||||
|
|
||||||
|
// Apply brightness
|
||||||
|
color.rgb *= brightness;
|
||||||
|
|
||||||
|
COLOR = color;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Applying Shaders in Code
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Apply shader material to a sprite
|
||||||
|
var shader_mat = ShaderMaterial.new()
|
||||||
|
shader_mat.shader = preload("res://shaders/pixelate.gdshader")
|
||||||
|
$Sprite2D.material = shader_mat
|
||||||
|
|
||||||
|
# Set uniform at runtime
|
||||||
|
shader_mat.set_shader_parameter("pixel_size", 8.0)
|
||||||
|
shader_mat.set_shader_parameter("brightness", 1.5)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Color Tint Shader
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
// Selective color tint — useful for damage flash, selection highlight
|
||||||
|
shader_type canvas_item;
|
||||||
|
|
||||||
|
uniform vec4 tint_color : hint_color = vec4(1.0, 0.0, 0.0, 1.0);
|
||||||
|
uniform float tint_amount : hint_range(0.0, 1.0) = 0.5;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec4 original = texture(TEXTURE, UV);
|
||||||
|
COLOR = mix(original, original * tint_color, tint_amount);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spatial Shader (3D)
|
||||||
|
|
||||||
|
Basic 3D shader with lighting:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
// Animated water surface
|
||||||
|
shader_type spatial;
|
||||||
|
render_mode unshaded, depth_prepass_alpha_blend;
|
||||||
|
|
||||||
|
uniform float wave_speed : hint_range(0.1, 5.0) = 1.0;
|
||||||
|
uniform float wave_height : hint_range(0.01, 0.5) = 0.1;
|
||||||
|
uniform vec4 water_color : hint_color = vec4(0.0, 0.5, 0.8, 0.7);
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
// Animate vertices with sine waves
|
||||||
|
float wave = sin(UV.x * 10.0 + TIME * wave_speed) * wave_height;
|
||||||
|
wave += sin(UV.y * 8.0 + TIME * wave_speed * 1.3) * wave_height * 0.5;
|
||||||
|
VERTEX.y += wave;
|
||||||
|
|
||||||
|
// Recalculate normal for lighting
|
||||||
|
NORMAL.y += wave * 2.0;
|
||||||
|
NORMAL = normalize(NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
// Fresnel-like effect — edges more reflective
|
||||||
|
float fresnel = pow(1.0 - abs(dot(NORMAL, VIEW_DIR)), 3.0);
|
||||||
|
vec3 color = mix(water_color.rgb, vec3(1.0), fresnel * 0.5);
|
||||||
|
|
||||||
|
ALBEDO = color;
|
||||||
|
ALPHA = water_color.a;
|
||||||
|
SPECULAR = 0.8;
|
||||||
|
ROUGHNESS = 0.2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Particle Systems
|
||||||
|
|
||||||
|
### GPUParticles2D
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Code-driven particle setup
|
||||||
|
var particles = GPUParticles2D.new()
|
||||||
|
particles.one_shot = true
|
||||||
|
particles.lifetime = 1.5
|
||||||
|
particles.amount = 50
|
||||||
|
particles.emitting = false
|
||||||
|
|
||||||
|
# Process mode
|
||||||
|
particles.process_material = ParticleProcessMaterial.new()
|
||||||
|
particles.process_material.initial_velocity_min = 50.0
|
||||||
|
particles.process_material.initial_velocity_max = 150.0
|
||||||
|
particles.process_material.gravity = Vector3(0, -200, 0)
|
||||||
|
particles.process_material.spread = 360.0
|
||||||
|
|
||||||
|
# Visual
|
||||||
|
particles.material = ParticleMaterial.new()
|
||||||
|
particles.material.texture = preload("res://assets/particle.png")
|
||||||
|
particles.material.h_color = Color(1.0, 0.5, 0.0, 1.0)
|
||||||
|
particles.material.h_color2 = Color(1.0, 1.0, 0.0, 0.0) # Fade to transparent
|
||||||
|
|
||||||
|
add_child(particles)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Triggering Particles
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
func emit_explosion(position: Vector2) -> void:
|
||||||
|
var particles = load("res://vfx/explosion.tscn").instantiate() as GPUParticles2D
|
||||||
|
particles.global_position = position
|
||||||
|
particles.emitting = true
|
||||||
|
get_parent().add_child(particles)
|
||||||
|
|
||||||
|
# Auto-destroy after particles finish
|
||||||
|
particles.finished.connect(func(): particles.queue_free())
|
||||||
|
```
|
||||||
|
|
||||||
|
### GPUParticles3D
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# 3D particle burst (fire, smoke, magic)
|
||||||
|
var particles_3d = GPUParticles3D.new()
|
||||||
|
particles_3d.one_shot = true
|
||||||
|
particles_3d.lifetime = 2.0
|
||||||
|
particles_3d.amount = 100
|
||||||
|
|
||||||
|
# Directional emission from a cone
|
||||||
|
particles_3d.direction = Vector3.UP
|
||||||
|
particles_3d.emitting = false
|
||||||
|
|
||||||
|
add_child(particles_3d)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Post-Processing
|
||||||
|
|
||||||
|
Apply screen-space effects via WorldEnvironment:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Access environment settings
|
||||||
|
var env = get_world_3d().environment
|
||||||
|
|
||||||
|
# Glow/bloom
|
||||||
|
env.glow_enabled = true
|
||||||
|
env.glow_bloom_intensity = 0.5
|
||||||
|
env.glow_bloom_soft = true
|
||||||
|
env.glow_bloom_threshold = 0.8
|
||||||
|
|
||||||
|
# Tone mapping
|
||||||
|
env.tone_mapping = Environment.TONE_MAPPING_ACES
|
||||||
|
|
||||||
|
# Fog
|
||||||
|
env.fog_mode = Environment.FOG_MODE_EXPONENTIATED
|
||||||
|
env.fog_color = Color(0.6, 0.7, 0.8)
|
||||||
|
env.fog_density = 0.02
|
||||||
|
|
||||||
|
# SSAO (3D ambient occlusion)
|
||||||
|
env.ssao_enabled = true
|
||||||
|
env.ssao_max_distance = 5.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### Screen Shader (WorldEnvironment)
|
||||||
|
|
||||||
|
For custom full-screen post-processing:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
// Vignette + chromatic aberration post-process
|
||||||
|
shader_type spatial;
|
||||||
|
render_mode unshaded;
|
||||||
|
|
||||||
|
uniform float vignette_amount : hint_range(0.0, 1.0) = 0.5;
|
||||||
|
uniform float chroma_amount : hint_range(0.0, 0.01) = 0.003;
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
// Chromatic aberration — shift RGB channels radially
|
||||||
|
vec2 center = UV - 0.5;
|
||||||
|
float dist = length(center);
|
||||||
|
vec2 dir = normalize(center) * chroma_amount * dist;
|
||||||
|
|
||||||
|
float r = texture(SCREEN_TEXTURE, UV + dir).r;
|
||||||
|
float g = texture(SCREEN_TEXTURE, UV).g;
|
||||||
|
float b = texture(SCREEN_TEXTURE, UV - dir).b;
|
||||||
|
|
||||||
|
// Vignette
|
||||||
|
float vignette = mix(1.0, 0.0, dist * vignette_amount * 2.0);
|
||||||
|
|
||||||
|
vec3 color = vec3(r, g, b) * vignette;
|
||||||
|
COLOR = vec4(color, 1.0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shader Performance Tips
|
||||||
|
|
||||||
|
### Minimize Fragment Operations
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
// Bad — expensive per-pixel
|
||||||
|
void fragment() {
|
||||||
|
COLOR.rgb = pow(texture(TEXTURE, UV).rgb, 3.0) * sin(TIME * 10.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good — precompute where possible
|
||||||
|
uniform float precomputed_value;
|
||||||
|
void fragment() {
|
||||||
|
COLOR.rgb = texture(TEXTURE, UV).rgb * precomputed_value;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use `hint_range` for Uniforms
|
||||||
|
|
||||||
|
Always constrain uniforms to prevent editor/script errors:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
uniform float speed : hint_range(0.1, 10.0) = 1.0;
|
||||||
|
uniform vec4 color : hint_color = vec4(1.0);
|
||||||
|
uniform sampler2D tex : hint_black; // Default black texture fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shader Material Reuse
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Share one ShaderMaterial across multiple nodes for performance
|
||||||
|
var shared_material = ShaderMaterial.new()
|
||||||
|
shared_material.shader = preload("res://shaders/wave.gdshader")
|
||||||
|
|
||||||
|
$Sprite2D_A.material = shared_material
|
||||||
|
$Sprite2D_B.material = shared_material # Same material instance
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 VFX Notes
|
||||||
|
|
||||||
|
- **GPUParticles** — Improved burst and emit rate controls
|
||||||
|
- **Screen shaders** — Enhanced SCREEN_TEXTURE sampling quality
|
||||||
|
- **render_mode** — New `world_vertex_coords` option for world-space shader effects
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify shader scripts
|
||||||
|
- **`scene_tree`** — Inspect particle node hierarchy and parameters
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [GSL Shader Patterns](references/gsl-patterns.md)
|
||||||
|
- [Particle System Guide](references/particle-systems.md)
|
||||||
|
| [Post-Processing Effects](references/post-processing.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-shaders-vfx
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "writing shaders in Godot"
|
||||||
|
- "Godot visual effects"
|
||||||
|
- "particle systems Godot"
|
||||||
|
- "post-processing Godot"
|
||||||
|
- "Godot Shader Language"
|
||||||
|
- "GSL shader patterns"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-shaders-vfx
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "writing shaders in Godot"
|
||||||
|
- "Godot visual effects"
|
||||||
|
- "particle systems Godot"
|
||||||
|
- "post-processing Godot"
|
||||||
|
- "Godot Shader Language"
|
||||||
|
- "GSL shader patterns"
|
||||||
|
|
@ -0,0 +1,285 @@
|
||||||
|
---
|
||||||
|
name: godot-state-management
|
||||||
|
description: "State management patterns for Godot 4.x: enum-based state, finite state machines (FSM), hierarchical FSM, and behavior trees for AI. Use when implementing character states, game flow, menu systems, or AI decision-making in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- state-machine
|
||||||
|
- fsm
|
||||||
|
- behavior-tree
|
||||||
|
- ai
|
||||||
|
created: 2026-06-21
|
||||||
|
---
|
||||||
|
|
||||||
|
# State Management in Godot
|
||||||
|
|
||||||
|
Patterns for managing game and entity states cleanly.
|
||||||
|
|
||||||
|
## Enum-Based State (Simple)
|
||||||
|
|
||||||
|
For straightforward state tracking without transitions:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name PlayerController extends CharacterBody2D
|
||||||
|
|
||||||
|
enum State { IDLE, RUNNING, JUMPING, FALLING, ATTACKING }
|
||||||
|
|
||||||
|
var current_state: State = State.IDLE
|
||||||
|
var velocity: Vector2
|
||||||
|
|
||||||
|
func _physics_process(delta: float) -> void:
|
||||||
|
match current_state:
|
||||||
|
State.IDLE:
|
||||||
|
_handle_idle(delta)
|
||||||
|
State.RUNNING:
|
||||||
|
_handle_running(delta)
|
||||||
|
State.JUMPING:
|
||||||
|
_handle_jumping(delta)
|
||||||
|
State.FALLING:
|
||||||
|
_handle_falling(delta)
|
||||||
|
State.ATTACKING:
|
||||||
|
_handle_attacking(delta)
|
||||||
|
|
||||||
|
func set_state(new_state: State) -> void:
|
||||||
|
if current_state == new_state:
|
||||||
|
return
|
||||||
|
current_state = new_state
|
||||||
|
```
|
||||||
|
|
||||||
|
## Finite State Machine (FSM)
|
||||||
|
|
||||||
|
For entities with defined transitions between states:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name FSM extends Node
|
||||||
|
|
||||||
|
signal state_entered(state_name: StringName)
|
||||||
|
signal state_exited(state_name: StringName)
|
||||||
|
|
||||||
|
var current_state: FSMState = null
|
||||||
|
var states: Dictionary = {}
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
for child in get_children():
|
||||||
|
if child is FSMState:
|
||||||
|
states[child.name] = child
|
||||||
|
|
||||||
|
func initialize(initial: StringName) -> void:
|
||||||
|
if not states.has(initial):
|
||||||
|
push_error("FSM: Initial state '%s' not found" % initial)
|
||||||
|
return
|
||||||
|
current_state = states[initial]
|
||||||
|
current_state.enter()
|
||||||
|
state_entered.emit(initial)
|
||||||
|
|
||||||
|
func change_state(new_state_name: StringName) -> void:
|
||||||
|
if not states.has(new_state_name):
|
||||||
|
push_error("FSM: State '%s' not found" % new_state_name)
|
||||||
|
return
|
||||||
|
if current_state:
|
||||||
|
current_state.exit()
|
||||||
|
state_exited.emit(current_state.name)
|
||||||
|
current_state = states[new_state_name]
|
||||||
|
current_state.enter()
|
||||||
|
state_entered.emit(new_state_name)
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if current_state:
|
||||||
|
current_state.update(delta)
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if current_state:
|
||||||
|
current_state.handle_input(event)
|
||||||
|
```
|
||||||
|
|
||||||
|
### FSM State Base Class
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name FSMState extends Node
|
||||||
|
|
||||||
|
func enter() -> void:
|
||||||
|
pass # Override
|
||||||
|
|
||||||
|
func exit() -> void:
|
||||||
|
pass # Override
|
||||||
|
|
||||||
|
func update(delta: float) -> void:
|
||||||
|
pass # Override
|
||||||
|
|
||||||
|
func handle_input(event: InputEvent) -> void:
|
||||||
|
pass # Override
|
||||||
|
|
||||||
|
# Helper to transition from within a state
|
||||||
|
func request_state(fsm: FSM, new_state: StringName) -> void:
|
||||||
|
fsm.change_state(new_state)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Concrete State Example
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# EnemyIdle.gd extends FSMState
|
||||||
|
class_name EnemyIdle extends FSMState
|
||||||
|
|
||||||
|
@export var idle_timer: float = 2.0
|
||||||
|
var _timer: float = 0.0
|
||||||
|
|
||||||
|
func enter() -> void:
|
||||||
|
_timer = 0.0
|
||||||
|
|
||||||
|
func update(delta: float) -> void:
|
||||||
|
_timer += delta
|
||||||
|
if _timer >= idle_timer:
|
||||||
|
get_parent().request_state(get_parent().get_node("FSM"), "Patrol")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Hierarchical State Machine
|
||||||
|
|
||||||
|
For nested states (e.g., combat has sub-states for melee/range):
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name HierarchicalFSM extends Node
|
||||||
|
|
||||||
|
var current_parent: StringName = ""
|
||||||
|
var current_child: StringName = ""
|
||||||
|
|
||||||
|
# Parent states: "Exploring", "Combat", "Dead"
|
||||||
|
# Combat child states: "Melee", "Ranged", "Dodging"
|
||||||
|
|
||||||
|
func enter_state(parent: StringName, child: StringName) -> void:
|
||||||
|
current_parent = parent
|
||||||
|
current_child = child
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hierarchical State Pattern
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# PlayerCombatState.gd — parent state delegates to children
|
||||||
|
class_name PlayerCombatState extends FSMState
|
||||||
|
|
||||||
|
var sub_states: Dictionary = {}
|
||||||
|
var active_sub_state: StringName = ""
|
||||||
|
|
||||||
|
func enter() -> void:
|
||||||
|
# Register sub-states from child nodes
|
||||||
|
for child in get_children():
|
||||||
|
if child is FSMState:
|
||||||
|
sub_states[child.name] = child
|
||||||
|
|
||||||
|
func update(delta: float) -> void:
|
||||||
|
# Choose sub-state based on context
|
||||||
|
var new_sub_state = _determine_sub_state()
|
||||||
|
if new_sub_state != active_sub_state:
|
||||||
|
_switch_sub_state(new_sub_state)
|
||||||
|
if sub_states.has(active_sub_state):
|
||||||
|
sub_states[active_sub_state].update(delta)
|
||||||
|
|
||||||
|
func _determine_sub_state() -> StringName:
|
||||||
|
# Example: switch based on weapon type or enemy distance
|
||||||
|
return "Melee"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Behavior Trees (AI)
|
||||||
|
|
||||||
|
For complex AI decision-making:
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name BehaviorTree extends Node
|
||||||
|
|
||||||
|
var root_node: BTNode = null
|
||||||
|
|
||||||
|
func tick(delta: float, blackboard: Dictionary) -> BTNode.Status:
|
||||||
|
if not root_node:
|
||||||
|
root_node = get_child(0)
|
||||||
|
return root_node.execute(blackboard, delta)
|
||||||
|
|
||||||
|
# Blackboard — shared data between nodes
|
||||||
|
# { "target": Node, "health": int, "last_seen_pos": Vector2 }
|
||||||
|
```
|
||||||
|
|
||||||
|
### BT Node Types
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
class_name BTNode extends Node
|
||||||
|
|
||||||
|
enum Status { SUCCESS, FAILURE, RUNNING }
|
||||||
|
|
||||||
|
func execute(blackboard: Dictionary, delta: float) -> Status:
|
||||||
|
return Status.SUCCESS # Override in subclasses
|
||||||
|
|
||||||
|
# --- Composite Nodes ---
|
||||||
|
|
||||||
|
class_name Sequence extends BTNode
|
||||||
|
func execute(bb: Dictionary, delta: float) -> Status:
|
||||||
|
for child in get_children():
|
||||||
|
if child is BTNode:
|
||||||
|
var result = child.execute(bb, delta)
|
||||||
|
if result != Status.SUCCESS:
|
||||||
|
return result
|
||||||
|
return Status.SUCCESS
|
||||||
|
|
||||||
|
class_name Selector extends BTNode
|
||||||
|
func execute(bb: Dictionary, delta: float) -> Status:
|
||||||
|
for child in get_children():
|
||||||
|
if child is BTNode:
|
||||||
|
var result = child.execute(bb, delta)
|
||||||
|
if result == Status.SUCCESS:
|
||||||
|
return Status.SUCCESS
|
||||||
|
return Status.FAILURE
|
||||||
|
|
||||||
|
# --- Leaf Nodes ---
|
||||||
|
|
||||||
|
class_name Condition extends BTNode
|
||||||
|
func check(blackboard: Dictionary) -> bool:
|
||||||
|
return true # Override
|
||||||
|
|
||||||
|
func execute(bb: Dictionary, _delta: float) -> Status:
|
||||||
|
return Status.SUCCESS if check(bb) else Status.FAILURE
|
||||||
|
|
||||||
|
class_name Action extends BTNode
|
||||||
|
@export var duration: float = 0.0
|
||||||
|
var elapsed: float = 0.0
|
||||||
|
|
||||||
|
func do_action(blackboard: Dictionary) -> void:
|
||||||
|
pass # Override
|
||||||
|
|
||||||
|
func execute(bb: Dictionary, delta: float) -> Status:
|
||||||
|
if duration > 0:
|
||||||
|
elapsed += delta
|
||||||
|
do_action(bb)
|
||||||
|
return Status.SUCCESS if elapsed >= duration else Status.RUNNING
|
||||||
|
else:
|
||||||
|
do_action(bb)
|
||||||
|
return Status.SUCCESS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Godot 4.7 State Notes
|
||||||
|
|
||||||
|
- **tween_await()** — Use for state transitions with animated duration: `await get_tree().create_tween().tween_property(...) .finished`
|
||||||
|
- **AnimationTree** — Combine FSM logic with AnimationTree state machines for visual feedback
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools can inspect and modify your project live:
|
||||||
|
|
||||||
|
- **`scene_tree`** — Inspect scene hierarchy to audit FSM node structure
|
||||||
|
- **`script_read`** / **`script_edit`** — Read and modify state machine scripts
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [FSM Implementation Guide](references/fsm-guide.md)
|
||||||
|
- [Behavior Tree Patterns](references/behavior-trees.md)
|
||||||
|
- [Hierarchical FSM Examples](references/hierarchical-fsm.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-state-management
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "implementing state machines in Godot"
|
||||||
|
- "Godot FSM patterns"
|
||||||
|
- "behavior trees Godot AI"
|
||||||
|
- "hierarchical state machine Godot"
|
||||||
|
- "character state management"
|
||||||
|
- "game flow states Godot"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-state-management
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "implementing state machines in Godot"
|
||||||
|
- "Godot FSM patterns"
|
||||||
|
- "behavior trees Godot AI"
|
||||||
|
- "hierarchical state machine Godot"
|
||||||
|
- "character state management"
|
||||||
|
- "game flow states Godot"
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
---
|
||||||
|
name: godot-ui
|
||||||
|
description: "UI systems for Godot 4.x: Control node hierarchy, containers and layout modes (Anchor, Margin, Center, Fill, Expand), theme resources, custom styles, RichTextLabel formatting, PopupMenu search, and control offset transforms. Use when building menus, HUDs, inventory screens, or any UI in Godot."
|
||||||
|
license: MIT
|
||||||
|
compatibility:
|
||||||
|
- godot-4.0
|
||||||
|
- godot-4.1
|
||||||
|
- godot-4.2
|
||||||
|
- godot-4.3
|
||||||
|
- godot-4.4
|
||||||
|
- godot-4.5
|
||||||
|
- godot-4.6
|
||||||
|
- godot-4.7
|
||||||
|
metadata:
|
||||||
|
author: vl4dt
|
||||||
|
version: 0.1.0
|
||||||
|
tags:
|
||||||
|
- godot
|
||||||
|
- ui
|
||||||
|
- control
|
||||||
|
- container
|
||||||
|
- theme
|
||||||
|
- rich-text
|
||||||
|
created: 2026-06-20
|
||||||
|
---
|
||||||
|
|
||||||
|
# UI Systems for Godot 4.x
|
||||||
|
|
||||||
|
Comprehensive patterns for user interfaces, menus, and HUD elements in Godot 4.x.
|
||||||
|
|
||||||
|
## Control Node Hierarchy and Containers
|
||||||
|
|
||||||
|
### Container Types
|
||||||
|
|
||||||
|
Godot 4.x provides several container nodes for responsive UI layouts.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Vertical layout
|
||||||
|
var vbox = VBoxContainer.new()
|
||||||
|
vbox.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_child(vbox)
|
||||||
|
|
||||||
|
# Horizontal layout
|
||||||
|
var hbox = HBoxContainer.new()
|
||||||
|
hbox.add_child(Button.new())
|
||||||
|
hbox.add_child(Button.new())
|
||||||
|
vbox.add_child(hbox)
|
||||||
|
|
||||||
|
# Grid layout
|
||||||
|
var grid = GridContainer.new()
|
||||||
|
grid.columns = 3
|
||||||
|
for i in range(9):
|
||||||
|
var btn = Button.new()
|
||||||
|
btn.text = str(i)
|
||||||
|
grid.add_child(btn)
|
||||||
|
vbox.add_child(grid)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Anchor Presets
|
||||||
|
|
||||||
|
Use anchor presets for responsive positioning.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Top-left corner, stays fixed
|
||||||
|
button.set_anchors_preset(Control.PRESET_TOP_LEFT)
|
||||||
|
|
||||||
|
# Full screen (fills parent)
|
||||||
|
panel.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
|
||||||
|
# Centered with margins
|
||||||
|
label.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
label.offset_left = -100
|
||||||
|
label.offset_right = 100
|
||||||
|
```
|
||||||
|
## Layout Modes (Anchor, Margin, Center, Fill, Expand)
|
||||||
|
|
||||||
|
### Anchor Mode
|
||||||
|
|
||||||
|
Control how a node positions itself relative to its parent.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Manual anchor setup
|
||||||
|
control.anchor_right = 1.0 # Right edge sticks to parent right
|
||||||
|
control.offset_right = -20 # But 20px from the edge
|
||||||
|
control.anchor_bottom = 1.0
|
||||||
|
control.offset_bottom = -20
|
||||||
|
```
|
||||||
|
|
||||||
|
### Margin Container
|
||||||
|
|
||||||
|
Add padding around a child node.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var margin = MarginContainer.new()
|
||||||
|
margin.add_theme_constant_override("margin_left", 16)
|
||||||
|
margin.add_theme_constant_override("margin_top", 16)
|
||||||
|
margin.add_theme_constant_override("margin_right", 16)
|
||||||
|
margin.add_theme_constant_override("margin_bottom", 16)
|
||||||
|
margin.add_child(content_panel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Center Container
|
||||||
|
|
||||||
|
Center a child node within its parent.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
var center = CenterContainer.new()
|
||||||
|
center.horizontal_alignment = CenterContainer.ALIGNMENT_CENTER
|
||||||
|
center.vertical_alignment = CenterContainer.ALIGNMENT_MIDDLE
|
||||||
|
center.add_child(dialog_panel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fill and Expand
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Fill available space
|
||||||
|
button.expand_mode = Button.BUTTON_EXPAND_FILL # Fill width and height
|
||||||
|
button.expand_mode = Button.BUTTON_EXPAND # Fill width only
|
||||||
|
|
||||||
|
# Control fills parent rect
|
||||||
|
control.size_flags_horizontal = Control.SIZE_FILL
|
||||||
|
control.size_flags_vertical = Control.SIZE_FILL
|
||||||
|
```
|
||||||
|
## Theme Resources and Custom Styles
|
||||||
|
|
||||||
|
### Creating a Custom Theme
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Create theme in code
|
||||||
|
var theme = Theme.new()
|
||||||
|
|
||||||
|
# Custom font
|
||||||
|
var custom_font = FontFile.new()
|
||||||
|
custom_font.font_data = load("res://fonts/my_font.tres")
|
||||||
|
theme.add_font("Default", "Source", custom_font)
|
||||||
|
theme.default_font_size = 16
|
||||||
|
|
||||||
|
# Custom button style
|
||||||
|
var normal_style = StyleBoxTexture.new()
|
||||||
|
normal_style.texture = load("res://ui/button_normal.png")
|
||||||
|
normal_style.set_margin_left(8)
|
||||||
|
normal_style.set_margin_right(8)
|
||||||
|
normal_style.set_margin_top(4)
|
||||||
|
normal_style.set_margin_bottom(4)
|
||||||
|
|
||||||
|
var hover_style = normal_style.duplicate()
|
||||||
|
hover_style.modulate = Color(1.2, 1.2, 1.2) # Lighter on hover
|
||||||
|
|
||||||
|
theme.add_stylebox("normal", "Button", normal_style)
|
||||||
|
theme.add_stylebox("hover", "Button", hover_style)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Overrides per Control
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Override theme on individual controls
|
||||||
|
button.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
button.add_theme_font_size_override("font_size", 20)
|
||||||
|
button.add_theme_stylebox_override("normal", custom_style)
|
||||||
|
|
||||||
|
# Apply theme to entire scene tree
|
||||||
|
add_child(button)
|
||||||
|
button.theme = theme
|
||||||
|
```
|
||||||
|
|
||||||
|
### Theme Variation Pattern
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Create themed button scenes for consistency
|
||||||
|
# ui_button_primary.tscn, ui_button_secondary.tscn, etc.
|
||||||
|
const PRIMARY_BTN = preload("res://ui/button_primary.tscn")
|
||||||
|
const SECONDARY_BTN = preload("res://ui/button_secondary.tscn")
|
||||||
|
|
||||||
|
func create_primary_button(text: String) -> Button:
|
||||||
|
var btn = PRIMARY_BTN.instantiate()
|
||||||
|
btn.text = text
|
||||||
|
return btn
|
||||||
|
```
|
||||||
|
## RichTextLabel Formatting and Image Sizing
|
||||||
|
|
||||||
|
### RichTextEffect Custom Tags
|
||||||
|
|
||||||
|
Godot 4.x supports custom rich text effects.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Use built-in BBCode tags in RichTextLabel
|
||||||
|
rich_text_label.text = "[color=red]Alert![/color] Health is low."
|
||||||
|
rich_text_label.text = "[b]Bold[/b] and [i]italic[/i] text"
|
||||||
|
rich_text_label.text = "[img=64]res://icons/heart.png[/img] 10/20 HP"
|
||||||
|
|
||||||
|
# Custom effect for damage numbers
|
||||||
|
func _process(delta: float):
|
||||||
|
# RichTextLabel processes BBCode every frame
|
||||||
|
# For performance, cache formatted text when possible
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image Sizing in RichTextLabel
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Fixed size images
|
||||||
|
rich_text_label.text = "[img=32]res://icons/coin.png[/img] [img=32]res://icons/coin.png[/img]"
|
||||||
|
|
||||||
|
# Scale by font size
|
||||||
|
rich_text_label.text = "[font_size=48][img=48]res://icons/star.png[/img][/font_size]"
|
||||||
|
|
||||||
|
# Responsive sizing using theme font
|
||||||
|
func get_icon_size() -> int:
|
||||||
|
return ThemeDB.get_font("Default").get_height() * 2
|
||||||
|
```
|
||||||
|
## Godot 4.7 UI Features
|
||||||
|
|
||||||
|
### PopupMenu Search (Godot 4.7)
|
||||||
|
|
||||||
|
Godot 4.7 adds search/filtering to PopupMenu for better UX in large menus.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# The PopupMenu now supports search filtering out of the box
|
||||||
|
# Connect the search_changed signal for custom filtering
|
||||||
|
@onready var popup_menu = $PopupMenu
|
||||||
|
@onready var search_line_edit = $SearchField
|
||||||
|
|
||||||
|
func _ready():
|
||||||
|
search_line_edit.text_changed.connect(_on_search_changed)
|
||||||
|
popup_menu.index_pressed.connect(_on_menu_selected)
|
||||||
|
|
||||||
|
func _on_search_changed(query: String):
|
||||||
|
# Filter menu items based on search query
|
||||||
|
for i in range(popup_menu.item_count):
|
||||||
|
var item_text = popup_menu.get_item_text(i)
|
||||||
|
popup_menu.set_item_hidden(i, not item_text.containsn(query))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Control Offset Transforms (Godot 4.7)
|
||||||
|
|
||||||
|
Godot 4.7 allows animating control offsets directly for smooth UI transitions.
|
||||||
|
|
||||||
|
```gdscript
|
||||||
|
# Animate panel sliding in using offset transforms
|
||||||
|
func slide_in(panel: Control):
|
||||||
|
panel.offset_left = -300 # Start off-screen
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_property(panel, "offset_left", 0, 0.3)
|
||||||
|
tween.set_ease(Tween.EASE_OUT)
|
||||||
|
|
||||||
|
func slide_out(panel: Control):
|
||||||
|
var tween = create_tween()
|
||||||
|
tween.tween_property(panel, "offset_left", -300, 0.25)
|
||||||
|
tween.connect("finished", panel.queue_free)
|
||||||
|
|
||||||
|
# Use for: sliding menus, notifications, dialog entrances
|
||||||
|
```
|
||||||
|
## UI Performance Tips
|
||||||
|
|
||||||
|
- Use **Container** nodes instead of manually positioning controls
|
||||||
|
- Prefer **Anchor presets** over manual offset adjustments when possible
|
||||||
|
- Cache `RichTextLabel` formatted text when content does not change
|
||||||
|
- Use `TextureRect` with `STRETCH_SCALE` for responsive images
|
||||||
|
- Avoid creating/destroying UI nodes every frame — use pooling instead
|
||||||
|
- Set `visible_ratio` to 0 on off-screen panels instead of queue_free()
|
||||||
|
|
||||||
|
## Quick Reference: Container Nodes
|
||||||
|
|
||||||
|
| Container | Layout |
|
||||||
|
|-----------|--------|
|
||||||
|
| `HBoxContainer` | Horizontal row |
|
||||||
|
| `VBoxContainer` | Vertical column |
|
||||||
|
| `GridContainer` | Grid with fixed columns |
|
||||||
|
| `CenterContainer` | Centered child |
|
||||||
|
| `MarginContainer` | Padded child |
|
||||||
|
| `ScrollContainer` | Scrollable area |
|
||||||
|
| `AspectRatioContainer` | Fixed aspect ratio |
|
||||||
|
| `Control` (root) | Full rect anchor |
|
||||||
|
|
||||||
|
## MCP Bridge Tools (Optional — Live Editor Integration)
|
||||||
|
|
||||||
|
When the MCP bridge is running (Phase 2), these tools enhance UI debugging:
|
||||||
|
|
||||||
|
- **`ui_inspect`** — Inspect the current control tree and their properties
|
||||||
|
- **`ui_preview_theme`** — Preview theme changes in real-time
|
||||||
|
- **`ui_resize_preview`** — Test responsive layouts at different screen sizes
|
||||||
|
- **`ui_find_control`** — Find controls by path or name in the scene tree
|
||||||
|
|
||||||
|
> **Note:** MCP tools require the Godot editor to be running with the MCP plugin enabled. Skills work independently without the bridge.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [Control Node Documentation](references/control-reference.md)
|
||||||
|
- [Container Layout Patterns](references/container-patterns.md)
|
||||||
|
- [Theme Resource Guide](references/theme-resources.md)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Claude Code extension for godot-ui
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "UI development in Godot"
|
||||||
|
- "Godot Control nodes and containers"
|
||||||
|
- "Godot custom themes and styling"
|
||||||
|
- "Godot responsive layouts"
|
||||||
|
- "Godot input controls UI"
|
||||||
|
- "Godot HUD design"
|
||||||
|
|
||||||
|
tool_permissions:
|
||||||
|
- read
|
||||||
|
- write
|
||||||
|
- bash
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
# OpenAI / Codex CLI extension for godot-ui
|
||||||
|
# Safely ignored by other agents per Agent Skills Open Standard
|
||||||
|
|
||||||
|
policy:
|
||||||
|
safe: true
|
||||||
|
no_network: true
|
||||||
|
|
||||||
|
when_to_use:
|
||||||
|
- "UI development in Godot"
|
||||||
|
- "Godot Control nodes and containers"
|
||||||
|
- "Godot custom themes and styling"
|
||||||
|
- "Godot responsive layouts"
|
||||||
|
- "Godot input controls UI"
|
||||||
|
- "Godot HUD design"
|
||||||
Loading…
Reference in New Issue