GFramework/docs/zh-CN/source-generators/get-node-generator.md
GeWuYou 38020c32a2 docs(source-generators): 添加源代码生成器完整文档
- 新增 GFramework.SourceGenerators 主文档,介绍编译时代码生成工具
- 详细说明 Log 属性生成器的使用方法和配置选项
- 完整描述 ContextAware 属性生成器的功能和测试场景配置
- 添加 GenerateEnumExtensions 属性生成器文档和使用示例
- 介绍 GetNode 生成器(Godot 专用)的节点获取功能
- 新增 BindNodeSignal 生成器文档,说明信号绑定与解绑机制
- 提供 Context Get 注入生成器的完整使用指南
- 添加诊断信息章节,涵盖所有生成器的错误提示和解决方案
- 包含性能优势对比和基准测试结果
- 提供多个完整使用示例,展示实际应用场景
- 整理最佳实践和常见问题解答
- 添加 BindNodeSignal 生成器专用文档,详细介绍其高级用法
2026-03-31 15:10:06 +08:00

12 KiB
Raw Blame History

GetNode 生成器

自动生成 Godot 节点获取逻辑,简化节点引用代码

概述

GetNode 生成器为标记了 [GetNode] 特性的字段自动生成 Godot 节点获取代码,无需手动调用 GetNode<T>() 方法。这在处理复杂 UI 或场景树结构时特别有用。

核心功能

  • 自动节点获取:根据路径或字段名自动获取节点
  • 多种查找模式:支持唯一名、相对路径、绝对路径查找
  • 可选节点支持:可以标记节点为可选,获取失败时返回 null
  • 智能路径推导:未显式指定路径时自动从字段名推导
  • _Ready 钩子生成:自动生成 _Ready() 方法注入节点获取逻辑

基础使用

标记节点字段

使用 [GetNode] 特性标记需要自动获取的节点字段:

using GFramework.Godot.SourceGenerators.Abstractions;
using Godot;

public partial class PlayerHud : Control
{
    [GetNode]
    private Label _healthLabel = null!;

    [GetNode]
    private ProgressBar _manaBar = null!;

    [GetNode("ScoreContainer/ScoreValue")]
    private Label _scoreLabel = null!;

    public override void _Ready()
    {
        __InjectGetNodes_Generated();
        _healthLabel.Text = "100";
    }
}

生成的代码

编译器会为标记的类自动生成以下代码:

// <auto-generated />
#nullable enable

namespace YourNamespace;

partial class PlayerHud
{
    private void __InjectGetNodes_Generated()
    {
        _healthLabel = GetNode<global::Godot.Label>("%HealthLabel");
        _manaBar = GetNode<global::Godot.ProgressBar>("%ManaBar");
        _scoreLabel = GetNode<global::Godot.Label>("ScoreContainer/ScoreValue");
    }

    partial void OnGetNodeReadyGenerated();

    public override void _Ready()
    {
        __InjectGetNodes_Generated();
        OnGetNodeReadyGenerated();
    }
}

配置选项

节点查找模式

通过 Lookup 参数控制节点查找方式:

public partial class GameHud : Control
{
    // 自动推断(默认):根据路径前缀自动选择
    [GetNode]
    private Label _titleLabel = null!;  // 默认使用唯一名 %TitleLabel

    // 唯一名查找
    [GetNode(Lookup = NodeLookupMode.UniqueName)]
    private Button _startButton = null!;  // %StartButton

    // 相对路径查找
    [GetNode("UI/HealthBar", Lookup = NodeLookupMode.RelativePath)]
    private ProgressBar _healthBar = null!;

    // 绝对路径查找
    [GetNode("/root/Main/GameUI/Score", Lookup = NodeLookupMode.AbsolutePath)]
    private Label _scoreLabel = null!;
}

查找模式说明

模式 路径前缀 适用场景
Auto 自动选择 默认行为,推荐用于大多数场景
UniqueName % 场景中使用唯一名的节点
RelativePath 需要相对路径查找的节点
AbsolutePath / 场景树根节点的绝对路径

可选节点

对于可能不存在的节点,可以设置为非必填:

public partial class SettingsPanel : Control
{
    // 必须存在的节点(默认)
    [GetNode]
    private Label _titleLabel = null!;

    // 可选节点,可能不存在
    [GetNode(Required = false)]
    private Label? _debugLabel;  // 使用可空类型

    // 显式路径的可选节点
    [GetNode("AdvancedOptions", Required = false)]
    private VBoxContainer? _advancedOptions;

    public override void _Ready()
    {
        __InjectGetNodes_Generated();

        // 安全地访问可选节点
        _debugLabel?.Hide();
        _advancedOptions?.Hide();
    }
}

路径规则

生成器根据字段名和配置自动推导节点路径:

public partial class Example : Control
{
    // 驼峰命名 → PascalCase 路径
    [GetNode]
    private Label _playerNameLabel = null!;  // → %PlayerNameLabel

    // m_ 前缀会被移除
    [GetNode]
    private Button m_confirmButton = null!;  // → %ConfirmButton

    // _ 前缀会被移除
    [GetNode]
    private ProgressBar _healthBar = null!;  // → %HealthBar

    // 显式路径优先于推导
    [GetNode("UI/CustomPath")]
    private Label _myLabel = null!;  // → UI/CustomPath
}

高级用法

与 [ContextAware] 组合使用

在 Godot 项目中结合使用 [GetNode][ContextAware]

using GFramework.Godot.SourceGenerators.Abstractions;
using GFramework.SourceGenerators.Abstractions.Rule;
using Godot;

[ContextAware]
public partial class GameController : Node
{
    [GetNode]
    private Label _scoreLabel = null!;

    [GetNode("HUD/HealthBar")]
    private ProgressBar _healthBar = null!;

    private IGameModel _gameModel = null!;

    public override void _Ready()
    {
        __InjectContextBindings_Generated();  // ContextAware 生成
        __InjectGetNodes_Generated();          // GetNode 生成

        _gameModel.Score.Register(OnScoreChanged);
    }

    private void OnScoreChanged(int newScore)
    {
        _scoreLabel.Text = newScore.ToString();
    }
}

复杂 UI 场景

处理复杂的嵌套 UI 结构:

public partial class InventoryUI : Control
{
    // 主容器
    [GetNode]
    private GridContainer _itemGrid = null!;

    // 详细信息面板
    [GetNode("DetailsPanel/ItemName")]
    private Label _itemNameLabel = null!;

    [GetNode("DetailsPanel/ItemDescription")]
    private RichTextLabel _itemDescription = null!;

    // 操作按钮
    [GetNode("Actions/UseButton")]
    private Button _useButton = null!;

    [GetNode("Actions/DropButton")]
    private Button _dropButton = null!;

    // 可选的统计信息
    [GetNode("DetailsPanel/Stats", Required = false)]
    private VBoxContainer? _statsContainer;

    public override void _Ready()
    {
        __InjectGetNodes_Generated();

        // 使用注入的节点
        _useButton.Pressed += OnUseButtonPressed;
        _dropButton.Pressed += OnDropButtonPressed;
    }
}

手动 _Ready 调用

如果类已经有 _Ready() 方法,需要手动调用注入方法:

public partial class CustomHud : Control
{
    [GetNode]
    private Label _statusLabel = null!;

    public override void _Ready()
    {
        // 必须手动调用节点注入
        __InjectGetNodes_Generated();

        // 自定义初始化逻辑
        _statusLabel.Text = "Ready";
        InitializeOtherComponents();
    }

    partial void OnGetNodeReadyGenerated()
    {
        // 这个方法会被生成器调用,可以在此添加额外初始化
    }
}

注意:如果不手动调用 __InjectGetNodes_Generated(),编译器会发出警告 GF_Godot_GetNode_006

诊断信息

生成器会在以下情况报告编译错误或警告:

GF_Godot_GetNode_001 - 不支持嵌套类

错误信息Class '{ClassName}' cannot use [GetNode] inside a nested type

解决方案:将嵌套类提取为独立的类

// ❌ 错误
public partial class Outer
{
    public partial class Inner
    {
        [GetNode]
        private Label _label = null!;  // 错误
    }
}

// ✅ 正确
public partial class Inner
{
    [GetNode]
    private Label _label = null!;
}

GF_Godot_GetNode_002 - 不支持静态字段

错误信息Field '{FieldName}' cannot be static when using [GetNode]

解决方案:改为实例字段

// ❌ 错误
[GetNode]
private static Label _label = null!;

// ✅ 正确
[GetNode]
private Label _label = null!;

GF_Godot_GetNode_003 - 不支持只读字段

错误信息Field '{FieldName}' cannot be readonly when using [GetNode]

解决方案:移除 readonly 关键字

// ❌ 错误
[GetNode]
private readonly Label _label = null!;

// ✅ 正确
[GetNode]
private Label _label = null!;

GF_Godot_GetNode_004 - 字段类型必须继承自 Godot.Node

错误信息Field '{FieldName}' must be a Godot.Node type to use [GetNode]

解决方案:确保字段类型继承自 Godot.Node

// ❌ 错误
[GetNode]
private string _text = null!;  // string 不是 Node 类型

// ✅ 正确
[GetNode]
private Label _label = null!;  // Label 继承自 Node

GF_Godot_GetNode_005 - 无法推导路径

错误信息Field '{FieldName}' does not provide a path and its name cannot be converted to a node path

解决方案:显式指定节点路径

// ❌ 错误:字段名无法转换为有效路径
[GetNode]
private Label _ = null!;

// ✅ 正确
[GetNode("UI/Label")]
private Label _ = null!;

GF_Godot_GetNode_006 - 需要在 _Ready 中调用注入方法

警告信息 Class '{ClassName}' defines _Ready(); call __InjectGetNodes_Generated() there or remove _Ready() to use the generated hook

解决方案:在 _Ready() 中手动调用 __InjectGetNodes_Generated()

public partial class MyHud : Control
{
    [GetNode]
    private Label _label = null!;

    public override void _Ready()
    {
        __InjectGetNodes_Generated();  // ✅ 必须手动调用
        // 其他初始化...
    }
}

最佳实践

1. 使用一致的命名约定

保持字段名与场景树中节点名的一致性:

// ✅ 推荐:字段名与节点名一致
[GetNode]
private Label _healthLabel = null!;  // 场景中的节点名为 HealthLabel

[GetNode]
private Button _startButton = null!;  // 场景中的节点名为 StartButton

2. 优先使用唯一名查找

在 Godot 编辑器中为重要节点启用唯一名Unique Name然后使用 [GetNode]

// Godot 场景中:%HealthBar唯一名已启用
// C# 代码中:
[GetNode]
private ProgressBar _healthBar = null!;  // 自动使用 %HealthBar

3. 合理处理可选节点

对于可能不存在的节点,使用 Required = false

public partial class DynamicUI : Control
{
    [GetNode]
    private Label _titleLabel = null!;

    // 可选组件
    [GetNode(Required = false)]
    private TextureRect? _iconImage;

    public override void _Ready()
    {
        __InjectGetNodes_Generated();

        // 安全地初始化可选组件
        if (_iconImage != null)
        {
            _iconImage.Texture = LoadDefaultIcon();
        }
    }
}

4. 组织复杂 UI 的路径

对于深层嵌套的 UI使用显式路径

public partial class ComplexUI : Control
{
    // 使用相对路径明确表达层级关系
    [GetNode("MainContent/Header/Title")]
    private Label _title = null!;

    [GetNode("MainContent/Body/Stats/Health")]
    private Label _healthValue = null!;

    [GetNode("MainContent/Footer/ActionButtons/Save")]
    private Button _saveButton = null!;
}

5. 与 GetNode 方法的对比

方式 代码量 可维护性 类型安全 推荐场景
手动 GetNode() 需要显式转换 简单场景
[GetNode] 特性 编译时检查 复杂 UI、控制器
// ❌ 不推荐:手动获取
public override void _Ready()
{
    _healthLabel = GetNode<Label>("%HealthLabel");
    _manaBar = GetNode<ProgressBar>("%ManaBar");
    _scoreLabel = GetNode<Label>("ScoreContainer/ScoreValue");
}

// ✅ 推荐:使用 [GetNode] 特性
[GetNode]
private Label _healthLabel = null!;

[GetNode]
private ProgressBar _manaBar = null!;

[GetNode("ScoreContainer/ScoreValue")]
private Label _scoreLabel = null!;

public override void _Ready()
{
    __InjectGetNodes_Generated();
}

相关文档