【新手教程】【羊了个羊】

感谢大佬分享,大佬新年好,恭喜发财大吉大利 :blush:

1赞


请教下按教程为啥这里会报错。,好像是 因为AudioManager里没有声明getInstance呀只有ResManager里有 :sob: :sob: :sob:新手不太懂实在

看目录18# 音频管理器(初步).把单例模式代码加上

1赞

认真看教程,按教程来,报错就看报错提示。clone 工程下来,对比一下就知道了。

1赞

棋子制作

羊了个羊的核心操作对象是棋子。
所以首先我们需要一些棋子。

现在让我们先来制作棋子脚本以及预制体。

棋子脚本

首先在 GamePlay/modules/Match3/ 文件夹下创建 Match3ZiUE.ts 脚本

image

修改其内容如下:

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赞

加载并创建棋子实例

即使对老手来说,很多事情不会一蹴而就,因为考虑到每一处细节,那是机器才能做到的事情。对开发者而言,在考虑一件事情的时候,能够顾全的点越多,说明经验越丰富,能力越强。但是最重要的,这种考虑越多,对人的精力消耗是越大的。为什么我们不轻松点开发呢?因此我们实现功能都是从简单开始,一次只做一件事,慢慢叠加上去。让大脑保持一个简单轻松的状态,享受开发的过程。

首先,我们在屏幕中显示一个棋子。这需要依次做 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;
}

重新运行,完美如初!

2赞

动态资源加载后的引用和释放问题

我们知道,有一个责任归属原则,就是【谁加载,谁释放】(或者更精确的说法是,谁引用,谁解引用)。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、只想安安静静写业务逻辑;

到底怎么解决这几个问题?后面的帖子慢慢解答。

1赞

这里在start()方法内设置屏幕参数时报错了:
[PreviewInEditor] Setting window size is not supported.

不知道你为啥会有这个报错

牛啊。又学到了新知识。谢谢大佬

1赞

mark一下跟着大佬系统学习一波

1赞

目前看到论坛里最详细的新手教程,真佩服大佬的耐心,能写这么细。
mark一下

1赞

引入加载对象类 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

运行后效果和预期一致: