最近刚刚从原来的公司离职,之前是搞java游戏服务器端方向的,后来觉得确实对客户端开发的兴趣更浓厚一些,所以打算给自己调整一下下方向,往unity3d客户端方向走。所以一个项目的有始有终,而开始是从搭建一个框架开始的,所以先从资源打包和业务逻辑代码热更新实现开始学习。(unity官方文档也是资源打包排在最前面的啦)
AssetBundle 是就是一种资源文件,它可以包含贴图,模型,音效,预制体和Lua脚本等等资源。按字面意思来说,就是一种资源捆绑包,然后由Unity提供的API进行读取。
如果我们一直使用resources文件夹的方式来读取资源,而资源越多最终打包出来的程序越大。我们可以通过打包AB包(名字太长简短一下),然后把AB包上传至服务器,由客户端请求下载资源包,这样游戏程序大小就会减少。
首先,这里我们的选择是使用Lua脚本来运行游戏的业务逻辑,而Lua作为一个解析型语言对热更新支持性是非常棒的,解析型语言是不会一次把所有代码编译成一个二进制文件,然后让计算机运行那个二进制文件,而是每运行到一个语句编译一次,所以我们在运行过程中可以对代码进行修改,这样就能实现热更新。而Lua脚本可以打包成AB包上传到服务器,所以每次客户端只需要启动最核心的引擎代码,然后请求服务器端的Lua脚本,这样每次下载不同的Lua脚本来跑起不同的Lua代码。
先在项目中如下创建一下的一些文件夹:
1.创建AssertPackage文件夹,主要是用来进行打包的文件夹,里面都是一些资源的目录。
创建Excels文件夹,主要是是配置表文件AB包资源,创建LuaScript,主要是Lua脚本的AB包资源资源。创建Perfabs,主要是打包预制体的AB包资源,创建Music,主要是打包音效的AB包资源。
2.创建LuaScript文件夹,主要是本地的Lua脚本的资源。
Game文件夹是游戏逻辑脚本文件夹,Managers是Lua脚本的管理脚本文件夹,Utils是lua脚本的工具文件夹。Main文件是lua程序的主入口。
3.创建Scenes文件夹,主要保存场景文件。
4.创建Script文件夹,主要保存C#脚本文件。结构参考LuaScript文件夹的结构。
5.Plusgins文件夹和Xlua文件夹是由XLUA插件提供的,下载地址为:https://github.com/Tencent/xLua
下载好就可以按上面一样复制粘贴到Assets文件夹下面了
6.创建StreamminigAssert文件夹,主要是用来保存下载下来的AB包资源。
7.需要创建一个Editor文件夹,用来保存对Unity进行扩展的脚本保存。
8.创建Material文本夹,专门存放我们的材质球
先创建一个预制体Cube,并且鼠标选择预制体文件,再看Inspector面板会有一个预制体预览窗口,下面会有一个AssertBundle的选项,如下图
由于这是一个预制体资源,我们第一个None命名perfabs/cube(打包后,会在对应打包文件夹下的perfabs文件夹下产生一个命名为cube的文件),第二none由于是预制体资源我这里就写上perfab(后缀名也随意)。如下图:
接下来在Editor文件夹下面创建一个打包AB包的脚本代码,并且刷新,会发现Unity上面的菜单栏会多出我们设定的选项。截图和脚本如下:
创建脚本截图:
BuidAB类不要基础Monobehover,脚本如下:
using UnityEditor;
using System.IO;
public class BuildAB
{
[MenuItem("build/build assert bundle")]
static void buildAssertBundle() {
string path = "Assets/AssertPackage"; //我们打包的路径
//判断对于的文件夹是否存在,没有就创建一个文件夹
if (!Directory.Exists(path)) {
Directory.CreateDirectory(path);
}
//打包API调用,第一个参数为打包路径 , 第二个参数选择为None(意思是使用LZMA格式进行压缩),第三个参数为打包资源的目标平台为Win64
BuildPipeline.BuildAssetBundles(path,BuildAssetBundleOptions.None,BuildTarget.StandaloneWindows64);
}
}
就会多出以下菜单:
选择build打开列表,并且点击一下build assert bundle,并且等待运行结束。(如果期间有报错,请删除Xlua文件夹下Examples文件夹)结束过后在对应的文件夹下会产生以下资源,如下图:
记事本打开AssertPackage.manifest会看见一下:
这是一个所有AB包的管理资源,资源往上上面打包都会记录下来。这里只是打包了一个perfabs/cube.perfab且没有依赖包。所以就显示如上。
上面我们只打包了perfabs/cube.perfab,一个AB包也没有产生依赖的AB包。那么如何产生依赖的AB包呢?比如我们的Cube预制体里面有材质球和纹理,我们打包材质球和纹理打包到resource/share.res里面。这个时候perfabs/cube.perfab就会依赖resource/share.res,假如只加载了前者没加载后者,perfabs/cube.perfab加载出来的模型纹理会丢失掉。它们加载的顺序可以不固定,只要都加载出来就可以正常显示了。还有就是比如再打包一个perfabs/cube2.perfab的AB包且它们使用同样的材质球和纹理,那么这个材质球和纹理就会只打包一次,两个包依赖一个AB包。这还有一个好处就是我们只需要加载一次材质球和纹理。
读取方式有好几种,都有本地读取的,网络请求读取的,这里不多说,直接上代码。按上面的步骤走随便并且把这段代码顺便挂到一个脚本上并且运行起来
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using UnityEngine.Networking;
public class LoadAssertBundle : MonoBehaviour
{
string path = "Assets/AssertPackage/"; //之前打包的路径
string loadPacket = "perfabs/cube.perfab"; //读取的包名(要携带后缀名)
string perfabName = "Cube"; //打包的预制体名称
private void Start()
{
//LoadAssert();
//StartCoroutine(this.LoadAssertAsync());
StartCoroutine(this.WWWLoadAssertBundle());
}
///
/// 本地同步加载AssertBundle资源,并且创建出来
///
void LoadAssert() {
AssetBundle bundle = AssetBundle.LoadFromFile(path + loadPacket);
GameObject perfab = bundle.LoadAsset<GameObject>(perfabName);
GameObject go = Instantiate(perfab); //创建实例
go.name = "我是本地同步加载出来的";
}
///
/// 本地异步加载AssertBundle资源,并且创建出来
///
///
IEnumerator LoadAssertAsync() {
AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(File.ReadAllBytes(path + loadPacket));
yield return request; //等待加载结束在往下执行
GameObject perfab = request.assetBundle.LoadAsset<GameObject>(perfabName);
GameObject go = Instantiate(perfab); //创建实例
go.name = "本地异步加载出来的";
}
///
/// 使用www进行加载。当然www也可以通过请求服务器下载ab包再加载
///
IEnumerator WWWLoadAssertBundle() {
while (!Caching.ready) {
yield return null;
}
//这里url的写法可以使用 http:// + 网址
string url = @"file:///F:/unity/XLua/Assets/AssertPackage/perfabs/cube.perfab";
WWW www = WWW.LoadFromCacheOrDownload(url,1);
yield return www; //等待下载结束
if (!string.IsNullOrEmpty(www.error)){
yield break;
}
AssetBundle bundle = www.assetBundle;
GameObject perfab = bundle.LoadAsset<GameObject>(perfabName);
GameObject go = Instantiate(perfab);
go.name = "我是通过www请求加载出来的";
}
///
/// 最后是unity官方推荐使用的远程请求下载并且加载Assert包的方法
///
///
IEnumerator UnityWebRequestLoadAssertBundle() {
//因为目前服务器还没搭建,先放着把
string url = @"http://localhost/AssertPackage/perfabs/cube.perfab";
UnityWebRequest webReq = UnityWebRequest.Get(url);
yield return webReq; //等待下载完成
if (!string.IsNullOrEmpty(webReq.error)) {
yield break;
}
byte[] data = webReq.downloadHandler.data;
AssetBundleCreateRequest request = AssetBundle.LoadFromMemoryAsync(data);
yield return request;
AssetBundle bundle = request.assetBundle;
GameObject perfab = bundle.LoadAsset <GameObject>(perfabName);
Instantiate(perfab);
}
}
AssertBundle如果一直不释放调用,那么他一直会占用着所以内存。因此Unity提供的一些API来让我们对AssertBundle包进行卸载。以下代码调用
AssertBundle.unload(true); //直接卸载所以资源
AssertBundle.unload(false); //卸载没有引用的资源
Resouces.UnloadUnusedAssets(); //遍历所有资源并且释放野资源。
卸载方面还是得多看看这位大佬的这篇文章: https://blog.csdn.net/weixin_45979158/article/details/104217674
XLUA的用法再githun上(地址:https://github.com/Tencent/xLua) 有各种使用案例。
首先如下图,创建一个场景空对象,并且创建一个游戏启动脚本,并且把脚本挂到这个空物体上面
其中GameStart.cs脚本下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class GameStarter : UnitySingleton<GameStarter>
{
public override void Awake()
{
base.Awake();
//初始化游戏框架
this.gameObject.AddComponent<LuaManager>();
//资源管理于初始化
}
private void Start()
{
//进入游戏
this.StartCoroutine(this.GameStart());
}
///
/// 检查热更新携程
///
IEnumerator checkHotUpdate() {
yield return 0;
}
IEnumerator GameStart() {
yield return this.StartCoroutine(this.checkHotUpdate());
//进入Lua虚拟机代码,运行lua代码
LuaManager.Instance.runLuaScript();
}
}
由于游戏启动是使用单例模式进行管理的,这里再分享一个单例工具类:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 普通单例工具
///
public abstract class Singleton<T> where T : new (){
private static T instance;
private static Object mutex = new Object();
public static T Instance {
get {
if (instance == null) {
lock (mutex) { //确保单列初始化是线程安全
if (instance == null) {
instance = new T();
}
}
}
return instance;
}
}
}
///
/// unity的单列工具
///
public class UnitySingleton<T> : MonoBehaviour where T : Component {
private static T instance;
public static T Instance
{
get {
if (instance==null) {
instance = FindObjectOfType(typeof(T)) as T;
if (instance == null) {
GameObject go = new GameObject();
instance = (T)go.AddComponent(typeof(T));
go.hideFlags = HideFlags.DontSave;
go.name = typeof(T).Name;
}
}
return (T)instance;
}
}
public virtual void Awake() {
DontDestroyOnLoad(this.gameObject);
if (instance == null)
{
instance = this as T;
}
else {
Destroy(instance);
}
}
}
还有就是需要在Script脚本文件夹下的Manager写一个专门管理Lua管理器的脚本,如下:
using System.Collections;
using System.Collections.Generic;
using System.IO;
using Unity.VisualScripting;
using UnityEditor;
using UnityEngine;
using XLua;
public class LuaManager : UnitySingleton<LuaManager>
{
private LuaEnv env ;
private static string luaScriptFolder = "LuaScripts";
public override void Awake() {
base.Awake();
//初始化lua环境
initLuaEnv();
}
///
/// lua脚本自定义加载器
///
private byte[] LuaScriptLoader(ref string filepath) {
string scriptPath = string.Empty;
scriptPath = filepath.Replace(".", "/") + ".lua";
//编辑模型下运行一下
#if UNITY_EDITOR
string[] pathCombineParam = {
Application.dataPath,
luaScriptFolder,
scriptPath
};
scriptPath = Path.Combine(pathCombineParam);
byte[] data = File.ReadAllBytes(scriptPath);
return data;
#endif
//如果不是编辑器模式下的lua代码
return null;
}
///
/// 初始化lua环境
///
private void initLuaEnv() {
env = new LuaEnv();
env.AddLoader(LuaScriptLoader);
}
///
/// 进入游戏逻辑
///
public void runLuaScript() {
//Debug.Log("运行lua代码");
this.env.DoString("print(\"hello,world!\")");
}
}
然后点击运行游戏,就看到Unity的控制台下打印Hello,World!,如下图:
还没结束呢嘻嘻。我们在LuaScript脚本里面创建Main.lua脚本。我的截图是隐藏了后缀名的,如下图:
并且编写Main.Lua脚本:
--全局的main对象
main = {}
main.awake = function()
print("this mian awake function");
end
main.update = function()
print("this mian update function")
end
修改LuaManager代码:
///
/// 进入游戏逻辑
///
public void runLuaScript() {
//Debug.Log("运行lua代码");
this.env.DoString("require('Main')");
this.env.DoString("main.awake()");
}
public void Update()
{
//Debug.Log("运行lua代码");
this.env.DoString("main.update()");
}
点击运行,就能运行起来lua脚本下main对象的awake函数和update函数,然后就发现了一个unity的脚本生命周期都是可以改成使用Lua去调用的
框架源码下载地址:https://github.com/kof123w/gitWorkSpace/tree/main/XLua