Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 09:08:19 +08:00
commit db818a4853
17 changed files with 4551 additions and 0 deletions

View File

@@ -0,0 +1,600 @@
---
name: godot-debugging
description: Expert knowledge of Godot debugging, error interpretation, common bugs, and troubleshooting techniques. Use when helping fix Godot errors, crashes, or unexpected behavior.
allowed_tools:
- mcp__godot__*
- Read
- Write
- Edit
- Glob
- Grep
- Bash
---
You are a Godot debugging expert with deep knowledge of common errors, debugging techniques, and troubleshooting strategies.
# Common Godot Errors and Solutions
## Parser/Syntax Errors
### Error: "Parse Error: Expected ..."
**Common Causes:**
- Missing colons after function definitions, if statements, loops
- Incorrect indentation (must use tabs OR spaces consistently)
- Missing parentheses in function calls
- Unclosed brackets, parentheses, or quotes
**Solutions:**
```gdscript
# WRONG
func _ready() # Missing colon
print("Hello")
# CORRECT
func _ready():
print("Hello")
# WRONG
if player_health > 0 # Missing colon
player.move()
# CORRECT
if player_health > 0:
player.move()
```
### Error: "Identifier not declared in the current scope"
**Common Causes:**
- Variable used before declaration
- Typo in variable/function name
- Trying to access variable from wrong scope
- Missing @ symbol for onready variables
**Solutions:**
```gdscript
# WRONG
func _ready():
print(my_variable) # Not declared yet
var my_variable = 10
# CORRECT
var my_variable = 10
func _ready():
print(my_variable)
# WRONG
@onready var sprite = $Sprite2D # Missing @
# CORRECT
@onready var sprite = $Sprite2D
```
### Error: "Invalid get index 'property_name' (on base: 'Type')"
**Common Causes:**
- Typo in property name
- Property doesn't exist on that node type
- Node is null (wasn't found in scene tree)
**Solutions:**
```gdscript
# Check if node exists before accessing
if sprite != null:
sprite.visible = false
else:
print("ERROR: Sprite node not found!")
# Or use optional chaining (Godot 4.2+)
# sprite?.visible = false
# Verify node path
@onready var sprite = $Sprite2D # Make sure this path is correct
func _ready():
if sprite == null:
print("Sprite not found! Check node path.")
```
## Runtime Errors
### Error: "Attempt to call function 'func_name' in base 'null instance' on a null instance"
**Common Causes:**
- Calling method on null reference
- Node removed/freed before accessing
- @onready variable references non-existent node
**Solutions:**
```gdscript
# Always check for null before calling methods
if player != null and player.has_method("take_damage"):
player.take_damage(10)
# Verify onready variables in _ready()
@onready var sprite = $Sprite2D
func _ready():
if sprite == null:
push_error("Sprite node not found at path: $Sprite2D")
return
# Check if node is valid before using
if is_instance_valid(my_node):
my_node.do_something()
```
### Error: "Invalid operands 'Type' and 'null' in operator '...'"
**Common Causes:**
- Mathematical operation on null value
- Comparing null to typed value
- Uninitialized variable used in calculation
**Solutions:**
```gdscript
# Initialize variables with default values
var health: int = 100 # Not null
var player: Node2D = null
# Check before operations
if player != null:
var distance = global_position.distance_to(player.global_position)
# Use default values
var target_position = player.global_position if player else global_position
```
### Error: "Index [number] out of range (size [size])"
**Common Causes:**
- Accessing array beyond its length
- Using wrong index variable
- Array size changed but code assumes old size
**Solutions:**
```gdscript
# Always check array size
var items = [1, 2, 3]
if index < items.size():
print(items[index])
else:
print("Index out of range!")
# Or use range-based loops
for item in items:
print(item)
# Safe array access
var value = items[index] if index < items.size() else null
```
## Scene Tree Errors
### Error: "Node not found: [path]"
**Common Causes:**
- Incorrect node path in get_node() or $
- Node doesn't exist yet (wrong timing)
- Node was removed or renamed
- Path case sensitivity issues
**Solutions:**
```gdscript
# Use @onready for scene tree nodes
@onready var sprite = $Sprite2D
@onready var timer = $Timer
# Check if node exists
func get_player():
var player = get_node_or_null("Player")
if player == null:
print("Player node not found!")
return player
# Use has_node() to check existence
if has_node("Sprite2D"):
var sprite = $Sprite2D
# For dynamic paths, use NodePath
var sprite = get_node(NodePath("Path/To/Sprite"))
```
### Error: "Can't change state while flushing queries"
**Common Causes:**
- Modifying physics objects during physics callback
- Adding/removing nodes during iteration
- Freeing nodes in wrong context
**Solutions:**
```gdscript
# Use call_deferred for physics changes
func _on_body_entered(body):
# WRONG
# body.queue_free()
# CORRECT
body.call_deferred("queue_free")
# Use call_deferred for collision shape changes
func disable_collision():
$CollisionShape2D.call_deferred("set_disabled", true)
# Defer node additions/removals
func spawn_enemy():
var enemy = enemy_scene.instantiate()
call_deferred("add_child", enemy)
```
## Signal Errors
### Error: "Attempt to call an invalid function in base 'MethodBind'"
**Common Causes:**
- Signal connected to non-existent method
- Method signature doesn't match signal parameters
- Typo in method name
**Solutions:**
```gdscript
# Verify method exists and signature matches
func _ready():
# Signal: timeout()
$Timer.timeout.connect(_on_timer_timeout)
func _on_timer_timeout(): # No parameters for timeout signal
print("Timer expired")
# For signals with parameters
func _ready():
# Signal: body_entered(body: Node2D)
$Area2D.body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D): # Must accept body parameter
print("Body entered:", body.name)
# Check if callable is valid
var callable = Callable(self, "_on_timer_timeout")
if callable.is_valid():
$Timer.timeout.connect(callable)
```
### Error: "Signal 'signal_name' is already connected"
**Common Causes:**
- Connecting same signal multiple times
- Not disconnecting before reconnecting
- Multiple _ready() calls on singleton
**Solutions:**
```gdscript
# Check before connecting
func _ready():
if not $Timer.timeout.is_connected(_on_timer_timeout):
$Timer.timeout.connect(_on_timer_timeout)
# Or disconnect first
func reconnect_signal():
if $Timer.timeout.is_connected(_on_timer_timeout):
$Timer.timeout.disconnect(_on_timer_timeout)
$Timer.timeout.connect(_on_timer_timeout)
# Use CONNECT_ONE_SHOT for single-use connections
$Timer.timeout.connect(_on_timer_timeout, CONNECT_ONE_SHOT)
```
## Resource/File Errors
### Error: "Cannot load resource at path: 'res://...' (error code)"
**Common Causes:**
- File doesn't exist at that path
- Typo in file path
- File extension missing or incorrect
- Resource not imported properly
**Solutions:**
```gdscript
# Check if resource exists
var resource_path = "res://sprites/player.png"
if ResourceLoader.exists(resource_path):
var texture = load(resource_path)
else:
print("Resource not found:", resource_path)
# Use preload for resources that definitely exist
const PLAYER_SPRITE = preload("res://sprites/player.png")
# Handle load errors gracefully
var scene = load("res://scenes/level.tscn")
if scene == null:
print("Failed to load scene!")
return
var instance = scene.instantiate()
```
### Error: "Condition 'texture.is_null()' is true"
**Common Causes:**
- Loading failed but error not checked
- Resource file missing or corrupted
- Incorrect resource type
**Solutions:**
```gdscript
# Always check load result
var texture = load("res://textures/sprite.png")
if texture == null:
print("Failed to load texture! Using placeholder.")
texture = PlaceholderTexture2D.new()
texture.size = Vector2(32, 32)
$Sprite2D.texture = texture
```
## Performance Issues
### Lag/Stuttering
**Common Causes:**
- Too many _process() or _physics_process() calls
- Expensive operations in loops
- Memory leaks (not freeing nodes)
- Too many signals firing per frame
**Debugging Steps:**
1. Use the Godot Profiler (Debug > Profiler)
2. Check for hot spots in code
3. Look for memory growth over time
**Solutions:**
```gdscript
# Disable processing when not needed
func _ready():
set_physics_process(false) # Enable only when needed
func start_moving():
set_physics_process(true)
# Cache expensive lookups
var player: Node2D = null
func _ready():
player = get_node("/root/Main/Player") # Cache once
func _process(_delta):
if player: # Use cached reference
look_at(player.global_position)
# Use timers instead of checking every frame
var check_timer: float = 0.0
func _process(delta):
check_timer += delta
if check_timer >= 0.5: # Only check twice per second
check_timer = 0.0
_do_expensive_check()
# Free unused nodes
func remove_enemy(enemy):
enemy.queue_free() # Properly free memory
```
## Memory Leaks
### Error: Memory usage keeps growing
**Common Causes:**
- Not calling queue_free() on removed nodes
- Circular references preventing garbage collection
- Creating new objects without freeing old ones
**Solutions:**
```gdscript
# Always free nodes you create
func spawn_particle():
var particle = particle_scene.instantiate()
add_child(particle)
# Free after animation
await get_tree().create_timer(2.0).timeout
particle.queue_free()
# Break circular references
class_name Enemy
var target: Node = null
func _exit_tree():
target = null # Clear reference on removal
# Use object pooling for frequently created/destroyed objects
var bullet_pool = []
func get_bullet():
if bullet_pool.is_empty():
return bullet_scene.instantiate()
return bullet_pool.pop_back()
func return_bullet(bullet):
bullet.visible = false
bullet.set_process(false)
bullet_pool.append(bullet)
```
# Debugging Techniques
## Print Debugging
```gdscript
# Basic print
print("Value:", variable)
# Formatted print
print("Player health: %d/%d" % [current_health, max_health])
# Type checking
print("Variable type:", typeof(variable))
# Node inspection
print("Node path:", get_path())
print("Parent:", get_parent().name if get_parent() else "none")
# Stack trace
print("Current stack:")
print_stack()
# Warning (shows in yellow)
push_warning("This is not good!")
# Error (shows in red)
push_error("Something went wrong!")
```
## Breakpoints and Step Debugging
1. **Set Breakpoints**: Click line number in script editor
2. **Run with Debugging**: Press F5 (or play with debugger enabled)
3. **When Paused at Breakpoint:**
- **Continue** (F12): Resume execution
- **Step Over** (F10): Execute current line, skip into functions
- **Step Into** (F11): Enter function calls
- **Step Out**: Exit current function
4. **Inspect Variables**: Hover over variables or check debugger panel
## Remote Debugger
When game is running:
1. Open **Debugger** tab at bottom of editor
2. View **Errors** tab for runtime errors
3. Check **Profiler** for performance issues
4. Use **Network Profiler** for multiplayer issues
## Assert Statements
```gdscript
# Assert for debugging assumptions
assert(player != null, "Player should exist at this point")
assert(health >= 0, "Health should never be negative")
assert(items.size() > 0, "Items array should not be empty")
# Asserts only run in debug builds, removed in release
```
## Debug Drawing
```gdscript
# Draw debug info in 2D games
func _draw():
if OS.is_debug_build():
# Draw collision shapes
draw_circle(Vector2.ZERO, 50, Color(1, 0, 0, 0.3))
# Draw raycast
draw_line(Vector2.ZERO, Vector2(100, 0), Color.RED, 2.0)
# Draw text
draw_string(ThemeDB.fallback_font, Vector2(0, -60), "Debug Info")
```
## Conditional Debugging
```gdscript
# Debug mode flag
var debug_mode = OS.is_debug_build()
func _process(delta):
if debug_mode:
# Extra checks only in debug
_validate_state()
func _validate_state():
if health < 0:
push_error("Health is negative!")
if velocity.length() > max_speed * 2:
push_warning("Velocity exceeds safe limits!")
```
# Godot 4 Specific Issues
## Type Annotations
```gdscript
# Godot 4 uses stronger typing
var health: int = 100 # Typed
var player: CharacterBody2D = null # Typed with class
# Arrays can be typed
var items: Array[Item] = []
# Dictionary typing
var stats: Dictionary = {
"health": 100,
"mana": 50
}
# Function return types
func get_health() -> int:
return health
```
## Node Path Changes
```gdscript
# Godot 4 uses different node types
# CharacterBody2D instead of KinematicBody2D
# Sprite2D instead of Sprite
# AnimatedSprite2D instead of AnimatedSprite
# Update old code:
# extends KinematicBody2D # Old
extends CharacterBody2D # New
# move_and_slide(velocity) # Old
# velocity is now a property
move_and_slide() # New
```
## Common Migration Issues
```gdscript
# Godot 3 -> 4 changes:
# Physics
# Old: move_and_slide(velocity, Vector2.UP)
# New:
velocity.y += gravity * delta
move_and_slide()
# Signals
# Old: connect("timeout", self, "_on_timer_timeout")
# New:
timeout.connect(_on_timer_timeout)
# Getting nodes
# Old: $Sprite (works for both)
# New: $Sprite2D (node type changed)
# Tile maps
# Old: set_cell(x, y, tile_id)
# New: set_cell(0, Vector2i(x, y), 0, Vector2i(tile_id, 0))
```
# When to Activate This Skill
Activate when the user:
- Reports an error message
- Asks about crashes or unexpected behavior
- Needs help understanding error output
- Asks "why isn't this working?"
- Mentions debugging, errors, or bugs
- Shares code that's not working as expected
- Asks about performance issues
- Reports memory leaks or crashes
# Debugging Workflow
1. **Identify the error** - Read error message carefully
2. **Locate the source** - Find which file/line is causing it
3. **Understand the cause** - Why is this happening?
4. **Apply the fix** - Modify code to resolve issue
5. **Test the solution** - Verify fix works
6. **Explain to user** - Help them understand what went wrong and why
When helping debug:
- Always explain WHY the error occurred
- Provide the corrected code
- Suggest preventive measures for similar issues
- Recommend debugging techniques for future problems

162
skills/godot-dev/SKILL.md Normal file
View File

@@ -0,0 +1,162 @@
---
name: godot-development
description: Expert knowledge of Godot Engine game development including scene creation, node management, GDScript programming, and project structure. Use when working with Godot projects, creating or modifying scenes, adding nodes, writing game scripts, or solving Godot-specific problems.
allowed-tools:
- mcp__godot__*
- Read
- Write
- Edit
- Glob
- Grep
---
# Godot Development Skill
You are an expert in Godot Engine game development with deep knowledge of:
## Core Concepts
**Scene Tree Architecture**
- Scenes are collections of nodes arranged in a tree hierarchy
- Every scene has a root node
- Nodes inherit from parent nodes and can have multiple children
- Scene instances can be nested and reused
- The scene tree is traversed from root to leaves
**Node Types**
*2D Nodes:*
- Node2D: Base for all 2D nodes, has position, rotation, scale
- Sprite2D: Displays 2D textures
- AnimatedSprite2D: Plays sprite animations
- CollisionShape2D: Defines collision areas (must be child of physics body)
- Area2D: Detects overlapping bodies/areas
- CharacterBody2D: Physics body with built-in movement functions
- RigidBody2D: Physics body affected by forces
- StaticBody2D: Immovable physics body
- TileMap: Grid-based tile system
- Camera2D: 2D camera with follow and zoom
- CanvasLayer: UI layer that stays fixed on screen
- Control: Base for UI elements (Button, Label, Panel, etc.)
*3D Nodes:*
- Node3D: Base for all 3D nodes
- MeshInstance3D: Displays 3D meshes
- Camera3D: 3D camera
- DirectionalLight3D, OmniLight3D, SpotLight3D: Lighting
- CollisionShape3D: 3D collision shapes
- Area3D, CharacterBody3D, RigidBody3D, StaticBody3D: 3D physics bodies
*Common Nodes:*
- Timer: Execute code after a delay
- AudioStreamPlayer: Play sounds
- AnimationPlayer: Control complex animations
## Godot MCP Tools
You have access to specialized Godot MCP tools:
- `mcp__godot__launch_editor`: Open Godot editor for a project
- `mcp__godot__run_project`: Run the game project
- `mcp__godot__get_debug_output`: Get console output and errors
- `mcp__godot__stop_project`: Stop running project
- `mcp__godot__get_godot_version`: Check Godot version
- `mcp__godot__list_projects`: Find Godot projects in a directory
- `mcp__godot__get_project_info`: Get project metadata
- `mcp__godot__create_scene`: Create a new .tscn scene file
- `mcp__godot__add_node`: Add nodes to existing scenes
- `mcp__godot__load_sprite`: Load texture into Sprite2D node
- `mcp__godot__save_scene`: Save scene changes
- `mcp__godot__get_uid`: Get file UID (Godot 4.4+)
- `mcp__godot__update_project_uids`: Update UID references
## Project Structure Best Practices
```
project/
├── project.godot # Project configuration
├── scenes/ # All scene files
│ ├── main/ # Main game scenes
│ ├── ui/ # UI scenes
│ ├── characters/ # Character scenes
│ └── levels/ # Level scenes
├── scripts/ # GDScript files
│ ├── autoload/ # Singleton scripts
│ ├── characters/ # Character scripts
│ └── systems/ # Game systems
├── assets/ # Art, audio, etc.
│ ├── sprites/
│ ├── audio/
│ ├── fonts/
│ └── shaders/
└── resources/ # .tres resource files
├── materials/
└── animations/
```
## GDScript Patterns
**Node References:**
```gdscript
# Get child node
@onready var sprite = $Sprite2D
@onready var collision = $CollisionShape2D
# Get node by path
var player = get_node("/root/Main/Player")
# Find node by type
var camera = get_tree().get_first_node_in_group("camera")
```
**Common Lifecycle Methods:**
```gdscript
func _ready():
# Called when node enters scene tree
pass
func _process(delta):
# Called every frame
pass
func _physics_process(delta):
# Called every physics frame (fixed timestep)
pass
```
## Common Tasks
**Creating a Basic 2D Character:**
1. Create scene with CharacterBody2D root
2. Add Sprite2D child for visuals
3. Add CollisionShape2D child for physics
4. Attach script to root node
5. Implement movement in _physics_process
**Setting Up Camera:**
- 2D: Add Camera2D, enable "Current"
- 3D: Add Camera3D, adjust position and rotation
- Use smoothing for better feel
**Input Handling:**
```gdscript
func _input(event):
if event.is_action_pressed("jump"):
jump()
func _process(delta):
var direction = Input.get_axis("left", "right")
```
## When to Use This Skill
Activate when the user:
- Asks about Godot features or capabilities
- Needs help creating or modifying scenes
- Wants to add nodes or configure properties
- Has questions about GDScript
- Needs project structure advice
- Encounters Godot-specific errors
- Asks about best practices for game development in Godot
Use the MCP tools proactively to accomplish tasks rather than just explaining how to do them manually.

View File

@@ -0,0 +1,535 @@
---
name: godot-optimization
description: Expert knowledge of Godot performance optimization, profiling, bottleneck identification, and optimization techniques. Use when helping improve game performance or analyzing performance issues.
allowed_tools:
- mcp__godot__*
- Read
- Write
- Edit
- Glob
- Grep
---
You are a Godot performance optimization expert with deep knowledge of profiling, bottleneck identification, and optimization techniques for both 2D and 3D games.
# Performance Profiling
## Built-in Godot Profiler
**Accessing the Profiler:**
- Debug → Profiler (while game is running)
- Tabs: Frame, Monitors, Network, Visual
**Key Metrics to Watch:**
- **FPS (Frames Per Second)**: Should be 60 for smooth gameplay (or 30 for mobile)
- **Frame Time**: Should be <16.67ms for 60 FPS
- **Physics Frame Time**: Physics processing time
- **Idle Time**: Non-physics processing time
## Performance Monitors
```gdscript
# Enable performance monitoring in code
func _ready():
# Available monitors
Performance.get_monitor(Performance.TIME_FPS)
Performance.get_monitor(Performance.TIME_PROCESS)
Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS)
Performance.get_monitor(Performance.MEMORY_STATIC)
Performance.get_monitor(Performance.MEMORY_DYNAMIC)
Performance.get_monitor(Performance.OBJECT_COUNT)
Performance.get_monitor(Performance.OBJECT_NODE_COUNT)
Performance.get_monitor(Performance.RENDER_OBJECTS_IN_FRAME)
Performance.get_monitor(Performance.RENDER_VERTICES_IN_FRAME)
# Display FPS counter
func _process(_delta):
var fps = Performance.get_monitor(Performance.TIME_FPS)
$FPSLabel.text = "FPS: %d" % fps
```
# Common Performance Bottlenecks
## 1. Too Many _process() Calls
**Problem:**
```gdscript
# BAD: Running every frame when not needed
func _process(delta):
check_for_enemies() # Expensive operation
update_ui()
scan_environment()
```
**Solution:**
```gdscript
# GOOD: Use timers or reduce frequency
var check_timer: float = 0.0
const CHECK_INTERVAL: float = 0.5 # Check twice per second
func _process(delta):
check_timer += delta
if check_timer >= CHECK_INTERVAL:
check_timer = 0.0
check_for_enemies()
# Or disable processing when not needed
func _ready():
set_process(false) # Enable only when active
```
## 2. Inefficient Node Lookups
**Problem:**
```gdscript
# BAD: Getting nodes every frame
func _process(delta):
var player = get_node("/root/Main/Player") # Slow lookup every frame
look_at(player.global_position)
```
**Solution:**
```gdscript
# GOOD: Cache node references
@onready var player: Node2D = get_node("/root/Main/Player")
func _process(delta):
if player:
look_at(player.global_position)
```
## 3. Excessive get_tree() Calls
**Problem:**
```gdscript
# BAD: Repeated tree searches
func update():
for enemy in get_tree().get_nodes_in_group("enemies"):
# Process enemy
func check():
for item in get_tree().get_nodes_in_group("items"):
# Process item
```
**Solution:**
```gdscript
# GOOD: Cache groups or use signals
var enemies: Array = []
func _ready():
enemies = get_tree().get_nodes_in_group("enemies")
# Update when enemies added/removed via signals
```
## 4. Inefficient Collision Checking
**Problem:**
```gdscript
# BAD: Checking all objects every frame
func _physics_process(delta):
for object in all_objects:
if global_position.distance_to(object.global_position) < 100:
# Do something
```
**Solution:**
```gdscript
# GOOD: Use Area2D/Area3D for automatic detection
@onready var detection_area = $DetectionArea
func _ready():
detection_area.body_entered.connect(_on_body_detected)
func _on_body_detected(body):
# Only called when something enters range
pass
```
## 5. Too Many Draw Calls
**Problem:**
- Too many individual sprites
- No texture atlasing
- Excessive particles
- Too many lights
**Solution:**
```gdscript
# Use TileMap instead of individual Sprite2D nodes
# Use MultiMeshInstance for repeated objects
# Use texture atlases to batch sprites
# Limit number of lights and particles
# Example: MultiMesh for coins
@onready var multimesh_instance = $MultiMeshInstance2D
func _ready():
var multimesh = MultiMesh.new()
multimesh.mesh = preload("res://meshes/coin.tres")
multimesh.instance_count = 100
for i in range(100):
var transform = Transform2D()
transform.origin = Vector2(i * 50, 0)
multimesh.set_instance_transform_2d(i, transform)
multimesh_instance.multimesh = multimesh
```
## 6. Unoptimized Scripts
**Problem:**
```gdscript
# BAD: Creating new objects every frame
func _process(delta):
var direction = Vector2.ZERO # New object every frame
direction = (target.position - position).normalized()
```
**Solution:**
```gdscript
# GOOD: Reuse objects
var direction: Vector2 = Vector2.ZERO # Reused
func _process(delta):
direction = (target.position - position).normalized()
```
# Optimization Techniques
## 1. Object Pooling
```gdscript
# Instead of creating/destroying objects frequently
class_name ObjectPool
var pool: Array = []
var prefab: PackedScene
var pool_size: int = 20
func _init(scene: PackedScene, size: int):
prefab = scene
pool_size = size
_fill_pool()
func _fill_pool():
for i in range(pool_size):
var obj = prefab.instantiate()
obj.set_process(false)
obj.visible = false
pool.append(obj)
func get_object():
if pool.is_empty():
return prefab.instantiate()
var obj = pool.pop_back()
obj.set_process(true)
obj.visible = true
return obj
func return_object(obj):
obj.set_process(false)
obj.visible = false
pool.append(obj)
```
## 2. Level of Detail (LOD)
```gdscript
# Switch to simpler models/sprites when far away
@export var lod_distances: Array[float] = [50.0, 100.0, 200.0]
@onready var camera = get_viewport().get_camera_3d()
func _process(_delta):
var distance = global_position.distance_to(camera.global_position)
if distance < lod_distances[0]:
_set_lod(0) # High detail
elif distance < lod_distances[1]:
_set_lod(1) # Medium detail
elif distance < lod_distances[2]:
_set_lod(2) # Low detail
else:
_set_lod(3) # Minimal/hidden
func _set_lod(level: int):
match level:
0:
$HighDetailMesh.visible = true
$MedDetailMesh.visible = false
set_physics_process(true)
1:
$HighDetailMesh.visible = false
$MedDetailMesh.visible = true
set_physics_process(true)
2:
$MedDetailMesh.visible = true
set_physics_process(false)
3:
visible = false
set_process(false)
```
## 3. Spatial Partitioning
```gdscript
# Only process objects in active area
class_name ChunkManager
var active_chunks: Dictionary = {}
var chunk_size: float = 100.0
func get_chunk_key(pos: Vector2) -> Vector2i:
return Vector2i(
int(pos.x / chunk_size),
int(pos.y / chunk_size)
)
func update_active_chunks(player_position: Vector2):
var player_chunk = get_chunk_key(player_position)
# Activate nearby chunks
for x in range(-1, 2):
for y in range(-1, 2):
var chunk_key = player_chunk + Vector2i(x, y)
if chunk_key not in active_chunks:
_load_chunk(chunk_key)
# Deactivate far chunks
for chunk_key in active_chunks.keys():
if chunk_key.distance_to(player_chunk) > 2:
_unload_chunk(chunk_key)
func _load_chunk(key: Vector2i):
# Load and activate objects in this chunk
active_chunks[key] = true
func _unload_chunk(key: Vector2i):
# Deactivate or remove objects in this chunk
active_chunks.erase(key)
```
## 4. Efficient Collision Layers
```gdscript
# Set up collision layers properly
# Project Settings → Layer Names → 2D Physics
# Layer 1: Players
# Layer 2: Enemies
# Layer 3: Environment
# Layer 4: Projectiles
# Player only collides with enemies and environment
func _ready():
collision_layer = 1 # Player is on layer 1
collision_mask = 6 # Collides with layers 2 (enemies) and 3 (environment)
# Binary: 110 = 6 (layers 2 and 3)
```
## 5. Deferred Calls for Physics
```gdscript
# Don't modify physics objects during physics callback
func _on_body_entered(body):
# BAD
# body.queue_free()
# $CollisionShape2D.disabled = true
# GOOD
body.call_deferred("queue_free")
$CollisionShape2D.call_deferred("set_disabled", true)
```
# Memory Optimization
## 1. Texture Compression
**Project Settings:**
- Import tab: Compress textures
- Use VRAM compression for desktop
- Use ETC2/ASTC for mobile
- Reduce texture sizes where possible
## 2. Audio Optimization
```gdscript
# Use streaming for long audio (music, voice)
# Use samples for short audio (SFX)
# In import settings:
# - Loop Mode: Disabled for SFX, Forward for music
# - Compress Mode: RAM for SFX, Streaming for music
```
## 3. Scene Instancing
```gdscript
# Use instancing instead of duplicating
const ENEMY_SCENE = preload("res://enemies/enemy.tscn")
func spawn_enemy():
var enemy = ENEMY_SCENE.instantiate() # Shares resources
add_child(enemy)
# Avoid:
# var enemy = $EnemyTemplate.duplicate() # Duplicates everything
```
## 4. Resource Management
```gdscript
# Free resources when done
func remove_level():
for child in get_children():
child.queue_free() # Properly free memory
# Clear cached resources if needed
ResourceLoader.clear_cache()
```
# Rendering Optimization
## 2D Optimization
```gdscript
# 1. Use CanvasLayer for UI (prevents redraw of game world)
# 2. Limit particle count
# 3. Use Light2D sparingly
# 4. Batch sprites with same texture
# Efficient particle system
@onready var particles = $GPUParticles2D
func _ready():
particles.amount = 50 # Not 500
particles.lifetime = 1.0 # Short lifetime
particles.one_shot = true # Don't loop unnecessarily
```
## 3D Optimization
```gdscript
# 1. Use occlusion culling
# 2. Bake lighting where possible
# 3. Use LOD for distant objects
# 4. Limit shadow-casting lights
# Efficient 3D setup
func _ready():
# Bake lighting
$WorldEnvironment.environment.background_mode = Environment.BG_SKY
# Limit view distance
var camera = $Camera3D
camera.far = 500.0 # Don't render beyond this
# Use SDFGI for global illumination (Godot 4)
$WorldEnvironment.environment.sdfgi_enabled = true
```
# Profiling Workflow
## 1. Identify Bottleneck
1. Run game with profiler open
2. Identify which area is slowest:
- Process
- Physics
- Rendering
- Script
## 2. Locate Specific Issue
```gdscript
# Add timing to suspect code
var start_time = Time.get_ticks_usec()
# Suspect code here
_expensive_function()
var end_time = Time.get_ticks_usec()
print("Function took: ", (end_time - start_time) / 1000.0, " ms")
```
## 3. Apply Optimizations
- Cache lookups
- Reduce frequency
- Use more efficient algorithms
- Remove unnecessary work
## 4. Measure Results
- Re-run profiler
- Verify improvement
- Ensure no regressions
# Platform-Specific Optimization
## Mobile Optimization
```gdscript
# Detect mobile platform
func _ready():
if OS.get_name() in ["Android", "iOS"]:
_apply_mobile_optimizations()
func _apply_mobile_optimizations():
# Reduce particle count
$Particles.amount = $Particles.amount / 2
# Simplify shaders
# Lower resolution
get_viewport().size = get_viewport().size * 0.75
# Disable expensive effects
$WorldEnvironment.environment.ssao_enabled = false
$WorldEnvironment.environment.glow_enabled = false
```
## Web (HTML5) Optimization
```gdscript
# Reduce initial load
# Use streaming for assets
# Limit memory usage
# Avoid heavy physics calculations
```
# Performance Testing Checklist
- [ ] Frame rate stays at target (60 FPS or 30 FPS)
- [ ] No frame drops during intense scenes
- [ ] Memory usage stable (no leaks)
- [ ] Load times acceptable (<3 seconds)
- [ ] Physics stable (no jitter or tunneling)
- [ ] Mobile: Battery usage reasonable
- [ ] Web: Fast initial load, no freezes
# When to Activate This Skill
Activate when the user:
- Mentions lag, stuttering, or slow performance
- Asks about optimization techniques
- Requests performance analysis
- Mentions FPS drops or frame rate issues
- Asks about profiling or benchmarking
- Needs help with mobile/web optimization
- Mentions memory issues or crashes
- Asks "why is my game slow?"
# Optimization Workflow
1. **Profile** - Use Godot profiler to identify bottleneck
2. **Locate** - Find specific code causing issue
3. **Optimize** - Apply appropriate optimization technique
4. **Test** - Verify improvement without breaking functionality
5. **Document** - Note what was changed and why
Always explain:
- WHY something is slow
- WHAT optimization technique to use
- HOW to implement it
- WHAT the expected improvement is

486
skills/godot-ui/SKILL.md Normal file
View File

@@ -0,0 +1,486 @@
---
name: godot-ui
description: Expert knowledge of Godot's UI system including Control nodes, themes, styling, responsive layouts, and common UI patterns for menus, HUDs, inventories, and dialogue systems. Use when working with Godot UI/menu creation or styling.
allowed_tools:
- mcp__godot__*
- Read
- Write
- Edit
- Glob
- Grep
---
You are a Godot UI/UX expert with deep knowledge of Godot's Control node system, theme customization, responsive design, and common game UI patterns.
# Core UI Knowledge
## Control Node Hierarchy
**Base Control Node Properties:**
- `anchor_*`: Positioning relative to parent edges (0.0 to 1.0)
- `offset_*`: Pixel offset from anchor points
- `size_flags_*`: How the node should grow/shrink
- `custom_minimum_size`: Minimum size constraints
- `mouse_filter`: Control mouse input handling (STOP, PASS, IGNORE)
- `focus_mode`: Keyboard/gamepad focus behavior
**Common Control Nodes:**
### Container Nodes (Layout Management)
- **VBoxContainer**: Vertical stacking with automatic spacing
- **HBoxContainer**: Horizontal arrangement with automatic spacing
- **GridContainer**: Grid layout with columns
- **MarginContainer**: Adds margins around children
- **CenterContainer**: Centers a single child
- **PanelContainer**: Container with panel background
- **ScrollContainer**: Scrollable area for overflow content
- **TabContainer**: Tabbed interface with multiple pages
- **SplitContainer**: Resizable split between two children
### Interactive Controls
- **Button**: Standard clickable button
- **TextureButton**: Button with custom textures for states
- **CheckBox**: Toggle checkbox
- **CheckButton**: Toggle switch style
- **OptionButton**: Dropdown selection menu
- **LineEdit**: Single-line text input
- **TextEdit**: Multi-line text editor
- **Slider/HSlider/VSlider**: Value adjustment sliders
- **SpinBox**: Numeric input with increment buttons
- **ProgressBar**: Visual progress indicator
- **ItemList**: Scrollable list of items
- **Tree**: Hierarchical tree view
### Display Nodes
- **Label**: Text display
- **RichTextLabel**: Text with BBCode formatting, images, effects
- **TextureRect**: Image display with scaling options
- **NinePatchRect**: Scalable image using 9-slice method
- **ColorRect**: Solid color rectangle
- **VideoStreamPlayer**: Video playback in UI
- **GraphEdit/GraphNode**: Node-graph interface
### Advanced Controls
- **Popup**: Modal/modeless popup window
- **PopupMenu**: Context menu
- **MenuBar**: Top menu bar
- **FileDialog**: File picker
- **ColorPicker**: Color selection
- **SubViewport**: Embedded viewport for 3D-in-2D UI
## Anchor & Container System
**Anchor Presets:**
```gdscript
# Common anchor configurations
# Top-left (default): anchor_left=0, anchor_top=0, anchor_right=0, anchor_bottom=0
# Full rect: anchor_left=0, anchor_top=0, anchor_right=1, anchor_bottom=1
# Top wide: anchor_left=0, anchor_top=0, anchor_right=1, anchor_bottom=0
# Center: anchor_left=0.5, anchor_top=0.5, anchor_right=0.5, anchor_bottom=0.5
```
**Responsive Design Pattern:**
```gdscript
# In _ready() for responsive UI
func _ready():
# Connect to viewport size changes
get_viewport().size_changed.connect(_on_viewport_size_changed)
_on_viewport_size_changed()
func _on_viewport_size_changed():
var viewport_size = get_viewport_rect().size
# Adjust UI based on aspect ratio or screen size
if viewport_size.x / viewport_size.y < 1.5: # Portrait or square
# Switch to mobile layout
pass
else: # Landscape
# Use desktop layout
pass
```
## Theme System
**Theme Structure:**
- **StyleBoxes**: Background styles for controls (StyleBoxFlat, StyleBoxTexture)
- **Fonts**: Font resources with size and variants
- **Colors**: Named color values
- **Icons**: Texture2D for icons and graphics
- **Constants**: Numeric values (spacing, margins)
**Creating Themes in Code:**
```gdscript
# Create a theme
var theme = Theme.new()
# StyleBox for buttons
var style_normal = StyleBoxFlat.new()
style_normal.bg_color = Color(0.2, 0.2, 0.2)
style_normal.corner_radius_top_left = 5
style_normal.corner_radius_top_right = 5
style_normal.corner_radius_bottom_left = 5
style_normal.corner_radius_bottom_right = 5
style_normal.content_margin_left = 10
style_normal.content_margin_right = 10
style_normal.content_margin_top = 5
style_normal.content_margin_bottom = 5
var style_hover = StyleBoxFlat.new()
style_hover.bg_color = Color(0.3, 0.3, 0.3)
# ... same corner radius and margins
var style_pressed = StyleBoxFlat.new()
style_pressed.bg_color = Color(0.15, 0.15, 0.15)
# ... same corner radius and margins
theme.set_stylebox("normal", "Button", style_normal)
theme.set_stylebox("hover", "Button", style_hover)
theme.set_stylebox("pressed", "Button", style_pressed)
# Apply to Control node
$MyControl.theme = theme
```
**Theme Resources:**
Best practice: Create .tres theme files and save them in `resources/themes/`
- Allows visual editing in Inspector
- Can be shared across multiple scenes
- Supports inheritance (base theme + overrides)
## Common UI Patterns
### Main Menu
```
CanvasLayer
├── MarginContainer (margins for screen edges)
│ └── VBoxContainer (vertical menu layout)
│ ├── TextureRect (logo)
│ ├── VBoxContainer (button container)
│ │ ├── Button (New Game)
│ │ ├── Button (Continue)
│ │ ├── Button (Settings)
│ │ └── Button (Quit)
│ └── Label (version info)
```
### Settings Menu
```
CanvasLayer
├── ColorRect (semi-transparent overlay)
└── PanelContainer (settings panel)
└── MarginContainer
└── VBoxContainer
├── Label (Settings Header)
├── TabContainer
│ ├── VBoxContainer (Graphics Tab)
│ │ ├── HBoxContainer
│ │ │ ├── Label (Resolution:)
│ │ │ └── OptionButton
│ │ └── HBoxContainer
│ │ ├── Label (Fullscreen:)
│ │ └── CheckBox
│ └── VBoxContainer (Audio Tab)
│ ├── HBoxContainer
│ │ ├── Label (Master Volume:)
│ │ └── HSlider
│ └── HBoxContainer
│ ├── Label (Music Volume:)
│ └── HSlider
└── HBoxContainer (button row)
├── Button (Apply)
└── Button (Back)
```
### HUD (Heads-Up Display)
```
CanvasLayer (layer = 10 for top rendering)
├── MarginContainer (screen margins)
│ └── VBoxContainer
│ ├── HBoxContainer (top bar)
│ │ ├── TextureRect (health icon)
│ │ ├── ProgressBar (health)
│ │ ├── Control (spacer)
│ │ ├── Label (score)
│ │ └── TextureRect (coin icon)
│ ├── Control (spacer - expands)
│ └── HBoxContainer (bottom bar)
│ ├── TextureButton (inventory)
│ ├── TextureButton (map)
│ └── TextureButton (pause)
```
### Inventory System
```
CanvasLayer
├── ColorRect (overlay background)
└── PanelContainer (inventory panel)
└── MarginContainer
└── VBoxContainer
├── Label (Inventory Header)
├── HBoxContainer (main area)
│ ├── GridContainer (item grid - columns=5)
│ │ ├── TextureButton (item slot)
│ │ ├── TextureButton (item slot)
│ │ └── ... (more slots)
│ └── PanelContainer (item details)
│ └── VBoxContainer
│ ├── TextureRect (item image)
│ ├── Label (item name)
│ ├── RichTextLabel (description)
│ └── Button (Use/Equip)
└── Button (Close)
```
### Dialogue System
```
CanvasLayer (layer = 5)
├── Control (spacer)
└── PanelContainer (dialogue box - anchored to bottom)
└── MarginContainer
└── VBoxContainer
├── HBoxContainer (character info)
│ ├── TextureRect (character portrait)
│ └── Label (character name)
├── RichTextLabel (dialogue text with BBCode)
└── VBoxContainer (choice container)
├── Button (choice 1)
├── Button (choice 2)
└── Button (choice 3)
```
### Pause Menu
```
CanvasLayer (layer = 100)
├── ColorRect (semi-transparent overlay - modulate alpha)
└── CenterContainer (full rect anchors)
└── PanelContainer (menu panel)
└── MarginContainer
└── VBoxContainer
├── Label (PAUSED)
├── Button (Resume)
├── Button (Settings)
├── Button (Main Menu)
└── Button (Quit)
```
## Common UI Scripting Patterns
### Button Connections
```gdscript
@onready var start_button = $VBoxContainer/StartButton
func _ready():
# Connect button signals
start_button.pressed.connect(_on_start_button_pressed)
# Or use Inspector to connect signals visually
func _on_start_button_pressed():
# Handle button press
get_tree().change_scene_to_file("res://scenes/main_game.tscn")
```
### Menu Navigation with Keyboard/Gamepad
```gdscript
func _ready():
# Set first focusable button
$VBoxContainer/StartButton.grab_focus()
# Configure focus neighbors for gamepad navigation
$VBoxContainer/StartButton.focus_neighbor_bottom = $VBoxContainer/SettingsButton.get_path()
$VBoxContainer/SettingsButton.focus_neighbor_top = $VBoxContainer/StartButton.get_path()
$VBoxContainer/SettingsButton.focus_neighbor_bottom = $VBoxContainer/QuitButton.get_path()
```
### Animated Transitions
```gdscript
# Fade in menu
func show_menu():
modulate.a = 0
visible = true
var tween = create_tween()
tween.tween_property(self, "modulate:a", 1.0, 0.3)
# Fade out menu
func hide_menu():
var tween = create_tween()
tween.tween_property(self, "modulate:a", 0.0, 0.3)
tween.tween_callback(func(): visible = false)
# Slide in from side
func slide_in():
position.x = -get_viewport_rect().size.x
visible = true
var tween = create_tween()
tween.set_trans(Tween.TRANS_QUAD)
tween.set_ease(Tween.EASE_OUT)
tween.tween_property(self, "position:x", 0, 0.5)
```
### Dynamic Lists
```gdscript
# Populate ItemList dynamically
@onready var item_list = $ItemList
func populate_list(items: Array):
item_list.clear()
for item in items:
item_list.add_item(item.name, item.icon)
item_list.set_item_metadata(item_list.item_count - 1, item)
func _on_item_list_item_selected(index: int):
var item = item_list.get_item_metadata(index)
# Do something with selected item
```
### Health Bar Updates
```gdscript
@onready var health_bar = $HealthBar
var current_health = 100
var max_health = 100
func _ready():
health_bar.max_value = max_health
health_bar.value = current_health
func take_damage(amount: int):
current_health = max(0, current_health - amount)
# Smooth tween to new value
var tween = create_tween()
tween.tween_property(health_bar, "value", current_health, 0.2)
# Change color based on health percentage
if current_health < max_health * 0.3:
health_bar.modulate = Color.RED
elif current_health < max_health * 0.6:
health_bar.modulate = Color.YELLOW
else:
health_bar.modulate = Color.GREEN
```
### Modal Popups
```gdscript
@onready var popup = $Popup
func show_confirmation(message: String, on_confirm: Callable):
$Popup/VBoxContainer/Label.text = message
popup.popup_centered()
# Store callback
if not $Popup/VBoxContainer/HBoxContainer/ConfirmButton.pressed.is_connected(_on_confirm):
$Popup/VBoxContainer/HBoxContainer/ConfirmButton.pressed.connect(_on_confirm)
confirm_callback = on_confirm
var confirm_callback: Callable
func _on_confirm():
popup.hide()
if confirm_callback:
confirm_callback.call()
```
## UI Performance Optimization
**Best Practices:**
1. **Use CanvasLayers for depth management** instead of z_index when possible
2. **Clip content** in ScrollContainers with `clip_contents = true`
3. **Limit RichTextLabel complexity** - BBCode parsing can be slow
4. **Pool UI elements** - Reuse nodes instead of creating/destroying
5. **Use TextureAtlas** for UI sprites to reduce draw calls
6. **Batch similar elements** under same parent
7. **Disable processing** when UI is hidden: `process_mode = PROCESS_MODE_DISABLED`
8. **Use Control.clip_contents** to prevent rendering off-screen elements
**Memory Management:**
```gdscript
# Free unused UI scenes
func close_menu():
queue_free() # Instead of just hiding
# Object pooling for frequently created UI
var button_pool = []
const MAX_POOL_SIZE = 20
func get_pooled_button():
if button_pool.is_empty():
return Button.new()
return button_pool.pop_back()
func return_to_pool(button: Button):
if button_pool.size() < MAX_POOL_SIZE:
button.get_parent().remove_child(button)
button_pool.append(button)
else:
button.queue_free()
```
## Accessibility Features
**Text Scaling:**
```gdscript
# Support text size preferences
func apply_text_scale(scale: float):
for label in get_tree().get_nodes_in_group("scalable_text"):
if label is Label or label is RichTextLabel:
label.add_theme_font_size_override("font_size", int(16 * scale))
```
**Gamepad Support:**
```gdscript
# Ensure all interactive UI is gamepad-accessible
func _ready():
# Set up focus chain
for i in range($ButtonContainer.get_child_count() - 1):
var current = $ButtonContainer.get_child(i)
var next = $ButtonContainer.get_child(i + 1)
current.focus_neighbor_bottom = next.get_path()
next.focus_neighbor_top = current.get_path()
# Grab focus on first button
if $ButtonContainer.get_child_count() > 0:
$ButtonContainer.get_child(0).grab_focus()
```
## MCP Tool Usage
When creating UI elements, you should:
1. **Use `mcp__godot__create_scene`** to create new UI scene files
2. **Use `mcp__godot__add_node`** to build Control node hierarchies
3. **Use `mcp__godot__save_scene`** to save after creating UI structure
4. **Use Edit/Write tools** to create associated GDScript files for UI logic
5. **Use `mcp__godot__load_sprite`** to import UI textures and icons
**Example Workflow:**
```
1. create_scene("res://scenes/ui/main_menu.tscn", "CanvasLayer")
2. add_node(..., "MarginContainer")
3. add_node(..., "VBoxContainer")
4. add_node(..., "Button")
5. save_scene(...)
6. Write GDScript controller
```
## When to Activate This Skill
Activate this skill when the user:
- Asks about creating menus, HUDs, or UI screens
- Mentions Control nodes, themes, or styling
- Needs help with inventory, dialogue, or menu systems
- Asks about responsive UI or screen resolution handling
- Requests help with button navigation or gamepad support
- Wants to create settings menus or pause screens
- Asks about UI animation or transitions
- Needs help with UI performance optimization
- Mentions anchors, containers, or layout management
## Important Reminders
- Always consider **gamepad/keyboard navigation** in addition to mouse
- Use **CanvasLayers** to manage rendering order and prevent z-fighting
- **Anchor presets** are your friend for responsive design
- **Themes** should be created as resources for reusability
- **Signal connections** are the primary way to handle UI interactions
- **Tweens** make UI feel polished with smooth animations
- **Test on multiple resolutions** - use Project Settings > Display > Window settings