老哥,拿你这个项目作为我找工作demo了,期待更新
那你可能要明年找工作了
我是打算明年找的
这个帖子写完是不是会得到一个super writer title
顺其自然,有最好,哈哈
关于框架的意义和演变
在上一节中,我们自己创建了一个名为【Match3BN】的 Asset Bundle 。我们进入玩法界面的时候,先加载了 Match3BN 包,再通过这个包去加载包内的预制体,最后实例化出了玩法界面。
后续我们继续加载其它预制体的时候,比如说羊了个羊的棋子预制体,也是要重复这个加载流程。
而这个流程,我们一般会抽象成资源加载模块的某个方法。并且这个资源加载模块,在我们做下个游戏的时候,也是能够【复用】的。
因此,为了【复用】我们的劳动成果,我们将这些东西抽象成了:
1、【库】
2、【框架】
3、【工具】
4、【解决方案】
我们所使用的 Cocos Creator 本身就是游戏开发沉淀下来的解决方案,当然,它肯定是包含了各种工具、框架和库的。
粒度更小的就是论坛里近期比较火热的几个框架:
鹤九日 oops-framework
马赛克 MKFramework
向前 XForge
当然还有很多已实现财富自由的老鸟们留下的框架,这里不在赘述,感谢这些大牛们愿意分享自己的劳动成果,本人的一些实现,也是受到了这些框架的启发和影响。
这些框架存在的意义就是:【复用】。
所谓的效率提升,本质就是复用,复用就是使用之前做过的东西,不用重新制造。所谓的开发效率高、生产力高,其底层逻辑就是【复用】逻辑,所谓站在前人的肩膀上,就是这个意思。
本教程是新手向的,这些框架对他们而言,其实会遇到使用上的困难。如果没有人教或者讲解,对他们而言,使用门槛还是很高的。
所以,我这边会以演变的形式,慢慢的抽象出一套 Framework,让新手不用走弯路,至少了解我本人的这套框架是如何演变出来的。
因此从资源管理入手,我们开始我们的抽象工作。
资源管理器
资源管理器 ResManager
首先进入我们框架演变的第一步,引入单例模式的 ResManager。
事实上,演变到最后,我们的游戏中,不会出现 XXManager.getInstance() 这种单例使用模式的。
我们在 assets 目录下新建 fw 文件夹,在其中新增 res 文件夹,在该文件夹下新建 ResManager.ts 文件,在其中添加以下代码,使其成为一个单例模式的类
/** 单例模式的资源管理器 */
export class ResManager {
private static _instance: ResManager = null!;
/** 获取单例的接口 */
static getInstance() {
if (this._instance === null) {
this._instance = new ResManager();
}
return this._instance;
}
private constructor() {
// 私有化的构造函数
}
}
接下来,将之前加载玩法界面预制体的接口移动到资源管理器中,并同步修改加载接口:
1、ResManager.ts 中,新增方法:
private constructor() {
// 私有化的构造函数
}
loadPrefab(bundleName: string, prefabPath: string, cb: (prefab: Prefab | null) => void) {
assetManager.loadBundle(bundleName, (e, bundle) => {
bundle.load(prefabPath, Prefab, (err, prefab: Prefab) => {
if (err) {
console.error(err);
cb(null);
return;
}
cb(prefab)
})
})
}
2、Boost.ts 中同步修改
this.scheduleOnce(() => {
ResManager.getInstance().loadPrefab("Match3BN", "Match3UI", prefab => {
let match3Node = instantiate(prefab);
this.node.addChild(match3Node);
match3Node.getComponent(UITransform).setContentSize(G_VIEW_SIZE.clone());
})
}, 1)
运行后,表现如初,一切正常。
当然,这仅仅是开始。目前 ResManager 并不完善,但至少我们完成了逻辑依赖的迁移:
资源的加载从【依赖引擎 API 】转化为【依赖框架层 API】。
这种依赖的迁移,也是我们编写框架的其中一个意义。
大佬上点强度,把资源释放管理也分享了吧
资源管理要先有资源加载器,循序渐进
背景音乐
为了丰富游戏的内容,同时也完善资源管理器,接下来我为游戏添加背景音乐。
资源添加
1、在 Match3 文件夹下新建 Audio 文件夹
2、Audio 文件夹下新增 bgm.mp3
背景音乐播放
资源管理器新增 Audio 类型资源加载接口
在 Cocos Creator 中,音频文件对应的资源类为 cc.AudioClip,官方文档。
因此,我们新增加的背景音乐,资源类型为 cc.AudioClip。于是,我们在 ResManager.ts 中,新增 AudioClip 类型资源的加载接口:
loadAudioClip(bundleName: string, audioPath: string, cb: (asset: AudioClip | null) => void) {
assetManager.loadBundle(bundleName, (e, bundle) => {
bundle.load(audioPath, AudioClip, (err, asset: AudioClip) => {
if (err) {
console.error(err);
cb(null);
return;
}
cb(asset)
})
})
}
播放背景音乐
在 Cocos Creator 中 AudioClip 的播放,需要依赖一个名为 AudioSource 的播放组件,该组件控制播放不同的音频资源来实现游戏内的背景音乐和音效。官方文档-AudioSource
在官方文档的示例中,通常播放音频的方式是:
1、添加 Node 节点;
2、为 Node 节点添加 AudioSource 组件;
3、AudioSource 组件上的 Clip 绑定对应的音频资源;
4、控制脚本(比如说 Boost.ts)获取 AudioSource 组件,并通过 AudioSource 组件控制音频播放;
但是,在我的这套使用框架下,音频资源是从来不会通过在编辑器中绑定到一个 AudioSource 组件的方式来实现加载的。
那官方的这种方式是不是没有意义了?怎么说呢,资源怎么使用,千人千面。有的人确实所有音频都通过提前挂载的方式去加载,理论上也是可行的,但是比起在编辑器中的操作,我更倾向于通过代码的方式。这种方式的一个好处是:可以方便的检索。
回到正题,既然音频播放需要通过 AudioSource 组件,那么我们就需要动态加载一个 AudioSource 组件后,再去播放 AudioClip:
因此在 Boost.ts 中,添加代码如下:
ResManager.getInstance().loadPrefab("Match3BN", "Match3UI", prefab => {
let match3Node = instantiate(prefab);
this.node.addChild(match3Node);
match3Node.getComponent(UITransform).setContentSize(G_VIEW_SIZE.clone());
});
// 加载背景音乐后播放
ResManager.getInstance().loadAudioClip("Match3BN", "Audio/bgm", audioClip => {
let audioSource = this.node.addComponent(AudioSource);
audioSource.clip = audioClip;
audioSource.loop = true;
audioSource.play();
})
运行后,点击一下界面,就可以听到背景音乐了。(Web 平台下,音频不会自动播放,需要用户触发一次点击或其他交互行为)。
通过这节的内容,我们为后续内容提供了优化框架所需的场景:
1、出现了加载多种类型资源的情形;
2、出现了音频播放逻辑,为后续抽象出音频播放器做好了准备;
3、业务逻辑出现在了首场景,为其脚本逻辑剥离到 Match3BN 这个包中做好了准备;
本小节就到此结束了。
手动点赞
手动点赞 + 催更
手动点赞
手动催更,点赞
手动催更,点赞
明确首场景任务,传递责任链
上节我们加载背景音频资源后,播放了背景音乐。但这并不是首场景的主要任务。
(顺便明确一下,这里的首场景就是指 Main.scene 被创建后,其挂载的 Boost.ts 组件的 start 方法被执行的过程)
对于首场景而言,它的核心任务并不是这个,它的任务主要是:
1、Boost 组件的 start 方法作为整个游戏的逻辑入口。
有的朋友会说:不是还有别的入口吗?import 脚本的时候,那些静态变量都是先于 Boost 脚本组件被初始化,那些地方难道不可以作为逻辑入口?
这里我只能说,别问,问就是行业惯例。
2、初始化框架
通常我们在 Boost 组件的 start 方法中,完成框架的初始化,这样游戏的运行就有了框架层的支持。
在 Android 或者 iOS 平台,通常有一些 SDK 需要接入。但是这些 SDK 在接入设计上,也是要想办法设计成【通过脚本 API 完成触发或完成回调】,这个是个人观点,如果不是很理解的话,也不比深究。
3、传递责任链
在完成框架的初始化后,首场景只需要将业务逻辑交给下一个“负责人”,也就是传递责任链。
首场景完成了游戏名称的显示、健康游戏忠告、初始化框架(教程还未加),就可以加载下一个【负责人】,将游戏的后续任务交给它。
而这个【负责人】,通常是业务逻辑的下一层(有些框架会抽象出流程管理器,比如 Unity GameFramework 中的 Procedure。但是这个看过去逻辑是不直观的,因此不采用 Procedure 的设计)。
在《羊了个羊》这款游戏中,这个【负责人】,我们设定为【登录模块】
那么我们在下一节中,就引入责任链的下一环【登录模块】
登录模块
创建 LoginBN Asset Bundle
接下来我们在工程里创建一个名为 LoginBN 的 Asset Bundle:
1、assets 文件夹下创建 Login 文件夹
2、将 Login 文件夹设置为一个 Asset Bundle 包,并命名为 LoginBN
3、将其优先级更改为 7
创建入口脚本
在 Login 文件夹下,新增 Script 文件夹,在 Script 文件夹下创建 LoginEntry.ts 脚本。
将 Boost.ts 中加载业务逻辑的内容移动到 LoginEntry.ts 中:
import { _decorator, AudioSource, Component, instantiate, UITransform } from 'cc';
import { G_VIEW_SIZE } from '../../Boost';
import { ResManager } from '../../fw/res/ResManager';
const { ccclass, property } = _decorator;
@ccclass('LoginEntry')
export class LoginEntry extends Component {
start() {
this.scheduleOnce(() => {
ResManager.getInstance().loadPrefab("Match3BN", "Match3UI", prefab => {
let match3Node = instantiate(prefab);
this.node.addChild(match3Node);
match3Node.getComponent(UITransform).setContentSize(G_VIEW_SIZE.clone());
});
// 加载背景音乐后播放
ResManager.getInstance().loadAudioClip("Match3BN", "Audio/bgm", audioClip => {
let audioSource = this.node.addComponent(AudioSource);
audioSource.clip = audioClip;
audioSource.loop = true;
audioSource.play();
})
}, 1)
}
}
在 ResMananger.ts 中新增加载 Asset Bundle 的方法:
/**
* 加载 Asset Bundle 接口
* @param bundleName Asset Bundle 的名称
* @param cb 可选回调
*/
loadBundle(bundleName: string, cb?: (bundle: AssetManager.Bundle | null) => void) {
assetManager.loadBundle(bundleName, (e, bundle) => {
cb && cb(bundle);
});
}
修改 Boost.ts 的 start 方法内容为:
start() {
const WIN_SIZE_W = screen.windowSize.width;
const WIN_SIZE_H = screen.windowSize.height;
let isScreenWidthLarger = this.adapterScreen();
if (isScreenWidthLarger) {
screen.windowSize = new Size(WIN_SIZE_W + 1, WIN_SIZE_H);
screen.windowSize = new Size(WIN_SIZE_W, WIN_SIZE_H);
}
ResManager.getInstance().loadBundle("LoginBN", _ => {
const loginEntryClass = js.getClassByName("LoginEntry") as typeof Component;
this.node.addComponent(loginEntryClass)
})
}
在 Boost.ts 中,我们不能直接去引用 LoginBN 包中的 LoginEntry.ts 中的 LoginEntry 组件。否则脚本的打包归属就会发生改变。而是通过引擎提供的 js.getClassByName 方法进行解引用。
运行游戏,表现如初!
修复 3.8.4 适配策略不生效的问题
在 屏幕适配实操 中,官方的 setDesignResolutionSize 接口没有生效,该版本在引入自定义渲染管线的时候,又出问题了。
我们通过以下方式临时修复:
1、设置设计分辨率后,派发画布尺寸变更消息 view.emit(“canvas-resize”)
/** 保证设计分辨率的内容都能显示出来 */
view.setDesignResolutionSize(designSize.width, designSize.height, targetResolutionPolicy);
view.emit("canvas-resize")
2、start 方法修改为
start() {
this.adapterScreen()
ResManager.getInstance().loadBundle("LoginBN", _ => {
const loginEntryClass = js.getClassByName("LoginEntry") as typeof Component;
this.node.addComponent(loginEntryClass)
})
}
请教一下,为什么要一定替换到自己的资源下