Initial commit
This commit is contained in:
600
skills/godot-debugging/SKILL.md
Normal file
600
skills/godot-debugging/SKILL.md
Normal 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
162
skills/godot-dev/SKILL.md
Normal 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.
|
||||
535
skills/godot-optimization/SKILL.md
Normal file
535
skills/godot-optimization/SKILL.md
Normal 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
486
skills/godot-ui/SKILL.md
Normal 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
|
||||
Reference in New Issue
Block a user