关于 godot-best-practices
为 AI Agent 提供 Godot 4.x 项目中生成、组织和审查 GDScript 的参考技能,涵盖编码规范、架构建议及游戏开发模式;不适用于 Godot 3.x、C# 或 GDExtension/C++。
主要功能
- 01GDScript 命名与类型规范
- 02节点引用和信号模式
- 03资源加载建议
- 04状态机与对象池模式
- 05存档及输入处理指南
- 06脚本与架构模板
适用场景
- 01为 Godot 4.x 游戏生成 GDScript
- 02组织场景和节点层级
- 03检查脚本中的常见反模式
- 04规划存档系统或状态机
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jwynia/ agent-skills --skill godot-best-practices安装后在 Claude Code 中输入/godot-best-practices确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/templates/autoload-manager.gd.mdbase-script.gd.mdstate-machine.gd.mdreferences/architecture/node-communication.mdproject-structure.mdscene-composition.mdreferences/gdscript/coroutines-await.mdtype-system.mdreferences/patterns/input-handling.mdobject-pooling.mdsave-load-system.mdstate-machine.md详细介绍
Godot 4.x GDScript 最佳实践
指导 AI Agent 编写高质量的 Godot 4.x GDScript 代码。本技能提供游戏开发的编码标准、架构模式和模板。
何时使用本技能
在以下情况下使用本技能:
- 生成新的 GDScript 代码
- 创建或整理 Godot 场景
- 设计游戏架构和节点层级
- 实现状态机、对象池或存档系统
- 回答有关 GDScript 模式或 Godot 约定的问题
- 检查 GDScript 代码的质量问题
以下情况请勿使用本技能:
- 在 Godot 中使用 C#(请使用 C# 模式)
- 使用 Godot 3.x(语法差异很大)
- 使用 GDExtension/C++(范式不同)
- 使用 Godot 的可视化脚本
核心原则
1. 命名约定
始终遵循 GDScript 命名标准:
# 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
2. 类型提示(静态类型)
在所有地方使用显式类型提示,以便自动补全和错误检测:
# 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
3. 节点引用
使用现代模式,让引用稳定且便于重构:
# 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
4. 信号驱动架构
使用信号进行解耦通信。遵循“向上发信号,向下调用”:
# 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()
5. 资源加载
选择合适的加载策略:
# 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。
存档/加载(概览)
使用资源或 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- 信号与直接调用
GDScript 深入解析
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- Autoload 单例模板
限制
- 仅支持 GDScript(不支持 C#、GDExtension 或 VisualScript)
- 使用 Godot 4.x 语法(部分模式与 3.x 不同)
- 面向游戏的模式(不涉及编辑器插件开发)
- 不提供运行时验证脚本(GDScript 需要 Godot 运行时)
评论
支持 Markdown