思考并回答以下问题:
本章涵盖:
- 消息类型定义和封装
关于游戏开发经验,网上资料很多,在这里总结的是我在实际项目开发中遇到的问题以及在开发中使用的一些核心技术,学习编程首先要从程序调试开始,不会调试的程序员永远学不好编程,只要是人为的因素都会有Bug出现,程序员只有通过调试才能解决这些Bug,下面先从调试开始讲起。
关于调试经验分享
不论开发什么样的程序,首要的是开发者必须会调试,这也是作为程序开发者必备的技能,不会调试的程序员,就不是一个好程序员。任何人写程序都会出现Bug,遇到问题要学会解决问题。在使用Unity开发程序时,经常会遇到在PC端运行程序没有问题,但是将程序打包成Android包安装在手机上时出现各种莫名其妙的Bug,为了解决移动端的Bug,开发者也要知道如何在移动端调试程序。在使用mono编辑器或者Visual Studio编辑器调试移动端程序时,需要在手机端安装一个Unity官方提供的软件Unity Remote,在手机端安装好了后,在编译Unity的Android包时要进行如图14-1所示的设置。
图14-1 Unity移动端调试设置界面
这样编译的apk包在安装到手机端时,运行手机端程序可以在mono编辑器中查看到硬件的名字,如图14-2所示。
图14-2 编辑器与移动端硬件绑定界面
设置好了以上信息后,运行手机端的程序,就可以在mono编辑器或者Visual Studio编辑器中断点调试了。也可以连接Profiler调试工具,查看程序内存分配和CPU的占用情况,操作界面如图14-3所示,它可以查看手机运行的情况,这是一种在移动端调试的方式。
图14-3 Profiler调试界面
第二种调试方式,Android SDK的调试。它可以通过Eclipse编辑器的DDMS查看手机运行的报错问题,在Eclipse开发工具的右上角可以轻松查看到DDMS。大家可以自行下载Eclipse体验一下,它可以查看到游戏运行时的空对象错误,非常实用。
第三种调试方式是通过adb logcat命令行查看Unity的日志信息,打开“运行”,在其中输入“cmd”打开dos的操作界面,然后输入adb logcat-s Unity命令,操作方式如图14-4所示。
图14-4 命令行调试
如果不知道如何使用,可以通过命令查看帮助说明,如果要把日志信息保存到手机卡上,可以输入命令adb logcat-f/sdcard/log.txt,这样log.txt就被保存到手机卡上了,可以查看或者导出,当然也可以保存到电脑上。掌握了以上三种调试方式,就可以解决任何在移动端运行无法解决的问题,接下来介绍移动端游戏防破解技术。
移动端游戏防破解技术
现在用Unity引擎开发的游戏产品越来越多,很多上线的游戏被破解了。游戏被破解的方式分两种:一种是游戏代码的破解,另一种是游戏计费文件的破解。关于代码的破解网上有很多相关的资料,这里就不一一介绍了。防代码破解一般采用的技术是代码混淆的方式。其实对于游戏公司来说最重要的是关于游戏数据的修改,以及关于计费文件的修改,这二者关乎公司的直接利益。游戏的基础数据通常都会放到本地,这样就会被一些玩家通过修改本地文件的方式去改写数据,防止本地数据文件的修改,采用的是服务器验证的方式,服务器数据库只保存变化的数据,如果一定要放在本地需要将本地的数据文件进行加密处理。通常采用的是SHA512算法或者是MD5加密方式。本节的加密方式是对项目的基础数据文件采用SHA512算法加密方式处理的,核心代码如下所示。1
2
3
4
5
6
7
8
9
10
11
12
13
14 //加密算法
public static string GetSHA512Password(string password)
{
byte[] bytes = Encoding.UTF7.GetBytes(password);
byte[] result;
SHA512 shaM = new SHA512Managed();
result = shaM.ComputeHash(bytes);
StringBuilder sb = new StringBuilder();
foreach (byte num in result)
{
sb.AppendFormat("{0:x2}", num);
}
return sb.ToString();
}
函数的参数是开发者自己定义的数据串,经过函数的处理后被称为加密的密码,然后将生成的密码字符串作为输入文本文件zip压缩的密码,压缩zip文件函数代码如下所示。1
2
3
4
5
6
7
8
9
10
11
12protected void SaveConfigXMLToZip()
{
using (ZipFile zipFile = new ZipFile(Encoding.UTF8))
{
zipFile.Password = configurationZipPwd;
zipFile.AddEntry(configurationFile, configuration.bytes);
zipFile.AddEntry(localizationFile, localization.bytes);
stringzipPath=Path.Combine(Application.persistentDataPath, configurationZipFile);
LogTool.Log("Saving configuration in \"" + zipPath + "\"");
zipFile.Save(zipPath);
}
}
程序的压缩是在PC端的编译模式下运行时压缩的,压缩完成后可以将其上传到资源服务器,程序运行时下载该文件然后读取其压缩格式,压缩文件的格式是xml文本文件格式。所以核心代码里面有对XML文件的解释,如果你使用的是Json文件可以更改成Json文件的解析函数,函数如下所示。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
38
39
40protected void TryLoadingXMLsFromZip()
{
string zipPath = Path.Combine(Application.persistentDataPath, configurationZipFile);
if (!File.Exists(zipPath))
{
LogTool.Log("Configuration not found!");
this.ParseConfigXML(configuration.text, false);
this.ParseLocalizationXML(localization.text, false);
return;
}
using (ZipFile zipFile = new ZipFile(zipPath, Encoding.UTF8))
{
zipFile.Password = configurationZipPwd;
ZipEntry xmlConfEntry = zipFile[configurationFile],
xmlLocaleEntry = zipFile[localizationFile];
if (null == xmlConfEntry || null == xmlLocaleEntry)
{
LogTool.Log("Downloaded configuration INVALID!");
this.ParseConfigXML(configuration.text, false);
this.ParseLocalizationXML(localization.text, false);
return;
}
using (MemoryStream ms = new MemoryStream())
{
xmlConfEntry.Extract(ms);
string xmlText = Encoding.UTF8.GetString(ms.GetBuffer(), 0, ms.GetBuffer().Length);
this.ParseConfigXML(xmlText, true);
ms.Seek(0, SeekOrigin.Begin);
xmlLocaleEntry.Extract(ms);
xmlText = Encoding.UTF8.GetString(ms.GetBuffer(), 0, ms.GetBuffer().Length);
this.ParseLocalizationXML(xmlText, true);
}
}
}
以上是关于文件的加密方式,需要在Unity中加入zip压缩库。下面介绍关于计费文件的破解,原理就是绕过计费文件,屏蔽计费,这样的破解对公司造成的损失是很大的。为了预防此类方式的破解,很多知名IT公司给开发者提供了防破解软件,常用的有360防破解、腾讯防破解以及爱游戏防破解等。这些防破解软件就是把Android包用该软件进行加固处理,这样不论什么软件都无法修改计费文件,效果如图14-5所示。
图14-5 360防破解软件界面
经过该软件处理的包,极大的预防了游戏包体的破解。该软件还有签名功能,使用它能有效地防止计费文件被破解。下面介绍另一个让程序员头疼的问题——减小包体的大小。
14.3 减小包体的大小
游戏包体的大小一直是影响游戏玩家体验的重要因素。不论做什么类型的游戏,首要的任务就是把包体大小缩减到一定范围内,对于玩家来说40M是一道坎,游戏都要朝着这个目标努力。包体的大小主要包括美术资源和代码,代码占用量非常小,可以忽略。美术资源的大小直接决定了包体的大小,所以要减小包体必须从美术资源入手。美术资源主要包含两部分:模型和材质贴图,模型的面数要适当控制,这取决于美术的品质要求。
下面介绍一下如何减小贴图的大小。减小贴图的大小常用的处理方式是将不带Alpha通道的贴图存为jpg格式,而将带有Alpha通道的贴图存成png格式。针对UI和3D场景美术建立通用的材质库,这样可以规范和节省美术资源。由于Unity打包图集固有的缺陷,可以采用Texture Packer工具打包解决其带来的问题。Unity图集打包存在主要的问题是,如果定义好了图集大小,那么这个图集不论是否满了,其在内存中占有的大小都是4M。Texture Packer工具会根据贴图的数量自动打成适当大小的图集供程序使用,从而避免这个问题。
另一个辅助检查图片大小的工具是BuildReport,在程序编译成Android的apk包时,要进行设置,效果如图14-6所示。通过图14-6可以看到包体的总量大小以及哪些资源占用量比较大。可以针对性的修改,帮助我们压缩图片的大小。
图14-6 BuildReport工具运行界面
BuildReport资源分布的运行效果如图14-7所示。
图14-7 BuildReport资源分布的运行界面
减小包体需要程序和美术的共同努力,找到问题所在。这就要求在游戏开发初期定义好规范,这样在程序优化后期可以减少很多麻烦。对于使用的库文件dll,可以在Assets目录下建一个link.xml文件,减少不需要的dll文件也可以减小包体,link文件内容样例如下所示。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22<linker>
<assembly fullname="System.Web.Services">
<type fullname="System.Web.Services.Protocols.SoapTypeStubInfo" preserve="all"/>
<type fullname="System.Web.Services.Configuration.WebServicesConfigurationSectionHandler" preserve="all"/>
</assembly>
<assembly fullname="System">
<type fullname="System.Net.Configuration.WebRequestModuleHandler" preserve="all"/>
<type fullname="System.Net.HttpRequestCreator" preserve="all"/>
<type fullname="System.Net.FileWebRequestCreator" preserve="all"/>
</assembly>
<assembly fullname="mscorlib">
<type fullname="System.AppDomain" preserve="fields"/>
<type fullname="System.InvalidOperationException" preserve="fields">
<method signature="System.Void .ctor()"/>
</type>
<type fullname="System.Object" preserve="nothing">
<method name="Finalize"/>
</type>
</assembly>
</linker>
其中,参数all表示保留所有类型与该库相关的库,fields仅保留该类型相关的库,nothing仅保留该类型库,这个可根据需求对不需要的dll进行设置。
总之,优化的方式很多,比如对资源本身的优化,包括顶点数量和材质数量。开发项目时需要对项目进行优化,这个优化包括很多方面,在这里给大家介绍一下:首先要考虑的问题是资源的优化,资源优化包括模型的顶点数量、材质数量、NGUI使用的图集大小、粒子特效数量。这些决定了包的大小,这是要优先要考虑的。其次要考虑的问题是程序的编写是否合理,资源是不是及时的删除或者清空,Shader渲染的运用是不是对于显卡要求比较高。优化程序伴随着游戏开发的始终。代码方面的优化建议开发者读一读《代码重构》一书。
14.4 动态对象资源的优化
不论手游开发还是端游开发,优化自始至终伴随着游戏产品的开发。Unity引擎对于资源优化处理的并不是很好,引擎内部只处理了静态对象的优化,对于动态对象并没有去处理,这需要开发者通过编程去实现动态优化的处理。程序优化主要涉及程序运行的效率,游戏运行出现卡顿以及内存溢出,编写代码要注意内存是否及时的释放掉了,影响游戏内存的因素很多,比如代码中打印的Log是否过多,是否有死循环的代码,以及程序切换场景时是否及时释放内存等。
优化的事情并不只是程序的任务,也需要美术协助。例如,遇到游戏运行出现卡帧现象时,借助Unity的Profiler工具查看内存占用情况,如图14-8所示。单击CPU Usage内存中的曲线可以帮助判断程序出现卡帧,下方的Camera.Render目录下对应的是Unity代码中的具体函数,从而帮助开发者非常方便地找到问题的所在,然后根据找到的问题检查程序逻辑是否有问题。单击Memory可以查看资源在内存中的占用情况,可以针对性的对资源进行处理,如图14-9所示。首先要检查美术资源包括面片数量、材质大小,最后检查一下运用在物体上的Shader,是否有比较好的Shader运用,比如后处理效果Bloom、Blur等。下面主要是针对游戏中使用的资源进行的优化,同时结合案例讲解如何降低DrawCall的数量,达到优化的目的。程序方面要具体问题具体分析,在这里就不一一列举了。
图14-8 查看CPU函数运行界面
图14-9 查看内存占用界面
由于策划需求,美术经常会在游戏场景中摆放一些具有骨骼动画的物件道具用于场景装饰,如果这些场景物件多了,DrawCall会增长的很快,严重影响程序运行效率。不论你是动态实例化生成还是直接拖放到场景中,DrawCall都会增长。面对这样的需求该如何处理呢?因为Unity只提供了静态对象的优化,我们可以直接勾选Static选项,而对动态对象的优化只能自己想办法解决,读者可以先做个实验,只在场景中放一个具有骨骼动画的物体,观察一下它的DrawCall和FPS运行帧率各是多少。场景中是一艘带有骨骼动画的船,程序运行时的DrawCall是10,FPS是76.8,如图14-10所示。
接下来增加两艘同样的船,如图14-11所示,此时,DrawCall是25,FPS是76.8,虽然DrawCall不是成倍增长,但是明显增加了一倍多。通过这个案例可以想象如果这些装饰物过多,对程序还是有影响的。面对这种类型的问题,先说一下解决问题的思路,首先分析一下:这些物体的共同点是带有相同的骨骼动画,而且材质也是一样的。
图14-10 单个带有骨骼动画的物体展示
图14-11 多个带有骨骼动画的物体展示
说一下解决问题的思路,如果把这些船的Mesh和Skeleton合并成一个物体,会出现什么情况呢?对于合并成的物体需要自己创建一个新的Mesh,然后对Mesh进行填充。下面先把代码分享一下,后面会给大家讲案例,完整的优化代码如下所示。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
38
39
40
41
42
43
44
45
46using UnityEngine;
using System.Collections.Generic;
public class CombineDrawCall : MonoBehaviour {
//用于初始化
void Start () {
CombineToOne(this.gameObject);//调用函数接口
}
//合并网格和动作
public static void CombineToOne(GameObject _go)
{
SkinnedMeshRenderer[] _smr = _go.GetComponentsInChildren<SkinnedMeshRenderer>();
List<CombineInstance> lcom = new List<CombineInstance>();
List<Material> lmat = new List<Material>();
List<Transform> ltra = new List<Transform>();
for(int i = 0;i < _smr.Length; i++)
{
lmat.AddRange(_smr[i].materials);
ltra.AddRange(_smr[i].bones);
for(int sub = 0; sub < _smr[i].sharedMesh.subMeshCount; sub++)
{
CombineInstance ci = new CombineInstance();
ci.mesh = _smr[i].sharedMesh;
ci.subMeshIndex = sub;
lcom.Add(ci);
}
Destroy(_smr[i].gameObject);
}
SkinnedMeshRenderer _r = _go.GetComponent<SkinnedMeshRenderer>();
if (_r == null)
_r = _go.AddComponent<SkinnedMeshRenderer>();
_r.sharedMesh = new Mesh();
_r.bones = ltra.ToArray();
_r.materials = new Material[] { lmat[0] };
_r.rootBone = _go.transform;
_r.sharedMesh.CombineMeshes(lcom.ToArray(), true, false);
}
}
以上代码实现了动态对象的优化处理,在使用代码时,首先要建立一个空对象作为父类,把要优化的动态对象放到父类下面作为子类,当然你也可以用一个动态对象作为父类,其他动态对象挂到它的下面。优化的动态对象需要带有自己的骨骼动画,材质最好只有一张贴图,当然多张也是可以的。优化的对象是同一个对象,这一般适用于装饰场景的物件,有的人可能将其应用到游戏中动态生成的怪物,这是不可以的,因为优化的本质是将所有要优化的动态对象组合成一个大的对象,所以不适合怪物的生成。下面将其应用到Unity中给大家展示一下运行效果。直接在场景中放了五艘船,同时把上面写好的代码CombineDrawCall.cs直接挂到GameObject对象上,如图14-12所示。
图14-12 优化代码挂接
它的运行效果如图14-13所示,DrawCall和FPS都有所优化,DrawCall是13,与放置一个物体的DrawCall结果类似,FPS是81.6略有提高,达到了优化的目的。
图14-13 优化运行效果
多线程资源下载技术
在游戏开发中经常会使用多线程去处理,比如在3D游戏开发中,对于大地形的加载,通常是使用分块的方式。用于加载魔兽世界地图场景的3D游戏引擎使用的技术就是多线程加载,在内存里放置9块地形,在缓存里面放置16块地形用于与内存进行地形块的交换。以前开发过端游,主线程负责地形的加载,单独开一个线程用于地形的卸载。在开发中,Unity只有一个主线程运行,也就是单线程,但是可以在Unity中使用多线程加载资源。当我们同时要处理很多事情并且与Unity的对象没有交互时,可以用thread多线程,否则只能使用协成coroutine。在多处理器的计算机上可以做到多个线程的真正同步,在Unity中,你仅能从主线程中访问Unity的组件对象和Unity系统调用,任何企图访问这些对象的第二个线程都将失败并引发错误,这是一个需要我们重视的限制。下面介绍一下实现多线程的思路,以及对资源的断点续传。
本节使用的多线程是用于处理游戏资源的下载,意思是在程序运行时,在用户不知情的情况下在后台开启一个线程下载游戏资源。首先将需要下载的资源打包用zip压缩,主要目的是减少资源包体的大小,然后借助工具生成md5的加密文件,名字为VersionMD5.xml,如果游戏资源有所改变,要重新命名生成的加密文件为VersionMD5_1.0.xml,并将其放到FTP资源服务器上。
游戏启动时,首先判断是否已连接WiFi。如果已开启,则自动启动资源下载。程序会在游戏后台打开线程去下载资源,先从服务器上下载版本控制文件VersionMD5_1.0.xml通过文件名和版本号,对比本地文件的VersionMD5.xml,确定是否需要更新资源,如有不同,则加入下载列表。如果没有打开WiFi,要保证游戏在一定时间内能正常运行,等运行到游戏需要加载的服务器资源时,如果本地无资源加载,就开启数据流量使用提醒强制下载,下载结束后游戏就可以继续运行了。多线程断点续传下载流程如图14-14所示。
图14-14 多线程断点续传下载流程
根据上面的流程图,我们把完整的代码展示一下。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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Xml;
using System.IO;
using System.Net;
using System.Threading;
using System;
public class ResUpdate {
public static ResUpdate Instance;
static public string MAIN_VERSION_FILE = "VersionMd5.xml";
static string SERVER_RES_URL;
static string LOCAL_RES_OUT_PATH = "";
public static string LOCAL_DECOMPRESS_RES = "";
public static Dictionary<string , ResReference> LocalResOutVersion = new Dictionary<string , ResReference>();//本地包外资源
static Dictionary<string , int> ServerResVersion = new Dictionary<string , int>();//服务器资源版本号
static List<string> NeedDownFiles = new List<string>();
//本地资源包信息
public struct ResReference {
public bool isFinish;//是否存在包外
public bool isUnZip;//是否解压DownFile
public int version;
public ResReference (bool isFinish , int version , bool isUnZip) {
this.isFinish = isFinish;
this.version = version;
this.isUnZip = isUnZip;
}
}
public int updatedNum = 0;
public int totalNeedUpdateNum = 0;
public long curFileTotalNum = 0;
public long curFileNum = 0;
public Thread downloadThread;
static ResUpdate () {
Instance = new ResUpdate();
SERVER_RES_URL="http://127.0.0.1/AssetBundle/Android/";
SERVER_RES_URL="http://127.0.0.1/AssetBundle/IOS/";
SERVER_RES_URL="http://127.0.0.1/AssetBundle/Windows32/";
System.Net.ServicePointManager.DefaultConnectionLimit = 512;//
LOCAL_RES_OUT_PATH = Application.persistentDataPath + "/AssetBundle/";
LOCAL_DECOMPRESS_RES = LOCAL_RES_OUT_PATH + "/DeCompress/";
SERVER_RES_URL += SGConstant.mSTR_CURVERSION + "/";
}
private static bool isDownload = false;
/// <summary>
/// 启动下载程序
/// </summary>
/// <param name="isAuto">是否自动(自动只允许Wi-Fi下载)</param>
public static void StartDownLoad (bool isAuto = true) {
if ( isAuto ) {
if ( AndroidSDKTool.GetNetWorkType() == NetworkType.WIFI ) {
Debug.Log("start download resource");
if ( !isDownload ) {
CoroutineManager.DoCoroutine(Instance.LoadVersion());
}
} else {
Debug.Log("no wifi");
}
} else {
if ( !isDownload ) {
CoroutineManager.DoCoroutine(Instance.LoadVersion());
}
}
}
//加载版本文件
IEnumerator LoadVersion () {
isDownload = true;
//清空变量
//LocalResOutVersion.Clear();
ServerResVersion.Clear();
NeedDownFiles.Clear();
string serverMainVersion = "";
//读取本地配置文件
string localVersion = System.IO.Path.Combine(LOCAL_RES_OUT_PATH , MAIN_VERSION_FILE);
if ( File.Exists(localVersion) )
Instance.ParseLocalVersionFile(localVersion , LocalResOutVersion);
//取得服务器版本
string versionUrl = SERVER_RES_URL + "VersionMd5_1.0/" + MAIN_VERSION_FILE;
WWW sw = new WWW(versionUrl);
yield return sw;
if ( !string.IsNullOrEmpty(sw.error) )
Debug.LogError("Server Version ..." + sw.error);
else {
serverMainVersion = sw.text;
Debug.Log(serverMainVersion);
ParseVersionFile(serverMainVersion , ServerResVersion);
}
if ( string.IsNullOrEmpty(sw.text) ) {
Debug.LogError("无法连接服务器");
//加载下一个场景
} else//可以连接服务器
{
//对比本地和服务器的配置
CompareServerVersion();
//开启下载
DownLoadResByThread();
}
}
public delegate void HandleFinishDownload (WWW www);
//在多线程环境中只要我们用下面的方式实例化HashTable就可以了
Hashtable ht = Hashtable.Synchronized(new Hashtable());
public void DownLoadResByThread () {
downloadThread = new Thread(new ThreadStart(DownFile));
downloadThread.Start();
}
//更新本地的version配置
private void UpdateLocalVersionFile () {
XmlDocument xmldoc = new XmlDocument();
XmlElement xmlelem;
//加入一个根元素
xmlelem = xmldoc.CreateElement("" , "VersionNum" , "");
xmldoc.AppendChild(xmlelem);
foreach ( KeyValuePair<string , ResReference> kvp in LocalResOutVersion ) {
XmlElement xe1 = xmldoc.CreateElement("File");//创建一个<Node>节点
xe1.SetAttribute("FileName" , kvp.Key);//设置该节点FileName属性
xe1.SetAttribute("Num" , kvp.Value.version.ToString());//设置该节点Num属性
xe1.SetAttribute("isFinish" , kvp.Value.isFinish.ToString());//设置该节点isFinish属性
xe1.SetAttribute("isUnZip" , kvp.Value.isUnZip.ToString());//设置该节点isUnZip属性
xmlelem.AppendChild(xe1);//添加到<Employees>节点中
}
xmldoc.Save(LOCAL_RES_OUT_PATH + MAIN_VERSION_FILE);
}
//与服务器版本比较
private void CompareServerVersion () {
foreach ( var version in ServerResVersion ) {
//Debug.Log(version.Key);
string fileName = version.Key;
int serverVersion = version.Value;
//if ( IsResOut(fileName) ) {
//如果本地配置表中无资源、版本号不匹配,或者未下载完,就下载
if ( ( LocalResOutVersion.ContainsKey(fileName) && LocalResOutVersion[fileName].version != ServerResVersion[fileName] ) ||
!LocalResOutVersion.ContainsKey(fileName) || ( LocalResOutVersion.ContainsKey(fileName) &&
LocalResOutVersion[fileName].version == ServerResVersion[fileName] && !LocalResOutVersion[fileName].isFinish ) ) {
NeedDownFiles.Add(fileName);
}
//AllResDic.Add(fileName , new ResReference(true , serverVersion));
}
totalNeedUpdateNum = NeedDownFiles.Count;
updatedNum = 0;
Debug.Log(string.Format("需下载资源数量:{0}" , totalNeedUpdateNum));
//删除旧资源
foreach ( string fileName in NeedDownFiles ) {
string localPath = LOCAL_RES_OUT_PATH + fileName;//下载文件
string localUnZipPath = string.Format("{0}{1}.{2}" , LOCAL_DECOMPRESS_RES , fileName.Substring(0 , fileName.IndexOf(".")) , "assetbundle");//解压文件
if ( File.Exists(localPath) )
File.Delete(localPath);
if ( File.Exists(localUnZipPath) )
File.Delete(localUnZipPath);
}
}
/// <summary>
/// 检查包外资源是否比包内资源新
/// <summary>
/// 将xml版本号转换为字典数据
/// </summary>
/// <param name="content"></param>
/// <param name="dict"></param>
private void ParseVersionFile (string content , Dictionary<string , int> dict) {
if ( content == null || content.Length == 0 ) {
return;
}
XmlDocument doc = new XmlDocument();
doc.LoadXml(content);
XmlNodeList nodes = doc.GetElementsByTagName("File");
for ( int i = 0 ; i < nodes.Count ; i++ ) {
XmlAttribute att = nodes[i].Attributes["FileName"];
if ( !dict.ContainsKey(att.Value) ) {
dict.Add(att.Value , int.Parse(nodes[i].Attributes["Num"].Value));
} else {
Debug.Log("Dict has same key ----->" + att.Value);
}
}
}
/// <summary>
/// 将xml版本号转换为字典数据
/// </summary>
/// <param name="filename"></param>
/// <param name="dict"></param>
private void ParseLocalVersionFile (string filename , Dictionary<string , ResReference> dict) {
if ( filename == null || filename.Length == 0 ) {
return;
}
XmlDocument doc = new XmlDocument();
doc.Load(filename);
XmlNodeList nodes = doc.GetElementsByTagName("File");
for ( int i = 0 ; i < nodes.Count ; i++ ) {
XmlAttribute att = nodes[i].Attributes["FileName"];
if ( !dict.ContainsKey(att.Value) ) {
dict.Add(att.Value , new ResReference(bool.Parse(nodes[i].Attributes["isFinish"].Value) , int.Parse(nodes[i].Attributes["Num"].Value) , bool.Parse(nodes[i].Attributes["isUnZip"].Value)));
} else {
Debug.Log("Dict has same key ----->" + att.Value);
}
}
}
//支持断点续传的下载
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;//是否下载好
//上个版本先删除(并删除解压好的文件)
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;
}
private string UrlNoCache (string url) {
return url + "?date=" + DateTime.Now.Ticks;
}
private void UpdateLocalVersionTemp (string fileName , bool isFinish , bool isUnZip) {
//下载更新版本号
if ( LocalResOutVersion.ContainsKey(fileName) ) {
LocalResOutVersion[fileName] = new ResReference(isFinish , ServerResVersion[fileName] , isUnZip);
} else {
LocalResOutVersion.Add(fileName,newResReference(isFinish , ServerResVersion[fileName] , isUnZip));
}
UpdateLocalVersionFile();
}
}
下面介绍一下代码,在该代码中首先声明两个变量,用于存储包体外的资源和服务器资源,包体外的资源主要是考虑到从服务器下载的资源不可能加载到包体里面,只能放到指定的文件夹下面,声明的变量是用字典Dictionary存放的。1
2public static Dictionary<string , ResReference> LocalResOutVersion = new Dictionary<string , ResReference>();//本地保外资源
static Dictionary<string , int> ServerResVersion = new Dictionary<string , int>();//服务器资源版本号
在声明的变量中有ResReference,它表示的是资源信息,在这里用结构体定义本地资源包信息,结构体表示如下。1
2
3
4
5
6
7
8
9
10
11
12//本地资源包信息
public struct ResReference {
public bool isFinish;//是否存在包外
public bool isUnZip;//是否解压DownFile
public int version;
public ResReference (bool isFinish , int version , bool isUnZip) {
this.isFinish = isFinish;
this.version = version;
this.isUnZip = isUnZip;
}
}
前期准备工作完成后,开始进入资源下载阶段,使用函数StartDownLoad开始启动资源下载。首先下载配置文件,本地的配置文件会与服务器的配置文件做对比决定下载哪个资源文件,在该函数中调用了函数LoadVersion,处理版本配置文件。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
37IEnumerator LoadVersion () {
isDownload = true;
//清空变量
//LocalResOutVersion.Clear();
ServerResVersion.Clear();
NeedDownFiles.Clear();
string serverMainVersion = "";
//读取本地配置文件
string localVersion = System.IO.Path.Combine(LOCAL_RES_OUT_PATH , MAIN_VERSION_FILE);
if ( File.Exists(localVersion) )
Instance.ParseLocalVersionFile(localVersion , LocalResOutVersion);
//取得服务器版本
string versionUrl = SERVER_RES_URL + "VersionNum/" + MAIN_VERSION_FILE;
WWW sw = new WWW(versionUrl);
yield return sw;
if ( !string.IsNullOrEmpty(sw.error) )
Debug.LogError("Server Version ..." + sw.error);
else {
serverMainVersion = sw.text;
Debug.Log(serverMainVersion);
//serverVersionByte = sw.bytes;
ParseVersionFile(serverMainVersion , ServerResVersion);
}
if ( string.IsNullOrEmpty(sw.text) ) {
Debug.LogError("无法连接服务器");
//CompareLoacalVersion();
//加载下一个场景
} else//可以连接服务器
{
//对比本地和服务器的配置
CompareServerVersion();
//开启下载
//CoroutineManager.DoCoroutine(DownLoadResByWWW());
DownLoadResByThread();
}
}
配置文件对比完成后,确定开始启动线程下载资源,调用函数DownLoadResByThread下载,代码如下所示。1
2
3
4public void DownLoadResByThread () {
downloadThread = new Thread(new ThreadStart(DownFile));
downloadThread.Start();
}
该函数起动了一个线程,在这个线程里面调用函数DownFile去下载资源并且它具有断点续传功能,函数中都有注释,读者可以自行查看。下面把断点续传内容的重点解释一下:它的HttpWebRequest和HttpWebResponse作为资源请求来使用,获取到服务器请求后,可以拿到资源流,根据流的大小判断是否下载完整,如果不完整可以做个记录,继续下载,在最后又调用了一次函数DownFile(),函数迭代进行直到资源下载完成,以下是处理断点续传的代码。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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64HttpWebRequest 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;
}
DownFile();
}
该代码脚本可以直接挂到对象上,完成资源的断点续传,断点续传主要是用于减小包体的,可以在玩家玩游戏的过程中,在后台开启一个线程来完成后续资源的下载。采用了断点续传技术也可以避免在下载的过程中由于网络不好,出现资源下载不完整的情况,在玩家不知情的情况下就完成了游戏资源的下载。