淘先锋技术网

首页 1 2 3 4 5 6 7

参考文章


文章出处:http://blog.csdn.net/u012385432

最近发现有些网站复制了我的文章却没有注明出处,表示鄙视...转载可以,还请注明出处...不要剽窃别人的劳动成果...


前面的几篇文章中提及了有关.Pak文件和文件下载的部分,这两部分组合起来,其实就是我们的资源热更新了.当然代码的热更新不在这个讨论范围内.代码的热更新的话就更加麻烦了.这次讨论的只限资源的热更新...

前面文章链接:

1.下载文件链接

2.Pak使用教程链接


我们在做项目的时候,通常会有这样的需求,比如说美术新出了几个模型,需要替换原有工程中的模型.但是包已经发出去了,用户已经安装.这个时候又不可能因为一点东西让用户重新下载整个软件.或者说包体发出来的时候太大,老板要求在下载好主体的情况下,在进入软件的时候才去下载要用到的资源.这几种情况通常就是我们说的资源热更新了.在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内容:


实现这个工具其实非常的简单.几十行代码就可以了.接下来看一下代码...其中选中引擎根目录,选中要打包的文件等代码就不贴了.篇幅太大.以下是打包的具体代码

  1. private void Btn_MultipleBuild_Click(object sender, EventArgs e)  
  2. {  
  3.     Btn_MultipleBuild.Enabled = false;  
  4.     Btn_MultipleBuild.Text = "正在打包...";  
  5.   
  6.     //sb,sw,textWirter均是为了生成Json字符串而使用的  
  7.     StringBuilder sb = new StringBuilder();  
  8.     StringWriter sw = new StringWriter(sb);  
  9.     JsonTextWriter textWriter = new JsonTextWriter(sw);  
  10.     textWriter.Formatting = Formatting.Indented;  
  11.     DateTime Today = DateTime.UtcNow;  
  12.     int second = Today.Second;  
  13.   
  14.     //生成文件的MD5值  
  15.     string fileMD5 = StrToMD5(second.ToString());  
  16.   
  17.     textWriter.WriteStartObject();  
  18.     textWriter.WritePropertyName("FileVersion");  
  19.     textWriter.WriteStartObject();  
  20.     textWriter.WritePropertyName("MD5");  
  21.     textWriter.WriteValue(fileMD5);  
  22.     textWriter.WriteEndObject();  
  23.   
  24.   
  25.       
  26.     // 检查选中的引擎根目录,其目录下是否包含有UnralPak.exe文件  
  27.     if (!File.Exists(TextBox_MultipleEnginePath.Text + @"\Engine\Binaries\Win64\UnrealPak.exe"))  
  28.     {  
  29.         MessageBox.Show("打包失败,没有找到 UnrealPak.exe,引擎路径不存在!");  
  30.         Btn_MultipleBuild.Enabled = true;  
  31.         Btn_MultipleBuild.Text = "打包";  
  32.         return;  
  33.     }  
  34.   
  35.     textWriter.WritePropertyName("Files");  
  36.     textWriter.WriteStartArray();  
  37.   
  38.     //根据多选框选中的文件来对文件进行打包  
  39.     string[] assetNameArray = TextBox_MultipleUassetPath.Text.Split(' ');  
  40.     for (int i = 0; i < assetNameArray.Length; i++)  
  41.     {  
  42.         string assetFullName = assetNameArray[i].Replace('\\','/');  
  43.         string[] assetArray = assetFullName.Split('/');  
  44.         string assetName = assetArray[assetArray.Length - 1].Replace(".uasset""");  
  45.         string assetMD5 = StrToMD5(assetName + second.ToString());  
  46.         string outPath = TextBox_MultipleOutPath.Text + "\\" + assetName + ".pak";  
  47.   
  48.         //通过Process相关类来多次调用UnrealPak.exe程序来打包  
  49.         ProcessStartInfo info = new ProcessStartInfo();  
  50.         info.FileName = TextBox_MultipleEnginePath.Text + @"\Engine\Binaries\Win64\UnrealPak.exe";  
  51.         info.Arguments = @outPath + @" " + @assetFullName;  
  52.         info.WindowStyle = ProcessWindowStyle.Minimized;  
  53.         Process process = Process.Start(info);  
  54.         process.WaitForExit();  
  55.   
  56.         //将文件的信息写入到Json文件中  
  57.         textWriter.WriteStartObject();  
  58.         textWriter.WritePropertyName("FileName");  
  59.         textWriter.WriteValue(assetName);  
  60.         textWriter.WritePropertyName("MD5");  
  61.         textWriter.WriteValue(assetMD5);  
  62.         textWriter.WriteEndObject();  
  63.     }  
  64.     MessageBox.Show("生成pak完毕!");  
  65.     textWriter.WriteEndArray();  
  66.     textWriter.WriteEndObject();  
  67.       
  68.     Btn_MultipleBuild.Text = "打包";  
  69.     Btn_MultipleBuild.Enabled = true;  
  70.   
  71.   
  72.     string saveData =  
  73.         TextBox_MultipleEnginePath.Text + ";" +  
  74.         TextBox_MultipleUassetPath.Text + ";" +  
  75.         TextBox_MultipleOutPath.Text;  
  76.     File.WriteAllText(Environment.CurrentDirectory + "/save.txt", saveData);  
  77.   
  78.     //生成Version.txt文件  
  79.     File.WriteAllText(TextBox_MultipleOutPath.Text + "/Version.txt",sb.ToString());  
  80. }  
        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值的代码是这样的:

  1. <span style="white-space:pre">    </span>public string StrToMD5(string str)  
  2.         {  
  3.             byte[] data = Encoding.GetEncoding("GB2312").GetBytes(str);  
  4.             MD5 md5 = new MD5CryptoServiceProvider();  
  5.             byte[] OutBytes = md5.ComputeHash(data);  
  6.   
  7.             string OutString = "";  
  8.             for (int i = 0; i < OutBytes.Length; i++)  
  9.             {  
  10.                 OutString += OutBytes[i].ToString("x2");  
  11.             }  
  12.             return OutString.ToLower();  
  13.         }  
<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:

  1. // Fill out your copyright notice in the Description page of Project Settings.  
  2.   
  3. #include "GameBaseState.h"  
  4. #include "IDHManagers/HttpLoader.h"  
  5. #pragma once  
  6.   
  7.   
  8.   
  9.  //class AHttpLoader;  
  10. class IDHOME_API GameUpdateResourcesState :public GameBaseState  
  11. {  
  12. private:  
  13.     struct FileMessage  
  14.     {  
  15.         FString FileName;  
  16.         FString FileMD5;  
  17.     };  
  18.   
  19. public:  
  20.     GameUpdateResourcesState();  
  21.     ~GameUpdateResourcesState();  
  22.   
  23.     void OnEnter(TArray<void*> Params) override;  
  24.     void OnExit() override;  
  25.   
  26. private:  
  27.     void GetServerResoucesVersionFile();  
  28.   
  29.     void CompareServerAndLocalVersion(bool bSuccess, FHttpResponsePtr Response);  
  30.   
  31.     const FString LocalVersionFileLocation = FPaths::GameContentDir() + TEXT("Data/Version.txt");  
  32.     const FString ServerPakDirectory = TEXT("http://localhost:80/icons/Data/");  
  33.     const FString SavePakDirectory = FPaths::GameContentDir() + TEXT("DownLoadPaks/");  
  34.   
  35.     bool GetVersionMessageFromString(FString JsonString, FString& VersionFileMD5, TArray<FileMessage>& FileMessages);  
  36.   
  37.     void UpdateResources(const TArray<FileMessage>& Files);  
  38.   
  39.     void DownloadFileComplete(FHttpResponsePtr Response, FString SavePath, FString FileName);  
  40.   
  41.     AHttpLoader* HttpLoader = nullptr;  
  42.   
  43. private:  
  44.     int DownloadCompleteNumber = 0;  
  45.     int NeedDownloadNumber = 0;  
  46.   
  47. };  
// 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:

  1. // Fill out your copyright notice in the Description page of Project Settings.  
  2.   
  3. #include "IDHome.h"  
  4. #include "GameUpdateResoucesState.h"  
  5. #include "IDHManagers/AssetManager.h"  
  6. #include "IDHGameState.h"  
  7.   
  8. GameUpdateResourcesState::GameUpdateResourcesState()  
  9. {  
  10. }  
  11.   
  12. GameUpdateResourcesState::~GameUpdateResourcesState()  
  13. {  
  14. }  
  15.   
  16. void GameUpdateResourcesState::OnEnter(TArray<void*> Params)  
  17. {  
  18.     //获取服务器上的资源版本文件  
  19.     GetServerResoucesVersionFile();  
  20. }  
  21.   
  22. void GameUpdateResourcesState::OnExit()  
  23. {  
  24.   
  25. }  
  26.   
  27. void GameUpdateResourcesState::GetServerResoucesVersionFile()  
  28. {  
  29.     if (HttpLoader == nullptr)  
  30.     {  
  31.         TArray<AActor*> Actors;  
  32.         UGameplayStatics::GetAllActorsOfClass(World, AHttpLoader::StaticClass(), Actors);  
  33.         HttpLoader = Cast<AHttpLoader>(Actors[0]);  
  34.     }  
  35.   
  36.     FString Url = TEXT("http://localhost:80/icons/Data/Version.txt");  
  37.     FString SendDataString;  
  38.     FDownloadDelegate DownloadServerFileDelegate;  
  39.     DownloadServerFileDelegate.BindRaw(this, &GameUpdateResourcesState::CompareServerAndLocalVersion);  
  40.       
  41.     HttpLoader->OnHttpRequest(Url, SendDataString, DownloadServerFileDelegate);  
  42. }  
  43.   
  44. void GameUpdateResourcesState::CompareServerAndLocalVersion(bool bSuccess, FHttpResponsePtr Response)  
  45. {  
  46.     //访问不到服务器或者服务器上没有该文件等...  
  47.     if (!bSuccess)  
  48.     {  
  49.         UE_LOG(LogClass, Log, TEXT("服务器上没有Version.txt文件,无需更新..."));  
  50.         AIDHGameState::Singleton()->ChangeState(GameStateEnum::LoginState);  
  51.         return;  
  52.     }  
  53.   
  54.   
  55.     FString LocalFile;  
  56.     FString LocalMD5;  
  57.     TArray<FileMessage> LocalFileMessages;  
  58.   
  59.     FString ServerFile = Response.Get()->GetContentAsString();  
  60.     FString ServerMD5;  
  61.     TArray<FileMessage> ServerFileMessages;  
  62.   
  63.     //获取服务器版本信息  
  64.     GetVersionMessageFromString(ServerFile, ServerMD5, ServerFileMessages);  
  65.   
  66.     //解析出来的MD5值非法或者没有需要下载的文件  
  67.     if(ServerMD5.IsEmpty() || ServerFileMessages.Num() == 0)  
  68.     {  
  69.         AIDHGameState::Singleton()->ChangeState(GameStateEnum::LoginState);  
  70.         return;  
  71.     }  
  72.   
  73.     //加载本地的资源版本文件  
  74.     if (FFileHelper::LoadFileToString(LocalFile, *LocalVersionFileLocation))  
  75.     {  
  76.         //获取本地版本信息  
  77.         GetVersionMessageFromString(LocalFile, LocalMD5, LocalFileMessages);  
  78.         if (LocalMD5.Equals(ServerMD5))  
  79.         {  
  80.             UE_LOG(LogClass, Log, TEXT("版本文件MD5值相同.无需更新"));  
  81.             AIDHGameState::Singleton()->ChangeState(GameStateEnum::LoginState);  
  82.             return;  
  83.         }  
  84.     }  
  85.     //覆盖本地的资源版本文件  
  86.     FFileHelper::SaveStringToFile(ServerFile, *LocalVersionFileLocation);  
  87.     //开始更新资源  
  88.     UpdateResources(ServerFileMessages);  
  89. }  
  90.   
  91. bool GameUpdateResourcesState::GetVersionMessageFromString(FString JsonString, FString& VersionFileMD5, TArray<FileMessage>& FileMessages)  
  92. {  
  93.     TSharedPtr<FJsonObject> JsonObject;  
  94.     TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);  
  95.     //将文件中的内容变成你需要的数据格式  
  96.     if (FJsonSerializer::Deserialize(Reader, JsonObject))  
  97.     {  
  98.         TSharedPtr<FJsonObject> FileObject = JsonObject->GetObjectField("FileVersion");  
  99.         VersionFileMD5 = FileObject->GetStringField("MD5");  
  100.         FileMessages.Empty();  
  101.         const TArray<TSharedPtr<FJsonValue>> Files = JsonObject->GetArrayField("Files");  
  102.         for (int i = 0; i < Files.Num(); i++)  
  103.         {  
  104.             const TSharedPtr<FJsonObject>* FileMessageObject;  
  105.             if (Files[i].Get()->TryGetObject(FileMessageObject))  
  106.             {  
  107.                 FileMessage* FileMes = new FileMessage();  
  108.                 FileMes->FileName = FileMessageObject->Get()->GetStringField("FileName");  
  109.                 FileMes->FileMD5 = FileMessageObject->Get()->GetStringField("MD5");  
  110.                 FileMessages.Add(*FileMes);  
  111.             }  
  112.         }  
  113.         return true;  
  114.     }  
  115.     else  
  116.     {  
  117.         UE_LOG(LogClass, Error, TEXT("无法解析json数据,Json数据可能有误..."));  
  118.         return false;  
  119.     }  
  120. }  
  121.   
  122. void GameUpdateResourcesState::UpdateResources(const TArray<FileMessage>& Files)  
  123. {  
  124.     NeedDownloadNumber = Files.Num();  
  125.     DownloadCompleteNumber = 0;  
  126.   
  127.     //一个个文件进行下载  
  128.     for (int i = 0; i < Files.Num(); i++)  
  129.     {  
  130.         FString FileURL = ServerPakDirectory + Files[i].FileName + TEXT(".pak");  
  131.         FString SaveURL = SavePakDirectory + Files[i].FileName + TEXT(".pak");  
  132.         FRequestDelegate DownloadCompleteDelegate;  
  133.         DownloadCompleteDelegate.BindRaw(this, &GameUpdateResourcesState::DownloadFileComplete, SaveURL, Files[i].FileName);  
  134.         AAssetManager::Singleton()->DownloadPakFile(FileURL, DownloadCompleteDelegate);  
  135.     }  
  136. }  
  137.   
  138. //文件下载完成回调  
  139. void GameUpdateResourcesState::DownloadFileComplete(FHttpResponsePtr Response, FString SavePath, FString FileName)  
  140. {  
  141.     FFileHelper::SaveArrayToFile(Response->GetContent(), *SavePath);  
  142.     UE_LOG(LogClass, Log, TEXT("文件:%s 已经下载完成,保存在%s"), *FileName, *SavePath);  
  143.     DownloadCompleteNumber++;  
  144.     //如果所有文件都下载完了.那么就进行下一个游戏状态.  
  145.     if (DownloadCompleteNumber == NeedDownloadNumber)  
  146.     {  
  147.         UE_LOG(LogClass, Log, TEXT("资源已全部更新完毕,跳转到登录状态"));  
  148.         AIDHGameState::Singleton()->ChangeState(GameStateEnum::LoginState);  
  149.     }  
  150. }  
// 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...有可能以后也不会讨论...

1.创建一个PAK文件:

用CMD运行打开D:\Epic Games\UE_4.15\Engine\Binaries\Win64下有个UnrealPak文件

cd到路径后 通过运行时传递参数 UnrealPak.exe [要生成的pak文件] -create=[要打包的文件列表] -order=[文件在pak中排序描述文件] [输出格式] [是否加密] [是否压缩]

例如:UnrealPak.exe test.pak -create=paklist.txt -order=CookerOpenOrder.log -UTF8Output -encrypt -compress

UnrealPak.exe D:\OutPak.Pak C:\Users\Administrator\Desktop\Client\UnrealProjects\IDHome\Content\Materials\bingxiang.uasset

这样就有一个PAK文件了,接下来创建代码

.h

[cpp]  view plain  copy
  1. UFUNCTION(BlueprintCallable, Category = "PAK")  
  2.     void LoadPakComplete();  
  3. UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PAK")  
  4.     UObject*  TheLoadObject ;  
.cpp

#include "IPlatformFilePak.h"

[cpp]  view plain  copy
  1. void AMyActor::LoadPakComplete()  
  2. {       //如果你想直接加载本地的pak的话,方法的参数列表删掉,然后把SaveArrayToFile这句话也删了,SaveContentDir填写你要加载的路径就可以了...  
  3.         //1.把下载好的文件保存起来  
  4.     FString SaveContentDir = FPaths::GameContentDir() + TEXT("OutPak.pak");  
  5.     //2.加载刚才保存的文件  
  6.     //获取当前使用的平台,这里使用的是WIN64平台  
  7.     IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();  
  8.     //初始化PakPlatformFile  
  9.     FPakPlatformFile* PakPlatformFile = new FPakPlatformFile();  
  10.     PakPlatformFile->Initialize(&PlatformFile, TEXT(""));  
  11.     FPlatformFileManager::Get().SetPlatformFile(*PakPlatformFile);  
  12.   
  13.     //获取Pak文件  
  14.     FPakFile PakFile(*SaveContentDir, false);  
  15.     UE_LOG(LogClass, Log, TEXT("get PakFile..."))  
  16.     //设置pak文件的Mount点.   
  17.     FString MountPoint(FPaths::EngineContentDir()); //"/../../../Engine/Content/"对应路径  
  18.     PakFile.SetMountPoint(*MountPoint);  
  19.     //对pak文件mount到前面设定的MountPoint  
  20.     if (PakPlatformFile->Mount(*SaveContentDir, 0, *MountPoint))  
  21.     {  
  22.         UE_LOG(LogClass, Log, TEXT("Mount Success"));  
  23.         TArray<FString> FileList;  
  24.         //得到Pak文件中MountPoint路径下的所有文件  
  25.         PakFile.FindFilesAtPath(FileList, *PakFile.GetMountPoint(), truefalsetrue);  
  26.         FStreamableManager StreamableManager;  
  27.         //对文件的路径进行处理,转换成StaticLoadObject的那种路径格式  
  28.         FString AssetName = FileList[0];  
  29.         FString AssetShortName = FPackageName::GetShortName(AssetName);  
  30.         FString LeftStr;  
  31.         FString RightStr;  
  32.         AssetShortName.Split(TEXT("."), &LeftStr, &RightStr);  
  33.         AssetName = TEXT("/Engine/") + LeftStr + TEXT(".") + LeftStr;    //我们加载的时候用的是这个路径  
  34.         FStringAssetReference reference = AssetName;  
  35.         //加载UObject  
  36.         UObject* LoadObject = StreamableManager.SynchronousLoad(reference);  
  37.         if (LoadObject != nullptr)  
  38.         {  
  39.             UE_LOG(LogClass, Log, TEXT("Object Load Success..."))  
  40.                 TheLoadObject = LoadObject;  
  41.         }  
  42.         else  
  43.         {  
  44.             UE_LOG(LogClass, Log, TEXT("Can not Load asset..."))  
  45.         }  
  46.     }  
  47.     else  
  48.     {  
  49.         UE_LOG(LogClass, Error, TEXT("Mount Failed"));  
  50.     }  
  51. }  
这样就加载到内存了。