在 GodotC# 的好处就是就是依靠引入 dotnet 大量支持, 相比 GDScript 更加现代化, 而目前主流单元测试框架以下方案:
xUnit: 比较简洁小型的测试单元框架
MSTest: 微软官方的测试单元框架
NUnit: 大型且文档齐全的的测试单元
这里采用比较简洁小巧的 xUnit, 避免引入过重测试单元
注意: GodotC# 和 GDScript 要求是不一样, GodotC# 更加需要严格工程化, 也就是 UI 层和功能层要拆分出来
另外还需要注意: 最好采用 Target Framework = net8.0, 强制版本升级到 net10.0 会导致很多组件更新出现异常
构建 GodotC# 项目应该拆分为以下目录, 这里假设以游戏项目 P21 为项目根目录:
P21/P21Game: GodotC# 创建游戏项目根目录, 由 Godot 直接创建
P21/P21Tests: GodotC# 内部测试单元目录,
P21/P21Utility: GodotC# 涉及到的 网络/Protobuf 等扩展工具
P21/P21Config: 策划配置生成的配表目录
P21/P21Resources: 美术资源放置目录, 程序打包的时候不会以其打入, 只是作为美术设计稿等客户端参考目录
P21/P21.csproj: 游戏项目的主要根目录 C# 工程文件, 用于引入其他相关模块
这里子工程命名规则为 {项目名目录}/{项目名}{子工程}, 也就是 ~/P21/P21Game 和 ~/P21/P21Tests(注意首字母必须是大小写英文)
GodotC# 项目更加追求的专业级的维护方式, 要求同级的目录需要声明不同的模块, 这也是为了后续的项目可维护性考虑
这里就是 GDScript 和 C# 作为开发主体区别最大的方法, 不同于以前直接 Godot 创建项目, 需要在命令行之中手动采用初始化项目:
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 # 这里基于本地路径创建个目录 mkdir -p ~/P21 && cd ~/P21/ # 这里就是游戏项目根目录 mkdir -p {P21Utility,P21Tests,P21Game} # 目前只需要这些目录, 后面扩展其他 # 创建空类库文件, 参数说明: # -n P21:指定工程名称为 P21(生成 P21.csproj) # --framework net8.0: 指定目标框架, 但是项目兼容最好采用 net8.0-lts, 有些第三方还没有处理好 net10.0-lts 支持 # -o . : 避免在当前目录重新创建一层目录 # 如果提示 “net8.0”不是“-f”的有效值 代表目前本地没有 net8.0 # 我这边是 apt 安装的, 所以需要 apt 安装 net8.0 的支持, 执行如下命令即可 # sudo apt install -y dotnet-sdk-8.0dotnet new classlib -n P21 --framework net8.0 -o . dotnet new classlib -n P21Utility --framework net8.0 -o P21Utility dotnet new classlib -n P21Tests --framework net8.0 -o P21Tests # 这里就会生成 # - {用户目录}/P21/P21.csproj # - {用户目录}/P21/P21Utility/P21Utility.csproj # - {用户目录}/P21/P21Tests/P21Tests.csproj # 删除掉多余的类文件 rm Class1.cs P21Utility/Class1.cs P21Tests/Class1.cs # 构建生成 Sln 方案 dotnet new sln -n P21 # 生成主项目工程方案 dotnet sln add P21.csproj # 将根目录加入工程 dotnet sln add P21Tests/P21Tests.csproj # 将 Tests 加入工程 dotnet sln add P21Utility/P21Utility.csproj # 将 Utility 加入工程
测试单元初始化
之后就是直接对所有子模块进行初始化配置, 首先指定 Tests 测试单元依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 # 进入 Tests 单元目录配置 cd ~/P21/P21Tests dotnet add package xunit.v3 # 添加 xunit 测试框架核心包 dotnet add package Microsoft.NET.Test.Sdk # 微软测试 SDK dotnet add package coverlet.collector # 添加代码覆盖率工具, 这个组件可选 # 还有一点, 在 P21Tests.csproj 文件当中添加或者修改配置 # <PropertyGroup> # <IsPackable>false </IsPackable> # </PropertyGroup> # 这个配置说明该项目不参与打包过程, 避免出包的时候做无意义打包 # 这里引入 P21Utility 项目, 让后续 P21Utility 内部功能也能被支持单元测试 # 其他后续 GodotC# 项目后续也是这样引入到内部测试单元测试 dotnet add reference ../P21Utility/P21Utility.csproj dotnet restore # 最后确认依赖已经下载完成
注意: 如果 Rider 直接打开 P21 目录会发现命令行执行是正确, 但 IDE 内部报错无法执行; C# 项目没有多项目概念所以需要按单独项目打开
需要 Rider 直接打开 /P21/P21Tests 项目, 这样 Rider 内部就会自动扫描到对应依赖, 在 Rider 添加 EasyTests.cs
文件内容如下:
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 using Xunit;namespace P21Tests ;public class EasyTests (ITestOutputHelper helper ){ [Fact ] public void AssertTest () { Assert.True(true ); Assert.False(false ); Assert.Equal("Hello World!" , "Hello World!" ); Assert.NotEqual("Hello" , "World" ); Assert.Empty(Enumerable.Empty<string >()); helper.WriteLine("Assert Test Pass" ); } }
dotnet8 之后的语法糖确实很不错, C# 这部分功能扩展相比较 GDScript 脚本规范合理很多
工具扩展初始化
通用工具和消息序列化等底层功能不要和游戏工程放置一起, 而是要提取出来单独构建成工程, 这就是 P21Utility 的由来
为什么需要这样区分开? 这是为了游戏的职责隔离出来从而降低耦合, 具体体现在如下方面:
游戏工程(P21Game): 只负责游戏逻辑、UI、动画 交互、节点管理, 并且负责调用底层扩展工具
底层扩展(P21Utility): 封装 C# 底层功能暴露给游戏当中调用处理, 并支持 Protobuf 这类序列化消息文件
目前项目先引入 Protobuf 来进行构建用于做网络传输功能, 输入以下命令来初始化项目:
1 2 3 4 5 6 7 8 # 进入 Tests 单元目录配置 cd ~/P21/P21Utility dotnet add package Google.Protobuf # Protobuf 核心库 dotnet add package Google.Protobuf.Tools # Protobuf 工具库 dotnet restore # 最后确认依赖已经下载完成 # 创建放置 *.proto 文件的目录, 编译打包的时候需要排除将这些文件打包进去 mkdir ProtoFiles
这里相对比较简单, 因为主要是 P21Utility.csproj 的配置文件需要加的东西比较多, 这里需要打开 P21Utility 工程修改以下配置:
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 <Project Sdk ="Microsoft.NET.Sdk" > <PropertyGroup > <TargetFramework > net8.0</TargetFramework > <ImplicitUsings > enable</ImplicitUsings > <Nullable > enable</Nullable > <ProtobufInputDir > ProtoFiles</ProtobufInputDir > <ProtobufOutputDir > Protobuf</ProtobufOutputDir > <ProtobufExecBIN > $(PROTOBUF_PROTOC)</ProtobufExecBIN > <ProtobufExecOS > $(PROTOBUF_TOOLS_OS)</ProtobufExecOS > <ProtobufExecCPU > $(PROTOBUF_TOOLS_CPU)</ProtobufExecCPU > </PropertyGroup > <ItemGroup > <PackageReference Include ="Google.Protobuf" Version ="3.33.4" /> <PackageReference Include ="Google.Protobuf.Tools" Version ="3.33.4" /> </ItemGroup > <ItemGroup > <Content Include ="$(ProtobufInputDir)/**/*.proto" > <ExcludeFromSingleFile > true</ExcludeFromSingleFile > <CopyToOutputDirectory > Never</CopyToOutputDirectory > <ExcludeFromPublish > true</ExcludeFromPublish > </Content > </ItemGroup > <Target Name ="PrintVariables" BeforeTargets ="CoreCompile" > <Message Importance ="high" Text ="Protobuf Execute Binary -> $(ProtobufExecBIN)" /> <Message Importance ="high" Text ="Protobuf Execute OS -> $(ProtobufExecOS)" /> <Message Importance ="high" Text ="Protobuf Execute CPU -> $(ProtobufExecCPU)" /> <Message Importance ="high" Text ="Protobuf Input Files - > $(ProtobufInputDir)/**/*.proto" /> <Message Importance ="high" Text ="Protobuf Output Directory -> $(ProtobufOutputDir)" /> </Target > <Target Name ="ProtobufGenerate" BeforeTargets ="CoreCompile" > <MakeDir Directories ="$(ProjectDir)$(ProtobufOutputDir)" Condition ="!Exists('$(ProjectDir)$(ProtobufOutputDir)')" /> <ItemGroup > <ProtobufFiles Include ="$(ProtobufInputDir)/**/*.proto" /> </ItemGroup > <Exec Command ="$(ProtobufExecBIN) -I=$(ProjectDir)$(ProtobufInputDir) --csharp_out=$(ProjectDir)$(ProtobufOutputDir) @(ProtobufFiles->'%(FullPath)', ' ')" /> </Target > </Project >
之后就是去下载 Protoc 编译器来配置好环境变量, 这里用命令行安装下载配置到全局:
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 # 注意: Protoc 的编译器版本最好是和 Google.Protobuf.Version 声明的版本一致 # 我这边使用的 Google.Protobuf.Version = 3.33.4, 所以需要下载对应版本 # 如果安装缓慢可能需要工具自行处理 wget https://github.com/protocolbuffers/protobuf/releases/download/v33.4/protoc-33.4-linux-x86_64.zip -O /tmp/protoc.zip unzip /tmp/protoc.zip -d /tmp/protoc # 注意: protoc 如果是在用户环境变量没办法被 Rider 识别到, 所以需要将其作为全局可用对象来处理 # 这里就需要用到 Linux 开发经验, 将 protoc 提升到全局可执行目录并且写入到全局环境变量 # 需要将 protoc 二进制放置 /usr/local/bin 目录之中并且设置为可执行命令 sudo cp /tmp/protoc/bin/protoc /usr/local/bin sudo chmod +x /usr/local/bin/protoc # 写入环境变量到全局, 在 /etc/profile.d 目录下创建自定义的 protoc.conf 环境变量 echo 'export PROTOBUF_PROTOC=/usr/local/bin/protoc export PROTOBUF_TOOLS_OS=linux export PROTOBUF_TOOLS_CPU=x64'|sudo tee /etc/profile.d/protoc.sh source ~/.bashrc # 如果重启 Rider 还没加载环境变量就重启下电脑查看 # 先构建个测试文件来处理, 这里输出 tests.proto 文件方便 Tests 项目做单元测试 echo "syntax = 'proto3'; option csharp_namespace = 'P21Utility.Generated'; // 输出的命名空间 // 简单 Echo 命令 message Echo{ string message = 1; } " > ~/P21/P21Utility/ProtoFiles/tests.proto # 你直接可以手动配置直接命令行处理打包, 在 P21Utility 目录下执行打包命令 dotnet build # 打包处理, 也可以直接在 Rider 运行编译命令 # 没有什么错误那么应该会输出如下内容: # Protobuf net8.0 已成功 (0.1 秒) → bin/Debug/net8.0/Protobuf.dll
现在就是构建 P21Utility 测试单元来该功能是否可用, 如果可用就可以让游戏项目直接引入 P21Utility 工程使用.
Protobuf 创建测试
需要确保目前 Tests.csproj 配置当中已经包含有 Protobuf 相关依赖:
1 2 3 4 5 6 7 <Project Sdk ="Microsoft.NET.Sdk" > <ItemGroup > <ProjectReference Include ="..\P21Utility\P21Utility.csproj" /> </ItemGroup > </Project >
Tests 项目编译测试的时候应该会出现以下错误:
1 2 3 4 5 6 0>Tests.cs(9,21): Error CS0400 : 未能在全局命名空间中找到类型或命名空间名“Google”(是否缺少程序集引用?) 0>Tests.cs(10,21): Error CS0400 : 未能在全局命名空间中找到类型或命名空间名“Google”(是否缺少程序集引用?) 0>Tests.cs(8,20): Error CS0400 : 未能在全局命名空间中找到类型或命名空间名“Google”(是否缺少程序集引用?) 0>Tests.cs(62,28): Error CS0538 : '显式接口声明中的“Google.Protobuf.IMessage”不是接口 0>Tests.cs(154,10): Error CS0538 : '显式接口声明中的“Google.Protobuf.IBufferMessage”不是接口 0>Tests.cs(218,10): Error CS0538 : '显式接口声明中的“Google.Protobuf.IBufferMessage”不是接口
这是因为 Protobuf 内部只是生成数据构建的类文件, 底层还是依赖 Google.Protobuf 来实现序列化二进制
所以对于 Tests 这部分依旧还是引入 Google.Protobuf 功能, 这里在 Tests 项目之中执行命令行:
1 dotnet add package Google.Protobuf # 只要使用到 Protobuf 都要引入核心库
在此执行编译之后应该就不会报错, 之后就是编写对应测试单元用于测试是否能够正常序列化使用:
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 using Google.Protobuf;using Xunit;using ProtobufGen = Protobuf.Generated; namespace Tests.Protobuf ; public class Tests (ITestOutputHelper logger ){ [Fact ] public void EchoTest () { var echoMessage = new ProtobufGen.Echo { Message = "Hello World" }; var echoBytes = echoMessage.ToByteArray(); var echoByteString = $"[{string .Join(", " , echoBytes.Select(b => ((sbyte )b).ToString()))} ]" ; logger.WriteLine($"{nameof (ProtobufGen.Echo)} Serialized: {echoByteString} " ); var newEchoMessage = ProtobufGen.Echo.Parser.ParseFrom(echoBytes); logger.WriteLine($"{nameof (ProtobufGen.Echo)} Deserialized: {newEchoMessage} " ); } }
执行之后测试单元就会输出以下内容(这里的序列化输出内容格式是方便 Java 服务端的 Arrays.toString 做数据打印比较):
1 2 Echo Serialized: [10, 11, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100] Echo Deserialized: { "message": "Hello World" }
这样就代表测试单元通过了, 工作当中就可以客户端同事来使用这个底层消息类, 方便后面和服务端做消息同步
Godot 初始化
现在就可以通过 GodotC# 来创建底层游戏迎请模块, 这里必须要创建在 P21/P21Game 目录中:
不要将代码版本控制设置 P21Game 目录之中, 而是要在全局项目根目录之中管理
这里先随便搭建成项目生成 dotnet 相关项目, 直接生成个空节点之后挂个 C# 脚本, Godot 才会构建 C# 相关依赖
目前 P21Utility 的 Protobuf 已通过单元测试, P21Game 游戏底层需要的话就必须像 Tests 引入对应组件, 所以需要执行以下命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 # 进入 Godot 游戏子工程 cd ~/P21/P21Game # 这里不要执行安全 dotnet 组件命令 # 比如执行之前 dotnet add package Google.Protobuf 引入 Protobuf 就会报错 # 提示内容 'P21/P21Game/ 中找不到任何项目' # 这里就是接下来需要讲 GodotC# 项目初始化另外的问题 # 只要使用到 Protobuf 都要引入核心库 dotnet add package Google.Protobuf # 声明 Game 内部项目依赖 Protobuf 项目 dotnet add reference ../P21Utility/P21Utility.csproj
采用 GodotC# 启动器创建项目都是默认基于 GDScript 脚本的工程, 所以不会初始化 *.csproj 工程文件
如果要让 Godot 生成 csproj 工程项目, 需要按照以下流程处理:
创建空场景并设置空节点
在空节点上构建 C# 脚本
Rider 识别之后会自动启动开发工具
到这里默认就会在游戏项目当中创建 csproj 工程文件
之后才能做 dotnet 的命令行第三方引入和依赖操作, 这里可以继续执行如下命令:
1 2 3 4 5 6 7 8 9 10 11 12 cd ~/P21/P21Game # 只要使用到 Protobuf 都要引入核心库 dotnet add package Google.Protobuf # 声明 Game 内部项目依赖 Utility 项目 dotnet add reference ../P21Utility/P21Utility.csproj dotnet restore # 构建依赖 # 将游戏工程加入到主工程之中 cd .. dotnet sln add P21Game/P21Game.csproj
这样就完成了 P21Utility 相关依赖, 从而可以编写 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 using System.Linq;using Godot;using Google.Protobuf;using ProtobufGen = P21Utility.Generated;namespace P21Game ;public partial class Main : Node { [Export ] public string Message { get ; set ; } = string .Empty; public override void _Ready() { var echoMessage = new ProtobufGen.Echo() { Message = Message }; var echoBytes = echoMessage.ToByteArray(); var echoByteString = $"[{string .Join(", " , echoBytes.Select(b => ((sbyte )b).ToString()))} ]" ; GD.Print($"{nameof (ProtobufGen.Echo)} Serialized: {echoByteString} " ); } public override void _EnterTree() { GD.Print($"Entered Tree" ); } public override void _Process(double delta) { SetProcess(false ); GD.Print($"Processing, Delta: {delta} " ); } public override void _PhysicsProcess(double delta) { SetPhysicsProcess(false ); GD.Print($"PhysicsProcessing, Delta: {delta} " ); } public override void _ExitTree() { GD.Print($"Exited Tree" ); } }
注意: 如果这里编译飘红字需要确认是不是 RIder 用到非 net8 的版本打包, 目前 Godot4.5 版本只支持 dotnet8 兼容
最后在编辑器面板输入消息并执行就能看到以下内容输出:
这样就可以开始规划网络库传输功能实现, 需要说明的是 GodotC# 之中不要用 Godot 底层网络库, 因为 C++ - GDScript - C# 调用链路.
C# 内部调用 GD 底层相关功能起始就是调用 GDScript 去执行更底层的 C++ 功能, 网络库数据返回会多走 GD 层数据拷贝(影响性能)
Logger 日志
一般在 Godot 需要打印下日志的时候你会怎么处理? 可能就是简单的采用 GD.Print 输出
这种方式打印日志方式简单但不合理, 现代开发之中都是统一采用专门日志库来处理游戏日志打印, 而 DotNet 社区之中也有很多日志管理方案.
现代 C# 日志方案推荐采用 Serilog 的社区的日志方案, 这个第三方库配置方便并且容易安装使用, 这边可以试用下该第三方库:
1 2 3 4 5 6 7 # 这里直接进入 P21Game 游戏工程 cd ~/P21/P21Game # 安装对应库 dotnet add package Serilog # Serilog 日志库核心 dotnet add package Serilog.Sinks.Console # Serilog 命令行打印输出 dotnet add package Serilog.Sinks.File # Serilog 文件打印输出
一般开发过程只需要打印到命令行即可, 而如果正式环境则会保存到本地日志, 在游戏崩溃异常可以联系玩家提交或上报日志文件用于还原现场
Serilog 代码库: https://github.com/serilog/serilog
直接安装就行了, 整个过程没有一点复杂的操作, 之后就是在启动的时候初始化全局日志初始化配置, 按照之前 C#
脚本追加日志库支持:
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 using System.Linq;using Godot;using Google.Protobuf;using Serilog;using ProtobufGen = P21Utility.Generated;namespace P21Game ;public partial class Main : Node { [Export ] public string Message { get ; set ; } = string .Empty; public override void _Ready() { var logFilename = ProjectSettings.GlobalizePath("user://game.log" ); GD.Print($"Output Log Filename: {logFilename} " ); Log.Logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.File( logFilename, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true , fileSizeLimitBytes: 1024 * 1024 * 5 , retainedFileCountLimit: 7 ) .CreateLogger(); Log.Information("Logger Initialized, Filename: {LogFilename}" , logFilename); var echoMessage = new ProtobufGen.Echo() { Message = Message }; var echoBytes = echoMessage.ToByteArray(); var echoByteString = $"[{string .Join(", " , echoBytes.Select(b => ((sbyte )b).ToString()))} ]" ; Log.Information("{EchoName} Serialized: {EchoByteString}" , nameof (ProtobufGen.Echo), echoByteString); } public override void _EnterTree() { Log.Information("Entering Tree" ); } public override void _Process(double delta) { SetProcess(false ); Log.Information("Processing, Delta: {Delta}" , delta); } public override void _PhysicsProcess(double delta) { SetPhysicsProcess(false ); Log.Information("PhysicsProcessing, Delta: {Delta}" , delta); } public override void _ExitTree() { Log.Information("Exiting Tree" ); Log.CloseAndFlush(); } }
启动之后就会打印类似以下日志在 Rider 的开发工具之中:
1 2 3 4 5 [16:04:47 INF] Logger Initialized, Filename: /home/meteorcat/.local/share/godot/app_userdata/P21Game/game.log [16:04:48 INF] Echo Serialized: [] [16:04:48 INF] PhysicsProcessing, Delta: 0.016666666666666666 [16:04:48 INF] Processing, Delta: 0.14753366666666667 [16:04:52 INF] Exiting Tree
虽然已经实现日志打印内容, 但是可以看到 Godot 编辑器内部日志没有打印, 所以需要命令行打印时候输出到 Godot 的 Output 日志中
这里需要额外编写 Godot 的 Serilog 扩展, 让其获取日志内容打印到 Godot 的之中, 创建以下日志处理器:
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 #nullable enable using System;using System.IO;using Godot;using Serilog.Core;using Serilog.Events;using Serilog.Formatting;using Serilog.Formatting.Display;namespace P21Game.Utils.Logging ;public class GodotSink : ILogEventSink { private readonly ITextFormatter _formatter; private const string DefaultOutputTemplate = "[{Timestamp:HH:mm:ss}] {Message:lj}{NewLine}{Exception}" ; public GodotSink (ITextFormatter? formatter = null ) { _formatter = formatter ?? new MessageTemplateTextFormatter(DefaultOutputTemplate); } public void Emit (LogEvent logEvent ) { ArgumentNullException.ThrowIfNull(logEvent); using var stringWriter = new StringWriter(); _formatter.Format(logEvent, stringWriter); var content = stringWriter.ToString().TrimEnd(); var (godotTag, logContent) = GetGodotLogContent(logEvent.Level, content); GD.PrintRich($"{godotTag} {logContent} " ); } private static (string Tag, string Content ) GetGodotLogContent (LogEventLevel level, string logText ) { return level switch { LogEventLevel.Fatal => ("[FATAL]" , logText), LogEventLevel.Error => ("[ERROR]" , logText), LogEventLevel.Warning => ("[WARN] " , logText), LogEventLevel.Information => ("[INFO] " , logText), LogEventLevel.Debug => ("[INFO] " , logText), LogEventLevel.Verbose => ("[INFO] " , logText), _ => ("[INFO] " , logText) }; } }
之后就是将日志处理器注册到 Serilog 内部之中, 这种就是 C# 特色的扩展对象注入功能:
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 #nullable enable using System;using Serilog;using Serilog.Configuration;using Serilog.Events;using Serilog.Formatting;namespace P21Game.Utils.Logging ;#nullable enable using System;using Serilog;using Serilog.Configuration;using Serilog.Events;using Serilog.Formatting;namespace P21Game.Utils.Logging ;public static class GodotSinkExtensions { public static LoggerConfiguration Godot ( this LoggerSinkConfiguration configuration, ITextFormatter? formatter = null , LogEventLevel level = LevelAlias.Minimum ) { ArgumentNullException.ThrowIfNull(configuration); return configuration.Sink( new GodotSink(formatter), level); } }
现在因为追加内部扩展方法, 目前支持我们自定义 WriteTo.Godot() 配置, 这里 Serilog 初始化功能就变成如下内容:
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 public partial class Main : Node { public override void _Ready() { Log.Logger = new LoggerConfiguration() .WriteTo.Console() .WriteTo.Godot() .WriteTo.File( logFilename, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true , fileSizeLimitBytes: 1024 * 1024 * 5 , retainedFileCountLimit: 7 ) .CreateLogger(); } }
至此 Godot 的日志面板就会输出正确的日志内容:
1 2 3 4 5 6 Output Log Filename: /home/meteorcat/.local/share/godot/app_userdata/P21Game/game.log [INFO] [16:40:17] Logger Initialized, Filename: /home/meteorcat/.local/share/godot/app_userdata/P21Game/game.log [INFO] [16:40:18] Echo Serialized: [] [INFO] [16:40:18] PhysicsProcessing, Delta: 0.016666666666666666 [INFO] [16:40:18] Processing, Delta: 0.13333333333333333 --- Debugging process stopped ---
如果想让 Godot Output 只打印警告及以上日志, 而控制台/文件打印全部日志, 可给 Godot Sink 指定级别:
1 2 // Godot Output 仅打印 Warning/Error/Fatal *.WriteTo.Godot(restrictedToMinimumLevel: LogEventLevel.Warning)
这里 Rider 看日志打印会发现相同日志输出两次, 这事因为 GD 和 Console 都是标准输出流, 可以屏蔽 .WriteTo.Console():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 // 初始化日志配置 Log.Logger = new LoggerConfiguration() // .WriteTo.Console() // 设置写入命令行打印, 屏蔽避免重复输出 .WriteTo.Godot() // 注入我们自己扩展的 Godot 输出 .WriteTo.File( // 设置写入本地日志文件, 这里日志落地目录要结合 Godot 的本地 logFilename, // 日志文件压缩规则, 按照每日做最大限制压缩 rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true, fileSizeLimitBytes: 1024 * 1024 * 5, // 单个日志文件最大5MB retainedFileCountLimit: 7 // 仅保留7天的日志文件 ) .CreateLogger();
另外为了避免日志重复初始化, 可以做是否判断来处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 if (Log.Logger is not Serilog.Core.Logger){ Log.Logger = new LoggerConfiguration() .WriteTo.Godot() .WriteTo.File( logFilename, rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true , fileSizeLimitBytes: 1024 * 1024 * 5 , retainedFileCountLimit: 7 ) .CreateLogger(); Log.Information("Logger Initialized, Filename: {LogFilename}" , logFilename); }
剩下就是做宏条件编译处理而已, 比如编辑器默认命令行输出/正式出包打印到本地日志, 还有环境变量设置全局日志等级
另外需要的配置只要使用的时候查一下资料即可, 都没什么难度
依靠 C# 的社区就可以引入大量企业级的开发库, 这点就是 GodotC# 最不容忽视的地方.