这几天一直在研究热更新方案
主要思路是:
1.先将代码打包成dll,然后用unity 打包成assetsbundle,
2.WWW加载进入主程序,
3使用System.Reflection.Assembly来创建程序集,
4.然后通过GetType(className),来获取这个类
5.AddComponent进入主程序,加载的dll就执行起来了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//打包工具,该工具是网上找来都。谢谢作者! public class ExportAssetBundles : MonoBehaviour { //在Unity编辑器中添加菜单 [MenuItem("Custom Editor/Create AssetBunldes ALL")] static void ExportResource() { // 打开保存面板,获得用户选择的路径 string path = EditorUtility.SaveFilePanel("Save Resource", "", "New Resource", "assetbundle"); if (path.Length != 0) { // 选择的要保存的对象 Object[] selection = Selection.GetFiltered(typeof(Object), SelectionMode.DeepAssets); //打包 BuildPipeline.BuildAssetBundle(Selection.activeObject, selection, path, BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets, BuildTarget.StandaloneWindows); } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
using UnityEngine; using System.Collections; using System.Reflection; //代码加载器 public class Index : MonoBehaviour { private WWW www; public static WWW uiWWW; private System.Reflection.Assembly assembly; // Use this for initialization void Start () { StartCoroutine(loadScript()); } private IEnumerator loadScript() { //加载我的代码资源 www = new WWW("http://localhost/Main.assetbundle"); yield return www; AssetBundle bundle = www.assetBundle; TextAsset asset = bundle.Load("Main", typeof(TextAsset)) as TextAsset; assembly = System.Reflection.Assembly.Load(asset.bytes); Assembly[] assLis = System.AppDomain.CurrentDomain.GetAssemblies(); System.Type script = assembly.GetType("Main"); gameObject.AddComponent(script); } } |
因为在加载的时候遇见安全沙箱问题,所以我将这个策略文件记录下来,方便下次复制粘贴
1 2 3 4 5 6 |
<?xml version="1.0"?> <cross-domain-policy> <site-control permitted-cross-domain-policies=”master-only” /> <allow-access-from domain="blog.gamerisker.com" /> <allow-access-from domain="*"/> </cross-domain-policy> |
本地调试程序时解决跨域问题的方法:
Edit->Project Settings->Eidtor
刚开始的时候想使用序列化来存储一些数据,但是后来却连一个很简单的类序列化dll里面都没法获得
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
[MenuItem("Custom Editor/WriteSpriteData")] static void FileWriteSpriteData() { TextAsset textasset = AssetDatabase.LoadAssetAtPath("Assets/Resources/Packer/Packer.txt", typeof(TextAsset)) as TextAsset; Atlas atlas = ScriptableObject.CreateInstance<Atlas>(); //Json其实是NGUIJson这个类,我只是把他提出来。改了个名字 atlas.mList = Json.LoadSpriteData(textasset as TextAsset); if (atlas.mList == null) return; string path = "Assets/Resources/Packer/Packer.asset"; AssetDatabase.CreateAsset(atlas, path); //Atlas是一个只有一个mList属性都类 mList = new List<UISpriteData>(); Object o = AssetDatabase.LoadAssetAtPath(path, typeof(Atlas)); Object texture = AssetDatabase.LoadAssetAtPath("Assets/Resources/Packer/Packer.mat", typeof(Material)); Object[] t = {texture}; BuildPipeline.BuildAssetBundle(o, t, "Assets/Resources/Packer/Packer.assetbundle"); //AssetDatabase.DeleteAsset(path); } |
这是使用序列化数据的加载方式,在不用反射的情况下,下面代码加载能够成功,但是使用了反射,下面的代码就加载不成功了。这个问题我也很费解,暂时我没办法解决
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
IEnumerator LoadAtlas() { www = new WWW("http://localhost/Packer.assetbundle"); //WoodenAtlas.assetbundle //Packer.assetbundle yield return www; //用来断点都时候看看里面所包含都数据 Object[] os = www.assetBundle.LoadAll(); Material mete = www.assetBundle.Load("Packer", typeof(Material)) as Material; Atlas atlas = www.assetBundle.mainAsset as Atlas; GameObject go = new GameObject("UIAtlas"); UIAtlas uiatlas = go.AddComponent<UIAtlas>(); uiatlas.spriteMaterial = mete; uiatlas.spriteList = atlas.mList; GameObject sprite = new GameObject("Sprite"); UISprite ui = NGUITools.AddChild<UISprite>(sprite); ui.atlas = uiatlas; ui.spriteName = "dynamite"; Debug.Log("Load"); www.assetBundle.Unload(false); www.Dispose(); } |
因为要看一下代码的执行效率,所以我寻找到了这个类。感觉还可以。使用josn数据,mat文件创建一个UIAtlas的时间大概是30毫秒左右。
1 2 3 4 5 |
System.Diagnostics.Stopwatch; Stopwatch stopWatch = new Stopwatch(); stopWatch.Start(); Thread.Sleep(10000); stopWatch.Stop(); |
总结:
我使用没有任何Unity环境以外代码来实现壳的制作(我们暂且将其称为Index,其实他就是上面的Index类,代码少得可怜。)
然后主程序是在另一个Unity项目中(这个项目在发布的时候打包成Main.dll)
Main.dll项目通过上面的Index来加载,然后添加到一个GameObject上,主程序的Awake()方法就会执行(Awake是整个程序的主入口)
这个时候所有的资源加载都会在Main.dll里面完成。
在这个过程中,遇到了一个比较麻烦的问题就是,我打包的一些UIAtlas.prefab文件上的UIAtlas这个类,无法找到。
这让我有一些无法理解,因为NGUI的代码已经打包进入了Main.dll,那么为什么我加载prefab的时候,却找不到UIAtlas这个类呢?
最后我只能动态的制作UIAtlas对象来完成这样工作。
那么这样的话,以后做界面,做任何prefab都不能绑定脚本了。都只能加载到内存中动态AddComponent了。这样界面也得用配置文件了。
不过对于我这种从页游转过来的程序,这到不是问题,我有现成的界面编辑器(我博客里有RookieEditor),直接生成XML在游戏中进行组装了。对于能够热更新来说,这点麻烦,其实应该不算麻烦了。
动态生成UIAtlas后,创建了几个Sprite、Button,基本的功能都已经实现,说明这个解决方案是可行的。接下来我将把这个方案运用到我的项目中,更加全面的去实验一下。
这样设计的优点:
1、对IOS的打包也是比较方便。打包IOS 直接拿Main项目打包就可以了。因为不需要热更新了。把代码打包在本地就行了。
2、打包Android项目的时候发布apk只需要发布Index,项目发布版本和没写代码一样大,想到这里我想吐槽一下,Unity就算不写任何代码,发布一个apk也得有7M左右。这也太大了点吧。我可啥都没做啊。
求助:
1、哪位大神能给我说说上面我遇到的那个问题,为什么找不到绑定在prefab上的类呢?这是程序集的问题么?哎,刚转C#的人伤不起。
22 Comments
[…] 3、这也是最主要的,如果你看过我上一篇文章《Unity代码热更新解决方案测试结果总结》你就会知道我为什么要这么做了,都是泪啊。。 […]
刚开始的时候想使用序列化来存储一些数据,但是后来却连一个很简单的类序列化dll里面都没法获得,我也遇到这个问题,你有想到解决办法了没有
我们换了热更新思路,我们将逻辑代码与基础代码分开,逻辑代码在发布的时候将他打包成一个dll,基础代码与安装包一起打包。 动态加载逻辑代码来实现热更新。注意:打包的时候 逻辑代码是在基础代码的之上打包的。
想请问一下如何将逻辑代码打包成一个DLL?
动态加载进来的类和Unity打包AssetBundle时候的type tree是不一样的(虽然类名一样,但是assembly不一样,Unity序列化的Type也不一样),NGUI及其他需要序列化的类推荐放到Plugins目录(动态加载的dll不包含NGUI代码)这样就能序列化NGUI的类型了。
[…] 之前的一篇文章《[Unity3D学习]Unity代码热更新解决方案测试结果总结》只是说了一下方案的流程,今天刚好有时间,又再看了一下热更新这一块!就直接将代码分享出来。代码热更新的核心基本实现,只是需要处理一些依赖等等。 […]
你好,(这个项目在发布的时候打包成Main.dll)你这句话,是指用vs将主项目的所有代码生成main.dll吗?
Main.dll 里面包含着你需要热更新的代码。 具体如何打包,你需要去网上搜索下。
现在这种方式,还是不能在prefab上挂脚本吧
我的这种方式 prefab 上是可以挂脚本的。
只能挂内容不变的脚本,我测试了,如果脚本内容变化,或者新加脚本,是无法识别的
一般挂在prefab上的脚本 我都放在index.dll 中,一般不会去修改
对于你所说的 改变的脚本不能更新,有时间我验证下。
谢谢你的回复
你好,请问你用的是4.6版本的unity吧,我测了一下,同样的东西,在unity5上有bug
你好,能描述一下是什么问题么?我确实用的是老版本的unity
在unity5上有几个问题,
例如:
1,你的index没有用过assetbundle.CreateFromFile, 更新的dll用了,会提示找不到这个方法。
2,如果必须要挂一些脚本到gameobject上,这些脚本必须要在index里面,如果在更新的dll里面,getcomponent的方式获取脚本,会得到null
最近实在是太忙,没时间去测试,有时间我再看看,如果你已经解决了,请告诉我噢,谢谢!
unity5上面,我把a脚本打包成dll,然后a脚本解压绑在物体上,a脚本调用场景已存在的脚本时,能找到gameobject,但不能找到他身上的脚本!例如:t.GetComponent().depth = 3;
是找不到这个uisprite的啊,很不理解为什么
你好,如果你指的是挂在gameobject对象上的脚本你找不到是对的,这种情况只能是你自己addcomponent去添加脚本才行,一般情况我们打包的脚本只是逻辑层的脚本,第三方的脚本都是跟随安装包走的。
也就是说,我a绑上去,要想调用b脚本的话,还得解压b,绑上去,然后再找b,调用??
例如我a是逻辑脚本,b是ui脚本,那么打包的时候,由于a调用过b的,那么也得把b脚本也放进去,不然报错,打包不了。也就是说,你是怎么只把逻辑脚本打包,然后用解压的逻辑脚本调用其他的?
刚才我试了下ab都打包,都解压到一个物体上,虽然a能通过get组件调用b,但b上面,t.GetComponent().depth()这句是空的,找不到uisprite
你需要将a逻辑脚本 基于b打包
能。。。说的详细点么??刚接触这些