mcpField
G
godot-best-practices

godot-best-practices

github.com/jwynia开发1792.1k 次安装35 周前更新
代码仓库

关于 godot-best-practices

为 AI Agent 提供 Godot 4.x 项目中生成、组织和审查 GDScript 的参考技能,涵盖编码规范、架构建议及游戏开发模式;不适用于 Godot 3.x、C# 或 GDExtension/C++。

主要功能

  1. 01GDScript 命名与类型规范
  2. 02节点引用和信号模式
  3. 03资源加载建议
  4. 04状态机与对象池模式
  5. 05存档及输入处理指南
  6. 06脚本与架构模板

适用场景

  1. 01为 Godot 4.x 游戏生成 GDScript
  2. 02组织场景和节点层级
  3. 03检查脚本中的常见反模式
  4. 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.md
base-script.gd.md
state-machine.gd.md
references/architecture/
node-communication.md
project-structure.md
scene-composition.md
references/gdscript/
coroutines-await.md
type-system.md
references/patterns/
input-handling.md
object-pooling.md
save-load-system.md
state-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
协程awaityield(已弃用)
分组场景专用分组所有内容都使用全局分组
自动加载仅用于服务/管理器在自动加载中放置游戏逻辑
属性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