godot-expert
3
总安装量
2
周安装量
#56982
全站排名
安装命令
npx skills add https://github.com/nguyenthienthanh/aura-frog --skill godot-expert
Agent 安装分布
opencode
2
gemini-cli
2
claude-code
2
github-copilot
2
windsurf
2
codex
2
Skill 文档
Version: 1.6.0
Type: Skill (Auto-Invoke)
Agent: game-developer
Overview
Comprehensive Godot game development patterns for Godot 4.x. Covers project structure, scene composition, GDScript best practices, physics, input handling, UI, animation, audio, performance optimization, multi-platform export (HTML5, Android, iOS, Desktop), and testing with GDUnit.
1. Project Structure
Standard Layout
res://
âââ project.godot # Project configuration
âââ export_presets.cfg # Export templates
â
âââ scenes/ # .tscn files (organized by type)
â âââ player/
â â âââ player.tscn
â â âââ player_hud.tscn
â âââ enemies/
â â âââ enemy_base.tscn
â â âââ enemy_flying.tscn
â âââ levels/
â â âââ level_01.tscn
â â âââ level_02.tscn
â âââ ui/
â âââ main_menu.tscn
â âââ pause_menu.tscn
â âââ game_over.tscn
â
âââ scripts/ # .gd files (mirrors scenes/ structure)
â âââ player/
â â âââ player.gd
â âââ enemies/
â â âââ enemy_base.gd
â â âââ enemy_flying.gd
â âââ managers/
â â âââ game_manager.gd
â â âââ audio_manager.gd
â âââ utils/
â âââ helpers.gd
â
âââ assets/
â âââ sprites/ # 2D graphics
â â âââ characters/
â â âââ environment/
â âââ models/ # 3D models
â âââ audio/
â â âââ sfx/
â â âââ music/
â âââ fonts/
â âââ shaders/
â
âââ autoload/ # Singleton scripts
â âââ globals.gd
â âââ events.gd
â âââ save_manager.gd
â
âââ resources/ # .tres files
â âââ themes/
â âââ data/
â
âââ addons/ # Plugins
â âââ gdunit4/ # Testing framework
â
âââ test/ # GDUnit tests
âââ player/
âââ enemies/
project.godot Configuration
[application]
config/name="My Game"
config/version="1.0.0"
run/main_scene="res://scenes/ui/main_menu.tscn"
config/features=PackedStringArray("4.3", "GL Compatibility")
config/icon="res://assets/icon.svg"
[autoload]
Globals="*res://autoload/globals.gd"
Events="*res://autoload/events.gd"
SaveManager="*res://autoload/save_manager.gd"
AudioManager="*res://autoload/audio_manager.gd"
[display]
window/size/viewport_width=1920
window/size/viewport_height=1080
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
[input]
move_left={...}
move_right={...}
jump={...}
attack={...}
[rendering]
renderer/rendering_method="gl_compatibility"
textures/vram_compression/import_etc2_astc=true
Naming Conventions
naming[6]{type,pattern,example}:
Scenes,snake_case.tscn,player_controller.tscn
Scripts,snake_case.gd,player_controller.gd
Classes,PascalCase,PlayerController
Functions,snake_case,move_and_slide()
Variables,snake_case,max_health
Constants,SCREAMING_SNAKE,MAX_SPEED
2. Scenes & Nodes
Scene Composition Patterns
# Composition over inheritance
# Player scene structure:
# Player (CharacterBody2D)
# âââ CollisionShape2D
# âââ Sprite2D
# âââ AnimationPlayer
# âââ StateMachine (Node)
# â âââ IdleState
# â âââ RunState
# â âââ JumpState
# âââ Hitbox (Area2D)
# âââ Hurtbox (Area2D)
Scene Instancing
# Preload for frequently used scenes
const BulletScene := preload("res://scenes/projectiles/bullet.tscn")
func shoot() -> void:
var bullet := BulletScene.instantiate() as Bullet
bullet.global_position = $Muzzle.global_position
bullet.direction = facing_direction
get_tree().current_scene.add_child(bullet)
Scene Inheritance
# Base enemy scene: enemy_base.tscn
# Inherited scene: enemy_flying.tscn (inherits enemy_base.tscn)
# enemy_base.gd
class_name EnemyBase
extends CharacterBody2D
@export var max_health: int = 100
@export var move_speed: float = 100.0
func take_damage(amount: int) -> void:
max_health -= amount
if max_health <= 0:
die()
func die() -> void:
queue_free()
# enemy_flying.gd (extends EnemyBase)
class_name EnemyFlying
extends EnemyBase
@export var flight_height: float = 50.0
func _physics_process(delta: float) -> void:
# Flying-specific behavior
velocity.y = sin(Time.get_ticks_msec() * 0.001) * flight_height
move_and_slide()
Node Groups
# Add to group in editor or code
add_to_group("enemies")
add_to_group("damageable")
# Find all nodes in group
func damage_all_enemies(amount: int) -> void:
for enemy in get_tree().get_nodes_in_group("enemies"):
if enemy.has_method("take_damage"):
enemy.take_damage(amount)
# Call method on all group members
get_tree().call_group("enemies", "alert", player_position)
3. GDScript Patterns
Type Hints (ALWAYS USE)
# Variables with types
var health: int = 100
var speed: float = 200.0
var player_name: String = "Hero"
var is_alive: bool = true
var items: Array[Item] = []
var stats: Dictionary = {}
# Typed arrays
var enemies: Array[Enemy] = []
var positions: Array[Vector2] = []
# Nullable types
var current_target: Node2D = null
# Function signatures
func calculate_damage(base: int, multiplier: float) -> int:
return int(base * multiplier)
func get_player() -> Player:
return get_tree().get_first_node_in_group("player") as Player
Export Variables
# Basic exports
@export var max_health: int = 100
@export var player_name: String = "Hero"
# Range constraints
@export_range(0, 100, 1) var health: int = 100
@export_range(0.0, 10.0, 0.1) var speed: float = 5.0
# Enums
@export_enum("Warrior", "Mage", "Rogue") var player_class: String
enum CharacterState { IDLE, RUNNING, JUMPING, FALLING }
@export var state: CharacterState = CharacterState.IDLE
# Resources
@export var character_data: CharacterResource
@export var weapon_stats: WeaponStats
# File paths
@export_file("*.tscn") var next_level: String
@export_dir var save_directory: String
# Grouped exports
@export_group("Movement")
@export var walk_speed: float = 100.0
@export var run_speed: float = 200.0
@export var jump_force: float = 400.0
@export_group("Combat")
@export var attack_damage: int = 10
@export var attack_cooldown: float = 0.5
# Subgroups
@export_subgroup("Advanced")
@export var crit_chance: float = 0.1
Signals
# Signal declarations
signal health_changed(new_health: int, max_health: int)
signal died
signal item_collected(item: Item)
signal level_completed(level_id: int, score: int)
# Emitting signals
func take_damage(amount: int) -> void:
health -= amount
health_changed.emit(health, max_health)
if health <= 0:
died.emit()
# Connecting signals (in code)
func _ready() -> void:
# Method 1: Connect with Callable
$Button.pressed.connect(_on_button_pressed)
# Method 2: Connect with lambda
$Timer.timeout.connect(func(): print("Timer done!"))
# Method 3: Connect to another node's method
player.health_changed.connect($HealthBar.update_display)
# Disconnect
func _exit_tree() -> void:
if player.health_changed.is_connected($HealthBar.update_display):
player.health_changed.disconnect($HealthBar.update_display)
Onready Variables
# Cache node references at ready
@onready var sprite: Sprite2D = $Sprite2D
@onready var anim_player: AnimationPlayer = $AnimationPlayer
@onready var collision: CollisionShape2D = $CollisionShape2D
@onready var raycast: RayCast2D = $RayCast2D
@onready var health_bar: ProgressBar = $UI/HealthBar
# Typed onready with path
@onready var state_machine: StateMachine = $StateMachine as StateMachine
Async/Await
# Wait for signal
func play_death_animation() -> void:
$AnimationPlayer.play("death")
await $AnimationPlayer.animation_finished
queue_free()
# Wait for timer
func delayed_spawn() -> void:
await get_tree().create_timer(2.0).timeout
spawn_enemy()
# Wait for next frame
func next_frame_operation() -> void:
await get_tree().process_frame
# Now in next frame
# Custom async function
func load_level_async(level_path: String) -> void:
$LoadingScreen.show()
ResourceLoader.load_threaded_request(level_path)
while ResourceLoader.load_threaded_get_status(level_path) == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
await get_tree().process_frame
var level = ResourceLoader.load_threaded_get(level_path)
get_tree().change_scene_to_packed(level)
Custom Resources
# weapon_stats.gd
class_name WeaponStats
extends Resource
@export var name: String = "Sword"
@export var damage: int = 10
@export var attack_speed: float = 1.0
@export var range: float = 50.0
@export var icon: Texture2D
func get_dps() -> float:
return damage * attack_speed
# Usage
@export var weapon: WeaponStats
func attack() -> void:
deal_damage(weapon.damage)
Singletons (Autoload)
# globals.gd - Project Settings > Autoload
extends Node
var score: int = 0
var high_score: int = 0
var current_level: int = 1
func reset_game() -> void:
score = 0
current_level = 1
# events.gd - Event bus pattern
extends Node
signal player_died
signal enemy_spawned(enemy: Enemy)
signal level_completed(level: int)
signal coin_collected(amount: int)
# Usage anywhere
Events.player_died.emit()
Events.coin_collected.connect(_on_coin_collected)
4. Physics & Collision
CharacterBody2D Movement
extends CharacterBody2D
const SPEED := 300.0
const JUMP_VELOCITY := -400.0
const GRAVITY := 980.0
func _physics_process(delta: float) -> void:
# Gravity
if not is_on_floor():
velocity.y += GRAVITY * delta
# Jump
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = JUMP_VELOCITY
# Horizontal movement
var direction := Input.get_axis("move_left", "move_right")
if direction:
velocity.x = direction * SPEED
else:
velocity.x = move_toward(velocity.x, 0, SPEED)
move_and_slide()
CharacterBody3D Movement
extends CharacterBody3D
@export var speed := 5.0
@export var jump_velocity := 4.5
@export var mouse_sensitivity := 0.002
var gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y -= gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
var input_dir := Input.get_vector("move_left", "move_right", "move_forward", "move_back")
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()
if direction:
velocity.x = direction.x * speed
velocity.z = direction.z * speed
else:
velocity.x = move_toward(velocity.x, 0, speed)
velocity.z = move_toward(velocity.z, 0, speed)
move_and_slide()
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
rotate_y(-event.relative.x * mouse_sensitivity)
$Camera3D.rotate_x(-event.relative.y * mouse_sensitivity)
$Camera3D.rotation.x = clamp($Camera3D.rotation.x, -PI/2, PI/2)
Collision Layers & Masks
# Layer setup (Project Settings > Layer Names > 2D Physics):
# Layer 1: Player
# Layer 2: Enemies
# Layer 3: Projectiles
# Layer 4: Environment
# Layer 5: Pickups
# Layer 6: Triggers
# Set in code
collision_layer = 1 # What I am
collision_mask = 6 # What I collide with (binary: layers 2 and 3)
# Or use bit flags
func set_collision_layer_bit(layer: int, enabled: bool) -> void:
if enabled:
collision_layer |= (1 << layer)
else:
collision_layer &= ~(1 << layer)
Area2D for Detection
# Hitbox/Hurtbox pattern
extends Area2D
class_name Hitbox
@export var damage: int = 10
func _ready() -> void:
area_entered.connect(_on_area_entered)
func _on_area_entered(area: Area2D) -> void:
if area is Hurtbox:
area.take_hit(self)
# Hurtbox
extends Area2D
class_name Hurtbox
signal hit_received(hitbox: Hitbox)
func take_hit(hitbox: Hitbox) -> void:
hit_received.emit(hitbox)
RayCast for Detection
@onready var raycast: RayCast2D = $RayCast2D
func _physics_process(_delta: float) -> void:
if raycast.is_colliding():
var collider = raycast.get_collider()
var collision_point = raycast.get_collision_point()
var collision_normal = raycast.get_collision_normal()
if collider.is_in_group("enemies"):
target_enemy(collider)
5. Input Handling
Input Actions (Project Settings)
# Define in Project Settings > Input Map
# Then use:
func _process(_delta: float) -> void:
if Input.is_action_pressed("move_right"):
move_right()
if Input.is_action_just_pressed("jump"):
jump()
if Input.is_action_just_released("attack"):
release_attack()
# Axis input (-1 to 1)
var horizontal := Input.get_axis("move_left", "move_right")
var vertical := Input.get_axis("move_up", "move_down")
var direction := Vector2(horizontal, vertical).normalized()
Input Events
func _input(event: InputEvent) -> void:
# Keyboard
if event is InputEventKey:
if event.pressed and event.keycode == KEY_ESCAPE:
toggle_pause()
# Mouse button
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
shoot()
# Mouse motion
if event is InputEventMouseMotion:
look_at_mouse(event.position)
# Touch (mobile)
if event is InputEventScreenTouch:
if event.pressed:
handle_touch(event.position)
func _unhandled_input(event: InputEvent) -> void:
# Only receives input not handled by UI
if event.is_action_pressed("pause"):
toggle_pause()
get_viewport().set_input_as_handled()
Touch Input (Mobile)
# Virtual joystick
var touch_start: Vector2
var touch_current: Vector2
var touch_index: int = -1
func _input(event: InputEvent) -> void:
if event is InputEventScreenTouch:
if event.pressed and touch_index == -1:
touch_index = event.index
touch_start = event.position
touch_current = event.position
elif not event.pressed and event.index == touch_index:
touch_index = -1
if event is InputEventScreenDrag:
if event.index == touch_index:
touch_current = event.position
func get_touch_direction() -> Vector2:
if touch_index == -1:
return Vector2.ZERO
return (touch_current - touch_start).normalized()
6. UI/Control Nodes
UI Scene Structure
MainMenu (Control)
âââ VBoxContainer
â âââ Title (Label)
â âââ PlayButton (Button)
â âââ OptionsButton (Button)
â âââ QuitButton (Button)
âââ OptionsPanel (Panel) [hidden]
âââ VBoxContainer
âââ VolumeSlider (HSlider)
âââ BackButton (Button)
Responsive UI
extends Control
func _ready() -> void:
# Anchor to full screen
set_anchors_preset(Control.PRESET_FULL_RECT)
# Handle window resize
get_tree().root.size_changed.connect(_on_window_resized)
func _on_window_resized() -> void:
var viewport_size := get_viewport_rect().size
# Adjust UI elements based on new size
Theme System
# Create theme resource (.tres)
# Apply to root Control node
# Override in code
func highlight_button(button: Button) -> void:
button.add_theme_color_override("font_color", Color.YELLOW)
button.add_theme_font_size_override("font_size", 24)
HUD Pattern
extends CanvasLayer
@onready var health_bar: ProgressBar = $HealthBar
@onready var score_label: Label = $ScoreLabel
@onready var ammo_label: Label = $AmmoLabel
func _ready() -> void:
Events.health_changed.connect(update_health)
Events.score_changed.connect(update_score)
Events.ammo_changed.connect(update_ammo)
func update_health(current: int, maximum: int) -> void:
health_bar.max_value = maximum
health_bar.value = current
func update_score(score: int) -> void:
score_label.text = "Score: %d" % score
func update_ammo(current: int, maximum: int) -> void:
ammo_label.text = "%d / %d" % [current, maximum]
See: references/ui-patterns.md for complete UI patterns
7. Animation & Audio
AnimationPlayer
@onready var anim: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
anim.animation_finished.connect(_on_animation_finished)
func play_attack() -> void:
anim.play("attack")
await anim.animation_finished
return_to_idle()
func _on_animation_finished(anim_name: StringName) -> void:
match anim_name:
"death":
queue_free()
"attack":
can_attack = true
AnimationTree (State Machine)
@onready var anim_tree: AnimationTree = $AnimationTree
@onready var state_machine: AnimationNodeStateMachinePlayback = anim_tree.get("parameters/playback")
func _physics_process(_delta: float) -> void:
anim_tree.set("parameters/blend_position", velocity.x)
if is_on_floor():
if velocity.x != 0:
state_machine.travel("run")
else:
state_machine.travel("idle")
else:
state_machine.travel("jump")
Tweens
# One-shot tween
func flash_white() -> void:
var tween := create_tween()
tween.tween_property($Sprite2D, "modulate", Color.WHITE, 0.1)
tween.tween_property($Sprite2D, "modulate", Color(1, 1, 1, 1), 0.1)
# Chained animations
func bounce_in() -> void:
var tween := create_tween()
tween.set_ease(Tween.EASE_OUT)
tween.set_trans(Tween.TRANS_ELASTIC)
tween.tween_property(self, "scale", Vector2.ONE, 0.5).from(Vector2.ZERO)
# Parallel animations
func fade_and_move() -> void:
var tween := create_tween()
tween.set_parallel(true)
tween.tween_property(self, "modulate:a", 0.0, 1.0)
tween.tween_property(self, "position:y", position.y - 50, 1.0)
tween.chain().tween_callback(queue_free)
Audio
# AudioManager singleton
extends Node
var music_player: AudioStreamPlayer
var sfx_players: Array[AudioStreamPlayer] = []
func _ready() -> void:
music_player = AudioStreamPlayer.new()
add_child(music_player)
for i in 8:
var player := AudioStreamPlayer.new()
add_child(player)
sfx_players.append(player)
func play_music(stream: AudioStream, fade_in: float = 1.0) -> void:
music_player.stream = stream
music_player.volume_db = -80
music_player.play()
var tween := create_tween()
tween.tween_property(music_player, "volume_db", 0, fade_in)
func play_sfx(stream: AudioStream) -> void:
for player in sfx_players:
if not player.playing:
player.stream = stream
player.play()
return
# All players busy, use first one
sfx_players[0].stream = stream
sfx_players[0].play()
8. Performance
Object Pooling
class_name ObjectPool
extends Node
var _pool: Array[Node] = []
var _scene: PackedScene
func _init(scene: PackedScene, initial_size: int = 10) -> void:
_scene = scene
for i in initial_size:
var instance := _scene.instantiate()
instance.set_process(false)
instance.hide()
_pool.append(instance)
func get_object() -> Node:
for obj in _pool:
if not obj.visible:
obj.show()
obj.set_process(true)
return obj
# Pool exhausted, create new
var new_obj := _scene.instantiate()
_pool.append(new_obj)
get_parent().add_child(new_obj)
return new_obj
func return_object(obj: Node) -> void:
obj.hide()
obj.set_process(false)
LOD (Level of Detail)
extends Node2D
@export var lod_distances: Array[float] = [100, 300, 600]
@export var lod_nodes: Array[Node2D]
var camera: Camera2D
func _process(_delta: float) -> void:
if not camera:
camera = get_viewport().get_camera_2d()
return
var distance := global_position.distance_to(camera.global_position)
for i in lod_nodes.size():
if i < lod_distances.size():
lod_nodes[i].visible = distance < lod_distances[i]
else:
lod_nodes[i].visible = true
Profiling
# Use built-in profiler: Debugger > Profiler
# Custom timing
func expensive_operation() -> void:
var start := Time.get_ticks_usec()
# ... operation ...
var elapsed := Time.get_ticks_usec() - start
print("Operation took: %d microseconds" % elapsed)
# Conditional processing
func _process(delta: float) -> void:
if not is_visible_on_screen():
return # Skip processing for off-screen objects
9. Export Targets
Export Overview
platforms[6]{name,format,requirements}:
HTML5,.html+.wasm,WebGL 2.0 browser
Android,.apk/.aab,Android SDK + JDK
iOS,.ipa,Xcode + Apple Developer
Windows,.exe,Windows SDK (optional)
macOS,.app/.dmg,Xcode CLI tools
Linux,Binary,None
HTML5 Export
# Check if running in browser
if OS.has_feature("web"):
# Disable features not supported in web
fullscreen_button.disabled = true
# Handle browser focus
func _notification(what: int) -> void:
if what == NOTIFICATION_WM_FOCUS_OUT:
# Browser tab lost focus
get_tree().paused = true
elif what == NOTIFICATION_WM_FOCUS_IN:
get_tree().paused = false
Mobile Export
# Detect mobile platform
func _ready() -> void:
if OS.has_feature("mobile"):
setup_mobile_controls()
else:
setup_desktop_controls()
func setup_mobile_controls() -> void:
$VirtualJoystick.show()
$TouchButtons.show()
# Adjust for notch/safe area
var safe_area := DisplayServer.get_display_safe_area()
$UI.offset_top = safe_area.position.y
See: references/export-platforms.md for complete export guide
10. Testing with GDUnit
Test Structure
# test/player/test_player.gd
extends GdUnitTestSuite
var player: Player
func before_test() -> void:
player = auto_free(preload("res://scenes/player/player.tscn").instantiate())
add_child(player)
func test_initial_health() -> void:
assert_int(player.health).is_equal(100)
func test_take_damage() -> void:
player.take_damage(25)
assert_int(player.health).is_equal(75)
func test_death_signal() -> void:
var signal_collector := signal_collector(player, "died")
player.take_damage(100)
await assert_signal(signal_collector).is_emitted("died")
See: references/testing-gdunit.md for complete testing guide
Quick Reference
Common Patterns
patterns[8]{name,use_case}:
State Machine,Complex entity behavior
Object Pool,Frequent spawn/despawn
Event Bus,Decoupled communication
Resource,Shared data/configuration
Autoload,Global managers
Scene Inheritance,Enemy variants
Composition,Modular abilities
Command,Input/action replay
File Extensions
extensions[6]{ext,purpose}:
.gd,GDScript source
.tscn,Scene (text format)
.scn,Scene (binary format)
.tres,Resource (text)
.res,Resource (binary)
.import,Import settings
Related
| Resource | Location |
|---|---|
| Export Platforms | references/export-platforms.md |
| UI Patterns | references/ui-patterns.md |
| GDUnit Testing | references/testing-gdunit.md |
| Scene Composition Rule | rules/godot-scene-composition.md |
| GDScript Typing Rule | rules/godot-gdscript-typing.md |
Version: 1.6.0 | Last Updated: 2025-12-26