关于 csharp-godot
面向 Godot 4.3+ 开发者的 C# 技能指南,涵盖语言差异、项目设置与互操作。通过示例和常见问题说明 GodotSharp 的使用约定。
主要功能
- 01C# 与 GDScript 语法对照
- 02Godot C# 项目与 IDE 设置
- 03信号及异步处理模式
- 04C# 与 GDScript 互操作
- 05性能指导与常见问题
适用场景
- 01将 GDScript 代码模式改写为 C#
- 02配置 Godot C# 项目和开发环境
- 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 语法对照
| GDScript | C# 等效写法 | 说明 |
|---|---|---|
var x = 5 | var 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 = $Label | private 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 Node | public 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 CharacterBody2D | node is CharacterBody2D | 关键字相同,语义也相同 |
node as CharacterBody2D | node as CharacterBody2D | 两者在失败时都会返回 null |
await signal_name | await ToSignal(source, SignalName.X); | 必须使用 ToSignal() 包装器 |
Array | Godot.Collections.Array | 不是 System.Collections.Generic.List<T> |
Dictionary | Godot.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. 命名约定
| 元素 | 约定 | 示例 |
|---|---|---|
| 方法 | PascalCase | public void TakeDamage(int amount) |
| 属性 | PascalCase | public float MaxHealth { get; set; } |
| 信号(委托) | PascalCase + EventHandler 后缀 | HealthChangedEventHandler |
[Export] 属性 | PascalCase | [Export] public float Speed { get; set; } |
| 私有字段 | _camelCase,以下划线为前缀 | private float _currentSpeed; |
| 局部变量 | camelCase | var newPosition = ... |
| 参数 | camelCase | void SetHealth(int newHealth) |
| Godot API 名称 | 与 Godot 的 PascalCase 完全一致 | GlobalPosition,而非 global_position |
| 枚举 | 类型使用 PascalCase,成员使用 PascalCase | enum State { Idle, Running, Dead } |
| 常量 | 根据团队风格使用 PascalCase 或 SCREAMING_SNAKE | const 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 对应写法
| GDScript | C# |
|---|---|
await get_tree().create_timer(1.0).timeout | await ToSignal(GetTree().CreateTimer(1.0), SceneTreeTimer.SignalName.Timeout) |
await animation_player.animation_finished | await ToSignal(animPlayer, AnimationPlayer.SignalName.AnimationFinished) |
await signal_name | await 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) | GDScript | C# 工具脚本需要完整的构建流程 |
指南
- 对于循环频繁的系统,请使用 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.Collections | Godot 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 Task | async 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