游戏当中不可获取的就是调整系统设置, 而其中最最容易被人忽略的显示设置, 这里列举下常见的显示设置
如果是简单的显示设置, 就是如下这种方式设置:
这种就是标准的将所有配置简略合并成单个配置修改, 适用于简单游戏类型(无3D交互和不做手柄操作)
而相对比较复杂的显示设置, 如下就示:
这种就是相对比较复杂的, 但是一般都是会把游戏显卡游戏效果单独移动到游戏设置(Game Settings)
游戏设置: 双显卡切换和特效切换(高/中/低), 还有伽马值/阴影/环境光/抗锯齿/帧率上限等高级选项
这里回过头来说下应该实现的 显示设置(Display Settings), 日常最底层用到的系统配置并且都是一次实现后续复用的功能
以最简单的设置来说, 哪怕最简单的游戏都需要实现以下显示设置功能
桌面模式(Display Mode): 设置游戏渲染窗口, 可以按照以下选项来切换(游戏桌面一律不运行鼠标点击拖动调整分辨率)
全屏(Fullscreen): 将界面直接填充独占当前主显示器, 全屏的分辨率直接读取当前显示器最大分辨率值
窗口化(Windowed): 将窗口默认调整 1024×768~3840×2160 分辨率多选配置, 下面有对应标准分辨率列表
无边框窗口(Borderless): 将窗口的状态栏去除的界面模式, 也就是有没有游戏界面上面最小化/关闭窗口那一栏, 其他和窗口化一致
显示分辨率(Display Resolution): 游戏窗口的强制分辨率, 一般都有标准化的对应分辨率配表, 下面有对应标准分辨率列表
垂直同步(VSync): 让游戏的渲染帧率与显示器的刷新率保持同步避免出现画面撕裂, 但是坏处就是输入带有延迟, 这配置由用户自己决定开关
运行帧率(FPS): 用于设置游戏运行的最大帧, 如果不是大型游戏的话保持默认默认帧率选择即可, 甚至没必要暴露该配置给玩家设置
30: 省电模式下的运行帧率
60: 常规模式下的运行帧率
120: 高刷新模式下运行帧率
144: 电竞级别的运行帧率
240: 高端旗舰显示器带支持运行帧率
无限制: 这个由 Godot 决定内部支持最大帧率设置, 尽可能能跑多少帧率就到多少帧率
Godot 运行帧率依靠 Godot 内置的 Engine.SetMaxFps(fps) 方法修改, 设置 0 就是解放无限制运行帧率
请注意: VSync开启(Enabled/Adaptive)时, MaxFps会失效并将帧率上限强制为显示器刷新率
开启 VSync + G-Sync/FreeSync 可以将帧率上限设为刷新率 -2~-3(如 144Hz 屏设 141 帧) 来避免输入延迟和画面撕裂
垂直同步和调整运行帧率不是必要的, 一般只有涉及到动作类会影响(视角切换导致延迟), 其他游戏默认开启不用额外配置
后续说明还是按照之前初始化说明的 P21Game 项目来做展开, 结合 Manager 全局管理器来做全局显示控制
分辨率相关知识点
实际上切换显示在游戏引擎当中可以归类成以下行为
全屏: 填充目前显示器窗口, 然后获取当前显示器最大分辨率来设置(注意:多屏的时候需要查找主显示器窗口)
窗口化: 默认切换到最低 1080P(1920×1080) 尺寸的小窗口模式, 然后支持手动调整不同档位的分辨率
无边框窗口化: 类似于窗口化实现, 但是将窗口时候的状态栏去除(也就是最小化/最大化/关闭那个栏关闭)
全屏模式下不允许调整窗口分辨率, 而是计算得出支持的最高分辨率来切换
显示分辨率的选项网上很少有人提及, 这部分也是后面自己摸索起来的, 也算是给需要的人做参考, 可以直接通用以下表格
标准命名
分辨率数值
行业代号和屏幕比例
画质等级
适配场景(游戏开发)
XGA
1024×768
无专属P数(4:3)
标清
极低配入门/复古游戏
HD
1280×720
720P(16:9)
高清
低配PC/核显玩家核心适配
HD+
1366×768
768P(4:3)
标清
笔记本低分辨率适配
HD+
1600×900
900P(16:9)
高清
中低配PC/主流笔记本适配
FHD
1920×1080
1080P(16:9)
全高清
桌面端核心适配(主推)
QHD
2560×1440
2K/1440P(16:9)
2K高清
中高配PC/电竞显示器适配
WQHD
2560×1600
2.5K(16:10)
2K高清
高端笔记本/27寸显示器适配
UHD
3840×2160
4K/超高清(16:9)
4K超高清
高配PC/32寸以上大屏适配
这就是日常游戏用到分辨率, 从 13~14寸笔记本(1024×768) 到超高清设备 4K 分辨率, 一般游戏通用这个分辨率模板即可
这部分的建议定义在 Utility(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 namespace P21Utility.Definitions.Settings.Display ;public enum Resolution{ PXga = 1 , P720 = 2 , P768 = 3 , P900 = 4 , P1080 = 5 , P1440 = 6 , P1600 = 7 , P2160 = 8 , }
扩展 DisplayResolution 枚举, 利用 C# 的扩展特性给这些枚举追加 GetSize(分辨率宽高尺寸) 和 GetRatio(分辨率宽高比例) 方法:
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 namespace P21Utility.Definitions.Settings.Display ;public static class ResolutionExtensions { public static (int , int ) GetSize (this Resolution resolution ) { return resolution switch { Resolution.PXga => (1024 , 768 ), Resolution.P720 => (1280 , 720 ), Resolution.P768 => (1366 , 768 ), Resolution.P900 => (1600 , 900 ), Resolution.P1080 => (1920 , 1080 ), Resolution.P1440 => (2560 , 1440 ), Resolution.P1600 => (2560 , 1600 ), Resolution.P2160 => (3840 , 2160 ), _ => (1920 , 1080 ) }; } public static (int , int ) GetRatio (this Resolution resolution ) { var (w, h) = resolution.GetSize(); var gcd = GetRatioDivisor(w, h); return (w / gcd, h / gcd); } private static int GetRatioDivisor (int a, int b ) { while (b != 0 ) { var temp = b; b = a % b; a = temp; } return a; } public static Resolution[] GetResolutions (int width, int height ) { return Enum .GetValues<Resolution>() .Where(resolution => { var (w, h) = resolution.GetSize(); return w <= width && h <= height; }).ToArray(); } }
而另外方面还有对应显示模式枚举, 这个枚举没什么好讲直接按照下面即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 namespace P21Utility.Definitions.Settings.Display ;public enum Mode{ Fullscreen = 1 , Windowed = 2 , Borderless = 3 , }
这就是通用的分辨率枚举扩展功能, 这个工具枚举无论是 Godot/Unity3d/UE 都可以使用, 涵盖目前主流的显示器分辨率
Godot 分辨率设置
说完通用部分就要结合 P21 项目来继续开发游戏工程, 之前简单声明 ISettingsManager 封装并实现动态修改显示设置
后面仔细考虑感觉写得太过粗糙, 我的想法是尽可能将知识点和通用规范融入项目之内, 尽可能不会让人照抄代码失去自己理解
最终达到的效果是让人理解游戏开发之中 "为什么要这么做" 和 "应该怎么去做" 的问题点和解决方案
包括上面的分辨率比例扩展出来的市面通用显示器分辨率知识, 就是为了让人知道为什么选择这些尺寸并选择不同分辨率尺寸
这里 Godot 关于显示设置常用的 API 如下所示, 具体可以参照官方文档说明:
功能需求
对应的Godot C# API
作用
获取主显示器ID
DisplayServer.GetPrimaryScreen()
在多个屏幕当中的主显示屏幕
获取当前窗口所在主显示器ID
DisplayServer.WindowGetCurrentScreen()
在多个屏幕当中的主显示屏幕
获取显示器原生分辨率
DisplayServer.ScreenGetSize(screenId)
获取指定主显示器当中的屏幕分辨率
设置窗口大小
DisplayServer.WindowSetSize(Vector2I)
将目前游戏窗口大小按指定尺寸调整
设置窗口最小尺寸
DisplayServer.WindowSetMinSize(Vector2I)
将目前游戏窗口大小按最小尺寸调整
设置窗口最大尺寸
DisplayServer.WindowSetMaxSize(Vector2I)
将目前游戏窗口大小按最大尺寸调整
设置窗口的位置
DisplayServer.WindowSetPosition(Vector2I)
调整目前游戏窗口位置
设置窗口模式(全屏/窗口)
DisplayServer.WindowSetMode(WindowMode)
设置窗口目前显示模式
窗口扩展配置
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, true/false)
隐藏状态栏等功能
设置VSync
DisplayServer.WindowSetVsyncMode(VSyncMode)
设置垂直同步功能
禁止窗口拖调
DisplayServer.WindowSetResizable(true/false)
不允许窗口被拖动调整分辨率
需要注意很多游戏开发者都忽略多显示器这种情况(多显示器的调整也是很复杂的情况), 一般会遇到的情况如下:
多屏幕调整坐标位置, 大部分调整主显示窗口位置(实现居中显示)的情况, 哪怕移动到副屏坐标都是从主显示器坐标系的位置计算
目前虽然有分辨率标准(720/900/1080P之类), 但是部分玩家是支持将屏幕旋转90度成纵向显示屏, 这时候为了游戏正常运行就需要需要调整上下大黑边
多显示器当中有比较小众的多屏合一情况(四个显示器扩展设置单块屏幕显示), 这种情况算是很小众的问题点, 不需要过多的关注处理
所以这里在 Game 层用到 C# 扩展功能来为所有 Display.Resolution 枚举扩展出 SetSize 等方法:
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 using Godot;using P21Utility.Definitions.Settings.Display;namespace P21Game.Manager.Settings.Extensions ;public static class GodotDisplayResolutionExtensions { public static void SetSize (this Resolution resolution,bool centered = false ) { var (width, height) = resolution.GetSize(); var screenId = DisplayServer.WindowGetCurrentScreen(); var screenSize = DisplayServer.ScreenGetSize(screenId); if (width > screenSize.X || height > screenSize.Y) return ; DisplayServer.WindowSetSize(new Vector2I(width, height)); DisplayServer.WindowSetMinSize(new Vector2I(width, height)); if (centered) { DisplayServer.WindowSetPosition((screenSize - new Vector2I(width, height)) / 2 ); } } public static Resolution[] GetResolutions () { var screenId = DisplayServer.WindowGetCurrentScreen(); var screenSize = DisplayServer.ScreenGetSize(screenId); return ResolutionExtensions.GetResolutions(screenSize.X, screenSize.Y); } }
最后随便演示下添加布局查看效果, 这里随便设置游戏的操作界面:
脚本内容如下:
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 using System;using Godot;using P21Game.Manager.Settings;using P21Game.Manager.Settings.Extensions;using P21Utility.Definitions.Settings;namespace P21Game ;public partial class Main : Node { [Export ] public OptionButton ResolutionSelect; [Export ] public Button UpdateButton; public override void _Ready() { if (ResolutionSelect != null ) { ResolutionSelect.Clear(); foreach (var resolution in GodotDisplayResolutionExtensions.GetResolutions()) { var resId = (int )resolution; var (w, h) = resolution.GetSize(); ResolutionSelect.AddItem($"{w} x {h} ({resolution.ToString()} )" , resId); } } if (UpdateButton != null ) { UpdateButton.Pressed += UpdateDisplayResolution; } } private void UpdateDisplayResolution () { var id = ResolutionSelect.GetSelectedId(); var resolution = (DisplayResolution)id; resolution.SetSize(); } }
在编辑器之中绑定按钮就可以手动切换不同分辨率样式, 我目前主显示器为 1080P 下拉列表如下:
这样默认最高分辨率只能到 1080P 渲染, 剔除掉不支持的 2K/4K 分辨率达到显示分辨率的最优解
Godot 显示模式
目前的功能已经支持调整分辨率处理, 接下来就是用到的是 DisplayServer.WindowSetMode 的功能来切换显示模式, 这里需要扩展额外知识点
对于桌面模式的全屏其实是有两种全屏模式: Fullscreen 和 ExclusiveFullscreen, 这两种虽然是全屏但是各自处理方式不同
Fullscreen: 覆盖画面全屏幕, 其实也就是类似于让游戏画面窗口处于顶级渲染, 同屏下程序渲染任务在后台进行(只是挡住不可见)
ExclusiveFullscreen: 独占屏幕全屏化, 这个模式下会停止同屏幕下其他程序渲染任务, 将渲染任务集中在当前游戏程序之中
这里的知识在其他游戏引擎通用, 是关于桌面渲染方面的知识点
ExclusiveFullscreen 因为是完全独占窗口渲染从而性能上好很多, 在大型游戏上面的可以最大限度压榨电脑性能
虽然看起来很美好, 但是实际上在日常使用当中也有坑点的, 主要问题还是 ExclusiveFullscreen 在部分直播时候使用采集卡游戏会出现布局混乱
ExclusiveFullscreen 在全屏切换模式下会导致某些二次开发的直播工具/利用桌面分栏布局的工具/直播设备采集卡等依赖窗口钩子的元素挤占到第二屏幕
所以更推荐采用更加温和的 Fullscreen 方式做全屏实现, 当然如果大型游戏直接用独占全屏以获得最高性能也可以, 这部分按照自身项目出发即可
而 Godot 方便实现也比较简单, 还是和上面一样扩展 Display.Mode 功能即可:
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 using System;using System.Linq;using Godot;using P21Utility.Definitions.Settings.Display;namespace P21Game.Manager.Settings.Extensions ;public static class GodotDisplayModeExtensions { public static Mode[] GetModes () { return Enum.GetValues<Mode>(); } public static void SetMode (this Mode mode ) { switch (mode) { case Mode.Windowed: DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed); DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, false ); break ; case Mode.Borderless: DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed); DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, true ); break ; default : case Mode.Fullscreen: var resolution = GetMaxScreenResolution(); var (width, height) = resolution.GetSize(); var resolutionVec2 = new Vector2I(width, height); DisplayServer.WindowSetMode(DisplayServer.WindowMode.Fullscreen); DisplayServer.WindowSetSize(resolutionVec2); DisplayServer.WindowSetMinSize(resolutionVec2); break ; } } private static Resolution GetMaxScreenResolution () { var screenId = DisplayServer.WindowGetCurrentScreen(); var screenSize = DisplayServer.ScreenGetSize(screenId); var sorted = Enum.GetValues<Resolution>() .OrderByDescending(res => { var (w, h) = res.GetSize(); return w * h; }); foreach (var res in sorted) { var (w, h) = res.GetSize(); if (w <= screenSize.X && h <= screenSize.Y) { return res; } } return Resolution.P1080; } public static void SetModeAndResolution (this Mode mode, Resolution resolution ) { resolution.SetSize(); mode.SetMode(); } }
最后就是在脚本之中实现对应功能, 这里追加 OptionButton 节点来作为下拉选择桌面模式:
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 using System;using Godot;using P21Game.Manager.Settings;using P21Game.Manager.Settings.Extensions;using P21Utility.Definitions.Settings.Display;namespace P21Game ;public partial class Main : Node { [Export ] public OptionButton ModeSelect; [Export ] public OptionButton ResolutionSelect; [Export ] public Button UpdateButton; private bool IsWindowed { get ; set ; } public override void _Ready() { if (ModeSelect != null ) { ModeSelect.Clear(); foreach (var mode in GodotDisplayModeExtensions.GetModes()) { var modeId = (int )mode; ModeSelect.AddItem($"{mode} - {modeId} " , modeId); } } if (ResolutionSelect != null ) { ResolutionSelect.Clear(); foreach (var resolution in GodotDisplayResolutionExtensions.GetResolutions()) { var (w, h) = resolution.GetSize(); ResolutionSelect.AddItem($"{w} x {h} ({resolution.ToString()} )" , (int )resolution); } } if (UpdateButton != null ) { UpdateButton.Pressed += UpdateDisplayResolution; } IsWindowed = DisplayServer.WindowGetMode() != DisplayServer.WindowMode.Fullscreen; } private void UpdateDisplayResolution () { var modeId = ModeSelect.GetSelectedId(); var mode = (Mode)modeId; IsWindowed = mode != Mode.Fullscreen; if (IsWindowed) { var id = ResolutionSelect.GetSelectedId(); var resolution = (Resolution)id; resolution.SetSize(); } mode.SetMode(); } public override void _Process(double delta) { ResolutionSelect.Disabled = !IsWindowed; } }
这里追加节点绑定功能即可, 最后在 Godot 编辑器设置下具体节点功能即可:
后续就是准备提升为全局视频管理器, 也就是将其 Manager 化处理
Manager 整合管理
全局管理器定义也比较简单, 但是需要涉及到落地保存本地和读取加载配置的情况, 一般游戏的系统配置都是直接写入到本地
而写入本地的系统配置一般都是以 JSON 形式保存, 这部分就需要用到 JSON 序列化功能, 所以这部分需要连接 C# 的 JSON 扩展
默认 C# 内部已经支持 JSON(System.Text.Json), 不要用 Godot 的 JSON 序列化库(功能欠缺严重, 而且不是 C# 原生)
这里 Manager 定义如下, 抽象成专门管理的 ISettingsManager 接口:
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 using System;using P21Utility.CDI;using P21Utility.Definitions.Settings;namespace P21Game.Manager.Settings ;public interface ISettingsManager : IManager { int IManager.Priority => -1 ; public void LoadSettings () ; public void SaveSettings () ; #region 显示设置 public event Action OnDisplayChanged; public bool IsFullscreenMode { set ; get ; } public DisplayMode DisplayMode { set ; get ; } public DisplayResolution DisplayResolution { set ; get ; } public bool DisplayVSync { set ; get ; } #endregion #region 音频设置 #endregion #region 游戏设置 #endregion #region 操作设置 #endregion }
这里目前只有显示设置这类功能已经支持, 后续会自动扩展不同设置, 这里需要定义数据类来装载具体的配置(也可以理解为数据容器):
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 using P21Utility.Definitions.Settings;namespace P21Game.Manager.Settings.Data ;public class GodotDisplaySettings { public DisplayMode Mode { get ; set ; } = DisplayMode.Fullscreen; public DisplayResolution Resolution { get ; set ; } = DisplayResolution.P1080; public bool VSync { get ; set ; } = true ; }
为什么需要数据类? 因为这些配置需要直接将类对象序列化到本地保存, 如果不采用数据类就要专门定义 Map/Dict 对象写入和提取
用 Map/Dict 对象读写很麻烦, 需要对每个字段做验证类型处理, 不如直接将类序列化/反序列化处理容易
这里就构建成新全局管理器对象:
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 using System;using System.Collections.Generic;using System.IO;using System.Text;using System.Text.Json;using Godot;using P21Game.Manager.Settings.Data;using P21Game.Manager.Settings.Extensions;using P21Utility.Definitions.Settings;using Serilog;namespace P21Game.Manager.Settings.Impl ;public class GodotSettingsManager (string filename ) : ISettingsManager{ public void Init () { Log.Information("GodotSettings File: {Filename}" , filename); try { LoadSettings(); } catch (Exception exception) { Log.Error(exception, "Failed to load Godot Settings" ); InitSettings(); } } public void Dispose () => Log.Information("Disposing Godot Settings, File: {Filename}" , filename); public void LoadSettings () { if (!File.Exists(filename)) { InitSettings(); return ; } var json = File.ReadAllText(filename, new UTF8Encoding(false )); var settings = JsonSerializer.Deserialize<Dictionary<string , JsonDocument>>(json); try { LoadDisplaySettings(settings[nameof (GodotDisplaySettings)].Deserialize<GodotDisplaySettings>()); Log.Information("Loaded Godot Display Settings( {DisplaySettings} )" , _displaySettings.ToString()); } catch (Exception exception) { Log.Error(exception, "Failed to load Godot Display Settings" ); LoadDisplaySettings(new GodotDisplaySettings()); } } private void LoadDisplaySettings (GodotDisplaySettings displaySettings ) { _displaySettings = displaySettings; _displaySettings.Mode.SetMode(); _displaySettings.Resolution.SetSize(); DisplayServer.WindowSetVsyncMode(_displaySettings.VSync ? DisplayServer.VSyncMode.Enabled : DisplayServer.VSyncMode.Disabled); } private void InitSettings () { LoadDisplaySettings(new GodotDisplaySettings()); SaveSettings(); } public void SaveSettings () { try { Dictionary<string , object > settings = new () { [nameof(GodotDisplaySettings) ] = _displaySettings ?? new GodotDisplaySettings(), }; var jsonSettings = JsonSerializer.Serialize(settings); Log.Information("Saving Godot Settings: {JsonSettings}" , jsonSettings); var dirname = Path.GetDirectoryName(filename); if (!string .IsNullOrEmpty(dirname) && !Directory.Exists(dirname)) { Directory.CreateDirectory(dirname); } File.WriteAllText(filename, jsonSettings, new UTF8Encoding(false )); } catch (Exception exception) { Log.Error(exception, "Failed to save Godot Settings" ); } } #region 显示设置 private GodotDisplaySettings _displaySettings; public event Action OnDisplayChanged; public bool IsFullscreenMode { get => _displaySettings.Mode == DisplayMode.Fullscreen; set => throw new NotSupportedException(); } public DisplayMode DisplayMode { get => _displaySettings.Mode; set { if (value == _displaySettings.Mode) return ; _displaySettings.Mode = value ; value .SetMode(); if (!IsFullscreenMode) _displaySettings.Resolution.SetSize(); OnDisplayChanged?.Invoke(); SaveSettings(); } } public DisplayResolution DisplayResolution { get => _displaySettings.Resolution; set { if (value == _displaySettings.Resolution) return ; if (IsFullscreenMode) return ; _displaySettings.Resolution = value ; value .SetSize(); OnDisplayChanged?.Invoke(); SaveSettings(); } } public bool DisplayVSync { get => _displaySettings.VSync; set { if (value == _displaySettings.VSync) return ; _displaySettings.VSync = value ; DisplayServer.WindowSetVsyncMode(value ? DisplayServer.VSyncMode.Enabled : DisplayServer.VSyncMode.Disabled); OnDisplayChanged?.Invoke(); SaveSettings(); } } #endregion }
ISettingsManager 看起来功能都是很通用组件, 如果在 Game 层内部被验证可以通用的话, 确认没问题就可以提升到 Utility 通用工具层来管理
注意: 一定要加上 if (value == DisplaySettings.* ) return 重复跳过, 因为 Godot C# 内部事件会让属性是会被多次唤起调用的
之后就是将该全局管理器在 AutoLoad 之中注册到全局:
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 using System;using Godot;using P21Game.Manager.Settings;using P21Game.Manager.Settings.Impl;using P21Game.Utils.Logging;using P21Utility.CDI;using Serilog;namespace P21Game ;public partial class AutoLoad : Node { public override void _EnterTree() { if (Log.Logger is not Serilog.Core.Logger) { var logFilename = ProjectSettings.GlobalizePath("user://game.log" ); 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); } try { RegisterManagers(); ManagerRegistry.Init(); Log.Information("Registered Managers Successes" ); } catch (Exception exception) { Log.Error(exception, "An error occured during initialization" ); OS.Alert(exception.Message, "An error occured during initialization" ); GetTree().Quit(1 ); } } private static void RegisterManagers () { if (ManagerRegistry.Initialized) return ; var settingsFilename = ProjectSettings.GlobalizePath("user://settings.json" ); ManagerRegistry.Register<ISettingsManager>(()=>new GodotSettingsManager(settingsFilename)); } public override void _ExitTree() { try { ManagerRegistry.Clear(); } catch (Exception exception) { Log.Error(exception, "An error occured during exiting" ); } Log.CloseAndFlush(); } }
最后就是通用实现的 Main 方法使用, 这里的代码也比较简单:
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 using Godot;using P21Game.Manager.Settings;using P21Game.Manager.Settings.Extensions;using P21Utility.CDI;using P21Utility.Definitions.Settings;using Serilog;namespace P21Game ;public partial class Main : Node { [Export ] public OptionButton ModeSelect; [Export ] public OptionButton ResolutionSelect; [Export ] public Button UpdateButton; private ISettingsManager _settingsManager; public override void _Ready() { _settingsManager = ManagerRegistry.Get<ISettingsManager>(); if (ModeSelect != null ) { ModeSelect.Clear(); var active = -1 ; foreach (var mode in GodotDisplayModeExtensions.GetModes()) { var modeId = (int )mode; ModeSelect.AddItem($"{mode} - {modeId} " , modeId); if (_settingsManager.DisplayMode.Equals(mode)) active = modeId; } if (active > -1 ) ModeSelect.Selected = ModeSelect.GetItemIndex(active); Log.Information("Selected Mode: {active}" , active); } if (ResolutionSelect != null ) { ResolutionSelect.Clear(); var active = -1 ; foreach (var resolution in GodotDisplayResolutionExtensions.GetResolutions()) { var resId = (int )resolution; var (w, h) = resolution.GetSize(); ResolutionSelect.AddItem($"{w} x {h} ({resolution.ToString()} )" , resId); if (_settingsManager.DisplayResolution.Equals(resolution)) active = resId; } if (active > -1 ) ResolutionSelect.Selected = ResolutionSelect.GetItemIndex(active); Log.Information("Selected Resolution: {active}" , active); } if (UpdateButton != null ) { UpdateButton.Pressed += UpdateDisplayResolution; } _settingsManager.OnDisplayChanged += () => { Log.Information("Display Settings Updated" ); }; } private void UpdateDisplayResolution () { var modeId = ModeSelect.GetSelectedId(); var mode = (DisplayMode)modeId; _settingsManager.DisplayMode = mode; if (_settingsManager.IsFullscreenMode) return ; var id = ResolutionSelect.GetSelectedId(); var resolution = (DisplayResolution)id; _settingsManager.DisplayResolution = resolution; } public override void _Process(double delta) { ResolutionSelect.Disabled = _settingsManager.IsFullscreenMode; } }
这样就实现切换游戏分辨率和显示模式的基础功能, 最后的效果如下: