mcpField
C
csharp-godot

csharp-godot

github.com/jame581开发889168 次安装4 天前更新
代码仓库

关于 csharp-godot

面向 Godot 4.3+ 开发者的 C# 技能指南,涵盖语言差异、项目设置与互操作。通过示例和常见问题说明 GodotSharp 的使用约定。

主要功能

  1. 01C# 与 GDScript 语法对照
  2. 02Godot C# 项目与 IDE 设置
  3. 03信号及异步处理模式
  4. 04C# 与 GDScript 互操作
  5. 05性能指导与常见问题

适用场景

  1. 01将 GDScript 代码模式改写为 C#
  2. 02配置 Godot C# 项目和开发环境
  3. 03处理 Godot 信号与异步操作

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jame581/godotprompter --skill csharp-godot

安装后在 Claude Code 中输入/csharp-godot确认已加载。

附带文件references/gdscript-interop.md

详细介绍

Godot 4.3+ 中的 C#

本技能介绍 C# 特有的约定、与 GDScript 的 API 差异、项目设置和互操作模式。所有示例均为 C#。目标版本为 Godot 4.3+,并使用 GodotSharp NuGet 包。

相关技能: 信号相关的 C# 模式见 csharp-signals,C# 项目脚手架见 godot-project-setup,使用 gdUnit4 进行 C# 测试见 godot-testing,当 C# 无法满足需求时使用原生 C++ 见 gdextension,C# 并发见 multithreading。


1. C# 与 GDScript 语法对照

GDScriptC# 等效写法说明
var x = 5var x = 5; 或带类型的 int x = 5;C# 的 var 会在编译时推断类型
func MyMethod() -> void:public void MyMethod() { }C# 中的方法使用 PascalCase
signal health_changed(amount: int)[Signal] delegate void HealthChangedEventHandler(int amount);必须使用 EventHandler 后缀
@export var speed: float = 100.0[Export] public float Speed { get; set; } = 100f;使用 PascalCase 和属性语法
@onready var label = $Labelprivate Label _label; + 在 _Ready() 中执行 _label = GetNode<Label>("Label");没有对应的 @onready;请使用 _Ready()
match value:switch (value) { case X: break; }C# 的 switch 也支持模式匹配
class_name MyClass[GlobalClass] public partial class MyClass : GodotObject { }需要 [GlobalClass] 特性
extends Nodepublic partial class MyScript : Node { }使用 : 进行继承
preload("res://scene.tscn")GD.Load<PackedScene>("res://scene.tscn")在运行时加载,而非编译时
push_error("msg")GD.PushError("msg");输出到 Godot 错误日志
print("msg")GD.Print("msg");还包括:GD.PrintS()、GD.PrintT()
node is CharacterBody2Dnode is CharacterBody2D关键字相同,语义也相同
node as CharacterBody2Dnode as CharacterBody2D两者在失败时都会返回 null
await signal_nameawait ToSignal(source, SignalName.X);必须使用 ToSignal() 包装器
ArrayGodot.Collections.Array不是 System.Collections.Generic.List<T>
DictionaryGodot.Collections.Dictionary不是 System.Collections.Generic.Dictionary<K,V>

2. 项目设置

.csproj 和解决方案

通过 Script > New Script > C# 创建第一个 C# 脚本时,Godot 会自动生成 .csproj。不要手动编辑生成文件的结构——让编辑器管理即可。

MyProject/
├── MyProject.csproj          # Auto-generated, edit only for NuGet packages
├── MyProject.sln             # Auto-generated solution file
├── project.godot
└── scripts/
    └── Player.cs
NuGet 包

在 MyProject.csproj 的 <ItemGroup> 内添加包:

<Project Sdk="Godot.NET.Sdk/4.3.0">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>
  <ItemGroup>
    
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
  </ItemGroup>
</Project>

运行 dotnet restore,或编辑后让 IDE 自动还原。

Godot.NET 预览绑定(Godot 4.7+)

Godot 4.7 的 Godot.NET.Sdk 接受一个可选的 MSBuild 属性,可将项目从经典 GodotSharp 绑定切换为预览版 Godot.NET 绑定(使用 Godot.Bindings 程序集,而非 GodotSharp):

<PropertyGroup>
  <EnableGodotDotNetPreview>true</EnableGodotDotNetPreview>
</PropertyGroup>

预览绑定尚处于实验阶段——生产项目请不要设置该属性。参见 GH-118001。

IDE 设置
IDE所需设置说明
JetBrains Rider安装 Godot 插件(Rider 2023.3+ 已捆绑)Godot C# 支持最佳;调试器开箱即用
VS Code安装 C# Dev Kit + Godot Tools 扩展调试器连接需要 launch.json
Visual Studio安装 Godot Visual Studio 扩展仅限 Windows;通过 Tools > Attach to Process 使用调试器

对于所有 IDE,请打开 .sln 文件(而不只是文件夹),以完整解析解决方案。


3. partial class 要求

所有继承 Godot 类型的类都必须声明为 partial。这是强制要求。

原因

Godot 使用 C# 源生成器,在你的类旁生成信号注册、属性绑定和 RPC 代码。源生成器需要 partial,才能注入同一类声明。

遗漏时的错误
error GD0001: Missing partial modifier on declaration of type 'Player'
that derives from 'Godot.GodotObject'

构建会失败,因此不会漏掉 partial。

规则
// CORRECT
public partial class Player : CharacterBody2D { }

// WRONG — will cause source generator errors
public class Player : CharacterBody2D { }

这适用于继承链中每个扩展 Godot 类型的类,包括中间基类。


4. 命名约定

元素约定示例
方法PascalCasepublic void TakeDamage(int amount)
属性PascalCasepublic float MaxHealth { get; set; }
信号(委托)PascalCase + EventHandler 后缀HealthChangedEventHandler
[Export] 属性PascalCase[Export] public float Speed { get; set; }
私有字段_camelCase,以下划线为前缀private float _currentSpeed;
局部变量camelCasevar newPosition = ...
参数camelCasevoid SetHealth(int newHealth)
Godot API 名称与 Godot 的 PascalCase 完全一致GlobalPosition,而非 global_position
枚举类型使用 PascalCase,成员使用 PascalCaseenum State { Idle, Running, Dead }
常量根据团队风格使用 PascalCase 或 SCREAMING_SNAKEconst float MaxSpeed = 200f;

始终确保 GodotSharp 的属性和方法名称完全一致——它们是 GDScript snake_case 名称对应的 PascalCase 形式(例如 is_on_floor() → IsOnFloor())。


5. C# 中的信号

信号需要使用 [Signal] 特性声明委托。委托名称必须以 EventHandler 结尾。

using Godot;

public partial class Player : CharacterBody2D
{
    // Declaration
    [Signal] public delegate void HealthChangedEventHandler(int newHealth);
    [Signal] public delegate void DiedEventHandler();

    private int _health = 100;

    public void TakeDamage(int amount)
    {
        _health -= amount;
        EmitSignal(SignalName.HealthChanged, _health);  // Use SignalName.X, not a string
        if (_health <= 0)
            EmitSignal(SignalName.Died);
    }
}
连接和断开连接
// Connect with +=
player.HealthChanged += OnHealthChanged;
player.Died += OnPlayerDied;

// Disconnect with -=
player.HealthChanged -= OnHealthChanged;
player.Died -= OnPlayerDied;

// Handler signatures must match the delegate
private void OnHealthChanged(int newHealth) { ... }
private void OnPlayerDied() { ... }

有关完整的信号模式,包括一次性连接、静态类型信号和跨语言信号连接,请参阅 csharp-signals 技能。


6. Async / Await

GDScript 的 await 对应 C# 的 await ToSignal(...)。Godot 信号会返回与 C# await 兼容的 SignalAwaiter。

等待 Godot 信号
public async void StartCutscene()
{
    // Wait for a timer to finish
    await ToSignal(GetTree().CreateTimer(2.0), SceneTreeTimer.SignalName.Timeout);

    // Wait for an animation to finish
    var anim = GetNode<AnimationPlayer>("AnimationPlayer");
    anim.Play("intro");
    await ToSignal(anim, AnimationPlayer.SignalName.AnimationFinished);

    GD.Print("Cutscene complete");
}
基于 Task 的模式

对于 CPU 密集型工作,使用 Task.Run——但绝不要从非主线程访问 Godot 对象:

public async void LoadHeavyData()
{
    // Off main thread: pure C# computation only
    var result = await Task.Run(() => ComputeSomethingExpensive());

    // Back on main thread: safe to use Godot API
    ApplyResult(result);
}

private int ComputeSomethingExpensive()
{
    // No Godot API calls here
    return Enumerable.Range(0, 1_000_000).Sum();
}
GDScript await 对应写法
GDScriptC#
await get_tree().create_timer(1.0).timeoutawait ToSignal(GetTree().CreateTimer(1.0), SceneTreeTimer.SignalName.Timeout)
await animation_player.animation_finishedawait ToSignal(animPlayer, AnimationPlayer.SignalName.AnimationFinished)
await signal_nameawait ToSignal(this, SignalName.YourSignal)

7. GDScript 互操作

C# 使用 Call、Get 和 Set 调用 GDScript 对象;当 C# 类注册为 [GlobalClass] 时,GDScript 可以直接使用该类。值会以 Variant 的形式跨越边界,参考列表也列出了编组时需要注意的问题(null、int[]、List<T>)。

有关 Call/Get/Set 示例、[GlobalClass] 模式和 Variant 编组表,请参阅 references/gdscript-interop.md。


8. 性能

C# 与 GDScript
工作负载更优者原因
数学密集型循环(寻路、模拟)C#(显著更快)编译后的 JIT 与解释执行的 GDScript 相比
大型数组/集合处理C#值类型数组可避免装箱
Godot API 调用(move_and_slide 等)大致相同两者都通过同一个 C++ 引擎进行调用
场景树操作大致相同瓶颈在于 C++ 开销,而非语言
快速原型开发GDScript样板代码更少,无需重新编译即可热重载
编辑器工具(插件、@tool)GDScriptC# 工具脚本需要完整的构建流程
指南
  • 对于循环频繁的系统,请使用 C#:例如物理求解器、程序化生成、AI 决策树和数据处理。
  • 对于编辑器插件、@tool 脚本以及快速迭代游戏逻辑,请使用 GDScript。
  • 同一项目中可以混用语言——偶尔进行跨语言调用的互操作成本很低。
  • 避免在性能关键路径中使用 Godot.Collections.Array 和 Array<T>——Array<T> 封装的是同一个引擎数组,因此每次访问元素时仍会封送一个 Variant。请使用 T[] 或 List<T>,并在边界处转换。
  • StringName 查找的时间复杂度为 O(1),但构造 StringName 并非如此——如果频繁创建,请缓存起来。

9. 常见陷阱

陷阱问题修复方法
Variant 转换为 C# 类型(float)v 与 v.AsSingle() 调用相同:两者都不会抛出异常——int、bool 和数字字符串会转换,其他类型都会得到 0当存储类型很重要时,检查 v.VariantType
null 与 default(Variant)Godot 信号未传递值时会给出 default(Variant),而非 C# 的 null检查 .VariantType == Variant.Type.Nil
Godot.Collections 与 System.CollectionsGodot API 方法会返回 Godot.Collections.Array;List<T> 无法转换为 Variant,因此传入后无法编译在 Godot API 边界处始终使用 Godot.Collections.Array/Dictionary
释放原生对象对已释放的 Godot 对象调用方法会抛出 ObjectDisposedException使用前检查 IsInstanceValid(obj)
在高频循环中构造 StringName每次调用 new StringName("my_signal") 都会分配内存缓存为 private static readonly StringName _signalName = "my_signal";
导出数组类型[Export] public Array Items; 会导出无类型数组使用 [Export] public Godot.Collections.Array<MyResource> Items { get; set; }
节点路径字符串如果路径发生变化,GetNode("../UI/Label") 会记录 Node not found,并在运行时返回 null使用唯一名称调用 GetNode<Label>("%Label"),或使用带类型的 [Export] 节点引用
忘记 partial class构建错误 GD0001每个继承 Godot 类型的类都必须是 partial
async void 与 async Taskasync void 无法等待;无人等待的 async Task 会隐藏其异常(Godot 会记录 async void 异常)返回 Task 并等待它;对于由 Godot 调用的顶层事件处理程序,使用 async void

⚠️ Godot 4.7 中的变更: 4.7 的 C# 程序集带来了多项兼容性变更。仅二进制不兼容(源代码仍可编译——只需重新构建):OptimizedTranslation.Generate() 现在返回 bool 而不是 void(GH-119563)。二进制和源代码均不兼容(需要修改代码):Animation.Length 的类型元数据从 float 改为 double(GH-116394)——C# 属性现在是 double,因此 float len = anim.Length; 将无法编译;Control.AccessibilityLive 的枚举类型从 DisplayServer.AccessibilityLiveMode 改为 AccessibilityServer.AccessibilityLiveMode(GH-116839)——请将声明更新为 AccessibilityServer 枚举。源代码不兼容(现有二进制仍可加载):RichTextLabel.ImageUpdateMask.WidthInPercent 已重命名为 WidthUnit(GH-112617;GDScript 中的 UPDATE_WIDTH_IN_PERCENT → UPDATE_WIDTH_UNIT),EditorSceneFormatImporter 的 IMPORT_* 常量已移至 ImportFlags 枚举中(GH-115788)。已移除:AudioEffectSpectrumAnalyzer.TapBackPos(GH-114355)。请针对 4.7 重新构建,并修复受影响的符号。请参阅 4.7 迁移指南。


10. 检查清单

  • 每个继承 Godot 类型的类都声明为 partial
  • 方法名称符合 GodotSharp PascalCase 命名规范(例如 _Ready、_PhysicsProcess、IsOnFloor())
  • [Export] 属性使用 PascalCase 命名,并采用属性语法({ get; set; })
  • 信号委托以 EventHandler 结尾,并使用 EmitSignal(SignalName.X)
  • 使用 += 连接信号,并在不再需要时使用 -= 断开连接
  • 对 Godot 信号使用 await ToSignal(...),不要将原始 Task.Delay 用于游戏计时
  • 在 Godot API 边界处使用 Godot.Collections.Array/Dictionary,而不是 System.Collections 类型
  • 使用任何可能已释放的 Godot 对象前,先检查 IsInstanceValid(obj)
  • 如果在循环或频繁调用的方法中构造 StringName 实例,请缓存这些实例
  • 不要在 Task.Run(...) 回调中调用 Godot API;所有引擎调用都必须在主线程上执行
  • 在 IDE 中通过 .sln 文件打开 .csproj,以完整解析解决方案
  • 计算密集型系统使用 C#;编辑器工具和 @tool 脚本保留使用 GDScript

评论

支持 Markdown