home / skills / chen19007 / my_skills / godot-dev

godot-dev skill

/godot-dev

This skill helps you write clean, type-safe GDScript by applying Godot-specific conventions, signals, and editor features to prevent anti-patterns.

npx playbooks add skill chen19007/my_skills --skill godot-dev

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
6.5 KB
---
name: godot-dev
description: |
  Godot code implementation guidelines.
  Use when: (1) Writing GDScript code, (2) Following style conventions, (3) Using Godot features correctly, (4) Avoiding anti-patterns.
---

# Godot 代码实现规范

本 Skill 提供 GDScript 代码实现的规范和最佳实践,确保代码符合 Godot 引擎特性和 TDD 友好原则。

## 适用场景

- 编写新的功能代码
- 重构现有代码
- 遵循 GDScript 风格规范
- 使用 Godot 特有功能

## GDScript 风格规则

### 类型注解

```gdscript
# 函数参数和返回值(强制)
func move_to(target: Vector2) -> void:
    ...

func calculate_damage(base: float, multiplier: float) -> float:
    return base * multiplier

# 变量类型注解
var health: int = 100
var speed: float = 300.0
var position: Vector2 = Vector2.ZERO
var enemies: Array[Node] = []
```

### 属性可见性

```gdscript
# 私有属性(使用下划线前缀)
var _health: int = 100
var _velocity: Vector2 = Vector2.ZERO
var _cooldowns: Dictionary = {}

# 只读属性(使用 getter)
var health: int:
    get:
        return _health

# 导出变量(编辑器配置)
@export var max_health: int = 100
@export var move_speed: float = 300.0
@export var damage: float = 10.0
```

### 常量命名

```gdscript
const MAX_HEALTH := 100
const MOVE_SPEED := 300.0
const GRAVITY := 980.0
const TAG_ENEMY := "enemy"
```

### 信号定义

```gdscript
# 状态变化信号
signal health_changed(new_health: int, old_health: int)
signal died()
signal revived()

# 事件信号
signal attack_completed(target: Node)
signal level_up(level: int)

# 使用信号
func take_damage(amount: int) -> void:
    var old_health = _health
    _health = max(0, _health - amount)
    health_changed.emit(_health, old_health)
```

## Godot 特性使用

### Signal(观察者模式)

```gdscript
# 定义信号
signal health_changed(new_health: int)

# 发送信号
func take_damage(amount: int) -> void:
    _health = max(0, _health - amount)
    health_changed.emit(_health)

# 连接信号(推荐使用 callable 语法)
func _ready() -> void:
    health_changed.connect(_on_health_changed)

func _on_health_changed(new_health: int) -> void:
    print("Health: ", new_health)

# 一次性信号
health_changed.connect(_on_health_changed, CONNECT_ONE_SHOT)
```

### @onready(节点引用)

```gdscript
extends Node2D

@onready var sprite: Sprite2D = $Sprite2D
@onready var animation: AnimationPlayer = $AnimationPlayer
@onready var collision: CollisionShape2D = $CollisionShape2D
```

### @export(编辑器配置)

```gdscript
@export var max_health: int = 100
@export var move_speed: float = 300.0

# 资源类型
@export var bullet_scene: PackedScene

# 枚举类型
@export_enum("Fast", "Normal", "Slow") var speed_mode: String = "Normal"

# 分组
@export_group("Damage Settings")
@export var base_damage: float = 10.0
@export var critical_multiplier: float = 2.0
```

### Autoload(单例)

```gdscript
# 获取 autoload
var game_manager = Engine.get_main_loop().get_node("/root/GameManager")

# 安全访问
func _ready() -> void:
    if Engine.get_main_loop().has_node("/root/GameManager"):
        var gm = Engine.get_main_loop().get_node("/root/GameManager")
        gm.register_player(self)
```

## 依赖管理

### 循环依赖问题

GUT 无法加载存在循环依赖的脚本。

```gdscript
# 错误:循环依赖
class_name Player extends Node

func _ready() -> void:
    var manager = GameManager.get_instance()
    manager.register(self)

# GameManager.gd
class_name GameManager
func register_player(player: Player):  # 引用 Player
    ...
```

### 解决方案

```gdscript
# 方案1:使用基类参数
func register_character(character: Node, height_x: float) -> void:
    ...

# 方案2:延迟绑定
func _ready() -> void:
    var manager = _get_game_manager()
    if manager:
        manager.register(self)

func _get_game_manager() -> Node:
    if Engine.get_main_loop().has_node("/root/GameManager"):
        return Engine.get_main_loop().get_node("/root/GameManager")
    return null

# 方案3:避免 class_name 循环
extends Node2D  # 不使用 class_name
```

## 碰撞检测

### Area2D vs PhysicsBody

```gdscript
extends Area2D

func _ready() -> void:
    body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node) -> void:
    # 使用 has_method 检查接口
    if body.has_method("take_damage"):
        body.take_damage(10)

# 检测 PhysicsBody
func check_player_overlap(player: Node) -> bool:
    return overlaps_body(player)

# 检测 Area2D
func check_hazard_overlap(hazard: Node) -> bool:
    return overlaps_area(hazard)
```

## 常见反模式

### 反模式 1:直接暴露内部变量

```gdscript
# 错误
var health: int = 100

# 正确
var _health: int = 100
func get_health() -> int:
    return _health
```

### 反模式 2:硬编码 autoload 引用

```gdscript
# 错误
func _ready() -> void:
    GameManager.add_player(self)  # 可能为 null

# 正确
func _ready() -> void:
    if Engine.get_main_loop().has_node("/root/GameManager"):
        Engine.get_main_loop().get_node("/root/GameManager").add_player(self)
```

### 反模式 3:测试逻辑污染

```gdscript
# 错误
func _process(delta: float) -> void:
    move_and_slide()
    if is_in_test_mode:  # 测试逻辑混入生产代码
        _record_test_data()

# 正确
# 测试逻辑完全分离,通过接口控制
```

### 反模式 4:缺少类型注解

```gdscript
# 错误
func move_to(target):  # 缺少类型
    ...

# 正确
func move_to(target: Vector2) -> void:
    ...
```

### 反模式 5:不使用 @onready

```gdscript
# 错误
func _ready() -> void:
    var sprite = get_node("Sprite2D")  # 每次调用都查找

# 正确
@onready var sprite: Sprite2D = $Sprite2D  # 缓存引用
```

## 避免的模式

### 对象池(不需要)

GDScript 使用引用计数,不是 GC 语言:

```gdscript
# 不需要对象池
var bullet = BulletScene.instantiate()
add_child(bullet)
# 当没有引用时自动释放
```

### ECS(除非 AAA 游戏)

Godot 的节点系统已经足够灵活:

```gdscript
# 使用节点组合而非 ECS
var player = PlayerScene.instantiate()
add_child(player)
player.attack.connect(_on_attack)
```

### 复杂状态机

状态超过 3-4 个才需要状态机:

```gdscript
# 简单情况使用状态变量
var state: String = "idle"

# 复杂情况使用 StateMachine 节点
```

## 检查清单

- [ ] 所有函数有类型注解
- [ ] 变量有类型注解
- [ ] 私有属性使用下划线前缀
- [ ] 使用 @onready 而非 get_node()
- [ ] 信号正确解耦
- [ ] autoload 安全访问
- [ ] 避免循环依赖

Overview

This skill defines practical GDScript implementation guidelines to keep Godot projects consistent, testable, and engine-friendly. It focuses on type safety, proper use of Godot features (signals, @onready, @export, autoload), and avoiding common anti-patterns. Use it to standardize new code, refactors, and unit-testable designs.

How this skill works

The skill inspects GDScript patterns and recommends concrete replacements: enforce type annotations, prefer private fields with getters, use @onready for node caching, and emit/connect signals cleanly. It flags risky practices like hardcoded autoload access, circular class_name dependencies, missing types, and test pollution, and suggests safe alternatives and code idioms.

When to use it

  • Writing new GDScript features or game systems
  • Refactoring existing Godot scripts to be test-friendly
  • Configuring exported properties and editor-facing values
  • Implementing signal-based event flows and node references
  • Diagnosing circular dependencies or autoload issues

Best practices

  • Annotate all function parameters and return types; annotate variables and constants
  • Prefix private properties with an underscore and expose read-only getters
  • Cache node references with @onready instead of repeated get_node calls
  • Connect signals using callable syntax and prefer emit for decoupled events
  • Access autoloads safely by checking /root existence to avoid null errors
  • Keep test logic out of production code; separate via interfaces or test-only hooks

Example use cases

  • Converting loose-typed movement code into fully-annotated, testable functions
  • Refactoring a manager/player circular dependency using delayed binding or generic Node parameters
  • Defining editor-configurable gameplay values with @export and organized export groups
  • Replacing repeated get_node calls with @onready cached references for performance
  • Implementing health and death flows with signals to decouple UI, audio, and gameplay

FAQ

Should I always use class_name for every script?

No. Avoid class_name when it creates circular dependencies between scripts; prefer extends and delayed binding or base-class interfaces instead.

When is an object pool appropriate in Godot?

Usually unnecessary due to reference counting and scene instancing; only consider pooling for proven performance hotspots after profiling.