完全的离线化仅适用于走快游戏 API 的资源,部分资源是直接通过 img 标签或者其他方式加载的(最终会 fallback 到 XHR)还是会到本地服务器发起请求。web.sdk.js 不对此类资源做特殊拦截处理,做离线化的本意是解决版本更新后在线资源的失效问题,对快游戏做 web 移植本身就需要一个静态服务器托管,所以这个并不算大问题。如果需要完完全全的离线化,可自行对游戏逻辑做魔改拦截。
针对使用 Cocos Creator 框架制作的游戏,在 _CCSettings 中一般能看到以下字段:remoteBundles、bundleVers,remoteBundles是远程 Bundle 列表,bundleVers是所有 Bundle 列表,比如:
window._CCSettings = {
remoteBundles: ["resources"],
bundleVers: {
internal:"b3cad",
b0:"10607",
b1:"f37a4",
data:"5bf88",
resources:"011ae",
main:"c0c83"
}
};远程 Bundle 可能会依赖其他 Bundle,为了远程 Bundle 能顺利的加载,需要先通过 loadBundle 拿到 Bundle 配置中的 deps,统计完整依赖链,再按依赖顺序逐个加载。
async function loadCCBundle(names, unload = false) {
names = Array.isArray(names) ? names : [names];
const bundles = Object.create(null);
const visited = Object.create(null);
const visiting = Object.create(null);
const order = [];
function unloadBundle(name) {
const bundle = cc.assetManager.getBundle(name);
if (bundle) {
bundle.releaseAll();
cc.assetManager.removeBundle(bundle);
}
}
function loadBundle(name) {
return new Promise(function (resolve, reject) {
if (unload) {
unloadBundle(name);
}
const exists = cc.assetManager.getBundle(name);
if (exists) {
resolve(exists);
return;
}
cc.assetManager.loadBundle(name, function (err, bundle) {
if (err) {
reject(err);
return;
}
resolve(bundle);
});
});
}
function loadDir(bundle) {
return new Promise(function (resolve, reject) {
window.qg.consolelog('loadDir start: ' + bundle.name);
bundle.loadDir('', function (err) {
if (err) {
reject(err);
return;
}
window.qg.consolelog(bundle.name + ' resources loaded');
resolve(bundle);
});
});
}
async function visit(name, stack) {
if (visited[name]) {
return;
}
if (visiting[name]) {
throw new Error('Circular bundle deps: ' + stack['concat'](name).join(' -> '));
}
visiting[name] = true;
const bundle = await loadBundle(name);
const deps = bundle.deps || [];
bundles[name] = bundle;
window.qg.consolelog('bundle deps: ' + name + ' -> ' + JSON.stringify(deps));
for (let i = 0; i < deps.length; i++) {
await visit(deps[i], stack['concat'](name));
}
visiting[name] = false;
visited[name] = true;
order.push(name);
}
try {
for (let i = 0; i < names.length; i++) {
await visit(names[i], []);
}
window.qg.consolelog('bundle load order: ' + order.join(' -> '));
for (let i = 0; i < order.length; i++) {
const name = order[i];
const bundle = bundles[name] || await loadBundle(name);
await loadDir(bundle);
}
window.qg.consolelog('all bundle resources loaded');
} catch (err) {
window.qg.consoleerror(err);
}
}
// 然后就可以正式下载远程 Bundle 了
loadCCBundle(window._CCSettings.remoteBundles);此时应该可以看到控制台已经在疯狂下载资源了,但还不够,因为我们需要将这些资源保存放到本地,以便后续离线化加载。
这时候就到了 remote-cache 上场了,它本质上就是一个转发保存程序,收到请求后,重定向到真正的远程资源,并将资源原样返回的同时将资源保存到本地。
修改 _CCSettings 中的 server 字段:
window._CCSettings = {
server: `http://127.0.0.1:9999/${originServer}`,
};然后再次尝试 loadCCBundle,当所有远程 Bundle 都进行 loadCCBundle之后,就可以对 _CCSettings 中的 server 字段做一个小调整,将远程资源改成本地资源:
window._CCSettings = {
server: `/https.xxx.com/xxxxxxxxxx`,
};此时游戏已经完成远程资源的本地化了。
某些游戏是运行时通过 JS 动态加载的,此类逻辑没法提前预加载,只能通过人工的方式去到对应的场景下触发下载,做这类逻辑的游戏一般都比较捞。