godot-dev

SKILL.md

Godot 代码实现规范

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

适用场景

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

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] = []

属性可见性

# 私有属性(使用下划线前缀)
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

常量命名

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

信号定义

# 状态变化信号
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(观察者模式)

# 定义信号
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(节点引用)

extends Node2D

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

@export(编辑器配置)

@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(单例)

# 获取 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 无法加载存在循环依赖的脚本。

# 错误:循环依赖
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
    ...

解决方案

# 方案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

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:直接暴露内部变量

# 错误
var health: int = 100

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

反模式 2:硬编码 autoload 引用

# 错误
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:测试逻辑污染

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

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

反模式 4:缺少类型注解

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

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

反模式 5:不使用 @onready

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

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

避免的模式

对象池(不需要)

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

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

ECS(除非 AAA 游戏)

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

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

复杂状态机

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

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

# 复杂情况使用 StateMachine 节点

检查清单

  • 所有函数有类型注解
  • 变量有类型注解
  • 私有属性使用下划线前缀
  • 使用 @onready 而非 get_node()
  • 信号正确解耦
  • autoload 安全访问
  • 避免循环依赖
Weekly Installs
1
First Seen
6 days ago
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1