Unity3D优化技巧系列(四):资源优化
前面给大家介绍了关于材质的压缩,图集的打包等优化方法,本篇文章主要是给大家介绍Unity3D中关于资源的优化处理,换句话说资源做好了后,该如何用程序实现,这个是作为开发者最需要关心的问题。
先把资源优化包含的功能架构图给大家展示如下:
一、静态资源优化方案
资源优化主要是也是围绕上面框架图介绍,游戏中的场景会摆放很多静态的物体,静态的物体,我们可以通过编码将其重新组装成一个大的物体,在组装成一个大物体之前,有个问题大家要思考一下,对于静态物体的摆放有几种情况,一是合并的物体可能有相同的,完全一样;二是合并的物体有不同的,对于相同的物体可以共用一种材质球,而不同的物体就按照不同的处理即可,核心代码如下:
for (int s = 0; s < meshFilter.sharedMesh.subMeshCount; s++) { int materialArrayIndex = Contains(materials, meshRenderer.sharedMaterials[s].name); if (materialArrayIndex == -1) { materials.Add(meshRenderer.sharedMaterials[s]); materialArrayIndex = materials.Count - 1; } combineInstanceArrays.Add(new ArrayList()); CombineInstance combineInstance = new CombineInstance(); combineInstance.transform = meshRenderer.transform.localToWorldMatrix; combineInstance.subMeshIndex = s; combineInstance.mesh = meshFilter.sharedMesh; (combineInstanceArrays[materialArrayIndex] as ArrayList).Add(combineInstance); }
我们看到上面有个函数Contains它就是用于判断是否有相同的材质,如果有就作为一张材质,Contains函数代码如下所示:
private int Contains(ArrayList searchList, string searchName) { for (int i = 0; i < searchList.Count; i++) { if (((Material)searchList[i]).name == searchName) { return i; } } return -1; }
这样就可以将静态物体组装成一个大的Mesh,这个Mesh包含一个或多个材质球。
另一种静态资源优化就是利用Unity3D引擎自身提供的功能,将物体选择为“Static”,利用引擎自身对其进行优化。动态物体的合并在前面已经给读者讲解过了,在这里就省略了。。。。。。
二、多线程加载
Unity3D引擎只提供了单线程,引擎自身无法实现多线程编程,需要我们自己去解决,多线程加载通常是针对模型资源去处理的,先说一下实现思路:
游戏启动时,首先判断是否已连接WiFi,如果已开启,自动启动资源下载,程序会在游戏后台打开线程去下载资源,先从服务器上下载版本控制文件VersionNum.xml,对比本地文件VersionMD5.xml通过文件名和版本号对比,确定是否需要更新资源,如有不同加入下载列表。
其次用户在启动游戏时,没有打开WiFi,要保证游戏在一定时间内能正常运行,等运行到游戏需要加载服务器资源时,如果本地无资源加载就开启强制下载,开启数据流量使用提醒下载结束后游戏就可以继续运行了。
流程图如下:
以上实现的多线程技术利用了断点续传,类似迅雷使用的断点续传技术,资源在下载过程中如果由于网络中断,它会保存下载的字节数,为后期续传做个记录,这样当网络恢复后,它会继续下载。通过这样的方式可以节省包体大小,缺点是目前网络不是很稳定容易造成资源下载不完整。
核心代码如下所示:
//支持断点续传的下载 private void DownFile () { if ( updatedNum >= NeedDownFiles.Count ) { UpdateLocalVersionFile(); return; } string fileName = NeedDownFiles[updatedNum]; string serverPath = SERVER_RES_URL + fileName; //判断目录 string[] fileNameArr = fileName.Split('/'); string filePath = ""; for ( int i = 0 ; i < fileNameArr.Length - 1 ; i++ ) { filePath += fileNameArr[i] + "/"; } filePath = LOCAL_RES_OUT_PATH + filePath;//下载文件目录 string localPath = LOCAL_RES_OUT_PATH + fileName;//下载文件 string localUnZipPath = string.Format("{0}{1}.{2}" , LOCAL_DECOMPRESS_RES , fileName.Substring(0 , fileName.IndexOf(".")) , "assetbundle");//解压文件 if ( !Directory.Exists(filePath) ) Directory.CreateDirectory(filePath); if ( !Directory.Exists(LOCAL_DECOMPRESS_RES) ) Directory.CreateDirectory(LOCAL_DECOMPRESS_RES); bool isRight = false;//是否下载好 //long DownloadByte = 0;//用于显示当前的进度 //long lStartPos = 0; //打开上次下载的文件或新建文件 //上个版本先删除(并删除解压好的文件) if ( LocalResOutVersion.ContainsKey(fileName) && ( LocalResOutVersion[fileName].version < ServerResVersion[fileName] ) ) { File.Delete(localPath); File.Delete(localUnZipPath); } FileStream fs = null; HttpWebRequest requestGetCount = null; HttpWebResponse responseGetCount = null; try { requestGetCount = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(serverPath); responseGetCount = (HttpWebResponse)requestGetCount.GetResponse(); curFileTotalNum = responseGetCount.ContentLength; if ( File.Exists(localPath) ) { fs = File.OpenWrite(localPath);//打开流 curFileNum = fs.Length;//通过字节流的长度确定当前的下载位置 if ( curFileTotalNum - curFileNum <= 0 ) { isRight = true; } fs.Seek(curFileNum , SeekOrigin.Current); //移动文件流中的当前指针 } else { fs = new FileStream(localPath , FileMode.CreateNew); curFileNum = 0; } } catch ( Exception ex ) { if ( fs != null ) fs.Close(); UpdateLocalVersionTemp(fileName , false , false); UpdateLocalVersionFile(); isRight = false; Debug.Log(ex.ToString()); } finally { if ( responseGetCount != null ) { responseGetCount.Close(); responseGetCount = null; } if ( requestGetCount != null ) { requestGetCount.Abort(); requestGetCount = null; } } HttpWebRequest request = null; HttpWebResponse response = null; Stream ns = null; string test = ""; try { //本地未下载完成 if ( !isRight ) { request = (HttpWebRequest)HttpWebRequest.Create(serverPath); if ( curFileNum > 0 ) request.AddRange((int)curFileNum); //设置Range值 response = (HttpWebResponse)request.GetResponse(); //向服务器请求,获得服务器回应数据流 ns = response.GetResponseStream(); byte[] nbytes = new byte[1024]; int nReadSize = 0; nReadSize = ns.Read(nbytes , 0 , 1024); while ( nReadSize > 0 ) { fs.Write(nbytes , 0 , nReadSize); nReadSize = ns.Read(nbytes , 0 , 1024); curFileNum += nReadSize; //Debug.Log(DownloadByte); } isRight = true; fs.Flush(); fs.Close(); ns.Close(); request.Abort(); } UpdateLocalVersionTemp(fileName , true , false); //解压(防止,更新下载不全,解压报错的文件占坑) if ( File.Exists(localUnZipPath) ) { File.Delete(localUnZipPath); } CompressUtil.DeCompress(localPath , localUnZipPath , null); UpdateLocalVersionTemp(fileName , true , true); updatedNum++; Debug.Log("down " + updatedNum + "/" + totalNeedUpdateNum + "," + fileName + "Loading complete"); } catch ( Exception ex ) { if ( fs != null ) fs.Close(); UpdateLocalVersionTemp(fileName , false , false); isRight = false; Debug.Log(ex.ToString()); //解压出错,删除下载文件 if ( File.Exists(localPath) ) { File.Delete(localPath); } //StartDownLoad(); } finally { if ( ns != null ) { ns.Close(); ns = null; } if ( response != null ) { response.Close(); response = null; } if ( request != null ) { request.Abort(); request = null; } //下载下一个 //if ( isRight ) { DownFile(); //} } //下载结束 isDownload = false; }
三、资源动态更新
Unity3D引擎中的资源是非常多的,这也是导致包体过大的原因,游戏中通常的做法是将一些场景资源放到资源服务器上,在程序启动时进行加载,放在服务器上的资源是经过压缩的assetbundle,程序会将其再进行压缩成zip,并且可以对其设置压缩密码,根据版本文件的最新版本号
四、资源管理
我们在游戏开发中提倡模块化开发,其实目的就是可以用它们对资源进行管理封装,下面介绍几种管理方式:
一、我以前做MMORPG游戏时,我将需要用到的音效资源,特效资源,对象池事先在文件中定义好,采用的是数组或者列表将其存放,这样设计便于通过索引就可以找到需要加载的资源,缺点是需要事先将游戏中所有用到的资源加载到内存中同时将其隐藏,使用的时候根据索引序号将其显示出来,这要求内存足够,否则容易崩掉,不过在目前硬件配置下是完全没有问题的。
二、就是采用通常的策略,用到什么资源加载什么资源,当然对于一些重复使用的资源还是需要用对象池进行处理。这样的好处是节省内存,但是需要用到预加载的还是要处理,这个不能省掉的,资源的处理可以通过读取配置文件。
以上是最常用的两种处理方式。
五、场景加载
对于游戏中场景的处理,一般是在Max工具中把场景模型建好,然后在场景上摆放一些道具或者触发器,场景的地表材质可以通过TM2工具做画刷处理。这样场景就完成了,保存成一个Scene或者是实例化,便于程序读取加载,这样的处理方式也是被大部分程序使用。接下来介绍一种利用配置文件加载场景的方法,配置文件可以使用xml或者json以及二进制文件都可以。
处理方式:地形是单独制作的模型,地形上的物件是从我们已有的模型库中取的,这句话的含义是,我们可以建一些通用的模型作为场景摆放的物件。所有场景需要的物件都是从模型库中取,最后将场景保存成配置文件,配置文件会包含模型的加载路径,模型名字,位置,缩放大小以及旋转方向等,如果大家做过端游对此就不陌生了。这样的好处是可以通过配置文件制作很多模型,加载场景时首先加载场景配置文件,然后根据配置文件加载模型,这种思路在游戏中经常使用,值得借鉴。