游戏项目中的小包更新机制

🗨️字数统计=1.8k字 ⏳阅读时长≈7分钟

项目中的小包更新机制

大部分成熟的线上的MMORPG项目,其包括的特效、图集、预制等资源的体积大小必定是庞大臃肿的,这个在业界也是公认的。

如果有打过安卓谷歌包的,想必都清楚谷歌有一个明文规定,上传至谷歌开发者后台的包体大小不能大于100M。对于包体大小大于100M的项目,Google官方也提供了一个方案,Google官方提供了Jobb工具用来生成obb文件,工具可以在 Android\sdk\tools\bin文件夹下找到,生成后于apk文件一起上传至开发者后台,待审核通过发布后供玩家下载。

另一方面,经过调查,国内玩家在下载游戏的时候更偏向于包体偏小的apk。这个时候,另辟蹊径的方案诞生了,以我经历的两款成熟的线上项目为案例,有两个方案供大家参考:

方案一

没有一个官方的名字,姑且叫它强制小包法叭。

在打apk的时候,将游戏用到的代码、闪屏图、loading图、主场景、登录场景、表格、开场动画等最基本的资源提前准备好,在打包时替换进去,这样包体不会很大,玩家所需的拓展资源则通过项目里的热更新机制加载。

这个方法有一个缺点,由于包体内所携带的资源并不完整,只能保证游戏可以运行不会崩溃,更多的功能资源包则需要在登录游戏之前下载。若热更新较大则会“吓跑”一些新玩家(不成文规定:新发布游戏的热更新包体宜小于30M),为了减少这一影响,第二种补充方案应运而生了。

方案二

游戏内小包资源法,作为方案一的补充,它对玩家更友好。

将项目内容分为必须功能和非必须功能,如玩家信息系统、公告系统、技能系统、排行榜系统、基本的新手任务系统以及主城场景等游戏正常运行所必须的或占资源不多的功能系统可以统一将预制图集资源打进包里。但是类似宠物系统、武将系统、野怪系统等占用较多预制资源分类打进AB小包中,用一个json文件来管理资源列表。

在loading主场景的时候,异步检测线上小包资源的json配置是否存在,若存在且为wifi环境则异步下载资源并解压预加载,主界面也会显示相应的窗口。若玩家当前网络环境不满足下载资源的要求,则会在非必须的功能入口处做出限制,告知需要加载资源包才能体验完整的游戏功能,并且会在加载完毕资源后给予一定的奖励。

这样玩家在初始阶段流失的相应较少,体验更佳,对玩家更为友好。

热更新机制

热更新机制已经在另一篇文章中详细阐述了,再此就不重复介绍了。–>热更新机制

游戏内小包资源法

1、小包有对应的资源版本号,在游戏打包前设定相应的资源版本号,而在拉取小包资源的时候也是以资源版本号作为索引去寻找相应的json配置文件。

2、商业游戏最重要的是游戏安全,可以在写入json配置文件列表的脚本中加入相应的文件大小及MD5等信息作为校验,这样就不会轻易被第三方非法修改。

3、在小包解压时,有概率会出现一些文件解压失败,损失一部分资源,或者玩家不小心删掉了一些资源文件,这个时候在重新下载所有小包资源就不明智了。在游戏中给玩家提供一个主动校验资源的功能,根据现有资源文件名遍历json配置资源列表来寻找需要缺失的文件,异步下载并解压。

4、游戏内小包资源法与热更新机制在本质上是一样的,都是通过unity的AssetBundle加载机制来实现的。

小包资源的加载状态

1
2
3
4
5
6
7
8
9
10
11
12
public enum AssetsPackageState
{
None,//初始状态,最初的未加载状态
ReadFileEnd,//读取完json配置文件
DownLoadStart,//开始下载josn文件列表中的小包资源文件
DownLoadStop,//暂停下载josn文件列表中的小包资源文件,并保存当前的下载进度
DownLoadComplete,//小包资源下载完成
DecompressStart,//小包资源开始解压
DecompresFail,//小包资源解压失败
AssetsReady,//资源校验已准备好
AssetsCheck,//资源校验中(成功时会通过回调告知玩家校验结果)
}

无感知下载小包资源

在加载进入主主场景时,检测是否需要加载资源,根据网络环境并自动加载,代码流程大致如下:

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
//初始化检测功能
public void CheckInitDownLoad()
{
//不是初始化状态,说明进入主场景后已经检查过了,不必再走下面的逻辑
if (m_curAssetsPackageState != AssetsPackageState.None)
{
return;
}
if (IsAssetsPackageReady())
{
m_isDecompressSuc = true;
m_isDownLoadSuc = true;
m_curAssetsPackageState = AssetsPackageState.AssetsReady;
}
else
{
if (m_curAssetsPackageState == AssetsPackageState.None)
{
LoginCheckDownLoadInfo();
}
}
}

// 资源包是否下载解压好
public bool IsAssetsPackageReady()
{
if(m_data.assetPackage != null)
{
return m_data.assetPackage.isAssetsPackageReady;
}
return false;
}

//用于登录检查下载资源
private void LoginCheckDownLoadInfo()
{
//创建下载目录
m_availablePath = GetPathForPlatform(platform, TargetAssetsBundlPath);
if (!Directory.Exists(m_availablePath))
{
Directory.CreateDirectory(m_availablePath);
}
if (m_updateStruct == null)
{
GetUpdateStructForSvr(InitUpateFileInfo);
}
else
{
InitUpateFileInfo();
}

}
//拉取下载文件清单信息
private void GetUpdateStructForSvr(Action succCB = null)
{
var url = "....";//json文件的url
DownloadSmallFileTask(url, (string text) =>//下载并解析json文件
{
m_updateStruct = JsonMapper.ToObject<UpdateStruct>(text);//将json数据转为C#结构
if (m_updateStruct != null)
{
if (succCB != null)
succCB();
}
});
//succCB批次任务执行成功回调,failCB批次任务执行失败回调,netConnectCheckCB网络连接检查回调,tryTimeLimit任务失败后尝试次数上限,value=0默认没有上限,不为0时,超过上限后结束任务执行
StartDownloadSmallFile(succCB, failCB, netConnectCheckCB, tryTimeLimit);
}

public class UpdateInfo
{
public string name;
public int size;
public string md5;
public int desize;
public string demd5;
public UpdateInfo()
{
name = "";
size = 0;
md5 = "";
desize = 0;
demd5 = "";
}
}

//初始化更新文件信息
private void InitUpateFileInfo()
{
//遍历json中的下载信息,统计已下载,总下载,初始化需要下载文件列表
for (int i = 0; i < m_updateInfo.Count; i++)//遍历json数据转换后的list
{
var updateInfo = m_updateInfo[i];
InitFileReadTask(m_updateInfo[i], m_availablePath);//添加异步下载任务
m_needDeleteFileList.Add(m_availablePath + m_updateInfo[i].name);
}
}

//初始化文件读取任务
private void InitFileReadTask(UpdateInfo updateInfo, string path)
{
string fileName = "ABC.ab";
string url = "https://www.AAA.com/ABC.ab";
string decompressFileName = "abc";

//下载文件读取
m_totalSize += updateInfo.size;
AddReadFileTask(fileName, updateInfo.md5, updateInfo.size,
//读取成功回调
() =>
{
m_curSize += updateInfo.size;
},
//读取失败回调
(TaskWorkStatus status) =>
{

AddDownloadBigFileTask(fileName, url, updateInfo.md5,
updateInfo.size,
() =>
{
m_childDownSize = 0;
m_curSize += updateInfo.size;
},
(TaskWorkStatus downStatus) =>
{
m_childDownSize = 0;
},
(long curSize) =>
{
m_childDownSize = curSize;
}
);
}
);

//解压后文件读取
var unDecompressFile = new CUnzipFile(fileName, decompressFileName, updateInfo.desize);
m_totalDecompressSize += updateInfo.desize;

AddReadFileTask(decompressFileName, updateInfo.demd5, updateInfo.desize,
() =>
{
m_curDecompressSize += updateInfo.desize;
},
(TaskWorkStatus status) =>
{
AddDecompressFileTask(decompressFileName, updateInfo.demd5,
updateInfo.desize, unDecompressFile,
() =>
{
m_curDecompressSize += updateInfo.desize;
});
}
);
}
分享到