引擎: CocosCreator 3.8.0
环境: Mac
Gitee: oops-game-kit
引言
oops-framework是由作者dgflash
编写,基于CocosCreator 3.x而实现的开源框架。
该框架以插件形式存在,主要目的是为了降低与项目的耦合,并且通过插件内部的命令快速的获取最新版本。
该框架的特性有:
- 提供游戏常用的功能库,提高开发效率
- 提供业务模块代码模版,降低程序设计难度
- 内置模块低耦合,可根据需要自行删减,以适应不同的类型
- 提供了常用的插件工具,支持Excel表转Json、支持热更新、AB包
- 增加了ECS、MVVM框架相关,以及常用的屏幕适配,UI管理,多语言等等
为了方便大家更好的学习和使用该框架,作者很贴心的准备了一些学习资料:
注:oops-framework框架QQ群: 628575875
在CocosCreator官方商店,可以通过 oops 搜索更多的框架项目Demo进行学习。
资源管理
在CocosCreator中,资源可以分为两大类:
- 静态引用资源,主要放在res目录中,引擎会对这些资源序列化,自动管理资源的加载和释放相关
- 动态引用资源,包含动态加载和远程下载资源,使用灵活,但需要开发者手动释放资源相关
这些资源的动态加载均通过 Bundle,Bundle 可存在多个,主要有两类:
- 内置Bundle, 主要是resources、start-scene、main等
- 自定义Bundle, 由开发者设定文件夹自定义的,用于对资源进行更好的管理和使用
Bundle 的使用主要通过CocosCreator引擎封装的 AssetManager
进行资源的远程下载、加载和释放等。简单的说框架的资源管理的底层实现就是对 AssetManager
的封装。
ResLoader
oops-framework 中的资源管理主要被 ResLoader
管理,用于管理各种不同类型资源的加载和释放。在 Oops.ts
中的定义如下:
// ../oops-plugin-framework/assets/core/Oop.ts
export class oops {
/** 资源管理 */
static res = new ResLoader();
}
提供的主要参数或接口有:
参数或接口 | 说明 |
---|---|
defaultBundleName | 全局默认加载的资源包名,默认为resources |
load() | 加载单一任意类型资源 |
loadAsync() | 异步加载单一任意类型资源 |
loadDir() | 加载文件夹中资源 |
loadRemote() | 加载远程资源 |
loadBundle() | 加载资源包 |
get() | 根据路径,资源类型获取资源 |
dump() | 打印缓存中所有资源信息 |
release() | 通过相对路径释放资源 |
releaseDir() | 通过文件夹路径释放所有文件夹中资源 |
使用的简单方式是:
// 获取指定目录下的图片资源,用于替换图片
let url = `game/textures/image/spriteFrame`;
oops.res.load(url, SpriteFrame, (err, spriteframe) => {
if (err) {
return console.error(err.message);
}
this._sprite.spriteFrame = spriteframe;
});
本地加载原理
资源的加载接口主要有:
-
load(...)/loadAsync(...)
加载/异步加载单一资源 -
loadDir(...)
加载目录资源
从本质来说,走的流程同 assetManager
是类似的。我们以 load
方法看下逻辑相关:
- 调用 load() 方法,支持传入参数: bundle名、目录名、资源类型、加载进度回调和完成回调
load<T extends Asset>(
bundleName: string,
paths?: string | string[] | AssetType<T> | ProgressCallback | CompleteCallback | null,
type?: AssetType<T> | ProgressCallback | CompleteCallback | null,
onProgress?: ProgressCallback | CompleteCallback | null,
onComplete?: CompleteCallback | null,
) {
let args: ILoadResArgs<T> | null = null;
if (typeof paths === "string" || paths instanceof Array) {
args = this.parseLoadResArgs(paths, type, onProgress, onComplete);
args.bundle = bundleName;
}
else {
args = this.parseLoadResArgs(bundleName, paths, type, onProgress);
args.bundle = this.defaultBundleName;
}
this.loadByArgs(args);
}
- parseLoadResArgs 主要分析参数相关
- args.bundle 如果设置了Bundle名字则获取,如果没有设置则走获取默认的Bundle名
- 调用 loadArgs() 方法,用于通过参数加载指定Bundle下的特定资源
private loadByArgs<T extends Asset>(args: ILoadResArgs<T>) {
if (args.bundle) {
// 调用assetManager检测bundle是否存在
if (assetManager.bundles.has(args.bundle)) {
// 获取指定名称的bundle
let bundle = assetManager.bundles.get(args.bundle);
this.loadByBundleAndArgs(bundle!, args);
}
else {
// bundle不存在,则加载指定参数的bundle
assetManager.loadBundle(args.bundle, (err, bundle) => {
if (!err) {
this.loadByBundleAndArgs(bundle, args);
}
})
}
}
else {
this.loadByBundleAndArgs(resources, args);
}
}
- 通过 loadByBundleAndArgs() 加载Bundle路径下的特定资源
private loadByBundleAndArgs<T extends Asset>(bundle: AssetManager.Bundle, args: ILoadResArgs<T>): void {
if (args.dir) {
bundle.loadDir(args.paths as string, args.type, args.onProgress, args.onComplete);
}
else {
if (typeof args.paths == 'string') {
bundle.load(args.paths, args.type, args.onProgress, args.onComplete);
}
else {
bundle.load(args.paths, args.type, args.onProgress, args.onComplete);
}
}
}
注: 这个加载的流程本质就是对assetManager下bundle下资源的加载。
关于默认的Bundle名字改变,在项目启动InitRes.ts
中加载必备资源是,可以看到默认Bundle名称的设置:
private loadBundle(queue: AsyncQueue) {
queue.push(async (next: NextFunction, params: any, args: any) => {
// 设置默认Bundle名
oops.res.defaultBundleName = oops.config.game.bundleName;
// ...
});
}
远程加载原理
远程加载资源的主要接口是:
-
loadRemote(...)
支持输入:url地址、资源后缀参数、完成回调
看下代码实现:
loadRemote<T extends Asset>(url: string, ...args: any): void {
var options: IRemoteOptions | null = null;
var onComplete: CompleteCallback<T> | null = null;
if (args.length == 2) {
options = args[0];
onComplete = args[1];
}
else {
onComplete = args[0];
}
assetManager.loadRemote<T>(url, options, onComplete);
}
注: 资源远程加载的本质是调用
assetManager.loadRemote
方法
释放原理
资源的释放主要接口有:
-
release(path: string, bundleName?: string)
释放单一资源 -
releaseDir(path: string, bundleName?: string)
释放指定目录资源
看下释放单一资源的核心逻辑:
release(path: string, bundleName?: string) {
// 没有设置bundle名字,则走默认bundle名
if (bundleName == null) bundleName = this.defaultBundleName;
// 从assetManager中获取指定bundle名
var bundle = assetManager.getBundle(bundleName);
if (bundle) {
// 根据路径获取bundle中指定的资源
var asset = bundle.get(path);
if (asset) {
this.releasePrefabtDepsRecursively(asset._uuid);
}
}
}
private releasePrefabtDepsRecursively(uuid: string) {
// 通过assetManager释放指定asset资源引用
var asset = assetManager.assets.get(uuid)!;
assetManager.releaseAsset(asset);
}
释放目录资源与之类似,但要注意bundle的释放:
releaseDir(path: string, bundleName?: string) {
if (bundleName == null) bundleName = this.defaultBundleName;
var bundle: AssetManager.Bundle | null = assetManager.getBundle(bundleName);
if (bundle) {
var infos = bundle.getDirWithPath(path);
if (infos) {
infos.map((info) => {
this.releasePrefabtDepsRecursively(info.uuid);
});
}
// 如果设置的bundle名字,非resources则进行释放
if (path == "" && bundleName != "resources") {
assetManager.removeBundle(bundle);
}
}
}
注: 在CocosCreator的AssetManager资源管理千万记住:释放Bundle和释放资源是不同的,一般先释放资源,再释放对应的bundle。
示例
资源的动态加载和释放
private load(name: string) {
this.node.active = false;
var path = GameResPath.getRolePath(name);
oops.res.load(path, sp.SkeletonData, (err:, sd: sp.SkeletonData) => {
if (err) {
console.error(err.message);
return;
}
this.spine.skeletonData = sd;
this.spine.skeletonData.addRef();
this.node.active = true;
});
}
onDestroy() {
if (this.spine.skeletonData) this.spine.skeletonData.decRef();
}
注意:
- 资源的动态加载是异步过程,因此在加载前建议将 node.active 设置为
false
, 在加载完成后再设置为true
- 资源的动态加载要考虑引用计数
资源远程加载
let url = "https://oops-1255342636.cos-website.ap-shanghai.myqcloud.com/img/bg.png";
var opt = { ext: ".png" };
var onComplete = (err: Error | null, data: ImageAsset) => {
const spriteFrame = new SpriteFrame();
const texture = new Texture2D();
texture.image = data;
spriteFrame.texture = texture;
this.sprite.spriteFrame = spriteFrame;
}
oops.res.loadRemote<ImageAsset>(url, opt, onComplete);
资源Bundle的加载
// 以InitRes中加载Bundle为例:
private loadBundle(queue: AsyncQueue) {
queue.push(async (next: NextFunction, params: any, args: any) => {
// 设置默认加载的外部资源包名
oops.res.defaultBundleName = oops.config.game.bundleName;
// 加载外部资源包
if (oops.config.game.bundleEnable) {
let server = oops.config.game.bundleServer;
let version = oops.config.game.bundleVersion;
await oops.res.loadBundle(server, version);
}
else {
await oops.res.loadBundle(oops.config.game.bundleName);
}
next();
});
}
资源目录加载
var onProgressCallback = (finished: number, total: number, item: any) => {
console.log("资源加载进度", finished, total);
}
var onCompleteCallback = () => {
console.log("资源加载完成");
}
oops.res.loadDir("game", onProgressCallback, onCompleteCallback);
其他
在框架中,由于我们动态资源在 LoadingViewComp 的进度条页面已经通过 loadDir
进行了加载,所以在动态更换某个资源时候,我们不需要再调用oops.res.load
方法,我们可以这样:
this._sprite.node.active = false;
let url = `game/textures/image/spriteFrame`;
// 获取资源,如果获取到则直接替换,如果没有则动态加载
let spriteFrame = oops.res.get(url, SpriteFrame);
if (spriteFrame != null) {
this._sprite.spriteFrame = spriteframe;
this._sprite.node.active = true;
}
else {
oops.res.load(url, SpriteFrame, (err, spriteframe) => {
if (err) {
return console.error(err.message);
}
this._sprite.spriteFrame = spriteframe;
this._sprite.node.active = true;
});
}
在项目的开发中,关于动态加载图片用到的地方很多,我们可以对此进行封装下:
// 精灵节点,路径,bundle名
static loadSprite(sprite: Sprite, resUrl: string, bundleName?: string) {
if (!sprite) {
console.error("sprite null!!!");
return;
}
// 获取bundle名称,如果为空,则为默认值
if (bundleName == null) {
bundleName = oops.res.defaultBundleName;
}
sprite.node.active = false;
let spriteFrame = oops.res.get(resUrl, SpriteFrame, bundleName);
if (spriteFrame != null) {
sprite.spriteFrame = spriteFrame;
sprite.node.active = true;
}
else {
oops.res.load(bundleName, resUrl, SpriteFrame, (err, spriteFrame) => {
if (err) {
return console.error(err.message);
}
if (sprite) {
sprite.spriteFrame = spriteFrame;
sprite.node.active = true;
}
});
}
}
最后,祝大家学习生活工作愉快!