[CocosCreator]AssetManager之管线
Posted ouyangshima
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[CocosCreator]AssetManager之管线相关的知识,希望对你有一定的参考价值。
creator 使用管线(pipeline
)来处理整个资源加载的流程,这样的好处是解耦了资源处理的流程,将每一个步骤独立成一个单独的管道,管道可以很方便地进行复用和组合,并且方便了我们自定义整个加载流程,我们可以创建一些自己的管道,加入到管线中,比如资源加密。
管线
管线 可以理解为一系列过程的串联组合,当一个请求经过管线时,会被管线的各个阶段依次进行处理,最后输出处理后的结果。如下图所示:
管线与一般的固定流程相比,优势在于管线中的所有环节都是可拼接和组合的,这意味着开发者可以在现有管线的任意环节插入新的阶段或者移除旧的阶段,极大地增强了灵活性和可扩展性。
任务
任务 就是在管线中流动的请求,一个任务中包括输入、输出、完成回调、可选参数 等内容。当任务在管线中流动时,管线的各个阶段会取出任务的输入,做出一定的处理后存回到输出中。
UML
- 管线pipeline是存储管道的数组容器
- 管道pipe是fun(task,done)形式参数的处理函数,pipeline同步执行时只需要task参数,不需要done参数。
- 任务task是管道处理函数的参数
内置管线
AssetManager
内置了3
条管线,普通的加载管线、预加载、以及资源路径转换管线,最后这条管线是为前面两条管线服务的。
// 正常加载
this.pipeline = pipeline.append(preprocess).append(load);
// 预加载
this.fetchPipeline = fetchPipeline.append(preprocess).append(fetch);
// 转换资源路径
this.transformPipeline = transformPipeline.append(parse).append(combine);
- 第一条管线用于转换资源路径,找到真实资源路径。为正常加载管线和预加载管线服务。
- 第二条管线用于正常加载,用到了下载器和解析器。
- 第三条管线用于预加载,用到了下载器。
pipeline正常加载管线
pipeline
由两部分组成 preprocess
和 load
。
启动加载管线【加载接口】
一个普通的资源是如何加载的,比如最简单的 cc.resource.load
,在 bundle.load
方法中,调用了 cc.assetManager.loadAny
,在 loadAny
方法中,创建了一个新的任务,并调用正常加载管线 pipeline
的 async
方法执行任务。
// bundle类的load方法
load (paths, type, onProgress, onComplete) {
var { type, onProgress, onComplete } = parseLoadResArgs(type, onProgress, onComplete);
cc.assetManager.loadAny(paths, { __requestType__: RequestType.PATH,
type: type, bundle: this.name }, onProgress, onComplete);
},
// assetManager的loadAny方法
loadAny (requests, options, onProgress, onComplete) {
var { options, onProgress, onComplete } = parseParameters(options, onProgress, onComplete);
options.preset = options.preset || 'default';
let task = new Task({input: requests, onProgress, onComplete: asyncify(onComplete), options});
pipeline.async(task);
},
preprocess预处理管道【准备阶段】
preprocess
由以下管线组成 preprocess
、transformPipeline { parse、combine }
,preprocess
实际上只创建了一个子任务,然后交由 transformPipeline
执行。对于加载一个普通的资源,子任务的 input
和 options
与父任务相同。
let subTask = Task.create({input: task.input, options: subOptions});
task.output = task.source = transformPipeline.sync(subTask);
transformPipeline 路径转换管线
transformPipeline
由 parse
和 combine
两个管线组成,parse
的职责是为每个要加载的资源生成 RequestItem
对象并初始化其资源信息(AssetInfo、uuid、config等):
先将 input
转换成数组进行遍历,如果是批量加载资源,每个加载项都会生成RequestItem
;
如果输入的 item
是 object
,则先将 options
拷贝到 item
身上(实际上每个 item
都会是 object
,如果是 string
的话,第一步就先转换成 object
了)
- 对于
UUID
类型的item
,先检查bundle
,并从bundle
中提取AssetInfo
,对于redirect
类型的资源,则从其依赖的bundle
中获取AssetInfo
,找不到bundle
就报错 PATH
类型和SCENE
类型与UUID
类型的处理基本类似,都是要拿到资源的详细信息DIR
类型会从bundle
中取出指定路径的信息,然后批量追加到input
尾部(额外生成加载项)URL
类型是远程资源类型,无需特殊处理
function parse (task) {
// 将input转换成数组
var input = task.input, options = task.options;
input = Array.isArray(input) ? input : [ input ];
task.output = [];
for (var i = 0; i < input.length; i ++ ) {
var item = input[i];
var out = RequestItem.create();
if (typeof item === 'string') {
// 先创建object
item = Object.create(null);
item[options.__requestType__ || RequestType.UUID] = input[i];
}
if (typeof item === 'object') {
// local options will overlap glabal options
// 将options的属性复制到item身上,addon会复制options上有,而item没有的属性
cc.js.addon(item, options);
if (item.preset) {
cc.js.addon(item, cc.assetManager.presets[item.preset]);
}
for (var key in item) {
switch (key) {
// uuid类型资源,从bundle中取出该资源的详细信息
case RequestType.UUID:
var uuid = out.uuid = decodeUuid(item.uuid);
if (bundles.has(item.bundle)) {
var config = bundles.get(item.bundle)._config;
var info = config.getAssetInfo(uuid);
if (info && info.redirect) {
if (!bundles.has(info.redirect)) throw new Error(`Please load bundle ${info.redirect} first`);
config = bundles.get(info.redirect)._config;
info = config.getAssetInfo(uuid);
}
out.config = config;
out.info = info;
}
out.ext = item.ext || '.json';
break;
case '__requestType__':
case 'ext':
case 'bundle':
case 'preset':
case 'type': break;
case RequestType.DIR:
// 解包后动态添加到input列表尾部,后续的循环会自动parse这些资源
if (bundles.has(item.bundle)) {
var infos = [];
bundles.get(item.bundle)._config.getDirWithPath(item.dir, item.type, infos);
for (let i = 0, l = infos.length; i < l; i++) {
var info = infos[i];
input.push({uuid: info.uuid, __isNative__: false, ext: '.json', bundle: item.bundle});
}
}
out.recycle();
out = null;
break;
case RequestType.PATH:
// PATH类型的资源根据路径和type取出该资源的详细信息
if (bundles.has(item.bundle)) {
var config = bundles.get(item.bundle)._config;
var info = config.getInfoWithPath(item.path, item.type);
if (info && info.redirect) {
if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`);
config = bundles.get(info.redirect)._config;
info = config.getAssetInfo(info.uuid);
}
if (!info) {
out.recycle();
throw new Error(`Bundle ${item.bundle} doesn't contain ${item.path}`);
}
out.config = config;
out.uuid = info.uuid;
out.info = info;
}
out.ext = item.ext || '.json';
break;
case RequestType.SCENE:
// 场景类型,从bundle中的config调用getSceneInfo取出该场景的详细信息
if (bundles.has(item.bundle)) {
var config = bundles.get(item.bundle)._config;
var info = config.getSceneInfo(item.scene);
if (info && info.redirect) {
if (!bundles.has(info.redirect)) throw new Error(`you need to load bundle ${info.redirect} first`);
config = bundles.get(info.redirect)._config;
info = config.getAssetInfo(info.uuid);
}
if (!info) {
out.recycle();
throw new Error(`Bundle ${config.name} doesn't contain scene ${item.scene}`);
}
out.config = config;
out.uuid = info.uuid;
out.info = info;
}
break;
case '__isNative__':
out.isNative = item.__isNative__;
break;
case RequestType.URL:
out.url = item.url;
out.uuid = item.uuid || item.url;
out.ext = item.ext || cc.path.extname(item.url);
out.isNative = item.__isNative__ !== undefined ? item.__isNative__ : true;
break;
default: out.options[key] = item[key];
}
if (!out) break;
}
}
if (!out) continue;
task.output.push(out);
if (!out.uuid && !out.url) throw new Error('unknown input:' + item.toString());
}
return null;
}
RequestItem
的初始信息,都是从bundle
对象中查询的,bundle
的信息则是从bundle
自带的config.json
文件中初始化的,在打包bundle
的时候,会将bundle
中的资源信息写入config.json
中。
经过 parse
方法处理后,我们会得到一系列 RequestItem
,并且很多 RequestItem
都自带了 AssetInfo
和 uuid
等信息,combine
方法会为每个 RequestItem
构建出真正的加载路径,这个加载路径最终会转换到 item.url
中。
function combine (task) {
var input = task.output = task.input;
for (var i = 0; i < input.length; i++) {
var item = input[i];
// 如果item已经包含了url,则跳过,直接使用item的url
if (item.url) continue;
var url = '', base = '';
var config = item.config;
// 决定目录的前缀
if (item.isNative) {
base = (config && config.nativeBase) ? (config.base + config.nativeBase) : cc.assetManager.generalNativeBase;
}
else {
base = (config && config.importBase) ? (config.base + config.importBase) : cc.assetManager.generalImportBase;
}
let uuid = item.uuid;
var ver = '';
if (item.info) {
if (item.isNative) {
ver = item.info.nativeVer ? ('.' + item.info.nativeVer) : '';
}
else {
ver = item.info.ver ? ('.' + item.info.ver) : '';
}
}
// 拼接最终的url
// ugly hack, WeChat does not support loading font likes 'myfont.dw213.ttf'. So append hash to directory
if (item.ext === '.ttf') {
url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}/${item.options.__nativeName__}`;
}
else {
url = `${base}/${uuid.slice(0, 2)}/${uuid}${ver}${item.ext}`;
}
item.url = url;
}
return null;
}
load加载管道【加载流程】
load
方法做的事情很简单,基本只是创建了新的任务,在 loadOneAssetPipeline
中执行每个子任务。
function load (task, done) {
if (!task.progress) {
task.progress = {finish: 0, total: task.input.length};
}
var options = task.options, progress = task.progress;
options.__exclude__ = options.__exclude__ || Object.create(null);
task.output = [];
forEach(task.input, function (item, cb) {
// 对每个input项都创建一个子任务,并交由loadOneAssetPipeline执行
let subTask = Task.create({
input: item,
onProgress: task.onProgress,
options,
progress,
onComplete: function (err, item) {
if (err && !task.isFinish && !cc.assetManager.force) done(err);
task.output.push(item);
subTask.recycle();
cb();
}
});
// 执行子任务,loadOneAssetPipeline有fetch和parse组成
loadOneAssetPipeline.async(subTask);
}, function () {
// 每个input执行完成后,最后执行该函数
options.__exclude__ = null;
if (task.isFinish) {
clear(task, true);
return task.dispatch('error');
}
gatherAsset(task);
clear(task, true);
done();
});
}
loadOneAssetPipeline
如其函数名所示,就是加载一个资源的管线,它分为2步,fetch
和 parse
:
fetch
方法:用于下载资源文件,由packManager
负责下载的实现,fetch
会将下载完的文件数据放到item.file
中。parse
方法:用于将加载完的资源文件转换成我们可用的资源对象,parser.parse
会将解析后的资源对象放到item.content
中。
- 对于原生资源,调用
parser.parse
进行解析,该方法会根据资源类型调用不同的解析方法
import
资源调用parseImport
方法,根据json
数据反序列化出Asset
对象,并放到assets
中- 图片资源会调用
parseImage
、parsePVRTex
或parsePKMTex
方法解析图像格式(但不会创建Texture
对象)- 音效资源调用
parseAudio
方法进行解析plist
资源调用parsePlist
方法进行解析
- 对于其它资源,如果
uuid
在task.options.__exclude__
中,则标记为完成,并添加引用计数;否则,根据一些复杂的条件来决定是否加载资源的依赖。
var loadOneAssetPipeline = new Pipeline('loadOneAsset', [
function fetch (task, done) {
var item = task.output = task.input;
var { options, isNative, uuid, file } = item;
var { reload } = options;
// 如果assets里面已经加载了这个资源,则直接完成
if (file || (!reload && !isNative && assets.has(uuid))) return done();
// 下载文件,这是一个异步的过程,文件下载完会被放到item.file中,并执行done驱动管线
packManager.load(item, task.options, function (err, data) {
if (err) {
if (cc.assetManager.force) {
err = null;
} else {
cc.error(err.message, err.stack);
}
data = null;
}
item.file = data;
done(err);
});
},
// 将资源文件转换成资源对象的过程
function parse (task, done) {
var item = task.output = task.input, progress = task.progress, exclude = task.options.__exclude__;
var { id, file, options } = item;
if (item.isNative) {
// 对于原生资源,调用parser.parse进行处理,将处理完的资源放到item.content中,并结束流程
parser.parse(id, file, item.ext, options, function (err, asset) {
if (err) {
if (!cc.assetManager.force) {
cc.error(err.message, err.stack);
return done(err);
}
}
item.content = asset;
task.dispatch('progress', ++progress.finish, progress.total, item);
files.remove(id);
parsed.remove(id);
done();
});
} else {
var { uuid } = item;
// 非原生资源,如果在task.options.__exclude__中,直接结束
if (uuid in exclude) {
var { finish, content, err, callbacks } = exclude[uuid];
task.dispatch('progress', ++progress.finish, progress.total, item);
if (finish || checkCircleReference(uuid, uuid, exclude) ) {
content && content.addRef();
item.content = content;
done(err);
} else {
callbacks.push({ done, item });
}
} else {
// 如果不是reload,且asset中包含了该uuid
if (!options.reload && assets.has(uuid)) {
var asset = assets.get(uuid);
// 开启了options.__asyncLoadAssets__,或asset.__asyncLoadAssets__为false,直接结束,不加载依赖
if (options.__asyncLoadAssets__ || !asset.__asyncLoadAssets__) {
item.content = asset.addRef();
task.dispatch('progress', ++progress.finish, progress.total, item);
done();
}
else {
loadDepends(task, asset, done, false);
}
} else {
// 如果是reload,或者assets中没有,则进行解析,并加载依赖
parser.parse(id, file, 'import', options, function (err, asset) {
if (err) {
if (cc.assetManager.force) {
err = null;
}
else {
cc.error(err.message, err.stack);
}
return done(err);
}
asset._uuid = uuid;
loadDepends(task, asset, done, true);
});
}
}
}
}
]);
以上是关于[CocosCreator]AssetManager之管线的主要内容,如果未能解决你的问题,请参考以下文章
CocosCreator入门CocosCreator组件 | LabelOutline(文本描边)组件 | LabelShadow(文本阴影) 组件