ChunkDownloader 是面向 虚幻引擎 的修补解决方案。它从远程服务下载资产,并将其在内存中挂载供游戏使用,以便你可以轻松提供更新和资产。本指南将向你展示如何在自己的项目中实现ChunkDownloader。完成本指南,你便能够:
+启用ChunkDownloader插件并将其添加到项目的依赖项中。 +将你的内容整理成块,将它们打包到.pak文件,并准备清单以供下载。 +在游戏代码中实现ChunkDownloader,以便下载远程.pak文件。 +安全访问已挂载.pak文件中的内容。
1.所需设置和建议资产
在继续进行任何操作之前,你应该阅读以下指南并遵循每个步骤:
+设置ChunkDownloader插件 +准备资产进行分块 +为ChunkDownloader托管清单和资产
这些指南将向你展示如何将ChunkDownloader插件添加到项目中,如何为资产设置分块方案以及如何将它们分发到本地测试服务器。回顾一下,你的示例项目应名为 PatchingDemo,并且其构造应如下所示:
-
这是一个基于 空白模板 的 C++项目。
-
插件(Plugins) 菜单中启用了 ChunkDownloader 插件。
-
在 项目设置(Project Settings) > 项目(Project) > 打包(Packaging) 中,同时启用 使用Pak文件(Use Pak File) 和 生成块(Generate Chunks)。
-
来自Paragon的 Boris、Crunch 和 Khaimera 资产已添加到项目中。 +你可以从 虚幻商城 免费下载这些内容。 +你可以使用你所需的任何资产,只要它们被分隔到离散文件夹。
-
这三个角色文件夹中的每个文件夹都有一个 主要资产标签 和以下 文件块ID:
| 文件夹 | 文件块ID |
|---|---|
| ParagonBoris | 1001 |
| ParagonCrunch | 1002 |
| ParagonKhaimera | 1003 |
-
你已经烘焙内容,并且上述每个文件块ID都具有.pak文件。
-
有一个名为
BuildManifest-Windows.txt的 清单文件,其中包含以下信息:
BuildManifest-Windows.txt
$NUM_ENTRIES = 3
$BUILD_ID = PatchingDemoKey
pakchunk1001-WindowsNoEditor.pak 922604157 ver 1001 /Windows/pakchunk1001-WindowsNoEditor.pak
pakchunk1002-WindowsNoEditor.pak 2024330549 ver 1002 /Windows/pakchunk1002-WindowsNoEditor.pak
pakchunk1003-WindowsNoEditor.pak 1973336776 ver 1003 /Windows/pakchunk1003-WindowsNoEditor.pak
每个分块的所有字段都必须在同一行,并且必须用制表符(tab)隔开,否则将无法正确解析。
-
.pak文件和清单文件已分发到本地托管的网站。有关如何进行设置的说明,请参考托管ChunkDownloader清单和资产。
-
项目的
DefaultGame.ini文件具有如下定义的 CDN URL:
DefaultGame.ini
[/Script/Plugins.ChunkDownloader PatchingDemoLive]
+CdnBaseUrls=127.0.0.1/PatchingDemoCDN
2.初始化和关闭ChunkDownloader
ChunkDownloader是对 FPlatformChunkInstall 接口的实现,该接口是许多接口之一,可以根据你的游戏运行平台交替加载不同的模块。需要先加载和初始化所有模块才能使用它们,还需要关闭并清理它们。
通过ChunkDownloader进行此操作的最简单方法是使用自定义 GameInstance 类。GameInstance不仅具有可以绑定的相应初始化和关闭函数,而且还可以在游戏运行时持续访问ChunkDownloader。以下步骤将引导你完成此实现过程。
- 使用 GameInstance 作为基类创建 新C++类。将其命名为 PatchingDemoGameInstance。
点击查看大图。
- 在IDE中打开
PatchingDemoGameInstance.h。在公共标头下,添加以下函数重载:
PatchingDemoGameInstance.h
public:
/** Overrides */
virtual void Init() override;
virtual void Shutdown() override;
Init 函数会在游戏启动时运行,使其成为初始化ChunkDownloader的理想位置。同样,Shutdown 函数会在游戏停止时运行,因此你可以用它来关闭ChunkDownloader模块。
- 在PatchingDemoGameInstance.h中,在受保护的标头下添加以下变量声明:
PatchingDemoGameInstance.h
protected:
//追踪我们的本地清单文件是否与我们网站上托管的清单文件保持同步最新
bool bIsDownloadManifestUpToDate;
- 打开
PatchingDemoGameInstance.cpp。在#include "PatchingDemoGameInstance.h"下的文件顶部添加以下#includes:
PatchingDemoGameInstance.cpp
#include "PatchingDemoGameInstance.h"
#include "ChunkDownloader.h"
#include "Misc/CoreDelegates.h"
#include "AssetRegistryModule.h"
这样你可以访问ChunkDownloader,以及一些用于管理资产和委托的有用工具。
- 在
受保护标头下的PatchingDemoGameInstance.h中声明以下函数:
PatchingDemoGameInstance.h
void OnManifestUpdateComplete(bool bSuccess);
- 在
PatchingDemoGameInstance.cpp中为OnManifestUpdateComplete创建以下实现:
PatchingDemoGameInstance.cpp
void UPatchingDemoGameInstance::OnManifestUpdateComplete(bool bSuccess)
{
bIsDownloadManifestUpToDate = bSuccess;
}
清单更新完成后,此函数将用作异步回调。
- 在
PatchingDemoGameInstance.cpp中为Init函数创建以下实现:
PatchingDemoGameInstance.cpp
void UPatchingDemoGameInstance::Init()
{
Super::Init();
const FString DeploymentName = "PatchingDemoLive";
const FString ContentBuildId = "PatcherKey";
//初始化文件块下载器
TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetOrCreate();
Downloader->Initialize("Windows", 8);
//加载缓存的版本ID
Downloader->LoadCachedBuild(DeploymentName);
//更新版本清单文件
TFunction<void(bool bSuccess)> UpdateCompleteCallback = [&](bool bSuccess){bIsDownloadManifestUpToDate = bSuccess; };
Downloader->UpdateBuild(DeploymentName, ContentBuildId, UpdateCompleteCallback);
}
我们总结一下这段代码的作用:
-
该函数定义 DeploymentName 和 ContentBuildID 以便匹配
DefaultGame.ini中使用的值。这些是目前用于测试的固定值,但是在完整版本中,你将使用HTTP请求获取ContentBuildID。该函数使用这些变量中的信息来向你的网站请求清单。 -
该函数调用
FChunkDownloader::GetOrCreate设置ChunkDownloader并获取它的引用,然后将其存储在TSharedRef中。这是获取对这个或类似平台接口引用的首选方法。 -
该函数使用所需平台名称(在本例中为Windows)调用
FChunkDownloader::Initialize。此示例为TargetDownloadsInFlight赋予值 8,该值设置ChunkDownloader同时处理的最大下载数量。 -
该函数使用
DeploymentName调用FChunkDownloader::LoadCachedBuild。这将检查磁盘上是否已经下载文件,如果它们是最新清单,则ChunkDownloader可以跳过下载流程。 -
该函数调用
FChunkDownloader::UpdateBuild以下载清单文件的更新版本。- 这就是系统不需要全新的可执行文件即可支持更新补丁的方式。
UpdateBuild获取DeploymentName和ContentBuildID以及输出操作成功或失败的回调。- 它还使用
OnManifestUpdateComplete设置bIsDownloadManifestUpToDate,以便GameInstance可以全局识别此修补阶段已完成。
执行以下步骤可确保ChunkDownloader已初始化,准备开始下载内容,并告知其他函数清单的状态。
- 为
UPatchingDemoGameInstance::Shutdown创建以下函数实现:
PatchingDemoGameInstance.cpp
void UPatchingDemoGameInstance::Shutdown()
{
Super::Shutdown();
//关闭ChunkDownloader
FChunkDownloader::Shutdown();
}
调用FChunkDownloader::Shutdown将停止当前正在进行的所有ChunkDownloader下载,然后清理并卸载该模块。
3.下载Pak文件
现在,你已经有ChunkDownloader的相应初始化和关闭函数,你可以公开其.pak下载功能。
- 在
PatchingDemoGameInstance.h中,添加GetLoadingProgress的以下函数声明:
PatchingDemoGameInstance.h
UFUNCTION(BlueprintPure, Category = "Patching|Stats")
void GetLoadingProgress(int32& FilesDownloaded, int32& TotalFilesToDownload, float& DownloadPercent, int32& ChunksMounted, int32& TotalChunksToMount, float& MountPercent) const;
- 在
PatchingDemoGameInstance.cpp中为GetLoadingProgress函数创建以下实现:
PatchingDemoGameInstance.cpp
void UPatchingDemoGameInstance::GetLoadingProgress(int32& BytesDownloaded, int32& TotalBytesToDownload, float& DownloadPercent, int32& ChunksMounted, int32& TotalChunksToMount, float& MountPercent) const
{
//获取ChunkDownloader的引用
TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();
//获取加载统计结构体
FChunkDownloader::FStats LoadingStats = Downloader->GetLoadingStats();
//获取已下载和要下载的的字节
BytesDownloaded = LoadingStats.BytesDownloaded;
TotalBytesToDownload = LoadingStats.TotalBytesToDownload;
//获取已挂载文件块数和要下载的文件块数
ChunksMounted = LoadingStats.ChunksMounted;
TotalChunksToMount = LoadingStats.TotalChunksToMount;
//使用以上统计信息计算下载和挂载百分比
DownloadPercent = (float)BytesDownloaded / (float)TotalBytesToDownload;
MountPercent = (float)ChunksMounted / (float)TotalChunksToMount;
}
- 在
PatchingDemoGameInstance.h中,在#includes下面添加以下动态组播委托:
PatchingDemoGameInstance.h
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FPatchCompleteDelegate, bool, Succeeded);
该委托输出一个布尔值,该布尔值将告知你补丁下载操作是否成功。委托通常用于响应异步操作,例如下载或安装文件。
- 在你的
UPatchingDemoGameInstance类中,在公共标头下添加以下委托声明:
PatchingDemoGameInstance.h
/**委托*/
/**修补过程成功或失败时触发*/
UPROPERTY(BlueprintAssignable, Category="Patching");
FPatchCompleteDelegate OnPatchComplete;
这为你提供了一个在修补操作完成后与蓝图挂接的位置。
- 在受保护的标头下,为
ChunkDownloadList添加以下声明:
PatchingDemoGameInstance.h
/**要尝试和下载的文件块ID列表*/
UPROPERTY(EditDefaultsOnly, Category="Patching")
TArray<int32> ChunkDownloadList;
你将使用此列表保存后续你要下载的所有文件块ID。在开发设置中,你可以根据需要使用资产列表进行初始化,但是出于测试目的,你只需公开默认值,以便我们可以使用蓝图编辑器填写它们。
- 在
公开标头下,为PatchGame添加以下声明:
PatchingDemoGameInstance.h
/**启动游戏修补过程。如果修补清单不是最新的,则返回false。*/
UFUNCTION(BlueprintCallable, Category = "Patching")
bool PatchGame();
此函数提供了蓝图的一种公开的修补过程启动方式。它返回布尔值指示成功还是失败。这是下载管理和其他类型异步任务中的通用模式。
- 在受保护标头下,添加以下函数声明:
PatchingDemoGameInstance.h
/**文件块下载过程完成时调用*/
void OnDownloadComplete(bool bSuccess);
/**ChunkDownloader加载模式完成时调用*/
void OnLoadingModeComplete(bool bSuccess);
/**ChunkDownloader完成挂载文件块时调用*/
void OnMountComplete(bool bSuccess);
你将使用它们响应下载过程中的异步回调。
- 在
PatchingDemoGameInstance.cpp中为OnDownloadComplete和OnLoadingModeBegin函数添加以下实现:
PatchingDemoGameInstance.cpp
void UPGameInstance::OnLoadingModeComplete(bool bSuccess)
{
OnDownloadComplete(bSuccess);
}
void OnMountComplete(bool bSuccess)
{
OnPatchComplete.Broadcast(bSuccess);
}
OnLoadingModeComplete将传递给OnDownloadComplete,后者将在后续步骤中继续挂载文件块。OnMountComplete将指示所有文件块均已完成挂载,并且内容可用。
- 在
PatchingDemoGameInstance.cpp中为PatchGame函数添加以下实现:
PatchingDemoGameInstance.cpp
bool UPGameInstance::PatchGame()
{
//确保下载清单是最新的
if (bIsDownloadManifestUpToDate)
{
//获取文件块下载器
TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();
//报告当前文件块状态
for (int32 ChunkID : ChunkDownloadList)
{
int32 ChunkStatus = static_cast<int32>(Downloader->GetChunkStatus(ChunkID));
UE_LOG(LogTemp, Display, TEXT("Chunk %i status: %i"), ChunkID, ChunkStatus);
}
TFunction<void (bool bSuccess)> DownloadCompleteCallback = [&](bool bSuccess){OnDownloadComplete(bSuccess);};
Downloader->DownloadChunks(ChunkDownloadList, DownloadCompleteCallback, 1);
//启动加载模式
TFunction<void (bool bSuccess)> LoadingModeCompleteCallback = [&](bool bSuccess){OnLoadingModeComplete(bSuccess);};
Downloader->BeginLoadingMode(LoadingModeCompleteCallback);
return true;
}
//我们无法联系服务器验证清单,因此我们无法修补
UE_LOG(LogTemp, Display, TEXT("Manifest Update Failed.Can't patch the game"));
return false;
}
此函数遵循以下步骤:
-
首先,它检查清单是否是当前最新的。如果你尚未初始化ChunkDownloader并成功获取清单的新副本,则
bIsDownloadManifestUpToDate将为false,并且此函数将返回false,表示无法开始修补。 -
接下来,如果修补过程可以继续,则该函数将获取ChunkDownloader的引用。然后,它遍历下载列表并检查每个文件块的状态。
- 定义了两个回调:
- 当每个单独的文件块完成下载时,将调用
DownloadCompleteCallback,当每个文件块成功下载或下载失败时,它将输出一条消息。 - 所有文件块下载完毕后,就会触发
LoadingModeCompleteCallback。
- 当每个单独的文件块完成下载时,将调用
-
该函数调用
FChunkDownloader::DownloadChunks开始下载所需文件块,这些文件块在ChunkDownloadList中列出。在调用此函数之前,必须用你想要的文件块ID填充此列表。它还传递DownloadCompleteCallback。 - 该函数使用你先前定义的回调调用
FChunkDownloader::BeginLoadingMode。- 加载模式将告知ChunkDownloader开始监视其下载状态。
- 可以在不调用加载模式的情况下在后台被动下载文件块,使用它将输出下载统计信息,使你可以创建一个可以跟踪用户下载进度的UI。
- 下载整批文件块时,你还可以使用该回调函数运行特定功能。
- 在
PatchingDemoGameInstance.cpp中为OnDownloadComplete添加以下实现:
PatchingDemoGameInstance.cpp
void UPatchingDemoGameInstance::OnDownloadComplete(bool bSuccess)
{
if (bSuccess)
{
UE_LOG(LogTemp, Display, TEXT("Download complete"));
//获取文件块下载器
TSharedRef<FChunkDownloader> Downloader = FChunkDownloader::GetChecked();
FJsonSerializableArrayInt DownloadedChunks;
(int32 ChunkID : ChunkDownloadList)
{
DownloadedChunks.Add(ChunkID);
}
//挂载文件块
TFunction<void(bool bSuccess)> MountCompleteCallback = [&](bool bSuccess){OnMountComplete(bSuccess);};
Downloader->MountChunks(DownloadedChunks, MountCompleteCallback);
OnPatchComplete.Broadcast(true);
}
else
{
UE_LOG(LogTemp, Display, TEXT("Load process failed"));
//调用委托
OnPatchComplete.Broadcast(false);
}
}
这是另一个复杂函数,我们将分解其运行模式。当你的.pak文件已成功下载到用户的设备上时,它将运行。
-
首先,它获取ChunkDownloader的引用。
-
接下来,该函数设置Json数组,并用
ChunkDownloadList中的信息填充它。这将用于发出你的请求。 -
该函数使用
MountCompleteCallback输出是否已成功应用补丁。 -
该函数调用
MountCompleteCallback(使用Json列表)和ChunkDownloader::MountChunks,开始挂载已下载文件块。 -
如果下载成功,则该函数激活值为true的
OnPatchComplete委托。如果失败,则会以false值激活。UE_LOG根据故障点输出错误消息。
4.设置修补游戏模式
要启动修补过程,你可以设置一个特定的关卡和游戏模式来调用PatchGame,并将修补统计信息输出到界面。
- 在虚幻编辑器中,在 内容浏览器 中创建新的 蓝图(Blueprints) 文件夹。然后,使用 PatchingDemoGameInstance 作为基类创建 新蓝图。
点击查看大图。
将新蓝图类命名为 CDGameInstance。
点击查看大图。
你将使用此蓝图以更轻松的方式编辑设置和追踪文件块下载过程。
-
创建名为 PatchingGameMode 的新 游戏模式 蓝图。
-
创建 地图(Maps) 文件夹,然后创建两个新关卡,分别称为 PatchingDemoEntry 和 PatchingDemoTest。入门级关卡应基于空白地图,而测试级关卡应基于默认地图。
点击查看大图。
- 在 PatchingDemoEntry 的 世界设置(World Settings) 中,将 GameMode重载(GameMode Override) 设置为 PatchingGameMode。
点击查看大图。
- 打开你的 项目设置(Project Settings),然后导航至 项目(项目) > 地图和模式(Maps & Modes)。设置下列参数:
点击查看大图。
| ID | 参数 | 值 |
|---|---|---|
| 1 | 游戏实例类 | CDGameInstance |
| 2 | 编辑器启动地图 | PatchingDemoTest |
| 3 | 游戏默认地图 | PatchingDemoEntry |
-
在 蓝图编辑器 中打开 CDGameInstance。在 默认(Defaults) 面板中,将三个条目添加到 文件块下载列表 中。条目值分别为1001、1002和1003。这是三个.pak文件的文件块ID。
-
在 蓝图编辑器 中打开 PatchingGameMode,并导航至 EventGraph。
-
创建 Get Game Instance 节点,然后将其投射到 CDGameInstance。
-
点击 As CDGameInstance 并从中拖出一根引线,然后点击 升级为变量(Promote to Variable),创建我们游戏实例的引用。调用新变量 Current Game Instance。
-
点击 设置当前游戏实例 的输出引脚并从中拖出一根引线,然后创建对 Patch Game 的调用。
-
点击 Patch Game 的 返回值 并从中拖出一根引线,然后点击 升级为变量(Promote to Variable),创建用于存储其值的布尔值。调用新变量 Is Patching In Progress
点击查看大图。
-
创建 Get Current Game Instance 节点,然后点击其输出引脚并从中拖出一根引线,创建对 Get Patch Status 的调用。
-
点击 Get Patch Status 的 返回值(Return Value) 引脚并从中拖出一根引线,然后创建 Break PatchStats 节点。
-
点击 更新函数 事件并从中拖出一根引线,然后创建新的 Branch 节点。将 Is Patching In Progress 连接到其 条件(Condition) 输入。
-
点击 Branch 节点的 True 引脚并从中拖出一根引线,然后创建 Print String 节点。使用 BuildString (float) 从 Break PatchStats 节点输出 下载百分比(Download Percent)。对 挂载百分比(Mount Percent) 重复此步骤。
点击查看大图。
-
从 Print String 节点上,创建 Branch 节点,然后创建 AND 节点并将其连接到 条件(Condition) 引脚。
-
创建 Greater Than or Equal To 节点,检查 下载百分比(Download Percent) 是否为1.0或更高,然后对 挂载百分比(Mount Percent) 执行相同的操作。将两者都连接到 AND 节点。如果这两个条件都为true,则使用 打开关卡(Open Level) 打开你的 PatchingGameTest 关卡。
点击查看大图。
现在,当你的游戏运行时,它将打开入门级地图,运行ChunkDownloader,并输出文件块下载(Chunk Download)列表中文件块的下载进度和挂载进度。下载完成后,将过渡到测试地图。
如果你尝试使用 在编辑器中运行 运行它,则下载将不会开始。你需要使用打包的版本测试ChunkDownloader。
5.显示已下载内容
要显示我们的角色网格体,你需要获取它们的引用。这将需要 软件对象引用,因为在使用资产之前你需要先验证它们是否已加载。本节将引导你完成简单的示例,该示例旨在说明如何生成Actor,并根据软引用填充它们的骨骼网格体。
-
打开 PatchingDemoTest 关卡,然后打开 关卡蓝图。
-
创建名为 网格体(Meshes) 的新变量。
- 对于其 变量类型(Variable Type),选择 骨骼网格体(Skeletal Mesh)。
- 将鼠标悬停在类型列表中的条目上,然后选择 软对象引用(Soft Object Reference)。这会将变量的颜色从蓝色更改为柔和的绿色。
点击查看大图。
软对象引用是一种智能指针,可以安全引用不明确的资产。我们可以在使用资产前用它检查你的网格体资产是否已加载并且可用。
- 点击 网格体(Meshes) 的 变量类型(variable type) 旁边的图标,将其更改为 数组(Array)。编译你的蓝图以便应用更改。
点击查看大图。
- 在 网格体(Meshes) 的 默认(Default) 值中,添加三个条目,然后为 Boris、Crunch 和 Khaimera 选择骨骼网格体。
点击查看大图。
-
在关卡的 EventGraph 中,点击 BeginPlay 事件并从中拖出一根引线,然后创建 面向每个循环(For Each Loop),并将其连接到 网格体 数组。
-
点击 面向每个循环(For Each Loop) 的 数组元素 引脚并从中拖出一根引线,然后创建 Is Valid Soft Object Reference 节点。从 循环主体 创建 分支,并将其连接到 返回值。
-
创建 Spawn Actor From Class 节点,并将其连接到 Branch 节点的 True 引脚。选择 类 的 骨骼网格体Actor。
-
点击 面向每个循环(For Each Loop) 的 数组索引 并从中拖出一根引线,然后创建 Integer x Float 节点。将浮点值设置为 192.0。
- 点击 Integer x Float 节点的 返回值 并从中拖出一根引线,以创建 Vector x Float 节点,并将向量的值设为 (1.0, 0.0, 0.0)。
- 每次我们遍历面向每个循环(For Each Loop)时,这将使坐标远离原点192个单位。这将在我们生成网格体时给每个网格体一些空间。
-
将上一步中的矢量用作 Make Transform 节点中的 位置,然后将 返回值 连接到 Spawn Actor 节点的 生成变换 输入。
-
点击 Spawn Actor 节点的 返回值 并从中拖出一根引线,然后获取其 骨骼网格体组件 的引用。用其调用 设置骨骼网格体。
- 点击 Array Element 节点并从中拖出一根引线,然后创建 Resolve Soft Object Reference 节点。将此节点的输出连接到 设置骨骼网格体 的 新网格体 输入引脚。
点击查看大图。
- 将关卡内的 玩家出生点 移动到 (-450, 0.0, 112.0)。
点击查看大图。
- 保存进度并编译蓝图。
关卡加载时,将生成每个角色的骨骼网格体。如果软对象引用不起作用,则每个角色的文件块尚未挂载,它们的资产将不可用,并且它们将不会生成。
当引用.pak文件中包含的资产时,你应该始终使用软对象引用而不是标准的硬引用。如果使用硬引用,它会破坏你的分块方案。
6.测试游戏
最后,你需要在独立版本中测试你的项目。Pak挂载不能在PIE模式下运行,因此这是测试修补功能的必要步骤。
-
打包项目。
-
将.pak文件和清单复制到 IIS测试网站 上相应的文件夹中。
-
确保IIS进程和网站都正在运行。
-
运行打包的可执行文件。
最终结果
你应该看到一个黑色界面,界面左上方显示修补输出,然后,当修补和挂载状态都达到100%时,你的游戏应加载到默认地图中,并显示Boris、Crunch和Khaimera。如果修补或挂载过程出现问题,则不会显示这些内容。
自行尝试
可以执行几个步骤来细化你的文件块下载方案:
-
编译在加载模式下显示的UI,并显示进度条和播放器提示。
-
编译UI错误提示,例如超时和安装失败。
-
创建PrimaryAssetLabel的自定义子类,以包含资产相关的其他元数据。例如,《战争破坏者》(Battle Breakers)的自定义PrimaryAssetLabel类包括一个父文件块,必须加载该父文件块,这是使用当前文件块的先决条件。