我们在做项目的时候,通常会有这样的需求,比如说美术新出了几个模型,需要替换原有工程中的模型.但是包已经发出去了,用户已经安装.这个时候又不可能因为一点东西让用户重新下载整个软件.或者说包体发出来的时候太大,老板要求在下载好主体的情况下,在进入软件的时候才去下载要用到的资源.这几种情况通常就是我们说的资源热更新了.在Unity里面,我们用AssetBunlde来做资源的热更新.不得不说UNITY对热更新的支持还是不错的.UNITY5.0版本以后,AssetBundle会自己生成Manifest.xml文件.不再需要用户去维护复杂的Dependency关系了.但是用UE4做的话就没那么舒服了...就像UNITY一样...在UE中,我们使用PAK来进行热更新.所以一般来说我们的项目发布的时候是没有那些UASSET资源的.加载uasset其实就是加载的pak文件中的uasset...
那么UE4的资源热更新主要有这么几步
1.打包好.Pak文件并将其放置到服务器上,其中需要一份数据文件Version.txt(json,xml或其他的格式都可以)来表示当前的版本信息.文件的内容我目前设计的很简单.主要就只有两个内容,一个是文件的MD5值,另一个是这次打包.Pak文件一共打包了什么Pak文件进去.
2.本地也需要一份Version.txt,用来比对服务器Version和本地的Version的区别,区别的标识就是文件的MD5值.如果发现MD5值不同的话就把服务器上的Version.txt中所涉及到的所有资源都下载下来并覆盖本地的资源,同时将服务器的Version.txt覆盖本地的Version.避免没必要的重复下载.
一.工具的准备 ---- .pak文件批量打包工具
首先,很蛋疼的是似乎在UE里面没办法通过C++来调用UnrealPak.exe工具,所以我用C#写了一个批量打包pak文件的工具.可以一次将多个uasset文件打包成一个.pak文件.或者将多个uasset文件打包成多个.pak文件.同时,在生成pak文件的时候还会生成版本文件.
页面效果如下:
打包出来的东西:
Version.txt内容:
实现这个工具其实非常的简单.几十行代码就可以了.接下来看一下代码...其中选中引擎根目录,选中要打包的文件等代码就不贴了.篇幅太大.以下是打包的具体代码
private void Btn_MultipleBuild_Click(object sender, EventArgs e) { Btn_MultipleBuild.Enabled = false; Btn_MultipleBuild.Text = "正在打包..."; //sb,sw,textWirter均是为了生成Json字符串而使用的 StringBuilder sb = new StringBuilder(); StringWriter sw = new StringWriter(sb); JsonTextWriter textWriter = new JsonTextWriter(sw); textWriter.Formatting = Formatting.Indented; DateTime Today = DateTime.UtcNow; int second = Today.Second; //生成文件的MD5值 string fileMD5 = StrToMD5(second.ToString()); textWriter.WriteStartObject(); textWriter.WritePropertyName("FileVersion"); textWriter.WriteStartObject(); textWriter.WritePropertyName("MD5"); textWriter.WriteValue(fileMD5); textWriter.WriteEndObject(); // 检查选中的引擎根目录,其目录下是否包含有UnralPak.exe文件 if (!File.Exists(TextBox_MultipleEnginePath.Text + @"\Engine\Binaries\Win64\UnrealPak.exe")) { MessageBox.Show("打包失败,没有找到 UnrealPak.exe,引擎路径不存在!"); Btn_MultipleBuild.Enabled = true; Btn_MultipleBuild.Text = "打包"; return; } textWriter.WritePropertyName("Files"); textWriter.WriteStartArray(); //根据多选框选中的文件来对文件进行打包 string[] assetNameArray = TextBox_MultipleUassetPath.Text.Split(' '); for (int i = 0; i < assetNameArray.Length; i++) { string assetFullName = assetNameArray[i].Replace('\\','/'); string[] assetArray = assetFullName.Split('/'); string assetName = assetArray[assetArray.Length - 1].Replace(".uasset", ""); string assetMD5 = StrToMD5(assetName + second.ToString()); string outPath = TextBox_MultipleOutPath.Text + "\\" + assetName + ".pak"; //通过Process相关类来多次调用UnrealPak.exe程序来打包 ProcessStartInfo info = new ProcessStartInfo(); info.FileName = TextBox_MultipleEnginePath.Text + @"\Engine\Binaries\Win64\UnrealPak.exe"; info.Arguments = @outPath + @" " + @assetFullName; info.WindowStyle = ProcessWindowStyle.Minimized; Process process = Process.Start(info); process.WaitForExit(); //将文件的信息写入到Json文件中 textWriter.WriteStartObject(); textWriter.WritePropertyName("FileName"); textWriter.WriteValue(assetName); textWriter.WritePropertyName("MD5"); textWriter.WriteValue(assetMD5); textWriter.WriteEndObject(); } MessageBox.Show("生成pak完毕!"); textWriter.WriteEndArray(); textWriter.WriteEndObject(); Btn_MultipleBuild.Text = "打包"; Btn_MultipleBuild.Enabled = true; string saveData = TextBox_MultipleEnginePath.Text + ";" + TextBox_MultipleUassetPath.Text + ";" + TextBox_MultipleOutPath.Text; File.WriteAllText(Environment.CurrentDirectory + "/save.txt", saveData); //生成Version.txt文件 File.WriteAllText(TextBox_MultipleOutPath.Text + "/Version.txt",sb.ToString()); }
代码的话实在是太简单了..以至于没什么好说的.其中生成MD5值的代码是这样的:
<span style="white-space:pre"> </span>public string StrToMD5(string str) { byte[] data = Encoding.GetEncoding("GB2312").GetBytes(str); MD5 md5 = new MD5CryptoServiceProvider(); byte[] OutBytes = md5.ComputeHash(data); string OutString = ""; for (int i = 0; i < OutBytes.Length; i++) { OutString += OutBytes[i].ToString("x2"); } return OutString.ToLower(); }
那么既然已经能批量打包pak文件并生成Version.txt了.那么我们就把生成出来的这堆东西都丢到服务器上去就可以了.第一步完成.下面在UE4中完成第二部.资源的更新
二. 资源更新
1.首先我们在代码里面增加一个状态叫做GameUpdateResourcesState,资源更新状态,用来处理资源的更新
GameUpdateResourcesState.h:
// Fill out your copyright notice in the Description page of Project Settings. #include "GameBaseState.h" #include "IDHManagers/HttpLoader.h" #pragma once /** * */ //class AHttpLoader; class IDHOME_API GameUpdateResourcesState :public GameBaseState { private: struct FileMessage { FString FileName; FString FileMD5; }; public: GameUpdateResourcesState(); ~GameUpdateResourcesState(); void OnEnter(TArray<void*> Params) override; void OnExit() override; private: void GetServerResoucesVersionFile(); void CompareServerAndLocalVersion(bool bSuccess, FHttpResponsePtr Response); const FString LocalVersionFileLocation = FPaths::GameContentDir() + TEXT("Data/Version.txt"); const FString ServerPakDirectory = TEXT("http://localhost:80/icons/Data/"); const FString SavePakDirectory = FPaths::GameContentDir() + TEXT("DownLoadPaks/"); bool GetVersionMessageFromString(FString JsonString, FString& VersionFileMD5, TArray<FileMessage>& FileMessages); void UpdateResources(const TArray<FileMessage>& Files); void DownloadFileComplete(FHttpResponsePtr Response, FString SavePath, FString FileName); AHttpLoader* HttpLoader = nullptr; private: int DownloadCompleteNumber = 0; int NeedDownloadNumber = 0; };
GameUpdateResourcesState.cpp:
// Fill out your copyright notice in the Description page of Project Settings. #include "IDHome.h" #include "GameUpdateResoucesState.h" #include "IDHManagers/AssetManager.h" #include "IDHGameState.h" GameUpdateResourcesState::GameUpdateResourcesState() { } GameUpdateResourcesState::~GameUpdateResourcesState() { } void GameUpdateResourcesState::OnEnter(TArray<void*> Params) { //获取服务器上的资源版本文件 GetServerResoucesVersionFile(); } void GameUpdateResourcesState::OnExit() { } void GameUpdateResourcesState::GetServerResoucesVersionFile() { if (HttpLoader == nullptr) { TArray<AActor*> Actors; UGameplayStatics::GetAllActorsOfClass(World, AHttpLoader::StaticClass(), Actors); HttpLoader = Cast<AHttpLoader>(Actors[0]); } FString Url = TEXT("http://localhost:80/icons/Data/Version.txt"); FString SendDataString; FDownloadDelegate DownloadServerFileDelegate; DownloadServerFileDelegate.BindRaw(this, &GameUpdateResourcesState::CompareServerAndLocalVersion); HttpLoader->OnHttpRequest(Url, SendDataString, DownloadServerFileDelegate); } void GameUpdateResourcesState::CompareServerAndLocalVersion(bool bSuccess, FHttpResponsePtr Response) { //访问不到服务器或者服务器上没有该文件等... if (!bSuccess) { UE_LOG(LogClass, Log, TEXT("服务器上没有Version.txt文件,无需更新...")); AIDHGameState::Singleton()->ChangeState(GameStateEnum::LoginState); return; } FString LocalFile; FString LocalMD5; TArray<FileMessage> LocalFileMessages; FString ServerFile = Response.Get()->GetContentAsString(); FString ServerMD5; TArray<FileMessage> ServerFileMessages; //获取服务器版本信息 GetVersionMessageFromString(ServerFile, ServerMD5, ServerFileMessages); //解析出来的MD5值非法或者没有需要下载的文件 if(ServerMD5.IsEmpty() || ServerFileMessages.Num() == 0) { AIDHGameState::Singleton()->ChangeState(GameStateEnum::LoginState); return; } //加载本地的资源版本文件 if (FFileHelper::LoadFileToString(LocalFile, *LocalVersionFileLocation)) { //获取本地版本信息 GetVersionMessageFromString(LocalFile, LocalMD5, LocalFileMessages); if (LocalMD5.Equals(ServerMD5)) { UE_LOG(LogClass, Log, TEXT("版本文件MD5值相同.无需更新")); AIDHGameState::Singleton()->ChangeState(GameStateEnum::LoginState); return; } } //覆盖本地的资源版本文件 FFileHelper::SaveStringToFile(ServerFile, *LocalVersionFileLocation); //开始更新资源 UpdateResources(ServerFileMessages); } bool GameUpdateResourcesState::GetVersionMessageFromString(FString JsonString, FString& VersionFileMD5, TArray<FileMessage>& FileMessages) { TSharedPtr<FJsonObject> JsonObject; TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString); //将文件中的内容变成你需要的数据格式 if (FJsonSerializer::Deserialize(Reader, JsonObject)) { TSharedPtr<FJsonObject> FileObject = JsonObject->GetObjectField("FileVersion"); VersionFileMD5 = FileObject->GetStringField("MD5"); FileMessages.Empty(); const TArray<TSharedPtr<FJsonValue>> Files = JsonObject->GetArrayField("Files"); for (int i = 0; i < Files.Num(); i++) { const TSharedPtr<FJsonObject>* FileMessageObject; if (Files[i].Get()->TryGetObject(FileMessageObject)) { FileMessage* FileMes = new FileMessage(); FileMes->FileName = FileMessageObject->Get()->GetStringField("FileName"); FileMes->FileMD5 = FileMessageObject->Get()->GetStringField("MD5"); FileMessages.Add(*FileMes); } } return true; } else { UE_LOG(LogClass, Error, TEXT("无法解析json数据,Json数据可能有误...")); return false; } } void GameUpdateResourcesState::UpdateResources(const TArray<FileMessage>& Files) { NeedDownloadNumber = Files.Num(); DownloadCompleteNumber = 0; //一个个文件进行下载 for (int i = 0; i < Files.Num(); i++) { FString FileURL = ServerPakDirectory + Files[i].FileName + TEXT(".pak"); FString SaveURL = SavePakDirectory + Files[i].FileName + TEXT(".pak"); FRequestDelegate DownloadCompleteDelegate; DownloadCompleteDelegate.BindRaw(this, &GameUpdateResourcesState::DownloadFileComplete, SaveURL, Files[i].FileName); AAssetManager::Singleton()->DownloadPakFile(FileURL, DownloadCompleteDelegate); } } //文件下载完成回调 void GameUpdateResourcesState::DownloadFileComplete(FHttpResponsePtr Response, FString SavePath, FString FileName) { FFileHelper::SaveArrayToFile(Response->GetContent(), *SavePath); UE_LOG(LogClass, Log, TEXT("文件:%s 已经下载完成,保存在%s"), *FileName, *SavePath); DownloadCompleteNumber++; //如果所有文件都下载完了.那么就进行下一个游戏状态. if (DownloadCompleteNumber == NeedDownloadNumber) { UE_LOG(LogClass, Log, TEXT("资源已全部更新完毕,跳转到登录状态")); AIDHGameState::Singleton()->ChangeState(GameStateEnum::LoginState); } }
这里面,AHttpLoader是我写的一个发起HttpRequest的一个单例类,主要用于和服务器交互,请求数据或者是下载文件等..还有几个DownLoadDelegate之类的委托.整体思路很简单
1.进入该状态以后,去下载服务器上的版本信息文件
2.如果连接不上服务器或者没有服务器上没有这个文件,那么就直接执行下一个游戏状态的逻辑.如果能连接上并下载到了版本信息文件.进行下一步
3.获取服务器版本文件信息,如果数据合法,那么开始加载本地信息文件,对两个文件的MD5值进行比较.如果相同,那么就进行下一游戏状态的逻辑,否则执行下一步
4.如果MD5值不同,那么就先覆盖本地的版本信息文件,然后开始更新资源
5.一个一个资源的下载,下载完最后一个文件的时候,资源更新状态的逻辑就已经都做完了,那么可以执行接下来的逻辑啦.
整个资源的热更新思路就是这么简单.
文章里面涉及到的Pak文件和下载部分的教程我之前都已经写过了.串在一起就是一份完整的热更新思路了.至于你拿到了pak文件以后要怎么使用,那是你的事,不在这次的讨论范围内.
但是有一点值得注意的,经过我测试,目前UE4的pak文件是没有依赖关系的.也就是说,如果你打包了一个UBlueprint,里面包含了一个UStaticMesh的话,那么当你直接加载这个UBlueprint到世界中的时候,他的Mesh是会丢失的.在Unity里面我们通过AssetBundle,可以先将Mesh加载进内存,这种情况下就能正常加载出来Prefab了..但是在UE4里面不行.所以可能我们需要自己去维护这一个依赖关系.甚至可能对于UBlueprint这些要生成一个文件来记录他包含了什么依赖,就像UNITY中做的那样...处理依赖这个问题在UE4里面估计会是一个比较复杂的问题...这一块就先不讨论了..mmmm...有可能以后也不会讨论...
整篇文章写下来发现好像没有什么特别的东西...最难搞的Pak加载都写在上一篇文章了...但是主要是提供一个思路给那些没有做过热更新的小伙伴吧.或者是不知道怎么批量打包pak的小伙伴...
这次的热更新到这里应该就差不多全部解决了...
下次见...