修改GameStart脚本
之前逻辑时GameStart直接执行加载ui的语句,现在变成将几个重要的语句如启动Lua的StartLua和启动UI的GameInit放进函数中,注册到EventManager,根据是否热更新,fire执行对应的事件。没有热更新直接init,有热更新,在HotUpdate中加载loadingUI最后EnterGame里Fire启动UI的GameInit事件
public class GameStart : MonoBehaviour
{
public GameMode GameMode;
public bool OpenLog;
// Start is called before the first frame update
void Start()
{
//开始时订阅事件
Manager.Event.Subscribe((int)GameEvent.StartLua, StartLua);
Manager.Event.Subscribe((int)GameEvent.GameInit, GameInit);
AppConst.GameMode = this.GameMode;
AppConst.OpenLog = this.OpenLog;
DontDestroyOnLoad(this);
if (AppConst.GameMode == GameMode.UpdateMode)
this.gameObject.AddComponent<HotUpdate>();//如果是热更新模式,启动更新后立刻解析filelist启动lua肯定不行,因为是旧文件,需要热更新完成后再调用
else
Manager.Event.Fire((int)GameEvent.GameInit);//不是热更新模式就直接执行前面注册的gameInit方法
}
private void GameInit(object args)
{
if (AppConst.GameMode != GameMode.EditorMode)
Manager.Resource.ParseVersionFile();//编辑模式下不需要filelist
Manager.Lua.Init();
}
void StartLua(object args)
{
//初始化完成之后(lua都加载完),在执行回调
Manager.Lua.StartLua("main"); //输入的文件名
//输入的是函数名
XLua.LuaFunction func = Manager.Lua.LuaEnv.Global.Get<XLua.LuaFunction>("Main");
func.Call();
Manager.Pool.CreateGameObjectPool("UI", 10);
Manager.Pool.CreateGameObjectPool("Monster", 120);
Manager.Pool.CreateGameObjectPool("Effect", 120);
Manager.Pool.CreateAssetPool("AssetBundle", 10);
}
public void OnApplicationQuit()
{
Manager.Event.UnSubscribe((int)GameEvent.StartLua, StartLua);
Manager.Event.UnSubscribe((int)GameEvent.GameInit, GameInit);
}
}
public enum GameEvent
{
GameInit = 10000,
StartLua,
}
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.Networking;
///
/// 热更新类:
/// 定义了下载信息DownFileInfo,和用来保存只读目录filelist和服务器filelist的变量
/// 通用UnityWebRequest,既可以从本地下载(释放)文件也可以从服务器下载文件,用DownLoadFile实现了单个或多个文件(封装了单个文件下载)下载
/// GetFileList可以从filelist获取需要更新的文件信息
/// 流程:
/// IsFirstInstall如果是初次安装,就从可读目录释放资源ReleaseResources到可读写目录(然后热更新CheckUpdate),如果可读目录没有资源就从服务器下载资源CheckUpdate到可读写目录。释放与下载逻辑一致
/// ReleaseResources释放可读目录filelist,释放完OnReleaseReadPathFileListComplete下载所有包,回调单个文件下载好OnReleaseFileComplete写入可读写目录,所有文件下载完OnReleaseAllFileComplete把可读目录filelist写入可读写目录,CheckUpdate
/// CheckUpdate下载服务器filelist,下载完OnDownLoadServerFileListComplete下载所有包,回调单个文件下载好OnUpdateFileComplete写入可读写目录,所有文件下载完OnUpdateAllFileComplete把服务器filelist写入可读写目录,进入游戏
///
public class HotUpdate : MonoBehaviour
{
//下载文件数量
private int m_DownloadCount;
private GameObject loadingObj;
private LoadingUI loadingUI;
private void Start()
{
GameObject go = Resources.Load<GameObject>("LoadingUI");
loadingObj = Instantiate(go);
loadingObj.transform.SetParent(this.transform);
loadingUI = loadingObj.GetComponent<LoadingUI>();
if(IsFirstInstall())
{
//如果是初次安装,先按照可读目录的filelist释放资源到可读写目录,再更新
ReleaseResources();
}
else
{
CheckUpdate();
}
}
private void ReleaseResources()
{
m_DownloadCount = 0;//释放资源时,把下载文件数量初始化一下
string url = Path.Combine(PathUtil.ReadPath, AppConst.FileListName);
DownFileInfo info = new DownFileInfo();
info.url = url;
//UnityWebRequest可以从本地下载
//先读取filelist里需要释放的文件
StartCoroutine(DownLoadFile(info, OnReleaseReadPathFileListComplete));
}
private void OnReleaseReadPathFileListComplete(DownFileInfo file)
{
//从只读目录加载完filelist后,保存filelist的内容
m_ReadPathFileListData = file.fileData.data;
//获取到filelist里的所有要释放的文件的文件信息
List<DownFileInfo> fileInfos = GetFileList(file.fileData.text, PathUtil.ReadPath);
StartCoroutine(DownLoadFile(fileInfos, OnReleaseFileComplete, OnReleaseAllFileComplete));
//开始释放资源时,将释放文件的数量去更新进度条的显示文本
loadingUI.InitProgress(fileInfos.Count, "Releasing resources, not consuming data...");
}
private void OnReleaseFileComplete(DownFileInfo fileinfo)
{
Debug.Log("OnReleaseFileComplete:" + fileinfo.url);
//可读写目录加bundle目录
string writeFile = Path.Combine(PathUtil.ReadWritePath, fileinfo.fileName);
FileUtil.WriteFile(writeFile, fileinfo.fileData.data);
m_DownloadCount++;
loadingUI.UpdateProgress(m_DownloadCount);//每释放完一个文件,都更新一下进度条
}
private void CheckUpdate()
{
m_DownloadCount = 0;
//更新前,先从本地的可读写目录先获取旧的filelist文件信息,读取md5
string oldFilelistPath = PathUtil.ReadWritePath + AppConst.FileListName;
oldFileMD5 = new Dictionary<string, string>();
if (FileUtil.IsExits(oldFilelistPath))
{
//对文件进行读取
string[] data = File.ReadAllLines(oldFilelistPath);
//解析文件信息
for (int i = 0; i < data.Length; i++)
{
string[] infos = data[i].Split('|');
string fileName = infos[1];
string md5 = infos[2];
oldFileMD5.Add(fileName, md5);
}
}
//获取filelist再资源服务器上的地址
string url = Path.Combine(AppConst.ResourcesUrl, AppConst.FileListName);
DownFileInfo info = new DownFileInfo();
info.url = url;
StartCoroutine(DownLoadFile(info, OnDownLoadServerFileListComplete));
}
private void OnDownLoadServerFileListComplete(DownFileInfo file)
{
m_DownloadCount = 0;//开始更新时重置下载文件数量
//保存最新的filelist
m_ServerFileListData = file.fileData.data;
//获取资源服务器的文件信息目录
List<DownFileInfo> fileInfos = GetFileList(file.fileData.text, AppConst.ResourcesUrl);
//定义需要下载的文件集合
List<DownFileInfo> downListFiles = new List<DownFileInfo>();
//遍历资源服务器的文件信息
for (int i = 0; i < fileInfos.Count; i++)
{
string localFile = Path.Combine(PathUtil.ReadWritePath, fileInfos[i].fileName);
//判断本地是否存在,如果不存在就下载
if(!FileUtil.IsExits(localFile))
{
fileInfos[i].url = Path.Combine(AppConst.ResourcesUrl, fileInfos[i].fileName);
downListFiles.Add(fileInfos[i]);
}
}
if (downListFiles.Count > 0)
{
StartCoroutine(DownLoadFile(fileInfos, OnUpdateFileComplete, OnUpdateAllFileComplete));
loadingUI.InitProgress(downListFiles.Count, "Downloading new resources...");//当判断完本地没有哪些文件,服务器哪些文件需要下载后,更新进度条显示,传进去需要下载的文件数量
}
else
{
EnterGame();
}
}
private void OnUpdateFileComplete(DownFileInfo file)
{
Debug.Log("OnUpdateFileComplete:" + file.url);
//下载新文件
string writeFile = Path.Combine(PathUtil.ReadWritePath, file.fileName);
FileUtil.WriteFile(writeFile, file.fileData.data);
m_DownloadCount++;
loadingUI.UpdateProgress(m_DownloadCount);//每更新下载完一个文件都更新一下进度条
}
private void OnUpdateAllFileComplete()
{
//所有文件下载完,写入最新的filelist
FileUtil.WriteFile(Path.Combine(PathUtil.ReadWritePath, AppConst.FileListName), m_ServerFileListData);
EnterGame();
loadingUI.InitProgress(0, "Loading...");
}
private void EnterGame()
{
Manager.Event.Fire((int)GameEvent.GameInit);
Destroy(loadingObj,2);//立刻退出太突兀了,适当延时
}
}
Loading界面需要在加载前执行,因此只能放在Resources路径下
编写LoadingUI脚本挂到LoadingUI.prefab上,把几个变量赋值进去。
重新构建ab包,删除可读写目录的缓存,就可以测试运行了
修改为安卓平台,修改main.bytes中的服务器地址和AppConst的资源地址为电脑实际的地址,不能是127了
重新生成Xlua code、构建Android的ab包,然后拷贝到web服务器
rapidjson需要编译一个android的才能在安卓运行
https://blog.csdn.net/qq_36480278/article/details/109803833
发现对于不同分辨率下UI界面显示有问题,因此将TestUI修改为拉伸,Canvas设置为拉伸,参考分辨率1920*1080,修改UIManager中的UI组的建立
public void SetUIGroup(List<string> group)
{
for (int i = 0; i < group.Count; i++)
{
GameObject go = new GameObject("Group-" + group[i]);
RectTransform RT = go.AddComponent<RectTransform>();
//将普通物体转化为适用于UI的RectTransform,并通过下面的组件设置为Stretch方法。
RT.anchorMin = Vector2.zero;
RT.anchorMax = Vector2.one;
RT.pivot = 0.5f * Vector2.one;
RT.offsetMin = Vector2.zero;
RT.offsetMax = Vector2.zero;
go.transform.SetParent(m_UIParent, false);
m_UIGroups.Add(group[i], go.transform);
}
}
为文件添加了MD5码校验
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
///
/// 构建工具类:
/// 创建了三种构建方法windows android ios。
/// Build方法:从所有路径查找文件,排除meta后,把每个文件名作为被打包资源名和bundle名(当然一个bundle可以打包多个文件),GetDependence获取所有文件的依赖文件,
/// 把这些信息写入到bundleInfos,BuildPipeline.BuildAssetBundles一下,就建好了ab包,然后把所有包的信息写入filelist中
///
public class BuildTool : Editor
{
//如何使用Build呢,直接添加工具栏
[MenuItem("Tools/Build Windows Bundle")]
static void BundleWindowsBuild()
{
Build(BuildTarget.StandaloneWindows);
}
//如何使用Build呢,直接添加工具栏
[MenuItem("Tools/Build Android Bundle")]
static void BundleAndroidBuild()
{
Build(BuildTarget.Android);
}
//如何使用Build呢,直接添加工具栏
[MenuItem("Tools/Build IOS Bundle")]
static void BundleIOSBuild()
{
Build(BuildTarget.iOS);
}
//为了能够构建多平台,需要把目标平台作为参数传入。
static void Build(BuildTarget target)
{
//主要目的是收集这个build信息,需要打哪些文件,需要给bundle包用一个什么样的名字,BuildAssetBundles函数用到这个Build数组
List<AssetBundleBuild> assetBundleBuilds = new List<AssetBundleBuild>();
//文件信息列表
List<string> bundleInfos = new List<string>();
//第一步搜索出我们这个所有文件的文件名Directory.GetDirectories和Directory.GetFiles对应两种打包策略一个获取文件夹一个获取文件,GetFiles比较简单
//searchPattern通配符,*是默认 https://www.cnblogs.com/ost/archive/2006/08/20/481625.html
string[] files = Directory.GetFiles(PathUtil.BuildResourcesPath, "*", SearchOption.AllDirectories);
//所有文件都找出来了,需要排除调meta文件
for (int i = 0; i < files.Length; i++)
{
if (files[i].EndsWith(".meta") || files[i].EndsWith(".json"))
{
continue;
}
//创建一个需要build的Bundle
AssetBundleBuild assetBundle = new AssetBundleBuild();
//处理出来的路径斜杠可能不同。需要规范一下
string fileName = PathUtil.GetStandardPath(files[i]);
string assetName = PathUtil.GetUnityPath(fileName);//获取unity相对路径
assetBundle.assetNames = new string[] { assetName };//assetBundle是一个相对路径文件名
string bundleName = fileName.Replace(PathUtil.BuildResourcesPath, "").ToLower();
assetBundle.assetBundleName = bundleName + ".ab";//Bundle需要后缀是.ab,,,,,,,,至此,Bundle的信息收集完了,需要放进list
assetBundleBuilds.Add(assetBundle);
//获取文件MD5
string md5 = GetMD5(assetName);
//添加文件和依赖信息
List<string> dependenceInfo = GetDependence(assetName);
//版本信息包括文件路径名、bundle名、依赖文件列表
string bundleInfo = assetName + "|" + bundleName + ".ab" + "|" + md5;
if (dependenceInfo.Count > 0)
bundleInfo = bundleInfo + "|" + string.Join("|", dependenceInfo);
bundleInfos.Add(bundleInfo);
}
//为什么不用另一个重载BuildAssetBundles(string outputPath, BuildAssetBundleOptions assetBundleOptions, BuildTarget targetPlatform),是因为需要自己去资源设置bundle名打标签,很麻烦
//第二个参数把list转为array数组
//第三个参数是压缩格式,选择默认
//第四个参数是目标平台,先选择win
if(Directory.Exists(PathUtil.BundleOutPath))
{
//判断是否有路径,如果有这个文件夹,就删掉文件,,递归recursive删掉所有文件和子文件。
Directory.Delete(PathUtil.BundleOutPath, true);
}
Directory.CreateDirectory(PathUtil.BundleOutPath);//删除路径后,创建路径
BuildPipeline.BuildAssetBundles(PathUtil.BundleOutPath, assetBundleBuilds.ToArray(), BuildAssetBundleOptions.None, target);
//写bundle信息文件
File.WriteAllLines(PathUtil.BundleOutPath + "/" + AppConst.FileListName, bundleInfos);
//创建好文件后,在unity资源库中刷新一下
AssetDatabase.Refresh();
}
private static string GetMD5(string path)
{
//var path = EditorUtility.OpenFilePanel("", "", "lua");
FileStream file = new FileStream(path, System.IO.FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
{
sb.Append(retVal[i].ToString("x2"));
}
Debug.Log(sb.ToString());
return sb.ToString();
}
}
internal class BundleInfo
{
public string AssetName;
public string BundleName;
public string md5;
public List<string> Dependeces;
}
public void ParseVersionFile()
{
//拿到版本文件路径
string url = Path.Combine(PathUtil.BundleResourcePath, AppConst.FileListName);
//对文件进行读取
string[] data = File.ReadAllLines(url);
//解析文件信息
for (int i = 0; i < data.Length; i++)
{
BundleInfo bundleInfo = new BundleInfo();
string[] info = data[i].Split('|');
bundleInfo.AssetName = info[0];
bundleInfo.BundleName = info[1];
bundleInfo.md5 = info[2];
//list特性:本质是数组,可动态扩容
bundleInfo.Dependeces = new List<string>(info.Length - 3);
for (int j = 3; j < info.Length; j++)
{
bundleInfo.Dependeces.Add(info[j]);
}
m_BundleInfos.Add(bundleInfo.AssetName, bundleInfo);
//查找luaScripts下的lua文件,添加到luamanager中
if(info[0].IndexOf("LuaScripts") > 0)
{
Manager.Lua.LuaNames.Add(info[0]);
}
}
}
private Dictionary<string, string> oldFileMD5;
///
/// 检查更新
///
private void CheckUpdate()
{
m_DownloadCount = 0;
//更新前,先从本地的可读写目录先获取旧的filelist文件信息,读取md5
string oldFilelistPath = PathUtil.ReadWritePath + AppConst.FileListName;
oldFileMD5 = new Dictionary<string, string>();
if (FileUtil.IsExits(oldFilelistPath))
{
//对文件进行读取
string[] data = File.ReadAllLines(oldFilelistPath);
//解析文件信息
for (int i = 0; i < data.Length; i++)
{
string[] infos = data[i].Split('|');
string fileName = infos[1];
string md5 = infos[2];
oldFileMD5.Add(fileName, md5);
}
}
//获取filelist再资源服务器上的地址
string url = Path.Combine(AppConst.ResourcesUrl, AppConst.FileListName);
DownFileInfo info = new DownFileInfo();
info.url = url;
StartCoroutine(DownLoadFile(info, OnDownLoadServerFileListComplete));
}
private void OnDownLoadServerFileListComplete(DownFileInfo file)
{
m_DownloadCount = 0;//开始更新时重置下载文件数量
//保存最新的filelist
m_ServerFileListData = file.fileData.data;
//获取资源服务器的文件信息目录
List<DownFileInfo> fileInfos = GetFileList(file.fileData.text, AppConst.ResourcesUrl);
//定义需要下载的文件集合
List<DownFileInfo> downListFiles = new List<DownFileInfo>();
//遍历资源服务器的文件信息
for (int i = 0; i < fileInfos.Count; i++)
{
string localFile = Path.Combine(PathUtil.ReadWritePath, fileInfos[i].fileName);
//判断本地是否存在,如果不存在就下载/或本地存在但MD5码不同,就要下载///如果没有旧MD5list说明以前没下载过
if(!FileUtil.IsExits(localFile) || (oldFileMD5.Count >0 && FileUtil.IsExits(localFile) && fileInfos[i].md5 != oldFileMD5[fileInfos[i].fileName]))
{
//Debug.Log(localFile);
fileInfos[i].url = Path.Combine(AppConst.ResourcesUrl, fileInfos[i].fileName);
downListFiles.Add(fileInfos[i]);
}
}
if (downListFiles.Count > 0)
{
StartCoroutine(DownLoadFile(downListFiles, OnUpdateFileComplete, OnUpdateAllFileComplete));
loadingUI.InitProgress(downListFiles.Count, "Downloading new resources...");//当判断完本地没有哪些文件,服务器哪些文件需要下载后,更新进度条显示,传进去需要下载的文件数量
}
else
{
EnterGame();
}
}
https://www.cnblogs.com/kire-cat/p/16361589.html
使用natapp内网穿透后,将AppConst和mian.bytes的服务器地址修改为对应地址,即可从该地址访问到本机的文件并下载更新。
NetBox服务器将资源路径设置为127.0.0.1:80占用了80端口,而natapp的zhe1123.natapp1.cc指向了127.0.0.1:80这个端口,可以从这个域名和端口从本地服务器中读取文件并下载。
需要每次往NetBox中手动写入最新的GameServer域名和地址,让main.bytes连接到这个端口,获取数据。使用花生壳新建一个内网穿透专门用作映射服务器客户端的地址。因为只有花生壳的地址能分出host和port
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using System;
using System.IO;
public class GameServerManager : MonoBehaviour
{
internal class DownFileInfo
{
public string url;
public DownloadHandler fileData;
}
public string GameServerDomain;
public string GameServerPort;
IEnumerator DownLoadFile(DownFileInfo info, Action<DownFileInfo> Complete)
{
//老版本用WWW,现在已经弃用
//新版需要使用UnityWebRequest,,,,,引入UnityEngine.Networking
UnityWebRequest webRequest = UnityWebRequest.Get(info.url);
//下载一个文件,等待,下载完继续执行
yield return webRequest.SendWebRequest();
//if(webRequest.isHttpError || webRequest.isNetworkError)
if (webRequest.result == UnityWebRequest.Result.ProtocolError || webRequest.result == UnityWebRequest.Result.ConnectionError)
{
Debug.Log("下载文件出错:" + info.url);
yield break;
//下载失败,重试,有次数限制
}
//yield return new WaitForSeconds(0.2f);
//下载完成后,给info赋值
info.fileData = webRequest.downloadHandler;
//如果下载的是filelist,直接解析,用webRequest.downloadHandler.text.
//如果是bundle,可以写入,用webRequest.downloadHandler.data
Complete?.Invoke(info);
//下载完成后释放掉。
webRequest.Dispose();
}
//下载完GameServer文件后,解析其中的host和port
private void OnDownLoadGameServerFileComplete(DownFileInfo file)
{
string content = file.fileData.text;
string[] files = content.Split(":");
GameServerDomain = files[0];
GameServerPort = files[1];
Debug.Log(GameServerDomain + ":" + GameServerPort);
}
//作为事件被注册到时间中心,在Start中运行一次,拿到最新的服务器的域名和端口
public void GetGameServerHostPort(object args)
{
//获取filelist再资源服务器上的地址
string url = Path.Combine(AppConst.GameServerUrl, AppConst.GameServerFileName);
DownFileInfo info = new DownFileInfo();
info.url = url;
StartCoroutine(DownLoadFile(info, OnDownLoadGameServerFileComplete));
}
private void Start()
{
Manager.Event.Subscribe((int)GameEvent.GameServer, GetGameServerHostPort);
Manager.Event.Fire((int)GameEvent.GameServer);
}
public void OnApplicationQuit()
{
Manager.Event.UnSubscribe((int)GameEvent.GameServer, GetGameServerHostPort);
}
}
public static GameServerManager GameServer
{
get { return _gameServer; }
}
public void Awake()
{
_resource = this.gameObject.AddComponent<ResourceManager>();
_lua = this.gameObject.AddComponent<LuaManager>();
_ui = this.gameObject.AddComponent<UIManager>();
_entity = this.gameObject.AddComponent<EntityManager>();
_scene = this.gameObject.AddComponent<MySceneManager>();
_sound = this.gameObject.AddComponent<SoundManager>();
_event = this.gameObject.AddComponent<EventManager>();
_pool = this.gameObject.AddComponent<PoolManager>();
_net = this.gameObject.AddComponent<NetManager>();
_gameServer = this.gameObject.AddComponent<GameServerManager>();
}
public enum GameEvent
{
GameInit = 10000,
StartLua,
GameServer,
}
public class AppConst
{
//GameServer地址
public const string GameServerUrl = "http://zhe1123.natapp1.cc/GameServerUrl/";
public const string GameServerFileName = "GameServer.txt";
}
function Main()
msg_mgr.init();--加载所有message模块
Manager.Net:Init();--初始化TCP客户端
--Manager.Net:ConnectServer("192.168.241.1", 8000);--连接网络
host = Manager.GameServer.GameServerDomain
port = Manager.GameServer.GameServerPort
print(host..":"..port)
Manager.Net:ConnectServer(host, port);--连接网络
--连接网络ConnectServer调用的NetClient的OnConnectServer,设置端口
--在调用OnConnect中GetStream和BeginRead获取网络数据流,读取到数据调用OnRead,OnRead中有ReceiveData解析数据
--解析完继续BeginRead,获取后面的数据
--print("hello main")
Manager.UI:OpenUI("TestUI", "UI", "ui.TestUI")
--Manager.Scene:LoadScene("Test01","scene.Scene01")
end
打开NetBox后,开启natapp和花生壳,删除Unity缓存路径下载的热更新资源,发布exe包前记得修改GameMode为Update,发布exe后,运行,就可以热更新~
演示效果:
Unity Xlua热更新Demo:仿塞尔达风格
本框架至此结束~