GodotC# 的热更新

目前 Godot 内部其实已经自带了动态模块功能, 可以依靠这部分功能来实现内部基础的的更新机制

最好理解以上官方内容, 都是实现 Godot 热更新的核心机制, 剩下就是要把场景打包成 Export PCK/ZIP 即可

对于 C# 项目必需先构建DLL放在项目目录中, 然后在加载资源包之前需要通过 Assembly.LoadFile("mod.dll")

注意: 这也说明主场景启动的 tscn 文件不要编写任何游戏业务逻辑, 而是要只负责检测验证下载远程热更新包到本地

游戏业务最好按照 通用资源/场景资源 分包, 一些共享的图片/字体资源最好放在通用资源包

包加载

这里创建以下场景来做示例

  • Main(主场景): 进入游戏的主界面场景, 只负责验证本地包是否存在

  • Game(业务场景): 主要游戏业务场景, 相当于进入之后的游戏界面(多个场景创建多个 TSCN)

对于 Main 场景实际上只需要挂脚本然后加 Cover(背景封面:TextureRect)LoadingBar(进度条:ProgressBar),
具体搭建如下:

elf

这里图片是 AI 生成 1920x1080(16:9) 图片, 关键词: 图片风格为 「复古动漫」,比例 16:9, 奇幻/梦幻 , 精灵之森林, 日系风格

这里的图片如下, 可以直接下载处理下使用:

elf

之后编写测试脚本, 用于查看百分比进度条递增的效果, 脚本内容如下

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
using System.Threading.Tasks;
using Godot;

namespace HotFix.Scene;

/// <summary>
/// 启动主场景
/// </summary>
public partial class Main : Control
{
/// <summary>
/// 封面节点
/// </summary>
[Export] public TextureRect Cover;

/// <summary>
/// 加载进度条
/// </summary>
[Export] public ProgressBar LoadingBar;


/// <summary>
/// 定时器是否启动
/// </summary>
private bool _timerRunning = true;

/// <summary>
/// 初始化
/// </summary>
public override void _Ready()
{
// 设置是否运行帧更新
ProcessMode = LoadingBar?.ProcessMode ?? ProcessModeEnum.Disabled;


// 启动定时器: 测试使用
_ = StartProgressBar();
}

/// <summary>
/// 启动定时程序
/// </summary>
private async Task StartProgressBar()
{
while (_timerRunning)
{
// 每秒执行一次
await ToSignal(GetTree().CreateTimer(1.0f), "timeout");
GD.Print("tick");
if (LoadingBar.Value < LoadingBar.MaxValue)
{
LoadingBar.Value++;
}
}
}

/// <summary>
/// 游戏帧递增
/// </summary>
public override void _Process(double ignore)
{
}
}

这里绑定好节点之后可以看到进度不断递增, 这就是用于测试效果进度递增是否正确, 确认没问题就考虑做 Game 场景并打包成资源

资源打包

现在就是另外创建新的场景用来测试我们热更新功能, 这里随便搭建个场景文件(注意: 这些热更新场景不要打包到主场景Main之中)

这里新建场景就比较简单点, 只需要放置背景带颜色图片和一段文字演示, 不过需要说明的是热更新目录我都是放在 /Packages 文件下

game

后面这个功能是要直接打包导出的, 这里直接打包处理: 点击 项目 - 导出 按钮和选项, 在导出选项之中选择平台

export

注意这里会提示没有导出模板内容, 需要去官网下载具体的导出模板(下拉到最后): https://godotengine.org/download/windows/

这里采用 C# 语言开发, 需要采用具体的 dotnet 导出模板下载

下载模板完成 编辑器 - 管理导出模板 - 从文件安装 之后选中刚刚下载好的 Godot_{版本号}-stable_mono_export_templates.tpz

安装完成就可以继续下面的热更包构建, 这里添加 Windows Desktop 的输出, 按照以下方面配置:

  • 热更新包不需要可执行, 所以将 可执行 设置关闭

  • 在子选项 选项 之中如果真是出包最好将 调试 - 导出控制台封装 设置为 No

  • 在子选项 选项 之中因为我们是热更新资源包打包, 所以 应用 - 修改资源 可以直接关闭

  • 在子选项 资源 选择相关的业务资源 导出选中的资源(包括依赖项) 勾选 Packages 所有内容

pac

最后点击 导出 PCK/ZIP 就可以导出我们的热更新资源包, 可以创建个资源目录专门放这部分热更新包

我这边放置在 /Assets 之中并且在 Git 加入排除规则避免同步上传

最后得到打出资源包就代表成功

assets

之后就是准备在 Main 场景之中加载 Game.pck 文件是否成功

打包加载

这里先写死加载项目路径下的 Assets 目录, 后面再扩展出加载 user:// 专属目录资源下载并验证

这里先简单点处理, 一般会去 user://Game.pck 判断文件是否存在, 不存在或者版本不匹配则是服务端下载最新的包覆盖本地

在 Main 脚本之中设置字符串暴露变量用于设置热更新包的路径地址, 这里填写 res://Assets/Game.pck

然后就是具体的更新包加载功能编写:

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
using System.Threading.Tasks;
using Godot;

namespace HotFix.Scene;

/// <summary>
/// 启动主场景
/// </summary>
public partial class Main : Control
{
/// <summary>
/// 封面节点
/// </summary>
[Export] public TextureRect Cover;

/// <summary>
/// 加载进度条
/// </summary>
[Export] public ProgressBar LoadingBar;


/// <summary>
/// 热更包的路径地址
/// </summary>
[Export] public string PckFilename;

/// <summary>
/// 加载的热更新包资源
/// </summary>
private PackedScene _imported = null;


/// <summary>
/// 定时器是否启动
/// </summary>
private bool _timerRunning = true;

/// <summary>
/// 初始化
/// </summary>
public override void _Ready()
{
// 设置是否运行帧更新
ProcessMode = LoadingBar?.ProcessMode ?? ProcessModeEnum.Disabled;


// 测试加载资源
if (!ProjectSettings.LoadResourcePack(PckFilename))
{
GD.PrintErr("Resource pack not found");
}
else
{
// 加载成功就可以直接获取场景
GD.Print("Resource pack loaded");

// 注意加载的资源路径必须要和打包的时候一致
_imported = GD.Load<PackedScene>("res://Packages/Game/Game.tscn");
GD.Print(_imported);

// 这里应该可以打印类似 <PackedScene#-9223371998837602871>
// 这就代表资源包加载完成可以将其挂载到场景
// 后面定时器每秒+10进度完成之后挂载该场景
}

// 启动定时器: 测试使用
_ = StartProgressBar();
}

/// <summary>
/// 启动定时程序
/// </summary>
private async Task StartProgressBar()
{
while (_timerRunning && _imported != null)
{
// 每秒执行一次
await ToSignal(GetTree().CreateTimer(1.0f), "timeout");
GD.Print("tick");
if (LoadingBar.Value < LoadingBar.MaxValue)
{
LoadingBar.Value += 10;
}
else
{
_timerRunning = false;
GD.Print("loading bar finished");

// 读条完成直接资源附加
var instance = _imported.Instantiate();
GetTree().Root.AddChild(instance);

// 隐藏|销毁启动界面
this.Hide();
this.QueueFree();
}
}
}
}

最后配置热更新包路径然后运行等待10s读条完成看看是否会加载到包内部场景, 如果成功跳转就代表没有错误

这里仅仅是作为简单的热更新加载包功能编写, 实际上内部还有很多复杂的问题要处理, 还有网络 CDN 分发和 MD5 哈希验证这些种种问题还没处理完成