godot-best-practices by jwynia/agent-skills
npx skills add https://github.com/jwynia/agent-skills --skill godot-best-practices指导 AI 代理为 Godot 4.x 编写高质量的 GDScript 代码。此技能提供游戏开发的编码标准、架构模式和模板。
在以下情况下使用此技能:
不要在以下情况下使用此技能:
始终遵循 GDScript 命名标准:
# Classes: PascalCase
class_name PlayerController
extends CharacterBody2D
# Signals: past_tense_snake_case (描述已发生的事情)
signal health_changed(new_health: int)
signal player_died
signal item_collected(item: Item)
# Constants: SCREAMING_SNAKE_CASE
const MAX_SPEED: float = 200.0
const JUMP_FORCE: int = -400
# Variables and functions: snake_case
var current_health: int = 100
var _private_variable: float = 0.0 # Leading underscore for private
func calculate_damage(base: int, multiplier: float) -> int:
return int(base * multiplier)
func _private_helper() -> void: # Leading underscore for private
pass
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
为获得自动完成和错误检测,请在所有地方使用显式类型提示:
# Variable declarations
var speed: float = 100.0
var player: CharacterBody2D
var items: Array[Item] = []
var stats: Dictionary = {}
# Function signatures with return types
func get_damage() -> int:
return _base_damage * _multiplier
func find_nearest_enemy(position: Vector2) -> Enemy:
# Implementation
return null
# Typed signals (Godot 4.x)
signal score_updated(new_score: int, old_score: int)
signal target_acquired(target: Node2D, distance: float)
# Node references with types
@onready var sprite: Sprite2D = $Sprite2D
@onready var collision: CollisionShape2D = $CollisionShape2D
@onready var animation_player: AnimationPlayer = %AnimationPlayer
使用现代模式以获得稳定、易于重构的引用:
# PREFER: @onready with type hints
@onready var health_bar: ProgressBar = $UI/HealthBar
@onready var weapon: Weapon = $WeaponMount/Weapon
# PREFER: Unique names with % for critical nodes
@onready var player: Player = %Player
@onready var game_manager: GameManager = %GameManager
# AVOID: get_node() in _ready()
func _ready() -> void:
# Don't do this
var sprite = get_node("Sprite2D")
# AVOID: Deep fragile paths
@onready var thing = $Parent/Child/GrandChild/GreatGrandChild # Fragile
使用信号进行解耦通信。遵循“信号向上,调用向下”:
# Child node emits signals (doesn't know about parent)
class_name HealthComponent
extends Node
signal health_changed(current: int, maximum: int)
signal died
var _health: int = 100
var _max_health: int = 100
func take_damage(amount: int) -> void:
_health = max(0, _health - amount)
health_changed.emit(_health, _max_health)
if _health <= 0:
died.emit()
# Parent connects to child signals (knows about children)
class_name Player
extends CharacterBody2D
@onready var health: HealthComponent = $HealthComponent
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:
health.health_changed.connect(_on_health_changed)
health.died.connect(_on_died)
func _on_health_changed(current: int, maximum: int) -> void:
# Update UI, play effects, etc.
pass
func _on_died() -> void:
sprite.modulate = Color.RED
queue_free()
选择合适的加载策略:
# preload(): Compile-time loading for critical/small assets
const BULLET_SCENE: PackedScene = preload("res://scenes/bullet.tscn")
const PLAYER_SPRITE: Texture2D = preload("res://sprites/player.png")
const DAMAGE_SOUND: AudioStream = preload("res://audio/damage.wav")
# load(): Runtime loading for optional/large assets
func load_level(level_name: String) -> void:
var path := "res://levels/%s.tscn" % level_name
var level_scene: PackedScene = load(path)
var level := level_scene.instantiate()
add_child(level)
# ResourceLoader for async loading (prevents stuttering)
func _load_level_async(path: String) -> void:
ResourceLoader.load_threaded_request(path)
# Check with: ResourceLoader.load_threaded_get_status(path)
# Get with: ResourceLoader.load_threaded_get(path)
| 类别 | 推荐 | 避免 |
|---|---|---|
| 节点引用 | @onready var x: Type = $Path | _ready() 中的 get_node() |
| 唯一节点 | %UniqueName | 深层路径 $A/B/C/D |
| 资源加载 | 小型/关键资源用 preload() | 到处都用 load() |
| 信号 | 带类型:signal x(val: int) | 字符串:emit_signal("x") |
| 类型安全 | 显式类型提示 | 无类型变量 |
| 常量 | const 或 @export | 魔法数字/字符串 |
| 空值检查 | is_instance_valid(node) | 对已释放节点使用 node != null |
| 协程 | await | yield(已弃用) |
| 组 | 场景特定组 | 所有东西都用全局组 |
| 自动加载 | 仅用于服务/管理器 | 在自动加载中放置游戏逻辑 |
| 属性 | Setter/Getter | 直接修改 |
| 通信 | 信号向上,调用向下 | 子节点调用父节点方法 |
按一致顺序排列各部分:
class_name MyClass
extends Node2D
## Brief description of this class.
##
## Longer description if needed, explaining purpose and usage.
# === Signals ===
signal state_changed(new_state: State)
# === Enums ===
enum State { IDLE, RUNNING, JUMPING }
# === Exports ===
@export var speed: float = 100.0
@export_group("Combat")
@export var damage: int = 10
@export var attack_range: float = 50.0
# === Constants ===
const MAX_HEALTH: int = 100
# === Public Variables ===
var current_state: State = State.IDLE
# === Private Variables ===
var _internal_counter: int = 0
# === Onready ===
@onready var sprite: Sprite2D = $Sprite2D
@onready var collision: CollisionShape2D = $CollisionShape2D
# === Lifecycle Methods ===
func _ready() -> void:
pass
func _process(delta: float) -> void:
pass
func _physics_process(delta: float) -> void:
pass
# === Public Methods ===
func take_damage(amount: int) -> void:
pass
# === Private Methods ===
func _calculate_knockback() -> Vector2:
return Vector2.ZERO
使用导出注解来配置编辑器可调整的值:
# Basic exports
@export var health: int = 100
@export var speed: float = 200.0
@export var player_name: String = "Player"
# Range constraints
@export_range(0, 100) var percentage: int = 50
@export_range(0.0, 1.0, 0.1) var volume: float = 0.8
# Resource exports
@export var texture: Texture2D
@export var scene: PackedScene
@export var audio: AudioStream
# Grouped exports
@export_group("Movement")
@export var walk_speed: float = 100.0
@export var run_speed: float = 200.0
@export_group("Combat")
@export var attack_damage: int = 10
# Enum exports
@export var difficulty: Difficulty = Difficulty.NORMAL
enum Difficulty { EASY, NORMAL, HARD }
# Flags (multiselect)
@export_flags("Fire", "Water", "Earth", "Air") var elements: int = 0
对于简单情况,使用基于枚举的状态机:
enum State { IDLE, WALK, JUMP, ATTACK }
var current_state: State = State.IDLE
func _physics_process(delta: float) -> void:
match current_state:
State.IDLE:
_process_idle(delta)
State.WALK:
_process_walk(delta)
State.JUMP:
_process_jump(delta)
State.ATTACK:
_process_attack(delta)
func change_state(new_state: State) -> void:
if current_state == new_state:
return
_exit_state(current_state)
current_state = new_state
_enter_state(new_state)
有关高级实现,请参阅 references/patterns/state-machine.md。
重用对象以避免实例化开销:
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 obj := _scene.instantiate()
obj.set_process(false)
_pool.append(obj)
func acquire() -> Node:
if _pool.is_empty():
return _scene.instantiate()
var obj := _pool.pop_back()
obj.set_process(true)
return obj
func release(obj: Node) -> void:
obj.set_process(false)
_pool.append(obj)
有关完整实现,请参阅 references/patterns/object-pooling.md。
使用 Resource 或 JSON 保存数据:
# Custom Resource for save data
class_name SaveData
extends Resource
@export var player_position: Vector2
@export var player_health: int
@export var inventory: Array[String]
@export var level_name: String
# Save
func save_game(data: SaveData) -> void:
ResourceSaver.save(data, "user://save.tres")
# Load
func load_game() -> SaveData:
if ResourceLoader.exists("user://save.tres"):
return load("user://save.tres") as SaveData
return SaveData.new()
有关完整指南,请参阅 references/patterns/save-load-system.md。
| 反模式 | 问题 | 解决方案 |
|---|---|---|
在 _process 中轮询 | 在未改变的状态上浪费 CPU | 使用信号处理状态变化 |
get_parent().get_parent() | 紧耦合,脆弱 | 信号向上,或使用组 |
深层节点路径 $A/B/C/D | 重构时易断裂 | 使用 %UniqueName |
在 _process 中使用 load() | 卡顿,内存抖动 | 使用 preload() 或缓存引用 |
字符串信号 emit_signal("x") | 拼写错误,无自动完成 | 带类型:signal_name.emit() |
无类型 @onready var x = $Node | 失去自动完成 | 始终添加类型提示 |
| 逻辑放在自动加载中 | 测试困难,耦合度高 | 保持自动加载精简 |
| 魔法数字 | 含义不明确 | 使用 const 或 @export |
对已释放节点使用 node != null | 对已释放节点返回 true | 使用 is_instance_valid() |
| 循环依赖 | 加载错误,流程不清晰 | 依赖注入或信号 |
references/patterns/state-machine.md - 完整的状态机实现references/patterns/object-pooling.md - 完整的对象池系统references/patterns/save-load-system.md - 全面的保存/加载指南references/patterns/input-handling.md - 输入缓冲和重绑定references/architecture/project-structure.md - 目录组织references/architecture/scene-composition.md - 场景设计模式references/architecture/node-communication.md - 信号与直接调用references/gdscript/type-system.md - 深入理解静态类型references/gdscript/coroutines-await.md - 使用 await 的异步模式assets/templates/base-script.gd.md - 标准脚本模板assets/templates/state-machine.gd.md - 状态机模板assets/templates/autoload-manager.gd.md - 自动加载单例模板每周安装数
645
仓库
GitHub 星标数
37
首次出现
2026年1月20日
安全审计
安装于
opencode539
codex527
gemini-cli517
github-copilot494
cursor458
kimi-cli452
Guide AI agents in writing high-quality GDScript code for Godot 4.x. This skill provides coding standards, architecture patterns, and templates for game development.
Use this skill when:
Do NOT use this skill when:
Follow GDScript naming standards consistently:
# Classes: PascalCase
class_name PlayerController
extends CharacterBody2D
# Signals: past_tense_snake_case (describe what happened)
signal health_changed(new_health: int)
signal player_died
signal item_collected(item: Item)
# Constants: SCREAMING_SNAKE_CASE
const MAX_SPEED: float = 200.0
const JUMP_FORCE: int = -400
# Variables and functions: snake_case
var current_health: int = 100
var _private_variable: float = 0.0 # Leading underscore for private
func calculate_damage(base: int, multiplier: float) -> int:
return int(base * multiplier)
func _private_helper() -> void: # Leading underscore for private
pass
Use explicit type hints everywhere for autocomplete and error detection:
# Variable declarations
var speed: float = 100.0
var player: CharacterBody2D
var items: Array[Item] = []
var stats: Dictionary = {}
# Function signatures with return types
func get_damage() -> int:
return _base_damage * _multiplier
func find_nearest_enemy(position: Vector2) -> Enemy:
# Implementation
return null
# Typed signals (Godot 4.x)
signal score_updated(new_score: int, old_score: int)
signal target_acquired(target: Node2D, distance: float)
# Node references with types
@onready var sprite: Sprite2D = $Sprite2D
@onready var collision: CollisionShape2D = $CollisionShape2D
@onready var animation_player: AnimationPlayer = %AnimationPlayer
Use modern patterns for stable, refactor-friendly references:
# PREFER: @onready with type hints
@onready var health_bar: ProgressBar = $UI/HealthBar
@onready var weapon: Weapon = $WeaponMount/Weapon
# PREFER: Unique names with % for critical nodes
@onready var player: Player = %Player
@onready var game_manager: GameManager = %GameManager
# AVOID: get_node() in _ready()
func _ready() -> void:
# Don't do this
var sprite = get_node("Sprite2D")
# AVOID: Deep fragile paths
@onready var thing = $Parent/Child/GrandChild/GreatGrandChild # Fragile
Use signals for decoupled communication. Follow "signal up, call down":
# Child node emits signals (doesn't know about parent)
class_name HealthComponent
extends Node
signal health_changed(current: int, maximum: int)
signal died
var _health: int = 100
var _max_health: int = 100
func take_damage(amount: int) -> void:
_health = max(0, _health - amount)
health_changed.emit(_health, _max_health)
if _health <= 0:
died.emit()
# Parent connects to child signals (knows about children)
class_name Player
extends CharacterBody2D
@onready var health: HealthComponent = $HealthComponent
@onready var sprite: Sprite2D = $Sprite2D
func _ready() -> void:
health.health_changed.connect(_on_health_changed)
health.died.connect(_on_died)
func _on_health_changed(current: int, maximum: int) -> void:
# Update UI, play effects, etc.
pass
func _on_died() -> void:
sprite.modulate = Color.RED
queue_free()
Choose the right loading strategy:
# preload(): Compile-time loading for critical/small assets
const BULLET_SCENE: PackedScene = preload("res://scenes/bullet.tscn")
const PLAYER_SPRITE: Texture2D = preload("res://sprites/player.png")
const DAMAGE_SOUND: AudioStream = preload("res://audio/damage.wav")
# load(): Runtime loading for optional/large assets
func load_level(level_name: String) -> void:
var path := "res://levels/%s.tscn" % level_name
var level_scene: PackedScene = load(path)
var level := level_scene.instantiate()
add_child(level)
# ResourceLoader for async loading (prevents stuttering)
func _load_level_async(path: String) -> void:
ResourceLoader.load_threaded_request(path)
# Check with: ResourceLoader.load_threaded_get_status(path)
# Get with: ResourceLoader.load_threaded_get(path)
| Category | Prefer | Avoid |
|---|---|---|
| Node references | @onready var x: Type = $Path | get_node() in _ready() |
| Unique nodes | %UniqueName | Deep paths $A/B/C/D |
| Resource loading | preload() for small/critical | load() everywhere |
Order sections consistently:
class_name MyClass
extends Node2D
## Brief description of this class.
##
## Longer description if needed, explaining purpose and usage.
# === Signals ===
signal state_changed(new_state: State)
# === Enums ===
enum State { IDLE, RUNNING, JUMPING }
# === Exports ===
@export var speed: float = 100.0
@export_group("Combat")
@export var damage: int = 10
@export var attack_range: float = 50.0
# === Constants ===
const MAX_HEALTH: int = 100
# === Public Variables ===
var current_state: State = State.IDLE
# === Private Variables ===
var _internal_counter: int = 0
# === Onready ===
@onready var sprite: Sprite2D = $Sprite2D
@onready var collision: CollisionShape2D = $CollisionShape2D
# === Lifecycle Methods ===
func _ready() -> void:
pass
func _process(delta: float) -> void:
pass
func _physics_process(delta: float) -> void:
pass
# === Public Methods ===
func take_damage(amount: int) -> void:
pass
# === Private Methods ===
func _calculate_knockback() -> Vector2:
return Vector2.ZERO
Use exports for editor-configurable values:
# Basic exports
@export var health: int = 100
@export var speed: float = 200.0
@export var player_name: String = "Player"
# Range constraints
@export_range(0, 100) var percentage: int = 50
@export_range(0.0, 1.0, 0.1) var volume: float = 0.8
# Resource exports
@export var texture: Texture2D
@export var scene: PackedScene
@export var audio: AudioStream
# Grouped exports
@export_group("Movement")
@export var walk_speed: float = 100.0
@export var run_speed: float = 200.0
@export_group("Combat")
@export var attack_damage: int = 10
# Enum exports
@export var difficulty: Difficulty = Difficulty.NORMAL
enum Difficulty { EASY, NORMAL, HARD }
# Flags (multiselect)
@export_flags("Fire", "Water", "Earth", "Air") var elements: int = 0
Use enum-based state machines for simple cases:
enum State { IDLE, WALK, JUMP, ATTACK }
var current_state: State = State.IDLE
func _physics_process(delta: float) -> void:
match current_state:
State.IDLE:
_process_idle(delta)
State.WALK:
_process_walk(delta)
State.JUMP:
_process_jump(delta)
State.ATTACK:
_process_attack(delta)
func change_state(new_state: State) -> void:
if current_state == new_state:
return
_exit_state(current_state)
current_state = new_state
_enter_state(new_state)
See references/patterns/state-machine.md for advanced implementations.
Reuse objects to avoid instantiation cost:
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 obj := _scene.instantiate()
obj.set_process(false)
_pool.append(obj)
func acquire() -> Node:
if _pool.is_empty():
return _scene.instantiate()
var obj := _pool.pop_back()
obj.set_process(true)
return obj
func release(obj: Node) -> void:
obj.set_process(false)
_pool.append(obj)
See references/patterns/object-pooling.md for complete implementation.
Use Resources or JSON for save data:
# Custom Resource for save data
class_name SaveData
extends Resource
@export var player_position: Vector2
@export var player_health: int
@export var inventory: Array[String]
@export var level_name: String
# Save
func save_game(data: SaveData) -> void:
ResourceSaver.save(data, "user://save.tres")
# Load
func load_game() -> SaveData:
if ResourceLoader.exists("user://save.tres"):
return load("user://save.tres") as SaveData
return SaveData.new()
See references/patterns/save-load-system.md for comprehensive guide.
| Anti-Pattern | Problem | Solution |
|---|---|---|
Polling in _process | Wastes CPU on unchanged state | Use signals for state changes |
get_parent().get_parent() | Tight coupling, fragile | Signal up, or use groups |
Deep node paths $A/B/C/D | Breaks on refactor | Use %UniqueName |
load() in _process | Stuttering, memory churn |
references/patterns/state-machine.md - Full state machine implementationsreferences/patterns/object-pooling.md - Complete pooling systemreferences/patterns/save-load-system.md - Comprehensive save/load guidereferences/patterns/input-handling.md - Input buffering and rebindingreferences/architecture/project-structure.md - Directory organizationreferences/architecture/scene-composition.md - Scene design patternsreferences/architecture/node-communication.md - Signals vs direct callsreferences/gdscript/type-system.md - Static typing in depthreferences/gdscript/coroutines-await.md - Async patterns with awaitassets/templates/base-script.gd.md - Standard script templateassets/templates/state-machine.gd.md - State machine templateassets/templates/autoload-manager.gd.md - Autoload singleton templateWeekly Installs
645
Repository
GitHub Stars
37
First Seen
Jan 20, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode539
codex527
gemini-cli517
github-copilot494
cursor458
kimi-cli452
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
136,300 周安装
| Signals | Typed: signal x(val: int) | String: emit_signal("x") |
| Type safety | Explicit type hints | Untyped variables |
| Constants | const or @export | Magic numbers/strings |
| Null checks | is_instance_valid(node) | node != null for freed nodes |
| Coroutines | await | yield (deprecated) |
| Groups | Scene-specific groups | Global groups for everything |
| Autoloads | Services/managers only | Game logic in autoloads |
| Properties | Setters/getters | Direct mutation |
| Communication | Signal up, call down | Child calling parent methods |
preload() or cache reference |
String signals emit_signal("x") | Typos, no autocomplete | Typed: signal_name.emit() |
Untyped @onready var x = $Node | Loses autocomplete | Always add type hint |
| Logic in autoloads | Testing difficulty, coupling | Keep autoloads thin |
| Magic numbers | Unclear meaning | Use const or @export |
node != null for freed nodes | Returns true for freed | Use is_instance_valid() |
| Circular dependencies | Load errors, unclear flow | Dependency injection or signals |