xlua 热更新原理及实现思路

目录

前言:

正题:

实现思路:


前言:

        本人刚刚接触编程的菜鸟,很少写过文章,在这里记录学习笔记并跟大家分享学习经验,

如有不对的地方,愿不吝赐教。

        unity C#代码,在运行的时候,先把所有代码(框架+逻辑)都加载到内存,然后执行代码,所以代码加载完成,就无法再改了, 所以普通的C#代码,无法做代码热更新。

        代码热更新的基本原理是内置一个代码解释器,游戏app运行的时候,先把底层C#代码与代码解释器运行起来,进入入口函数后,再加载游戏逻辑代码,加载完成,再使用解释器来解释执行 “逻辑代码”。这样就有机会在入口函数的时候,先到云端的服务器上把最新的逻辑代码下载下来,然后再加载到内存,完成加载后,跑最新的代码逻辑。

        搞懂的热更原理以后,问题就变成了,内置哪个脚本语言的解释器?使用哪个脚本语言开发。游戏行业和其他行业不一样,大部分的核心功能都在游戏引擎代码中,而游戏引擎的代码,基本上每个引擎各成一体,没有统一的标准, 所以做为游戏行业的脚本语言,就需要2大特点:高效,轻量级。高效指的是脚本语言执行高效,轻量级指的是脚本语言很容易嵌入游戏引擎,同时脚本的体系不大,没有过多的与语言本身无关的系统和库内置在里面。满足这两大特点,的脚本语言其实不多。有一个编程语言,在它创立之初就是保持这2大理念,在2001年左右的时候被魔兽世界选为脚本语言,而风靡游戏行业,这个语言就是Lua。当Unity 需要做热更新的时候(2013年开始),而普通的C#又做不到的时候,而对于游戏行业来说Lua脚本热更新已经是很成熟的方案,自然Lua 热更新就成为了Unity热更新的首选。

正题:

        要实现热更新我们需要做以下几点:

1.一个服务器来存放资源

2.一套打包工具(包含打AB包,生成对比文件)

3.一个从服务器下载资源的脚本 AssetBundleDownloader

4.腾讯的Xlua插件(连接虚拟机,在lua脚本上写游戏逻辑,以便实现代码热更)

实现思路:

在准备工作齐全后,我们先要打包游戏中所需要的资源(打包的同时生成这一批资源的version版本文件),至于游戏里资源的加载和使用,可以封装一个ABManager用来加载P目录的资源,打包好之后把打好的AB包上传到资源服务器,游戏中监选资源服务器的变动(这里我简单实现一下,只在游戏开始时对比版本文件判断是否需要更新)。如果需要更新,把资源下载到P目录就实现了资源热更新。

打包工具 Builder:

using UnityEngine;
using System.Collections;
using UnityEditor;
using System.IO;
using System.Collections.Generic;
using System.Text;
using System;

/// <summary>  
/// 把Resource下的资源打包成.unity3d 到StreamingAssets目录下  
/// </summary>  
public class Builder : Editor
{
    public static string ResPath = Application.dataPath + "/Resources";
    const string AssetBundlesOutputPath = "Assets/StreamingAssets";

    [MenuItem("Tools/Builder/一键打包")]
    public static void BuildAssetBundle()
    {
        //ClearAssetBundlesName();
        List<string> all=new List<string>();
        string resPath=Path.Combine(Application.dataPath, "Resources");
        //获取Resources目录下所有文件
        string[] allfiles=Directory.GetFiles(resPath, "*",SearchOption.AllDirectories);
        foreach (var item in allfiles)
        {
            //Path.GetExtension返回指定的路径字符串的扩展名
            if (!Path.GetExtension(item).Equals(".meta"))
            {
                string temp=Path.Combine(Application.dataPath,"Resources\\");
                string p = item.Replace(temp, "");
                all.Add(p);
            }
        }

        AssetBundleBuild[] buildMap = new AssetBundleBuild[all.Count];
        for(int i = 0; i < all.Count; i++)
        {
            buildMap[i].assetBundleName = all[i].Substring(0, all[i].LastIndexOf('.'));
            buildMap[i].assetBundleVariant = "u3d";
            buildMap[i].assetNames = new string[] { "Assets/Resources/" + all[i] };
        }    

        string outputPath = Path.Combine(AssetBundlesOutputPath, Platform.GetPlatformFolder(EditorUserBuildSettings.activeBuildTarget));
        if (!Directory.Exists(outputPath))
        {
            Directory.CreateDirectory(outputPath);
        }

        //根据BuildSetting里面所激活的平台进行打包  
        BuildPipeline.BuildAssetBundles(outputPath, buildMap,0, EditorUserBuildSettings.activeBuildTarget);
        CreateVerisonTxt();
        AssetDatabase.Refresh();
        Debug.Log("打包完成");
    }

    /// <summary>  
    /// 清除之前设置过的AssetBundleName,避免产生不必要的资源也打包  
    /// 之前说过,只要设置了AssetBundleName的,都会进行打包,不论在什么目录下  
    /// </summary> 
    [MenuItem("Tools/Builder/清空包名")]
    public static void ClearAssetBundlesName()
    {
        int length = AssetDatabase.GetAllAssetBundleNames().Length;
        Debug.Log("请空前:"+length);
        string[] oldAssetBundleNames = new string[length];
        for (int i = 0; i < length; i++)
        {
            oldAssetBundleNames[i] = AssetDatabase.GetAllAssetBundleNames()[i];
        }

        for (int j = 0; j < oldAssetBundleNames.Length; j++)
        {
            AssetDatabase.RemoveAssetBundleName(oldAssetBundleNames[j], true);
        }
        length = AssetDatabase.GetAllAssetBundleNames().Length;
        Debug.Log("清空后:"+length);
    }


    static string Replace(string s)
    {
        return s.Replace("\\", "/");
    }

    [MenuItem("Tools/Builder/按包名打包")]
    public static void BuildPackByName()
    {
        string outPutPath = Path.Combine(AssetBundlesOutputPath, Platform.GetPlatformFolder(EditorUserBuildSettings.activeBuildTarget));
        if (!Directory.Exists(outPutPath))
        {
            Directory.CreateDirectory(outPutPath);
        }
        BuildPipeline.BuildAssetBundles(outPutPath, 0, EditorUserBuildSettings.activeBuildTarget);
        //CreateVerisonTxt();
        AssetDatabase.Refresh();
    }

    /// <summary>
    /// 一键设置AB包名
    /// </summary>
    [MenuItem("Tools/Builder/设置所选资源的AB包名")]
    public static void SetABName()
    {
        //返回当前选中的所有对象(Project目录下的)
        UnityEngine.Object[] selects = Selection.objects;

        foreach (var item in selects)
        {
            string path = AssetDatabase.GetAssetPath(item);
            //把一个目录的对象检索为AssetImporter
            AssetImporter asset = AssetImporter.GetAtPath(path);
            asset.assetBundleName = item.name;
            asset.assetBundleVariant = "u3d";
            asset.SaveAndReimport();
        }
        
        AssetDatabase.Refresh();
    }

    [MenuItem("Tools/Builder/打开P目录")]
    public static void OpenPml()
    {
        System.Diagnostics.Process.Start(Application.persistentDataPath);
        //EditorUtility.OpenFilePanel("P目录", Application.persistentDataPath, "");
    }
    [MenuItem("Tools/Builder/生成索引文件")]

    public static void CreateVerisonTxt()
    {
      string[] allFiles=  Directory.GetFiles(Application.streamingAssetsPath, "*.*", SearchOption.AllDirectories);
        StringBuilder sb= new StringBuilder();
        sb.Append(Application.version + "\r\n");
        foreach (string file in allFiles)
        {
            if (!Path.GetExtension(file).Contains(".meta")&&!Path.GetExtension(file).Contains(".manifest"))
            {
                string tmp = file.Split(new string[] { "StreamingAssets\\" }, System.StringSplitOptions.RemoveEmptyEntries)[1];
                FileInfo fio = new FileInfo(file);
                sb.Append(tmp + "|" + fio.Length +"|"+ GetMD5(file).Replace("-","") + "\r\n");
            }
           
        }
        File.WriteAllText(Application.streamingAssetsPath + "/Windows/version.txt", sb.ToString());//
        AssetDatabase.Refresh();
    }
    public static  string GetMD5(string path)
    {
        byte[] bytes= File.ReadAllBytes(path);
        System.Security.Cryptography.MD5 md5= new System.Security.Cryptography.MD5CryptoServiceProvider();
        byte[]  data= md5.ComputeHash(bytes);
        return BitConverter.ToString(data);
    }
}

public class Platform
{
    public static string GetPlatformFolder(BuildTarget target)
    {
        switch (target)
        {
            case BuildTarget.Android:
                return "Android";
            case BuildTarget.iOS:
                return "IOS";
            case BuildTarget.WebGL:
                return "WebGL";
            case BuildTarget.StandaloneWindows:
            case BuildTarget.StandaloneWindows64:
                return "Windows";
            case BuildTarget.StandaloneOSX:
                return "OSX";
            default:
                return null;
        }
    }
    
}

资源下载 AssetBundleDownloader:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using UnityEngine.Networking;
using System.IO;
using System.Text;

public class AssetBundleDownloader : MonoBehaviour
{
    private Version local_version;
    private Version sever_version;
    private string localURL = string.Empty;
    private string severURL = string.Empty;
    private Dictionary<string, ABitem> dic_local;
    private Dictionary<string, ABitem> dic_sever;
    private Queue<ABitem> Que_ABitem;
    private string SavePath=string.Empty;
    private void Awake()
    {
        localURL = Application.persistentDataPath + "/Windows/";
        severURL = "http://127.0.0.1/";
        dic_local = new Dictionary<string, ABitem>();
        dic_sever = new Dictionary<string, ABitem>();
        Que_ABitem = new Queue<ABitem>();
    }
    // Start is called before the first frame update
    void Start()
    {
        string localPath = localURL + "version.txt";
        if(File.Exists(localPath))
        {
            string[] arr = File.ReadAllLines(localPath);
            local_version = Version.Get(arr[0]);
            for (int i = 1; i < arr.Length; i++)
            {
                ABitem ab = new ABitem();
                ab.name = arr[i].Split('|')[0];
                ab.len = int.Parse(arr[i].Split('|')[1]);
                ab.md5 = arr[i].Split('|')[2];
                dic_local.Add(ab.name, ab);
            }
        }

        string sever = severURL + "Windows/version.txt";
        DownLoadAB(sever, (data) =>
        {
            SavePath = Encoding.UTF8.GetString(data);
            string[] arr = SavePath.Split(new string[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
            sever_version = Version.Get(arr[0]);
            for (int i = 1; i < arr.Length; i++)
            {
                ABitem ab = new ABitem();
                ab.name = arr[i].Split('|')[0];
                ab.len = int.Parse(arr[i].Split('|')[1]);
                ab.md5 = arr[i].Split('|')[2];
                dic_sever.Add(ab.name, ab);
            }


            if(local_version!=null)
            {
                if(local_version.Big!=sever_version.Big)
                {
                    print("强制更新");
                }
                else if(local_version.Small<sever_version.Small)
                {
                    print("开始更新");
                    GetNeedDownLoadABitem();

                    DownLoadABitem();
                }
                else
                {
                    EnterGame();
                }
            }
            else
            {
                GetNeedDownLoadABitem();

                DownLoadABitem();
            }
        });
    }

    private void EnterGame()
    {
        print("进入游戏");
    }

    private void DownLoadABitem()
    {
        if(Que_ABitem.Count>0)
        {
            ABitem ab = Que_ABitem.Dequeue();
            string path = severURL + "/" + ab.name;
            DownLoadAB(path, (data) =>
            {
                string localsave = Application.persistentDataPath + "/" + ab.name;
                string localDir = Path.GetDirectoryName(localsave);
                if(!Directory.Exists(localDir))
                {
                    Directory.CreateDirectory(localDir);
                }
                File.WriteAllBytes(localsave, data);
                if(Que_ABitem.Count>0)
                {
                    DownLoadABitem();
                }
                else
                {
                    File.WriteAllText(localURL + "version.txt", SavePath);
                    EnterGame();
                }
            });
        }
    }

    private void GetNeedDownLoadABitem()
    {
        foreach (var item in dic_sever.Values)
        {
            dic_local.TryGetValue(item.md5, out ABitem ab);
            if (ab != null)
            {
                if(ab.md5!=item.md5)
                {
                    Que_ABitem.Enqueue(ab);
                }
            }
            else
            {
                Que_ABitem.Enqueue(item);
            }
        }
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    public void DownLoadAB(string url,Action<byte[]> completeCallBack)
    {
        StartCoroutine(Run(url, completeCallBack));
    }

    IEnumerator Run(string url, Action<byte[]> completeCallBack)
    {
        UnityWebRequest www = UnityWebRequest.Get(url);
        yield return www.SendWebRequest();
        if(!(www.isNetworkError  || www.isHttpError))
        {
            completeCallBack?.Invoke(www.downloadHandler.data);
        }
        else
        {
            print(www.error);
        }
    }
}
public class Version
{
    public int Big;
    public int Small;
    public static Version Get(string str)
    {
        Version version = new Version();
        version.Big = int.Parse(str.Split('.')[0]);
        version.Small = int.Parse(str.Split('.')[1]);
        return version;
    }
}
public class ABitem
{
    public string name;
    public int len;
    public string md5;
}

至于代码热更,和资源热更同样思路。我们先打包资源,把lua脚本的文件夹放在AB包文件夹下面

。这样我们在修改脚本中的游戏逻辑后,然后生成版本文件,就会对应生成lua脚本的MD5码。

我们只需要把原本lua连接虚拟机部分的连接路径改为P目录下的lua接口在,对比完版本文件后把新的资源(包含lua脚本)下载下来,就实现了脚本热更

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using XLua;
/// <summary>
/// 连接虚拟机
/// </summary>
public class LuaManager : MonoBehaviour
{
    LuaEnv m_le;
    Action m_Start, m_Update;
    void Start()
    {
        m_le=new LuaEnv();
        m_le.AddLoader(GetBytes);
        m_le.DoString("require'Main'");
        m_Start = m_le.Global.GetInPath<Action>("Start");
        m_Update = m_le.Global.GetInPath<Action>("Update");
        m_Start.Invoke();
    }

    private byte[] GetBytes(ref string filepath)
    {
        //连接p目录下的lua脚本
        string path = Application.persistentDataPath + "/Windows/LuaScripts/" + filepath + ".lua";
        if(File.Exists(path))
        {
            return File.ReadAllBytes(path);
        }
        return null;
    }

    // Update is called once per frame
    void Update()
    {
        m_Update?.Invoke();
    }
}

xlua热更新基本实现,再次感谢

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值