感谢大佬分享,大佬新年好,恭喜发财大吉大利
看目录18# 音频管理器(初步).把单例模式代码加上
认真看教程,按教程来,报错就看报错提示。clone 工程下来,对比一下就知道了。
棋子制作
羊了个羊的核心操作对象是棋子。
所以首先我们需要一些棋子。
现在让我们先来制作棋子脚本以及预制体。
棋子脚本
首先在 GamePlay/modules/Match3/ 文件夹下创建 Match3ZiUE.ts 脚本
修改其内容如下:
import { _decorator, Component, Sprite } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('Match3ZiUE')
export class Match3ZiUE extends Component {
/** 棋子白板 */
@property(Sprite) private bg: Sprite = null;
/** 棋子表面花色 */
@property(Sprite) private sprite: Sprite = null;
}
我们先定义了 2 个属性,一个是棋子白板,一个是棋子表面花色。后续不同的棋子,可以通过动态加载后,修改表面花色即可。
棋子预制体
新建 Match3ZiUE.prefab 预制体,将其尺寸更改为 102x120。(为什么这个尺寸,后面再细说)
分别创建子节点 bg 和 sprite。
然后添加 Match3ZiUE 组件后,关联属性和节点即可。
为 bg 关联 spriteFrame
我们将资源 match3-pai 关联到 bg 节点的 spriteFrame 属性,发现其尺寸过大。因此我们需要将资源 match3-pai 改为 九宫格 的模式:
现在我们来调整 bg 节点的 Sprite 组件的属性:
置空 sprite 节点关联的 spriteFrame
sprite 节点的资源是动态创建的,因此我们先清空这个节点上 Sprite 组件的 spriteFrame 属性:
这样我们的预制体就做好了。
如果你写好了预制体配置自动化脚本,那么运行后会自动生成:
/**
* !!! 本文件由下方脚本自动生成,请勿手动修改.
* !!! /Users/z/Desktop/CCYang/_tool/nodejs/src/app/gen_res_cfg/gen.ts
*/
import { BL } from "db://assets/GScript/core/modules/res/ResConst";
export const PrefabCfg = {
// -------- Match3BN --------
Match3UI: BL("Match3UI", "Match3BN"),
Match3ZiUE: BL("Match3ZiUE", "Match3BN"),
}
这样,基本完成了预制体的制作。
加载并创建棋子实例
即使对老手来说,很多事情不会一蹴而就,因为考虑到每一处细节,那是机器才能做到的事情。对开发者而言,在考虑一件事情的时候,能够顾全的点越多,说明经验越丰富,能力越强。但是最重要的,这种考虑越多,对人的精力消耗是越大的。为什么我们不轻松点开发呢?因此我们实现功能都是从简单开始,一次只做一件事,慢慢叠加上去。让大脑保持一个简单轻松的状态,享受开发的过程。
首先,我们在屏幕中显示一个棋子。这需要依次做 2 件事情:
1、加载 Match3ZiUE.prefab 预制体
2、实例化
加载 Match3ZiUE.prefab 预制体
首先在 ResMananger.ts 文件中,添加一个通用的异步加载资源的方法
loadAssetAsync<T extends Asset>(bUrl: IBundleUrl, type: Constructor<T> | null) {
return new Promise<T>(rs => {
assetManager.loadBundle(bUrl.b, (e, bundle) => {
bundle.load(bUrl.l, type, (err, _asset) => {
if (err) {
console.error(err);
return rs(null);
}
rs(_asset)
})
})
})
}
注意这里的 Asset、 Constructor 等都是从 cc 引入。
还有一个注意事项,这里只是实现了最基础版本的资源加载,这里有一个关键的地方没有处理,就是加载失败的处理。加载失败我认为只能有 2 个原因:
1、你开发功能的时候,写错了,这种就让它报错,你修复后就好了;
2、网络问题导致下载失败;
针对第二种情况,通常是引入重试机制,这种又涉及到网络状态模块和资源加载重试模块。由于篇幅原因只能留给有需求的开发自行实现了。这里我只能展示一下我线上产品的一个实现,让大家有一个概念:
/**
* 加载函数
* @param bUrl
* @param type
*/
async Load<T extends Asset>(bUrl: IBundleUrl, type: Constructor<T> | null, promiseFunc: IPromiseFunc = PF_Default): Promise<T> {
if (PREVIEW) {
if (bUrl.id === undefined) {
console.error(`BundleUrl 没有 id:`, bUrl.b, bUrl.l)
debugger;
}
}
// 从缓存拿资源
let res = this.GetKeyAsset(bUrl.id);
if (res) {
// console.error(`从缓存中拿到资源`, bundleUrl, res)
return res as T;
}
let bundle = assetManager.getBundle(bUrl.b);
if (!bundle) {
bundle = await this.LoadBundle(bUrl.b, (promiseFunc === PF_Waiting) ? PF_Waiting : PF_Default);
}
const asset: T = await promiseFunc(finalResolve => {
fw.netState.addRetry(finalResolve, (jobResolve) => {
bundle.load(bUrl.l, type, (e, r) => {
if (e) {
console.warn(`资源加载失败:`, bUrl, e)
jobResolve(null);
return;
}
jobResolve(r)
});
});
})
return asset as any as T;
}
回到主题。我们在 Match3UI.ts 文件的 start 方法中添加预制体加载的代码,由于使用了 await,所以 start 方法前需要加上 async 关键字修饰
async start() {
console.log(`主玩法界面`)
let prefab = await gtr.res.loadAssetAsync(PrefabCfg.Match3ZiUE, Prefab);
}
这样,我们完成了预制体的加载,现在我们来实例化它。
实例化棋子预制体
在加载完预制体后,进行实例化
async start() {
console.log(`主玩法界面`)
let prefab = await gtr.res.loadAssetAsync(PrefabCfg.Match3ZiUE, Prefab);
let ziNode: Node = instantiate(prefab);
ziNode.setParent(this.node);
let ziUE: Match3ZiUE = ziNode.getComponent(Match3ZiUE);
ziUE.init();
}
Match3ZiUE 的 init 方法是新加的,因此在 Match3ZiUE.ts 中补充上:
init() {
console.log(`Match3ZiUE init`)
}
运行以后,发现屏幕中多了棋子。因此完成了棋子的加载流程。
整理实例化代码到 UIManager 中
上个帖子中,我们实例化的过程是写在 start 中,这个实例化的过程,实际上可以整合到 UIManager 中。这样会让代码看起来更简洁。使用起来也更方便。现在我们来做这件事情。
首先我们在 UIMananger.ts 中新增一个 instantiate 方法,这个方法和官方的保持一致,这样有经验的开发一看到这个方法名,就知道这个方法的用途了。
async start() {
console.log(`主玩法界面`)
let prefab = await gtr.res.loadAssetAsync(PrefabCfg.Match3ZiUE, Prefab);
let ziUE = gtr.ui.instantiate(Match3ZiUE, prefab);
ziUE.node.setParent(this.node);
ziUE.init();
}
将 Match3UI 中的实例化方法移动到 UIManager 中
instantiate<UE extends Component>(ueClass: Constructor<UE>, prefab: Prefab): UE {
let node: Node = instantiate(prefab);
return (node.getComponent(ueClass as any) || node.addComponent(ueClass as any)) as any as UE;
}
这里我们发现,每次实例化的时候,需要传入预制体对象。
但实际上,我们已经传入了 Match3ZiUE 类,根据默认规则,这个类有一个同名的预制体。然后我们提前加载好了它。理论上是不需要再传入预制体的。根据这个类,是能够拿到预制体资源的。
现在我们就来实现这个获取预制体资源的链路:
首先删除传入的预制体参数
async start() {
console.log(`主玩法界面`)
await gtr.res.loadAssetAsync(PrefabCfg.Match3ZiUE, Prefab);
let ziUE = gtr.ui.instantiate(Match3ZiUE);
ziUE.node.setParent(this.node);
ziUE.init();
}
重新优化预制体获取链路:
instantiate<UE extends Component>(ueClass: Constructor<UE>): UE {
let bUrl = getUIClassBUrl(ueClass);
let bundle = assetManager.getBundle(bUrl.b);
let prefab: Prefab = bundle.get(bUrl.l, Prefab);
let node: Node = instantiate(prefab);
return (node.getComponent(ueClass as any) || node.addComponent(ueClass as any)) as any as UE;
}
重新运行,完美如初!
动态资源加载后的引用和释放问题
我们知道,有一个责任归属原则,就是【谁加载,谁释放】(或者更精确的说法是,谁引用,谁解引用)。Match3UI 组件加载并引用了 Match3ZiUE.prefab,它要负责解引用该预制体。
我们在上个贴中动态加载了预制体 Match3ZiUE.prefab,在 Match3UI 界面销毁后并没有释放。
参考官方的资源释放文档:资源释放 | Cocos Creator
我们在界面中加载 Match3ZiUE.prefab 后,理论上资源是由 Match3UI 产生的引用。所以:
1、在加载完资源的时候,要由 Match3UI 对 Match3ZiUE.prefab 进行 addRef 操作;
2、在 Match3UI 销毁的时候,要由 Match3UI 对 Match3ZiUE.prefab 进行 decRef 操作;
现在我们先简单的实现这个流程:
@ccclass('Match3UI')
export class Match3UI extends Component {
private m_Match3ZiUE_Prefab: Prefab = null;
async start() {
console.log(`主玩法界面`)
this.m_Match3ZiUE_Prefab = await gtr.res.loadAssetAsync(PrefabCfg.Match3ZiUE, Prefab);
console.log(`增加引用`)
this.m_Match3ZiUE_Prefab.addRef();
let ziUE = gtr.ui.instantiate(Match3ZiUE);
ziUE.node.setParent(this.node);
ziUE.init();
}
onDestroy() {
this.m_Match3ZiUE_Prefab.decRef(true);
}
}
上面展示了这个资源管理的流程。
那么现在问题来了,我在不同界面中实例化不同类型的预制体,那每次都要这么写代码,不是疯了?既要管理依赖资源的加载用于实例化对象,又要管理资源引用。目前的这个写法(【沙】【雕】方案)不是要了老子老命?
我们的诉求是:
1、不想写显示代码处理资源加载;
2、不想写代码管理引用计数;
3、只想安安静静写业务逻辑;
到底怎么解决这几个问题?后面的帖子慢慢解答。
这里在start()方法内设置屏幕参数时报错了:
[PreviewInEditor] Setting window size is not supported.
不知道你为啥会有这个报错
牛啊。又学到了新知识。谢谢大佬
mark一下跟着大佬系统学习一波
目前看到论坛里最详细的新手教程,真佩服大佬的耐心,能写这么细。
mark一下
引入加载对象类 ResLoader,负责资源加载、引用和解引用
为了复用资源加载、引用和解引用的工作,我们引入一个名为 ResLoader 的对象来承担这样的责任。
在 GScript/core/modules/res/ 文件夹下新建 ResLoader.ts 文件。
为了揭示 ResLoader API 的设计过程,我们先将 Match3UI.ts 中加载和引用的内容移动到 ResLoader.ts 中:
import { Prefab } from "cc";
import { PrefabCfg } from "../../../auto/PrefabCfg";
export class ResLoader {
private m_Match3ZiUE_Prefab: Prefab = null;
async load() {
this.m_Match3ZiUE_Prefab = await gtr.res.loadAssetAsync(PrefabCfg.Match3ZiUE, Prefab);
console.log(`增加引用`)
this.m_Match3ZiUE_Prefab.addRef();
}
/** 释放已加载资源的引用计数 */
releaseResRef() {
this.m_Match3ZiUE_Prefab.decRef(true);
}
}
然后修改 Match3UI.ts 中的代码如下:
import { _decorator, Component, instantiate, Node, Prefab } from 'cc';
import { PrefabCfg } from '../../../auto/PrefabCfg';
import { Match3ZiUE } from './Match3ZiUE';
import { ResLoader } from '../../../core/modules/res/ResLoader';
const { ccclass, property } = _decorator;
@ccclass('Match3UI')
export class Match3UI extends Component {
private m_ResLoader: ResLoader = null;
async start() {
console.log(`主玩法界面`)
this.m_ResLoader = new ResLoader();
await this.m_ResLoader.load();
let ziUE = gtr.ui.instantiate(Match3ZiUE);
ziUE.node.setParent(this.node);
ziUE.init();
}
onDestroy() {
this.m_ResLoader.releaseResRef();
}
}
可以看到,我们有了加载api: load 和释放 api:releaseResRef。
但是,我们需要加载哪些资源,是写死在 load 函数中的。而实际上,需要加载哪些 api,应该是要有一个收集接口来处理的,现在我们来做这件事情:
在 ResLoader 中添加 addUI API:
import { Asset, Component, Constructor, Prefab } from "cc";
import { getUIClassBUrl } from "../ui/UIManager";
export class ResLoader {
/** 待加载的资源 */
private toLoadAssets: { type: Constructor<Asset> | null, bUrl: IBundleUrl }[] = []
/** 已加载的资源 */
private loadedAssets: Asset[] = []
addUI<UI extends Component>(uiClass: Constructor<UI>) {
let prefabBUrl = getUIClassBUrl(uiClass);
this.toLoadAssets.push({
type: Prefab,
bUrl: prefabBUrl,
})
return this;
}
async load() {
let toLoadPromises = this.toLoadAssets.map(toLoad => gtr.res.loadAssetAsync(toLoad.bUrl, toLoad.type));
let toLoadResults = await Promise.all(toLoadPromises);
toLoadResults.forEach(asset => {
// 资源加引用计数
asset.addRef();
this.loadedAssets.push(asset);
})
}
/** 释放已加载资源的引用计数 */
releaseResRef() {
while (this.loadedAssets.length) {
this.loadedAssets.pop().decRef(true);
}
}
}
然后 Match3UI.ts 更改为
import { _decorator, Component } from 'cc';
import { ResLoader } from '../../../core/modules/res/ResLoader';
import { Match3ZiUE } from './Match3ZiUE';
const { ccclass, property } = _decorator;
@ccclass('Match3UI')
export class Match3UI extends Component {
private m_ResLoader: ResLoader = null;
async start() {
console.log(`主玩法界面`)
this.m_ResLoader = new ResLoader();
this.m_ResLoader.addUI(Match3ZiUE);
await this.m_ResLoader.load();
let ziUE = gtr.ui.instantiate(Match3ZiUE);
ziUE.node.setParent(this.node);
ziUE.init();
}
onDestroy() {
this.m_ResLoader.releaseResRef();
}
}
这里需要说明一下:对目前实现的 ResLoader 来说,关键的只有 3 个 API:
1、addUI 方法,它负责收集传入的 UI 类依赖哪些资源,解析后方便 ResLoader 加载;
2、load 方法,它负责加载收集的所有资源(后续可以添加更多的 addUI 这样的方法,或者提供更通用的 addRes 方法,都可以),至于资源是怎么加载的,并发加载、顺序加载、加载失败重试之类的,这些外部都不关心,外部只关心你得给我加载到这些资源,并引用住,所以这里我先用了一个简单的加载实现,你可以根据需求自己修改。
3、releaseResRef 方法,这个方法负责对已引用住的资源解引用。
到目前为止,还有几个问题点,在我这个实现中还没处理:
1、所有使用 ResLoader 的地方,都要在其 onDestory 方法中去调用 releaseResRef。这个明显是很不好用。
2、如果 Match3UI 这样的宿主组件销毁了,releaseResRef,已经调用过了,有些资源才加载完毕,该怎么处理。很显然,这种情况下,只要标记 releaseResRef 已调用。资源加载完毕的时候,额外判断一次是否已调用即可:
import { Asset, Component, Constructor, Prefab } from "cc";
import { getUIClassBUrl } from "../ui/UIManager";
export class ResLoader {
/** 待加载的资源 */
private toLoadAssets: { type: Constructor<Asset> | null, bUrl: IBundleUrl }[] = []
/** 已加载的资源 */
private loadedAssets: Asset[] = []
/** 标记已调用释放资源接口 */
private m_Released: boolean = false;
addUI<UI extends Component>(uiClass: Constructor<UI>) {
let prefabBUrl = getUIClassBUrl(uiClass);
this.toLoadAssets.push({
type: Prefab,
bUrl: prefabBUrl,
})
return this;
}
async load() {
let toLoadPromises = this.toLoadAssets.map(toLoad => gtr.res.loadAssetAsync(toLoad.bUrl, toLoad.type));
let toLoadResults = await Promise.all(toLoadPromises);
toLoadResults.forEach(asset => {
// 资源加引用计数
asset.addRef();
if (!this.m_Released) {
this.loadedAssets.push(asset);
} else {
asset.decRef(true);
}
})
}
/** 释放已加载资源的引用计数 */
releaseResRef() {
if (this.m_Released) return;
this.m_Released = true;
while (this.loadedAssets.length) {
this.loadedAssets.pop().decRef(true);
}
}
}
如果你问我你想中途加资源,或者复用 ResLoader 又要怎么处理?
那我只能告诉你,如果你使用 ResLoader 已经达到这个细节的话,说明你能力很强了。对 ResLoader 的改造对你来说应该是轻松的事情了。
最后,为了不在每个界面中的 onDestory 中写 this.m_ResLoader.releaseResRef() 这样的代码,我们在 GScript/core/modules/res/ 文件夹下新增 DestroyHook.ts 组件,它负责在 onDestroy 的时候,去做这样的处理工作:
import { Component, _decorator } from 'cc';
const { ccclass } = _decorator;
@ccclass("DestroyHook")
export class DestroyHook extends Component {
onDestroy() {
for (let i = this.m_Hooks.length - 1; i >= 0; --i) {
this.m_Hooks[i]();
}
}
private m_Hooks: Function[] = []
addHook(hook: Function) {
this.m_Hooks.push(hook)
}
}
然后在 ResLoader.ts 中添加 autoRelease 方法:
autoRelease(comp: Component) {
comp.node.addComponent(DestroyHook).addHook(() => {
this.releaseResRef()
});
return this;
}
最后修改 Match3UI.ts 的 start 方法:
@ccclass('Match3UI')
export class Match3UI extends Component {
async start() {
console.log(`主玩法界面`)
const resLoader = new ResLoader().autoRelease(this);
resLoader.addUI(Match3ZiUE);
await resLoader.load();
let ziUE = gtr.ui.instantiate(Match3ZiUE);
ziUE.node.setParent(this.node);
ziUE.init();
}
}
这样,我们完成了资源加载、引用和解引用功能的封装。
在动态加载一些资源上,已经感觉好用了不少。
但是,这还不够。除非真的非常需要动态加载,理论上我们应该很少显示创建 ResLoader 这样的对象。
这是什么意思呢?下个帖子通过例子来说明。
打开界面时加载依赖资源
上个帖子中,我们手动创建 ResLoader,并添加了资源,然后加载资源。
实际上,我们可以有更好的方式来做这件事:
打开界面 Match3UI 的时候,界面自动帮我加载好这个界面依赖的资源,然后再打开界面。
首先修改 UIManager.ts 中 open 方法的实现:
async open<UI extends Component>(uiClass: Constructor<UI>): Promise<UI> {
const resLoader = new ResLoader();
resLoader.addUI(uiClass);
await resLoader.load();
let ui = this.instantiate(uiClass);
this.m_Layers[EViewLayer.UI].node.addChild(ui.node);
ui.getComponent(UITransform).setContentSize(G_VIEW_SIZE.clone());
resLoader.autoRelease(ui);
return ui;
}
然后 Match3UI 中的实现修改为:
import { _decorator, Component } from 'cc';
import { Match3ZiUE } from './Match3ZiUE';
const { ccclass, property } = _decorator;
@ccclass('Match3UI')
export class Match3UI extends Component {
async start() {
console.log(`主玩法界面`)
let ziUE = gtr.ui.instantiate(Match3ZiUE);
ziUE.node.setParent(this.node);
ziUE.init();
}
}
Match3UI,ResLoader 对象通过 addUI 方法知道要加载 Match3UI.prefab 资源。但是,它**【如何根据 Match3UI 这个类对象获得到它的依赖资源列表?】**
一个简单的方案就是,在 Match3UI 中定义一个专门用于收集依赖资源的静态函数(这也是我线上游戏的方案):
import { _decorator, Component } from 'cc';
import { Match3ZiUE } from './Match3ZiUE';
import { ResLoader } from '../../../core/modules/res/ResLoader';
const { ccclass, property } = _decorator;
@ccclass('Match3UI')
export class Match3UI extends Component {
static R(loader: ResLoader) {
loader.addUI(Match3ZiUE)
}
async start() {
console.log(`主玩法界面`)
let ziUE = gtr.ui.instantiate(Match3ZiUE);
ziUE.node.setParent(this.node);
ziUE.init();
}
}
最后修改一下 ResLoader 中的 addUI 接口的实现:
addUI<UI extends Component>(uiClass: Constructor<UI>) {
let prefabBUrl = getUIClassBUrl(uiClass);
this.toLoadAssets.push({
type: Prefab,
bUrl: prefabBUrl,
})
if (typeof uiClass['R'] === "function") {
(uiClass['R'] as Function).call(uiClass, this);
}
return this;
}
这样,我们完成了依赖资源的递归收集工作。
于是如果 Match3ZiUE 中也定义了 static R 方法,那么我们无需改动代码,依赖的资源将在打开界面时自动加载。
现在,可以看到 Match3UI 使用起来非常简洁了。
但是,这里还有2个小小的问题,就留给大家自行解决了:
1、static R 每次使用起来都要import ResLoader,所以建议引入一个 declare global type IResLoader 的接口声明,这样不用每次 import。
2、资源引用计数目前是统一和打开的界面节点绑定。是否需要细化到更具体的对象,可以自行决定。
到这里,我们已经完成了依赖资源递归加载、引用和解引用的关系。
确定 UI 的层级
目前为止,在 UIManager 中,我们打开的所有界面,节点层都是 EViewLayer.UI。
那如何定义这个层呢?
实际上方案可以有很多种,这里介绍简单的,就是:
1、所有界面的默认层是 EViewLayer.UI 这一层。
2、如果有特殊定义,那么通过这个类的静态成员变量 static readonly viewLayer = EViewLayer.XX 进行定义
于是,改造一下 UIManager.ts 中的 open 方法:
async open<UI extends Component>(uiClass: Constructor<UI> & { readonly viewLayer?: EViewLayer; }): Promise<UI> {
const viewLayer: EViewLayer = typeof (uiClass.viewLayer) === 'number' ? uiClass.viewLayer : EViewLayer.UI
const resLoader = new ResLoader();
resLoader.addUI(uiClass);
await resLoader.load();
let ui = this.instantiate(uiClass);
this.m_Layers[viewLayer].node.addChild(ui.node);
ui.getComponent(UITransform).setContentSize(G_VIEW_SIZE.clone());
resLoader.autoRelease(ui);
return ui;
}
这里主要改动点 3 个地方:
1、传入的类,声明静态成员变量
uiClass: Constructor<UI> & { readonly viewLayer?: EViewLayer; }
2、确定界面层级
const viewLayer: EViewLayer = typeof (uiClass.viewLayer) === 'number' ? uiClass.viewLayer : EViewLayer.UI
3、在该层级下添加界面
this.m_Layers[viewLayer].node.addChild(ui.node);
ResManager 的优化
到目前为止,之前留下的坑基本填完。
打开 ResManager,可以发现,以下几个 API 已经没有什么用了,可以删除:
1、loadPrefabByBUrl
2、loadAudioByBUrl
3、loadPrefab
4、loadAudioClip
同时修改 AudioManager 中的资源加载方法为通用类型:
/** 默认播放背景音乐接口 */
async playMusic(bUrl: IBundleUrl) {
// 加载背景音乐后播放
const audioClip = await gtr.res.loadAssetAsync(bUrl, AudioClip);
let audioSource = this.m_AudioSource;
audioSource.clip = audioClip;
audioSource.loop = true;
audioSource.play();
}
这里有一个我自己开发上的思路和大家分享下:
API 应该尽量提供通用的 API,比如
loadAssetAsync<T extends Asset>(bUrl: IBundleUrl, type: Constructor<T> | null)
它写了一次以后,这个类不用再更改了。说明这个 API 的通用性非常强,扩展能力也非常强。
而如果你每新增一种类型的资源,都要修改到这个 ResManager 类,这个时候,就要考虑一下自己的设计是否是合理了。
最后 ResManager 优化后,只有 4 个接口:
import { Asset, AssetManager, assetManager, Constructor } from "cc";
/** 单例模式的资源管理器 */
export class ResManager {
/**
* 加载 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);
});
}
/**
* 加载 Asset Bundle 接口
* @param bundleName
* @returns
*/
loadBundleAsync(bundleName: string): Promise<AssetManager.Bundle> {
return new Promise<AssetManager.Bundle>(rs => this.loadBundle(bundleName, rs))
}
loadAsset<T extends Asset>(bUrl: IBundleUrl, type: Constructor<T> | null, cb: (asset: T | null) => void) {
assetManager.loadBundle(bUrl.b, (e, bundle) => {
bundle.load(bUrl.l, type, (err, _asset) => {
if (err) {
console.error(err);
return cb(null);
}
cb(_asset)
})
})
}
loadAssetAsync<T extends Asset>(bUrl: IBundleUrl, type: Constructor<T> | null) {
return new Promise<T>(rs => this.loadAsset(bUrl, type, rs as any))
}
}
羊了个羊的棋盘设定
之前使用的棋子的尺寸是 102x120,这个尺寸是怎么来的?当然是参考《羊了个羊》的第二关布局设定而来的。
首先,在棋盘上,平铺的情况下,每行能放下 10 个棋子。一共 7 行。也就是,棋盘是 10x7 = 70 格的。
同时,在羊了个羊游戏中,棋子是能够遮挡其它棋子,这样的设定增加了游戏的难度,从而增强游戏的趣味性。
因此实际每一个棋子格,又被我分成了 6x6 的小格子。每个格子都是一个小矩形。
游戏的设计分辨率是 750x1334,从宽度适配入手,750/7 = 107,但是尺寸又要被 6 整除,因此采用了 102。
再确定 102 的基础上,去调整棋子高度(要被 6 整除),大致确定是 120 这个尺寸比较合适。
这就是 102x120 尺寸的由来。
那么总结一下就是:
1、棋盘一共被分成了 10x7 的棋子格;
2、每个棋子格又被分成了 6x6 的最小单元格;
3、每个棋子格的尺寸是 102x120 (所以最小单元格是 17x20 的尺寸)
在确定了以上参数后,就可以来构建我们的棋盘了。
加载棋子逻辑
现在让我们来模拟加载 3 个棋子。
先定义棋子的位置,分别在:
1、第 1 行,第 1 列
2、第 2 行,第 2 列
3、第 3 行,第 3 列
因此 Match3UI.ts 的 start 方法修改如下:
async start() {
console.log(`主玩法界面`)
let ziArr = [
[0, 0],
[1, 1],
[2, 2]
]
ziArr.forEach(zi => {
let ziUE = gtr.ui.instantiate(Match3ZiUE);
ziUE.node.setParent(this.node);
ziUE.init();
ziUE.node.setPosition(zi[0] * 102, zi[1] * 120)
})
}
运行后逻辑正确:
现在这个界面看过去有点乱。因为之前设置了 Match3UI 界面的透明度为 80,是为了让大家看到,界面加载后,场景中的标题等节点还没销毁。
这里我们再补上这个流程:
在登录成功后,我们需要移除第一个场景的内容,为了实现这个目的,我们在 Boost.ts 中加入需要销毁的渲染节点属性:
@property(Node) private toReleaseNode: Node = null;
然后在编辑器中,把 Label 和 advice 节点拖拽成为 BG 节点的子节点:
接着将 BG 节点拖拽到 toReleaseNode 上:
然后在 Boost.ts 中向 GCtr 的 init 方法传入一个销毁回调函数:
async start() {
/** 加载全局脚本包 */
await this.loadBundle("GScriptBN");
const gCtr: any = this.node.addComponent("GCtr");
await gCtr.init({
canvas2d: this.canvas2d,
releaseBoostFun: () => {
// 这里进行销毁首场景的渲染节点和释放资源等操作
if (this.toReleaseNode === null) {
return;
}
this.toReleaseNode.destroy();
this.toReleaseNode = null;
}
})
}
同步修改 GCtr.ts 的 init 方法:
async init(param: {
canvas2d: Canvas,
releaseBoostFun: Function,
}) {
// 全局变量设置
(globalThis as any)["gtr"] = this;
// 提前注册预制体信息
registerBUrlByCfg(PrefabCfg);
// 界面管理器二段构造
gtr.ui.init(param.canvas2d);
// 登录模块二段构造
gtr.loginCtr.init();
// 显示登录界面(传入登录成功回调函数)
gtr.loginCtr.showLogin(async () => {
await gtr.ui.open(Match3UI);
param.releaseBoostFun();
})
}
修改的地方有3个:
1、init 方法增加 releaseBoostFun: Function 参数
2、去掉 await gtr.res.loadBundleAsync(“Match3BN”); 的调用,因为 gtr.ui.open(Match3UI) 会自动去加载对应的 bundle。
3、界面打开完毕的时候,调用回调来释放主场景: param.releaseBoostFun();
最后,将 Match3UI.prefab 预制体的颜色修改为: CFFB9A
运行后效果和预期一致: