Godot Linux 的 C# 配置

因为目前主要系统迁移到 Ubuntu/Debian 上, 基于这种情况就全面转移到 Linux 平台做相关游戏业务开发

这里主要是针对 Ubuntu 的 Godot C# 版本, 需要配置 dotnet 环境相关和 JetBrains Rider 的开发环境, 首先需要安装对应组件依赖:

这里官方文档都有脚本安装和 apt 安装配置; 注意不要安装 snap 版本, 这个版本会让 Rider 无法自动扫描到环境变量配置

dotnet 安装: https://learn.microsoft.com/zh-cn/dotnet/core/install

新版本 Rider 现在对于个人已经是免费提供, 所以直接去官网下载安装配置就行了; 只要你提前安装好 dotnet, Rider 就会自动扫描工具链

工具链配置在 Settings → Build,Execution,Deployment → Toolset And Build 内部, 确认 CLI 和 MSBuild 已经自动扫描到

之后就是 Godot 版本, 目前 Godot 官网默认下载不带 C#, 需要去对应页面选择下载

GodotC# Linux 下载: https://godotengine.org/download/linux

这里必须前置安装好 dotnet-sdk-8.0 以上, 剩下就是一些细节配置和功能相关, 启动 GodotC# 二进制首先配置好代码风格:

code-style

这里选择项目目录代码风格, 各自分割选项如下:

  • kebab-case: 中划线风格, 单词之间采用 “-” 划分, 比如 “godot-project”

  • snake_case: 下划线风格, 单词之间采用 “_” 划分, 比如 “godot_project”

  • camelCase: 小驼峰风格, 首字母必须采用小写, 之后单词采用大写划分, 如 “godotProject”

  • PascalCase: 大驼峰风格, 首字母和单词之间必须采用大写划分, 如 “GodotProject”

  • Title Case: 自然风格, 单词之间采用空格划分的自动声明, 如 “Godot Project”

这几种风格首先不推荐 Title Case, 因为目录空格处理可能因为某些情况不好区分几个空格导致错误.

剩下这几种对于默认版本 Godot 其实没什么差别, 但是如果采用 GodotC# 版本最好和 Unity3D 尽可能匹配, 所以采用 PascalCase 风格

C# 官方编码规范(Microsoft 推荐)明确以下代码规范:

  • 类名、结构体名、枚举名、命名空间必须用 PascalCase

  • 公共方法、公共属性、常量也推荐用 PascalCase

这样方便后续切换到 Unity3D 等相关 C# 的平台

另外需要注意, 如果你采用官方的脚本安装 dotnet, 启动 GodotC# 会出现以下异常:

1
2
3
4
Unable to load .NET runtime, specifically hostfxr.
Attempting to create/edit a project will lead to a crash.

Please install the .NET SDK 8.0 or later from https://get.dot.net and restart Godot.

这代表没有扫描到系统 dotnet 的配置, Linux 下开发 Godot C#, dotnet 环境必须是系统全局可访问的

所以这部分开发最好采用 apt 安装方式来安装, 确保删除脚本安装的 dotnet 并执行以下命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 首先删除掉内部脚本安装的 .dotnet 目录
# 之后删除 ~/.bashrc 当中的 DOTNET_ROOT 和 PATH=$PATH:$DOTNET_ROOT 配置
# 最后更新管理员环境变量
source ~/.bashrc

# 最后采用 apt 安装配置 dotnet 环境, 目前官方 LTS 版本为 10.0
# hostfxr 是运行时宿主库
# sdk 是开发库
sudo apt install dotnet-hostfxr-10.0 dotnet-sdk-10.0 apt-transport-https ca-certificates

# 系统安装完成之后查看具体版本信息
# 确保命令有输出内容
dotnet --info|grep DOTNET_ROOT

这样重新安装之后启动 GodotC# 就不会出现启动找不到 dotnet 的问题

IDE 配置

这里随便建立项目方便对开发环境做进一步配置, 如果是在比较新 Ubuntu 版本就会发现输入法冲突异常

因为最新版本主流 Linux 环境强推 Wayland, 而默认 Godot 启动的是 x11 桌面环境, 建议首选 Wayland 环境启动

编辑器配置(Editor Settings) → 运行(Run) → 平台(Platforms) 下勾选首选 Wayland 并重启:

use-wayland

这种就不会出现输出法卡死的问题, 之后就是启用默认开发工具为 Rider

编辑器设置(Editor Settings) → 勾选高级设置(Advanced Settings) → 找到 Dotnet → Editor 配置:

use-rider

这样默认的启动 IDE 就是 JetBrains Rider, 现在可以开始 GodotC# 的具体开发

示例: 聊天室

这里采用官方的 WebSocket GDScript 例子来开发 C# 版本的简单聊天室功能, 这里简单布局如下:

godot-layout

如果创建之后发现界面元素错位, 需要检查是否 Container 节点元素的 Expand 已经启用

container-expand

之后创建 ChatSession.cs 脚本,双击之后就会启动 Rider 来运行 GodotC# 项目代码, 这里简单挂载代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
using System;
using System.Net.Sockets;
using System.Text;
using Godot;
using Environment = System.Environment;

// 这里将项目统一命名到具体命名空间, 可以防止污染全局
namespace ChatRoom;

/// <summary>
/// 基于 Godot 聊天室会话脚本, 这里采用 Godot 内部 TCP, 而非 C# 原生 TCP 方式
/// </summary>
public partial class ChatSession : Control
{
/// <summary>
/// TCP 客户端句柄
/// </summary>
private StreamPeerTcp _client;

/// <summary>
/// 是否已经连接
/// </summary>
private bool _connected;


#region 连接服务器信息

/// <summary>
/// 暴露给编辑器的绑定服务器地址功能
/// </summary>
[Export]
public LineEdit ConnectHostnameNode { get; set; }


/// <summary>
/// 暴露给编辑器的绑定服务器端口功能
/// </summary>
[Export]
public SpinBox ConnectPortNode { get; set; }

/// <summary>
/// 暴露给编辑器的绑定连接服务器功能
/// </summary>
[Export]
public Button ConnectButtonNode { get; set; }

#endregion


#region 推送给消息功能

/// <summary>
/// 暴露给编辑器的绑定发送服务器信息功能
/// </summary>
[Export]
public LineEdit SendMessageNode { get; set; }


/// <summary>
/// 暴露给编辑器的绑定发送消息给服务器功能
/// </summary>
[Export]
public Button SendButtonNode { get; set; }

#endregion

#region 响应服务端消息

/// <summary>
/// 响应消息节点
/// </summary>
[Export]
public RichTextLabel ReceiveMessageNode { get; set; }

#endregion

/// <summary>
/// 初始化启动方法, 可以视为启动回调
/// </summary>
public override void _Ready()
{
_client = new StreamPeerTcp(); // 实例化 TCP 对象


// GodotC# 方式尽可能采用原生 C# 事件绑定, 所以需要手动在代码编写绑定功能

// 绑定点击连接服务器事件
if (ConnectButtonNode != null) ConnectButtonNode.Pressed += OnConnectButtonPressed;

// 绑定发送消息给服务器事件
if (SendButtonNode != null) SendButtonNode.Pressed += OnSendButtonPressed;
}


/// <summary>
/// 触发连接服务器
/// </summary>
private void OnConnectButtonPressed()
{
var hostname = ConnectHostnameNode.Text.Trim();
var port = (int)ConnectPortNode.Value;

if (hostname.Length == 0) return;
GD.Print($"Connecting to {hostname}:{port}");


// 准备连接服务器
try
{
ConnectToServer(hostname, port);
SendButtonNode.Disabled = false; // 启用发送消息按钮
}
catch (Exception exception)
{
SendButtonNode.Disabled = true;
OS.Alert(exception.Message, "Connect Error");
}
}


/// <summary>
/// 触发连接服务器
/// </summary>
private void ConnectToServer(string hostname, int port, bool disableNagle = true)
{
var err = _client.ConnectToHost(hostname, port);
if (Error.Ok != err)
{
_connected = false;
throw new SocketException(Convert.ToInt32(err));
}

// 禁用Nagle算法, 减少延迟
_client.SetNoDelay(disableNagle);
_connected = true;
}


/// <summary>
/// 触发发送消息给服务器
/// </summary>
private void OnSendButtonPressed()
{
if (!_connected || _client.GetStatus() != StreamPeerTcp.Status.Connected)
{
GD.PrintErr("未连接到服务器, 无法发送消息");
return;
}

var message = SendMessageNode.Text.Trim();
if (message.Length == 0) return;

// 发送消息
var data = Encoding.UTF8.GetBytes(message);
var err = _client.PutData(data);
if (Error.Ok != err)
{
GD.PrintErr($"发送失败: {message}");
}
else
{
GD.Print($"发送成功: {message}");

if (ReceiveMessageNode != null)
{
ReceiveMessageNode.Text += $"[REQUEST] : {message} {Environment.NewLine}";
}
}
}


/// <summary>
/// 游戏帧更新
/// </summary>
/// <param name="delta"></param>
public override void _Process(double delta)
{
if (!_connected) return;

// 检查连接状态
_client.Poll();
var status = _client.GetStatus();
switch (status)
{
case StreamPeerTcp.Status.Connected:
// 接收数据
OnReceiveData();
break;
case StreamPeerTcp.Status.Error:
case StreamPeerTcp.Status.None:
OS.Alert("Connect Error");
_connected = false;
SendButtonNode.Disabled = true;
break;
case StreamPeerTcp.Status.Connecting:
break;
}
}


/// <summary>
/// 提取内部消息内容
/// </summary>
private void OnReceiveData()
{
var sz = _client.GetAvailableBytes();
if (sz <= 0) return;
var data = _client.GetData(sz);
OnMessageReceived((byte[])data[1]); // 首位是标识错误码, 第二个元素是 PackedByteArray, 直接转化成 byte[] 就能识别
}


/// <summary>
/// 最后消息返回的回调方法
/// </summary>
/// <param name="message">二进制消息</param>
private void OnMessageReceived(byte[] message)
{
GD.Print($"Message received: {message}");

// 转化为消息内容
if (ReceiveMessageNode != null)
{
ReceiveMessageNode.Text += $"[RESPONSE] : {Encoding.UTF8.GetString(message)} {Environment.NewLine}";
}
}


/// <summary>
/// 节点退出方法, 可以视为退出回调
/// </summary>
public override void _ExitTree()
{
if (_client != null)
{
_client.DisconnectFromHost();
_client.Dispose();
}
}
}

需要处理下 RichTextLabel 节点, 因为之前没考虑到容器扩展问题, 会导致服务端输出内容没办法 “撑开高度”, 这里修改节点位置:

richtext-change

需要吐槽: GodotC# 内部功能返回的 Array 对象是没办法直接转化成 byte[], 只能依靠丑陋的 (byte[])data[1] 强制转化 byte[]

这里因为懒得编写 tcp-echo 服务器, 所以安装工具来简单启动 echo 服务, 输入以下命令行来安装并启动服务:

1
2
3
4
5
# 安装简单网络工具, 用于搭建 echo 服务端, 内置 nc 服务功能有欠缺没办法处理
sudo apt install ncat

# 启动监听本地 8090 端口, 处理接收到的数据原样返回服务
ncat -l -p 8090 -k -c cat

最后的实现效果如下:

run-chatroom

这里就简单实现基于 GodotC# 的 TCP 连接客户端服务, 其他后续比较深入内容最好查询官方文档来处理

Godot C# 文档: https://docs.godotengine.org/zh-cn/4.x/tutorials/scripting/c_sharp/index.html

需要说明: 目前官方 GodotC# 4.x 版本还不支持 Web 模板导出; 也就是编写 H5 游戏要么降级 Godot, 要么采用 GDScript

上面例子采用 Godot 的 TCP 相关功能, 但 GodotC# 当中更加推荐采用 dotnet 的 TCP/UDP

GDScript 底层的网络 PackedByteArray 的存在严重拷贝问题, 所有网络数据必须通过 Godot 的 Variant 系统且没办法提取 byte[]

后记说明

目前 C# 版本并不是 Godot 官方支持的 “一等公民(专门做优化的开发脚本语言)”, 所以官方内部接口支持很混乱导致问题都需要自己处理

这里的转换 byte[] 方式还只是小问题, 因为 Godot 底层采用 C++ 开发导致涉及到 C# 和 C++ 底层对接的时候可能说不定出现异常

官方目前的主要开发语言是 GDScript, 但是我个人很不喜欢 GDScript 来维护项目, 无论是语法严谨程度和社区第三方库支持差距都很大

GDScript 社区 Protobuf 支持太差了, 因为本身 Godot 内部 API 都是破坏性更新, 导致社区支持方案不同版本都会出现不同问题

所以真的很希望 Godot 后续将主要脚本语言重心转移到 C# 之中, 不止能够享受 dotnet 社区支持, 还能方便 Unity3D 平滑转入技术栈

按照目前来看估计还是遥遥无期, 甚至 GodotC# 目前的 Web 支持都还不稳定, 很多组件都需要迭代更新