Godot with C#

Godot 有两套语言支持构建:
- 默认的支持 GDScript,GDExtension
- 另一套则集成了 .NET 6 支持 C#, GDScript, GDExtension。
要使用 C# 语言进行 Godot 项目开发,系统上需要安装 .NET SDK 6.0 或 7.0,.NET 7.0 的支持还不完善。相比 GDScript 脚本,C# 是一个编译型的高级语言,Godot 通过开源的 Mono 6.x 框架支持 C# 8.0 语言版本。与作用快速原型开发使用 GDScript 脚本不同,每次执行之前都需要进行编译,以生成最新的 C# 程序集。但是,作为预编译语言,它的运行效率虽然不能和 C++ 看齐,但比 GDSCript 有非常大的效率提升,简单情况有 4x 提升。并且,在使用的便利程序上,要比 C++ 好,所以在不是极限性能需要情况下,C# 是值得一试的方案。
怎么选开发语言,就是权衡开发效率与程序运行性能:
- 选择 GDScript 可以快速地做原型迭代;
- 选择 C# 一方面提升了性能,另外它比 C++/Rust 更容易上手,同时编译速度也不太慢;
- 最后,极致性能要求,那么就选择 C++/Rust 折腾去吧!
Godot 3.2.3 开始,不需安装 Mono SDK,除非需要从源代码构建 Godot,但是 .NET SDK 还是要安装。注意,使用的 Godot 要与 SDK 的版本比特位一致,建议使用 64-bit 的版本。由于 Godot 只提供了 C# 的最小支持,可以考虑使用外部编辑器,如 Visual Studio Code,以提供更完善的自动完成、调试等功能。Godot 目前支持以下作用外部编辑器,可以通过 Editor → Editor Settings → Mono → Editor 修改:
- Visual Studio 2019
- Visual Studio Code
- MonoDevelop
- Visual Studio for Mac
- JetBrains Rider
以 VSCode 配置为例,最新的 Godot 4 不需要配置 Builds:
- Set **Mono** -> **Editor** -> **External Editor** to **Visual Studio Code**.
- Set **Mono** -> **Builds** -> **Build Tool** to **dotnet CLI**.
并且在 Visual Studio Code 中安装以下扩展:
- [C#](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp)
- [Mono Debug](https://marketplace.visualstudio.com/items?itemName=ms-vscode.mono-debug)
- [C# Tools for Godot](https://marketplace.visualstudio.com/items?itemName=neikeq.godot-csharp-vscode)
C# Tools for Godot 扩展支持 Godot 3.2.2 以上版本,可以提供以下功能:
- Debugging.
- Launch a game directly in the Godot editor from VSCode.
- Additional code completion for Node paths, Input actions, Resource paths, Scene paths and Signal names.
如果使用 Linux 操作系统,需要安装 Mono SDK 以支持 Godot 的 C# 工具插件。
配置 VSCdoe 以实现 Godot 工程的运行:
- 执行菜单 Run → **Add Configuration...** → **C# Godot**
- 编辑配置文件 ``tasks.json`` 和 ``launch.json``,使用 executable 和 Command 指向正确的 Godot 可执行文件。
完成以上配置后,就可以直接在 VScode 中运行 Godot 项目:
- 使用命令调板,Ctrl-Shift-P 打开面板并输入 C# Godot 查询相关命令;
- 在状态栏中,点击 Godot Project 字样处选择工程目录;
- 在状态栏中,点击 Play in Editor (mono) 字样处运 Godot 工程;
- 直接打开 Run and Debug 侧栏面板选择 ``launch.json`` 配置的调试的方式:
- Play in Editor 直接通过编辑器中运行;
- Launch 执行 `Godot.exe --path ${workspaceRoot}`
- Launch (Select Scene) 选择场景执行 `Godot.exe --path ${workspaceRoot} ${command:SelectLaunchScene}`
- Attach 附着到本地调试进程;
如果工程不能正常运行,而 VScode 又看不到错误信息,就可以到 Godot 的 MSBuild 面板查询编译日志信息。
配置文件 ``tasks.json`` 默认只提供了一个构建任务,可以通过菜单 Terminal → Run Build Task... 执行:
如果添加运行配置时,没有提供 **C# Godot** 配置,请检查以上三个扩展是否完成安装,并且处于启用状态。配置好开发环境,就可以在 Godot 给节点附加脚本时,在 Attach Script 对话框中选择 C# 语言。
Node 节点类扩展代码示例:
使用 C# 与 GDScript 的一些差异:
- C# 使用 ``PascalCase`` 代码风格,GDSCript/C++ 使用 ``snake_case`` 风格,`AddChild()` vs. `add_child()`。
- C# 类名要求代码文件名一致,否由会提示 *"Cannot find class XXX for script res://XXX.cs"*
- C# 中使用 Godot 命令空间下的 GD 管理 @GDscript 和 @GlobbalScope 全局函数符号。
- C# 导出符号生效之前,需要重新编译程序集,通过 Godot 界面右上角的 Build 按钮构建工程。
- C# 语句使用分号作为结束符号,而 GDScript 不需要。
- C# 中以 `Godot.Object` 作为所有类型的基类,新版本更名为 `Godot.GodotObject`。
- C# 使用 this 引用当前类实例,GDScript 使用 self 引用当前类实例。
在 C# 中,也不能像 GDScript 那样,直接拖动节点到脚本中创建引用,也不能使用 onready,而需要在 Ready 这类事件中,使用 FindNode 或者 GetNode 获取节点引用:
使用 C# 进行 Godot 项目开发,还需要解决以下这些基本问题:
- 不同语言之间的相互调用问题;
- Godot 信号系统的使用方式的差异;
- C# 与 GDScript API 之间的差异;
- Godot 不同版本之间的差异处理等等;
Godot 考虑到了不同语言之间相互调用,C# 调用 GDScript API 或者属性读写使用 GodotObject 提供的方法:
而在 GDscript 访问 C# API 则是直接调用,就像使用其它 GDScript 对象一样,实例化操作如下所示:
注意,实例化得到的类型以 Godot 内置类型为准,而不是按 C# 或 GDScript 中声明的类型作为判断标准。
比如,后面的 MyNode2D 在使用 GDScript `is` 关键字做类型判断时,需要使用内置类型 Node 作为参考。
C# 调用 GDScript API 时注意,如果第一个参数是一个数组,那么就需要显式转换为 `object` 类型。否则,
数组元素就会被当作一个参数使用,并可能导致函数签名不匹配。
编写 C# 类代码时注意,类名与 ``.cs`` 代码文件名一致,否则提示错误:
Invalid call. Nonexistent function `new` in base.
比如,MyCoolNode.cs 文件就应该定义 MyCoolNode 类型。并且需要继承自 ``Godot.Object`` 或其它子类。最后,C# 工程文件 ``.csproj``中要正确引用``.cs`` 文件,这样才会生效。
Godot 4.x Mono 信号机制在 C# 使用委托机制实现,并且可以使用更高效的 += 和 -= 运算符监听、或者取消监听。另外,Connect 方法也有更新,使用 Callable 对象包装回调函数及回调参数。另外,通过节点的嵌套类 SignalName 可以访问信号名称,它继承自 GodotObject.SignalName。清理节点时,Godot 会负责所有信号监听事件的清理:
以下是 Callable 类参考文档中展示的用法,使用了嵌套类 MethodName 或者 nameof 获取方法名称。但是在默认参数绑定操作上有差别,GDScript 中可以直接调用 Callable 对象的 bind() 方法绑定默认参数。而目前在 C# 中给信号绑定默认参数则需要使用 lambdas 来构造出一个包含默认参数的 Callable 对象。如果方法没有参数,也没有返回值,可以包装成 Action,由 Callable.From() 再包装成可调用对象。
Godot 4.x Mono 所有信号定义名称使用 EventHandler 结尾,定义好信号后,完成后,Godot 在幕后自动使用 C# `event` 关键字创建相应的事件。然后,可以像其他 Godot 信号一样使用自定义信号事件。
注意,类型定义使用的 partial class,即表示类定义代码是分开文件存放的,还可以包含在其它代码文件。
除了直接使用委托方式定义信号,还可以使用 AddUserSignal() 方法添加自定义信号。
Godot 3.x Mono 自定义信号的使用有些差别,获取信号名称使用 nameof(MySignal) 语法,并且信号连接依然是使用节点的 Connect 方法进行。信号名称的获取这种操作一致性不够好,例如,获取按钮节点的信号就不能使用 nameof(Button.Pressed) 这样的表达,而是直接使用 "pressed" 字符串字面量。
总结起来,C# 中有三种获取或使用信号名称的方式,Godot 3.x 只支持前两种:
- `EmitSignal("MySignal");` 直接使用字符串字面量
- `EmitSignal(nameof(MySignal));` 使用 nameof 关键字
- `EmitSignal(SignalName.MySignal);` 使用内嵌类 SignalName
使用自定义信号可能遇到的问题是:调用 EmitSignal() 时报错,表示信号不存在,而调用 AddUserSignal() 手动添加信号时,又表示不能重复添加已经定义的信号,这可能是定义信号的代码没有写到类体内部。
一些 GDScript 遇不到的问题,如下:
一个类型中结构体,Vector2,在 C# 中是通过拷贝进行赋值的,也就是说在获取 Position 属性时,获取到的是一个副本,对这个副本赋值并不会影响到原来归属的节点。解决方法:创建一个变量引用 Position 再进行赋值操作。在 C# 10 版本中,可以对结构体使用 with 表达式来解决这种问题。
通常,使用 C# 编程而不是 GDScript,一个主要的目的可能是为了提升程序运行速度,所以应该避免编写无效率的代码,以下是两种节点的移动方式对比,后者更高效:
给 Godot C# API 传递原始的 byte[] 或者字符串,需要 marshalling 操作,这相对不够高效。隐式转换 string 为 NodePath 或 StringName 会产生原生互操作和 marshalling 成本,因为字符串必须编组并传递给相应的原生构造函数。
参考:
- http://www.mono-project.com/docs/about-mono/compatibility
- https://docs.godotengine.org/en/3.5/tutorials/scripting/c_sharp/c_sharp_features.html
- https://docs.godotengine.org/en/3.5/tutorials/scripting/c_sharp/c_sharp_differences.html
- https://docs.godotengine.org/en/stable/tutorials/scripting/cross_language_scripting.html
- https://docs.godotengine.org/en/stable/tutorials/scripting/creating_script_templates.html
- https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/index.html
- https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/c_sharp_basics.html
- https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/c_sharp_exports.html
- https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/c_sharp_differences.html
- https://docs.godotengine.org/en/latest/tutorials/scripting/c_sharp/c_sharp_signals.html
- https://docs.godotengine.org/en/latest/contributing/development/core_and_modules/object_class.html#doc-binding-properties-using-set-get-property-list
- [Bunnymark](https://github.com/cart/godot3-bunnymark)
- [Godot.NET.Sdk](https://www.nuget.org/packages/Godot.NET.Sdk/#versions-body-tab)
- [Mono SDK](https://www.mono-project.com/download/stable/#download-lin)
- [.NET SDK](https://dotnet.microsoft.com/download)
- [Visual Studio Code](https://code.visualstudio.com/)
- [Godot 3.5.1 Mono](https://downloads.tuxfamily.org/godotengine/3.5.1/mono/)
- [CustomType for Godot 3.x](https://github.com/Jeangowhy/Godot-Tour/tree/main/mono-3x/addons/CustomType)
- [CustomType for Godot 4.x](https://github.com/Jeangowhy/Godot-Tour/tree/4.x/mono-4x/addons/CustomType)