公司让做网站违法,网站的风格与布局的设计方案,二次开发焦点吧,制作简单的个人网站大家好#xff0c;我是阿赵。 阿赵我做手机游戏已经有十几年时间了。记得刚开始从做页游的公司转到去做手游的公司#xff0c;在面试的时候很重要的一个点#xff0c;就是会不会用Lua。使用Lua的原因很简单#xff0c;就是为了热更新。 热更新游戏内容很重要。如果… 大家好我是阿赵。 阿赵我做手机游戏已经有十几年时间了。记得刚开始从做页游的公司转到去做手游的公司在面试的时候很重要的一个点就是会不会用Lua。使用Lua的原因很简单就是为了热更新。 热更新游戏内容很重要。如果游戏内容需要改动如果每次都要去平台出新的安装包提审周期不可控甚至像iOS这种提审特别麻烦的平台还有不过审的风险。比如出个春节活动提审完可能春节都过完了那么这个内容也就没有意义。 但如果游戏的内容自己可以通过某些方法进行修改不需要通过平台的审核就能直接改动那么游戏制作的灵活性就大大增加了。我们使用Unity引擎来开发游戏游戏资源是可以通过AssetBundle的方式热更新的但C#代码以前是不能热更新的至少在iOS平台是不能的安卓和pc有办法可以热更新dll。 Lua作为一个可以通过字符串或者字节资源的形式加载的脚本在游戏热更新上起到了重要的作用起码在近十年时间是出于统治地位的。但由于性能问题Lua也一直受到各种诟病特别是在微信小游戏或者抖音小游戏上面性能的确不是很好。 最近和UWA沟通的过程中听说很多公司已经不用Lua而改为用了HybridCLR(华佗)热更新。华佗热更新的原理是更新c#的程序集也就是通过加载dll来实现代码层面的热更新而且iOS也能用。不过由于公司的项目都是在用Lua开发的如果换成华佗等于整个项目要重新用C#写一篇成本还是很高的。 学多一点东西肯定是有好处的说不定以后新项目就能用得上。于是阿赵我也来学习一下华佗热更新的用法并且记录一下一些使用的问题。
一、 安装
HybridCLR华佗热更新的在线文档地址是 文档地址 里面有比较详细的安装说明可以根据步骤一步步来安装。我只记录一下我遇到的问题。
1、 对应的Unity版本 在官方文档里面说华佗支持的Unity版本有这些 由于我有一个项目是使用2019.4.24开发的一看文档说支持2019.4.x感觉挺好但继续看下去会看到 从文档看2019只能在2019.4.40上面安装。其实2019.4前面的版本的确挺多问题的比如我之前发现的URP的SRPBatcher合并问题等各位如果还在用2019版本开发的朋友我也挺建议大家都升级到2019.4.40。无奈的是如果项目已经上线了再来换版本可能会导致AssetBundle打包的资源会全部变更Unity打包AssetBundle的时候会把版本号写在文件开头所以就算你所有内容都没变只是换个Unity版本打出来的AssetBundle文件也会全部改变的…… 不过幸好华佗的文档里面也有针对这种情况的处理办法就是先把项目切换到2019.4.40然后安装华佗再切换会原来的版本。 抛开项目已有版本的问题其实就无所谓了因为之后的版本很多都支持 比如直接安装2022.3.x版本就没这个问题了。
2、下载代码 由于代码库是从git下载所以必须安装git。 然后通过Unity的PackageManager里面的Add package from git URL来安装 库地址 https://gitee.com/focus-creative-games/hybridclr_unity.git 或 https://github.com/focus-creative-games/hybridclr_unity.git 我自己尝试的结果是没办法通过Add package from git URL来安装安装了GIT和加了环境变量PATH也不行。我自己用GIT手动克隆却是没问题的这一点很神奇。 于是解决这个问题的方法是可以手动把地址检出克隆到本地然后把文件夹改名com.code-philosophy.hybridclr并复制到项目里面和Assets文件夹同级的Packages文件夹 复制后打开项目会看到有华佗的菜单 选择安装器然后安装 按照文档说明基本都可以自动安装成功但我还是失败了看报错还是GIT的问题于是我根据报错自己检出克隆https://gitee.com/focus-creative-games/hybridclr到项目的HybridCLRData/hybridclr_repo文件夹 检出后再次点击Install按钮就可以安装成功了。 HybridCLR菜单下出现了所有的选项子菜单。
二、 浅尝华佗热更新
1、 一些概念 在使用华佗热更新之前需要先了解一些概念
1. 程序集 先来操作最后再说为什么。在项目里面创建一个文件夹叫做HotUpdate或者叫其他都行你自己喜欢 然后在这个文件夹里面创建一个Assembly Definition文件 帮这个文件起个名字比如我这里就叫做HotUpdate 注意要把Auto Referenced的勾选去掉。 然后在这个文件夹里面创建一个C#脚本我这里随便命名为Hello 创建完之后点选这个Hello脚本会看到里面多了一个Assembly信息里面说明了这个Hello的脚本是属于HotUpdate.dll的。 操作到此结束下面解释一下 这个创建文件夹和Assembly Definition文件的过程是Unity引擎的程序集功能其实就是指定了某个文件夹作为一个程序集的范围。只要在这个文件夹下面的所有文件包括子文件夹里面的文件都属于当前这个Assembly Definition文件的程序集里面的内容。 一个程序集字面意思就是程序的集合了可以理解成是把里面的代码都打包了之后需要热更新代码其实就是热更新这个程序集的dll文件了。
2. AOT程序集和热更新程序集 使用Unity引擎制作游戏各位肯定应该都会写C#。在项目里面所写的C#代码就算我们不特意的打程序集它们也会出现在一个程序集里面就是Assembly-CSharp.dll然后我们又可以根据自己的需要创建一些程序集所以最后打包的时候除了Assembly-CSharp.dll还会有一些自己的dll。这些多个程序集之后会用于华佗热更新。 这里有个问题热更新是以dll为单位的那些可以热更新的程序集在使用华佗热更新的时候是会剥离出去不会包含在主工程包里面的。而我们需要写代码加载这些dll文件就必须有一些代码是包含在主工程里面不能热更新的。 所以在使用华佗热更新的时候需要把程序集分成2部分第一部分是包含在游戏主包里面不能热更新的成为AOT程序集第二部分是可以热更新的dll成为热更新程序集。
3. 程序集的规划和程序集之间的引用关系 由于程序集起码要有AOT和可热更两个甚至更多所以在做之前我们必须先规划一下它们之间的关系。具体来说就是总共需要多少个程序集才能满足我们需要既能热更又可以划分清楚模块做到分块更新。 程序集之间的引用有2种方式第一种就是在程序集上面勾上Auto Referenced这样它自动被其他程序集引用可以互相调用里面的方法。 另外一种就是在程序集上面指定依赖关系比如我再建一个HotUpdate2的程序集不勾选Auto Referenced 这个时候如果HotUpdate程序集要访问HotUpdate2程序集可以选择HotUpdate程序集然后添加引用关系 只要在HotUpdate程序集的Assembly Definition References里面添加了HotUpdate2的引用那么HotUpdate就能调用HotUpdate2里面的方法了。 华佗热更新里面有一个规则AOT程序集是不能直接引用热更新程序集的不然在打包的时候会出错。所以我们在创建自己的可热更程序集的时候必须把Auto Referenced的勾选去掉然后自己维护可热更新程序集之间的引用关系。
2、 尝试使用华佗热更新
1. 指定需要热更新的程序集 在HybridCLR菜单下面选择Settings设置 然后添加可热更新的程序集 在这里设置了的程序集在打主包的时候程序集是不会包含在主包里面的。
2. 生成必须的东西 在首次使用华佗热更新的时候必须先选择Generate——All生成所有必须的文件其实就是All上面的哪些东西了。在之后的使用中就不一定要生成All可以根据实际需要来生成上面的内容。
3. 写热更新的测试代码 首先为了打包之后看到控制台的打印先创建一个脚本
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ConsoleToScreen : MonoBehaviour
{const int maxLines 50;const int maxLineLength 120;private string _logStr ;private readonly Liststring _lines new Liststring();public int fontSize 15;void OnEnable() { Application.logMessageReceived Log; }void OnDisable() { Application.logMessageReceived - Log; }public void Log(string logString, string stackTrace, LogType type){foreach (var line in logString.Split(\n)){if (line.Length maxLineLength){_lines.Add(line);continue;}var lineCount line.Length / maxLineLength 1;for (int i 0; i lineCount; i){if ((i 1) * maxLineLength line.Length){_lines.Add(line.Substring(i * maxLineLength, maxLineLength));}else{_lines.Add(line.Substring(i * maxLineLength, line.Length - i * maxLineLength));}}}if (_lines.Count maxLines){_lines.RemoveRange(0, _lines.Count - maxLines);}_logStr string.Join(\n, _lines);}void OnGUI(){GUI.matrix Matrix4x4.TRS(Vector3.zero, Quaternion.identity,new Vector3(Screen.width / 1200.0f, Screen.height / 800.0f, 1.0f));GUI.Label(new Rect(10, 10, 800, 370), _logStr, new GUIStyle() { fontSize Math.Max(10, fontSize) });}
}在场景上面建个空物体然后把脚本拖上去 这样做的目的只是为了让我们在接下来的测试中把控制台打印输出到屏幕让我们知道热更新有没有生效。 然后给Hello脚本修改一下
using UnityEngine;public class Hello
{static public void Print(){Debug.Log(Hello World);}
}这里只有一个静态方法如果执行了会打印Hello World到控制台通过上面的脚本控制台的打印就会出现在屏幕。 最后要加一个AOT脚本作为游戏启动、加载dll和调用dll。正常来说热更新的dll文件应该放在CDN上然后下载到本地。这里为了测试就写死放在StreamingAssets文件夹了。这里建一个叫做TestLoadDll的C#脚本
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;public class TestLoadDll : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){Assembly dllLoader;
#if !UNITY_EDITORdllLoader Assembly.Load(File.ReadAllBytes(${Application.streamingAssetsPath}/HotUpdate.dll.bytes));
#elsedllLoader System.AppDomain.CurrentDomain.GetAssemblies().First(a a.GetName().Name HotUpdate);
#endifType type dllLoader.GetType(Hello);type.GetMethod(Print).Invoke(null, null);}// Update is called once per framevoid Update(){}
}然后在场景里面建一个空物体把脚本拖上去 这时候在编辑器里面运行会看到Hello World打印出来了 脚本里面的内容很简单只是规定了在编辑器就直接读取工程里面的HotUpdate程序集在非编辑器的情况下就读取StreamingAssets文件夹下面的HotUpdate.dll.bytes文件。由于Unity的诡异规定所以dll文件是不能直接读取的要把后缀改成bytes。 然后后面的那段反射代码
Type type dllLoader.GetType(Hello);
type.GetMethod(Print).Invoke(null, null);不用害怕这是因为AOT程序集不能直接引用可热更新的HotUpdate程序集所以才用反射调用一下仅此而已如果没有特殊情况是不需要这样做的。
4. 打包热更新用的dll 在HybridCLR菜单选择CompileDll——ActiveBuildTarget 这时候会把对dll进行打包打包的结果在 项目文件夹\HybridCLRData\HotUpdateDlls\对应的平台文件夹\ 由于我现在的平台是Windows所以实际路径会在StandaloneWindows64文件夹下。这里会看到了项目里面所用到的所有程序集的dll文件其中就有我们想要热更新的HotUpdate.dll。我们刚才也指定了HotUpdate2程序集但由于里面一个脚本都没有所以是不会有dll打出来的。 把HotUpdate.dll复制到StreamingAssets文件夹并重命名为HotUpdate.dll.bytes
5. 打包测试 选择一个文件夹常规的打个PC包出来 发现打不出来因为刚才指定了HotUpdate2程序集但现在这个程序集是没有内容的 去华佗设置里面把HotUpdate2程序集从可热更新的程序集里面去掉。这次就能正常打包了。 运行打出来的包能看到HelloWorld证明打包成功从刚才的读取dll的代码我们可以知道现在是读取了StreamingAssets里面的HotUpdate.dll.bytes作为代码执行的。
6. 验证热更新修改代码 回到Hello脚本修改一下
using UnityEngine;public class Hello
{static public void Print(){Debug.Log(Hello Azhao);}
}把原来的Hello World改成Hello Azhao 然后再次HybridCLR菜单选择CompileDll——ActiveBuildTarget打包dll 再次在HybridCLRData\HotUpdateDlls\StandaloneWindows64目录找到HotUpdate.dll文件然后拷贝到之前打的PC包的StreamingAssets文件夹 这时候再次运行之前的PC包 可以看到现在PC包显示的内容已经变成了Hello Azhao。到此为止华佗热更新的基本流程已经跑通了。
三、 华佗热更新的深入使用
1、 尝试AssetBundle加载资源 接下来尝试把C#脚本挂在GameObject上并通过AssetBundle加载这个GameObject看看 在HotUpdate程序集建一个PrintObject的C#脚本
using UnityEngine;public class PrintObject : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){Debug.Log(GameObject: gameObject.name);}// Update is called once per framevoid Update(){}
}然后建一个cube把脚本挂上去 把这个Cube做成Prefab并且设置AssetBundleName打包AssetBundle 然后打包AssetBundle把AssetBundle文件放到StreamingAssets文件夹 修改Hello脚本
using UnityEngine;public class Hello
{static public void Print(){string path Application.streamingAssetsPath /ab/cube.unity3d;AssetBundle ab AssetBundle.LoadFromFile(path);if(ab){Object obj ab.LoadAsset(Cube);if(obj ! null){GameObject.Instantiate(obj);} }}
}生成dll并且和AssetBundle一起拷贝到pc包的StreamingAssets文件夹 这时候运行PC包并没有出现我们想要的情况而是有个报错 这是为什么呢
2、关于代码裁剪 如果在打包的时候没有用到某些Unity自带的API但后期在热更新的代码上加上就会出现报错找不到方法。原因是IL2CPP的情况下代码裁剪是不能被禁止的而之前没有用过的API在Unity打包的时候被裁剪掉了。 一般来说为了防止需要的Unity原生API代码被裁剪的问题可以在项目里面建一个link.xml文件然后把需要保留不被裁剪的内容填进去。不过这样手动收集是很麻烦的华佗的工具里面自带了收集link.xml的功能 只要点一下就会把项目里面有调用过的API加入到link.xml里面。 这里还有2个问题 1、 需要保留的代码除了加在link.xml之外代码还要必须显式的引用过这些类或者函数不然也还是会被裁剪。 2、 重新收集完link.xml之后必须重新打包才能生效…… 这样似乎就回到了使用Lua时的导出接口的操作了没有导出过接口的类和方法不能热更新……关键这一步你在编辑器内还很难发现毕竟编辑器内的Unity自带API是不会被裁剪的。 这是一个我认为使用华佗热更新最大的问题。毕竟Unity很多API可能在一开始的时候没考虑到需要使用后面用到才收集就不能热更新了。 既然是需要重新出包了所以也就不止是点一下LinkXml了直接Generate——All生成所有那样就稳妥了。 全部重新生成之后再次出包就可以看到之前的报错没有了可以加载AssetBundle里面的Cube并且挂在上面的脚本也正常运行了
3、 新增程序集的热更新 之前的例子里面只有1个可热更新的程序集叫做HotUpdate现在我想在不重新出包的情况下增加一个HotUpdate2的可热更新程序集试试能不能热更新。 由于之前是在AOT代码里面写死了需要加载HotUpdate.dll.bytes所以如果增加新的程序集dll文件肯定是不能加载的所以要改成需要加载哪些dll文件要通过可热更的文件来决定。 这里为了测试简单我放一个dll.txt文本在StreamingAssets文件夹然后在里面用逗号分隔需要加载的程序集名字。由于HotUpdate.dll需要通过反射来调用所以我就不写在txt里面了这也说明如果需要有一个程序调用入口那么至少有一个dll是需要写在代码里面加载的。于是加载dll的代码会变成这样
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;public class TestLoadDll : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){Assembly dllLoader;
#if !UNITY_EDITORstring path Application.streamingAssetsPath /dll.txt;string content File.ReadAllText(path);if(string.IsNullOrEmpty(content)false) {string[] fileNames content.Trim().Split(,);for(int i 0;i fileNames.Length; i) { string fileName fileNames[i];if(string.IsNullOrEmpty(fileName)false){Assembly.Load(File.ReadAllBytes(${Application.streamingAssetsPath}/fileName.dll.bytes));}}}dllLoader Assembly.Load(File.ReadAllBytes(${Application.streamingAssetsPath}/HotUpdate.dll.bytes));
#elsedllLoader System.AppDomain.CurrentDomain.GetAssemblies().First(a a.GetName().Name HotUpdate);
#endifType type dllLoader.GetType(Hello);type.GetMethod(Print).Invoke(null, null);}// Update is called once per framevoid Update(){}
}到现在为止先打个PC包作为热更新的基础包。 接下来同样的手法建立HotUpdate2文件夹和程序集在里面添加一个PrintGameObject的脚本
using UnityEngine;public class PrintGameObject : MonoBehaviour
{// Start is called before the first frame updatevoid Start(){Debug.Log(HotUpdate2: gameObject.name);}// Update is called once per framevoid Update(){}static public void Run(){Debug.Log(This is HotUpdate2);}
}把这个PrintGameObject脚本挂在之前的Cube预设上原来的PrintObject脚本就不挂了 在HotUpdate程序集添加HotUpdate2程序集的引用 修改Hello脚本
using UnityEngine;public class Hello
{static public void Print(){PrintGameObject.Run();string path Application.streamingAssetsPath /ab/cube.unity3d;AssetBundle ab AssetBundle.LoadFromFile(path);if(ab){Object obj ab.LoadAsset(Cube);if(obj ! null){GameObject.Instantiate(obj);} }}
}主要是加了一句PrintGameObject.Run(); 接下来还是常规操作把HotUpdate2加到可热更新的列表 在dll.txt里面写入HotUpdate2。然后打包AssetBundle、打包Dll把这些东西都拷贝到PC包的StreamingAssets然后运行会看到 发现一个神奇的事情HotUpdate2的代码其实已经加载了PrintGameObject里面的Run方法都打印出来This is HotUpdate2了但挂在Cube上的PrintGameObject脚本却找不到…… 接下来改一下做法把Cube上面的PrintGameObject脚本去掉变成在实例化GameObject之后用AddComponent来添加脚本
using UnityEngine;public class Hello
{static public void Print(){PrintGameObject.Run();string path Application.streamingAssetsPath /ab/cube.unity3d;AssetBundle ab AssetBundle.LoadFromFile(path);if(ab){Object obj ab.LoadAsset(Cube);if(obj ! null){GameObject go (GameObject)GameObject.Instantiate(obj);go.AddComponentPrintGameObject();} }}
}再次打包AssetBundle打包dll拷贝到PC包的StreamingAssets文件夹运行PC包 会看到添加成功了PrintGameObject脚本也运行成功了。 关于新增的程序集挂到GameObject的AssetBundle热更的问题我到最后都没有解决不知道是不是有解决办法。我只能暂时得出结论如果新增程序集纯代码调用时没问题的但如果挂在GameObject上通过AssetBundle加载就会有问题。 这就导致一个问题我们如果想出了安装包之后可以长时间的热更新不需要重新出包就必须对可能用到的程序集做好规划尽量不要去改变了。