本文参考https://blog.ch-wind.com/ue4-patch-release-dlc/

UE4的热更新,目的就是更新Pak包,生成Pak包的方法网上很多,根据需求看使用UE4自带的(搜索DLC),还是自己根据自己的规则打pak都是可以的(搜索UnrealPak.exe)

UE4生成Pak的规则是(基于UE4提供的Launch),先生成游戏主体,游戏主体会要填版本号,后面不管是DLC还是Patch都是基于这个游戏主体生成的,后面的不管项目改动什么,在生成DLC或者Patch的时候,都有一个基于版本号,这个版本号就是主体的版本号,都是基于主体做的增量更新,生成游戏主体在没有指定保存路径的情况下,是生成在项目根目录下的Release文件夹下面

下载pak包在蓝图里面提供的有Request Content 和 Start Install就可以完成pak的下载,这个下载只能一次下载一个pak,所有还是要有一个更新的文件列表,我自己简单写了一个Windows Application的工具后面添上源码,让人奇怪的是 UE4蓝图里面并没有提供Http的下载接口,所有自己也写了一个后面添上

Windows Application代码(生成Version的)

using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Data;
 using System.Drawing;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows.Forms;
 using Newtonsoft.Json;
 using System.IO;namespace MakeVersion
 {
     public partial class Form1 : Form
     {
         private string mainfestPath = "";
         private string versionPath = "";
         private string version = "";
         private List<string> mainfestList = new List<string>();
         public Form1()
         {
             InitializeComponent();
         }        //选择mainfest文件,并加入List
         private void button1_Click(object sender, EventArgs e)
         {
             FolderBrowserDialog dialog = new FolderBrowserDialog();
             dialog.Description = "选择MainfestDir目录";
             if (dialog.ShowDialog() == DialogResult.OK)
             {
                 if (string.IsNullOrEmpty(dialog.SelectedPath))
                 {
                     MessageBox.Show(this, "选择文件夹不能为空");
                     return;
                 }                MainfestPathText.Text = mainfestPath = dialog.SelectedPath;
                 DirectoryInfo folder = new DirectoryInfo(dialog.SelectedPath);
                 mainfestList.Clear();
                 foreach (FileInfo file in folder.GetFiles())
                 {
                     if (!file.Name.Contains(".manifest"))
                         continue;                    mainfestList.Add(file.Name);
                 }
             }
         }        //选择保存version的目录
         private void button2_Click(object sender, EventArgs e)
         {
             FolderBrowserDialog dialog = new FolderBrowserDialog();
             dialog.Description = "选择保存version的目录";
             if (dialog.ShowDialog() == DialogResult.OK)
             {
                 if (string.IsNullOrEmpty(dialog.SelectedPath))
                 {
                     MessageBox.Show(this, "请选择文件夹");
                     return;
                 }                SavePathText.Text = versionPath = dialog.SelectedPath;
             }
         }        private void button3_Click(object sender, EventArgs e)
         {
             if (mainfestList.Count < 1 || string.IsNullOrEmpty(mainfestPath) || string.IsNullOrEmpty(versionPath))
             {
                 MessageBox.Show(this, "前面的路径选择操作违法或没有要生成的mainfest文件");
                 return;
             }            if (string.IsNullOrEmpty(VersionText.Text))
             {
                 MessageBox.Show(this, "请输入生成的版本号");
                 return;            }
            StringBuilder sb = new StringBuilder();
             StringWriter sw = new StringWriter(sb);
             JsonTextWriter jsonWrite = new JsonTextWriter(sw);
             jsonWrite.Formatting = Formatting.Indented;            jsonWrite.WriteStartObject();
             jsonWrite.WritePropertyName("ClientVersion:");
             jsonWrite.WriteValue(VersionText.Text);            jsonWrite.WritePropertyName("Files");
             jsonWrite.WriteStartArray();            string readTxt = string.Empty;
             string readPath = string.Empty;
             string value = string.Empty;
             Newtonsoft.Json.Linq.JObject jo = null;
             for (int i = 0; i < mainfestList.Count; i++)
             {
                 readPath = mainfestPath + "\\" + mainfestList[i];
                 if (!File.Exists(readPath)) continue;
                 readTxt = File.ReadAllText(readPath, Encoding.ASCII);
                 if (string.IsNullOrEmpty(readTxt)) continue;                jo = (Newtonsoft.Json.Linq.JObject)JsonConvert.DeserializeObject(readTxt);
                 jsonWrite.WriteStartObject();
                 jsonWrite.WritePropertyName("FileName");
                 jsonWrite.WriteValue(jo["FileManifestList"][0]["Filename"].ToString());
                 jsonWrite.WritePropertyName("FileHash");
                 jsonWrite.WriteValue(jo["FileManifestList"][0]["FileHash"].ToString());
                 jsonWrite.WriteEndObject();
             }            MessageBox.Show("生成完毕");
             jsonWrite.WriteEndArray();
             jsonWrite.WriteEndObject();            File.WriteAllText(versionPath + "/Version.txt", sb.ToString());
         }
     }
 }
  Windows Appication(生成Pak的,要在项目的build.cs文件里面添加 Http 和 Json)
using Newtonsoft.Json;
 using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Data;
 using System.Diagnostics;
 using System.Drawing;
 using System.IO;
 using System.Linq;
 using System.Security.Cryptography;
 using System.Text;
 using System.Threading.Tasks;
 using System.Windows.Forms;namespace UnrealPakTool
 {
     public partial class Form1 : Form
     {
         private string m_UnrealToolPath = "";
         private string m_InputFolderPath = "";
         private string m_OutPutPakPath = "";
         private List<string> m_NeedMakePakList = new List<string>();
         public Form1()
         {
             InitializeComponent();
         }        private void label1_Click(object sender, EventArgs e)
         {        }
         //UnrealPak所在的文件夹路径
         private void button1_Click(object sender, EventArgs e)
         {
             FolderBrowserDialog dialog = new FolderBrowserDialog();
             dialog.Description = "请选择UnrealPak所在的文件目录";
             if (dialog.ShowDialog() == DialogResult.OK)
             {
                 if (string.IsNullOrEmpty(dialog.SelectedPath))
                 {
                     MessageBox.Show(this, "选择文件夹不能为空");
                     return;
                 }                m_UnrealToolPath = dialog.SelectedPath;
                 textBox1.Text = dialog.SelectedPath;
             }
         }
         //需要打包的文件夹路径
         private void button2_Click(object sender, EventArgs e)
         {
             FolderBrowserDialog dialog = new FolderBrowserDialog();            dialog.Description = "请选择uasset所在的文件夹";
             if (dialog.ShowDialog() == DialogResult.OK)
             {
                 if (string.IsNullOrEmpty(dialog.SelectedPath))
                 {
                     MessageBox.Show(this, "选择文件夹不能为空");
                     return;
                 }                m_InputFolderPath = dialog.SelectedPath;
                 textBox2.Text = dialog.SelectedPath;
                 DirectoryInfo folder = new DirectoryInfo(dialog.SelectedPath);
                 m_NeedMakePakList.Clear();
                 foreach (FileInfo file in folder.GetFiles())
                 {
                     m_NeedMakePakList.Add(file.Name);
                 }
             }
         }
         //生成pak的保存路径
         private void button3_Click(object sender, EventArgs e)
         {
             FolderBrowserDialog dialog = new FolderBrowserDialog();
             dialog.Description = "请选择pak文件的保存路径";
             if (dialog.ShowDialog() == DialogResult.OK)
             {
                 if (string.IsNullOrEmpty(dialog.SelectedPath))
                 {
                     MessageBox.Show(this, "选择文件夹不能为空");
                     return;
                 }                m_OutPutPakPath = dialog.SelectedPath;
                 textBox3.Text = dialog.SelectedPath;
             }
         }
         //批量打包
         private void button4_Click(object sender, EventArgs e)
         {
             if (string.IsNullOrEmpty(m_InputFolderPath) || string.IsNullOrEmpty(m_OutPutPakPath) || string.IsNullOrEmpty(m_UnrealToolPath))
             {
                 MessageBox.Show(this, "有至少一个文件夹的路径为空,请选择相应的路径");
                 return;
             }            StringBuilder sb = new StringBuilder();
             StringWriter sw = new StringWriter(sb);
             JsonTextWriter json = new JsonTextWriter(sw);
             json.Formatting = Formatting.Indented;
             DateTime dateTime = DateTime.UtcNow;
             int second = dateTime.Second;            string fileMD5 = StrToMD5(second.ToString());
             json.WriteStartObject();
             json.WritePropertyName("FileVersion");
             json.WriteStartObject();
             json.WritePropertyName("MD5");
             json.WriteValue(fileMD5);
             json.WriteEndObject();            if (!File.Exists(m_UnrealToolPath + @"\UnrealPak.exe"))
             {
                 MessageBox.Show(this, "UnrealPak.exe文件没找到");
                 return;
             }            json.WritePropertyName("Files");
             json.WriteStartArray();            string assetNamePath = m_InputFolderPath.Split(' ')[0].Replace("\\", "/");
             for (int i = 0; i < m_NeedMakePakList.Count; i++)
             {
                 string assetPath = m_InputFolderPath + "\\" + m_NeedMakePakList[i];
                 string assetName = ReplaceFileSuffixes(m_NeedMakePakList[i]);
                 string md5String = StrToMD5(assetPath);
                 string outPath = m_OutPutPakPath + "\\" + assetName + ".pak";                ProcessStartInfo info = new ProcessStartInfo();
                 info.FileName = m_UnrealToolPath + @"\UnrealPak.exe";
                 info.Arguments = @outPath + @" " + @assetPath;
                 info.WindowStyle = ProcessWindowStyle.Minimized;
                 Process process = Process.Start(info);
                 process.WaitForExit();                json.WriteStartObject();
                 json.WritePropertyName("FileName");
                 json.WriteValue(assetName);
                 json.WritePropertyName("MD5");
                 json.WriteValue(md5String);
                 json.WriteEndObject();
             }            MessageBox.Show("生成pak完毕");
             json.WriteEndArray();
             json.WriteEndObject();            string saveData = m_UnrealToolPath + ";" + m_InputFolderPath + ";" + m_OutPutPakPath;
             File.WriteAllText(Environment.CurrentDirectory + "/save.txt", saveData);            File.WriteAllText(m_OutPutPakPath + "/Version.txt", sb.ToString());
         }        public string ReplaceFileSuffixes(string fileName)
         {
             if (fileName.Contains("."))
             {
                 fileName = fileName.Split('.')[0];
             }
             return fileName;
         }        public string StrToMD5(string str)
         {
             string md5Str = "";
             byte[] data = Encoding.GetEncoding("GB2312").GetBytes(str);
             MD5 md5 = new MD5CryptoServiceProvider();
             byte[] outBytes = md5.ComputeHash(data);            for (int i = 0; i < outBytes.Length; i++)
             {
                 md5Str += outBytes[i].ToString("x2");
             }            return md5Str.ToLower();
         }
     }
 }

UE4下载Http(蓝图可调用)

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
 #include "UObject/Interface.h"
 #include"Http.h"
 #include "HttpRequestTest.generated.h"UCLASS(BlueprintType)
 class UHttpDownLoadContont : public UObject
 {
     GENERATED_BODY()public:
     UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "DownLoadContont")
         TArray<FString> FileNameArray;
 };DECLARE_DYNAMIC_DELEGATE_OneParam(FHttpDownLoadSuccess, UHttpDownLoadContont*, DownLoadContont);
 DECLARE_DYNAMIC_DELEGATE_TwoParams(FHttpDownLoadFailed, int32, ErrorCode, FString, ErrorMsg);
 // This class does not need to be modified.
 UINTERFACE(meta = (CannotImplementInterfaceInBlueprint))
 class UHttpRequestTest : public UInterface
 {
     GENERATED_BODY()
 };/**
  * 
  */
 class CHUNKSTEST_API IHttpRequestTest
 {
     GENERATED_BODY()    // Add interface functions to this class. This is the class that will be inherited to implement this interface.
 public:
     UFUNCTION(BlueprintCallable, Category = "HttpDownload")
     virtual void HttpDownLoad(const FString &URL, FHttpDownLoadSuccess OnSuccess, FHttpDownLoadFailed OnFailed);    //获取Mainfeast中的FileName
     void HttpRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FHttpDownLoadSuccess OnSuccess, FHttpDownLoadFailed OnFailed);private:
     void LoadLocalVersion();
     bool bCompareLocalAndServerFileHash(FString FileNameByServer, FString FileHashByServer);    FString LocalVersionContent;
     TMap<FString, FString> LocalVersionFileNameMap;
 }; 
// Fill out your copyright notice in the Description page of Project Settings.
#include "HttpRequestTest.h"
 #include "HttpModule.h"
 #include "JsonSerializer.h"
 #include "ModuleManager.h"
 #include "Paths.h"
 #include "PlatformFilemanager.h"
 #include "FileHelper.h"
 //#include "Interfaces/IHttpResponse.h" // Add default functionality here for any IHttpRequestTest functions that are not pure virtual.
 void IHttpRequestTest::LoadLocalVersion()
 {
     
     LocalVersionContent.Empty();
     LocalVersionFileNameMap.Empty();
     FString Path = FPaths::ConvertRelativePathToFull(FPaths::GameDir()) + "/Version.txt";
     IPlatformFile &PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
     if (!PlatformFile.FileExists(*Path))
     {
         return;
     }
     FFileHelper::LoadFileToString(LocalVersionContent, *Path);
     if (LocalVersionContent.IsEmpty())
     {
         return;
     }    TSharedPtr<FJsonObject> JsonObject;
     TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(LocalVersionContent);    if (FJsonSerializer::Deserialize(Reader, JsonObject))
     {
         UHttpDownLoadContont *DownLoadContont = NewObject<UHttpDownLoadContont>();
         DownLoadContont->FileNameArray.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))
             {
                 FString FileName = FileMessageObject->Get()->GetStringField("FileName");
                 FString FileHash = FileMessageObject->Get()->GetStringField("FileHash");
                 LocalVersionFileNameMap.Add(FileName, FileHash);
             }
         }
     }
 }bool IHttpRequestTest::bCompareLocalAndServerFileHash(FString FileNameByServer, FString FileHashByServer)
 {
     if (FileNameByServer.IsEmpty() || FileHashByServer.IsEmpty())
     {
         return false;
     }    if (LocalVersionFileNameMap.Contains(FileNameByServer))
     {
         FString *HashValue = LocalVersionFileNameMap.Find(FileNameByServer);
         if (HashValue != nullptr && HashValue->Compare(FileHashByServer))
         {
             return false;
         }
     }    return true;
 } void IHttpRequestTest::HttpDownLoad(const FString &URL, FHttpDownLoadSuccess OnSuccess, FHttpDownLoadFailed OnFailed)
 {
     if (URL.IsEmpty())
     {
         UE_LOG(LogClass, Log, TEXT("URL Is Null"));
         return;
     }    //LoadLocalVersion();
    FHttpModule& HttpModule = FModuleManager::LoadModuleChecked<FHttpModule>("HTTP");
     TSharedRef<class IHttpRequest> HttpRequest = HttpModule.Get().CreateRequest();
     HttpRequest->OnProcessRequestComplete().BindRaw(this, &IHttpRequestTest::HttpRequestComplete, OnSuccess, OnFailed);
     HttpRequest->SetURL(URL);
     HttpRequest->SetHeader(TEXT("Content-Type"), TEXT("application/json"));
     HttpRequest->SetVerb(TEXT("GET"));
     HttpRequest->ProcessRequest();
 } void IHttpRequestTest::HttpRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bWasSuccessful, FHttpDownLoadSuccess OnSuccess, FHttpDownLoadFailed OnFailed)
 {
     if (!bWasSuccessful || !Response.IsValid())
     {
         OnFailed.ExecuteIfBound(400,"NetWork Connect Failed");
         return;
     }    if (!EHttpResponseCodes::IsOk(Response->GetResponseCode()))
     {
         OnFailed.ExecuteIfBound(Response->GetResponseCode(),"NetWork Connect Failed");
         return;
     }    FString MainfestTxt = Response->GetContentAsString();
     if (MainfestTxt.IsEmpty())
     {
         OnFailed.ExecuteIfBound(401,"Mainfest Not Content");
         return;
     }    TSharedPtr<FJsonObject> JsonObject;
     TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(MainfestTxt);
     //将文件中的内容变成你需要的数据格式
     if (FJsonSerializer::Deserialize(Reader, JsonObject))
     {
         UHttpDownLoadContont *DownLoadContont = NewObject<UHttpDownLoadContont>();
         DownLoadContont->FileNameArray.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))
             {
                 FString FileName = FileMessageObject->Get()->GetStringField("FileName");
                 /*FString FileHash = FileMessageObject->Get()->GetStringField("FileHash");
                 if (bCompareLocalAndServerFileHash(FileName, FileHash))
                 {
                     DownLoadContont->FileNameArray.Add(FileName);
                 }*/
                 DownLoadContont->FileNameArray.Add(FileName);
             }
         }        OnSuccess.ExecuteIfBound(DownLoadContont);
     }
     else
     {
         OnFailed.ExecuteIfBound(402,"Read Mainfest File Failed");
     }
 }

 

-------------------------------------------------------------

今天测试HttpChunksInstall的时候发现每次下载都会全部下载,并且会删掉第一个,查看源码发现

游戏客户端热更技术lua_游戏客户端热更技术lua

每次都会去安装目录下的第一个mainfest文件作为基础文件进行对比!所以解决方案,大家根据需求去做吧!不用修改源码的情况下就是一个目录下放一个pak文件~

欢迎大家提问,大家一起进步!