godot-expert

Version: 1.6.0 Type: Skill (Auto-Invoke) Agent: game-developer

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "godot-expert" with this command: npx skills add nguyenthienthanh/aura-frog/nguyenthienthanh-aura-frog-godot-expert

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

  1. 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)

  1. 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)

  1. 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()

  1. 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

  1. 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()

  1. 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 &#x3C; lod_distances.size():
        lod_nodes[i].visible = distance &#x3C; 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

  1. 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

  1. 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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-expert

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-simplifier

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

code-reviewer

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

dev-expert

No summary provided by upstream source.

Repository SourceNeeds Review