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

那你可能要明年找工作了:flushed:

我是打算明年找的

这个帖子写完是不是会得到一个super writer title

顺其自然,有最好,哈哈

关于框架的意义和演变

在上一节中,我们自己创建了一个名为【Match3BN】的 Asset Bundle 。我们进入玩法界面的时候,先加载了 Match3BN 包,再通过这个包去加载包内的预制体,最后实例化出了玩法界面。

后续我们继续加载其它预制体的时候,比如说羊了个羊的棋子预制体,也是要重复这个加载流程。

而这个流程,我们一般会抽象成资源加载模块的某个方法。并且这个资源加载模块,在我们做下个游戏的时候,也是能够【复用】的。

因此,为了【复用】我们的劳动成果,我们将这些东西抽象成了:
1、【库】
2、【框架】
3、【工具】
4、【解决方案】

我们所使用的 Cocos Creator 本身就是游戏开发沉淀下来的解决方案,当然,它肯定是包含了各种工具、框架和库的。

粒度更小的就是论坛里近期比较火热的几个框架:
鹤九日 oops-framework
马赛克 MKFramework
向前 XForge

当然还有很多已实现财富自由的老鸟们留下的框架,这里不在赘述,感谢这些大牛们愿意分享自己的劳动成果,本人的一些实现,也是受到了这些框架的启发和影响。

这些框架存在的意义就是:【复用】。
所谓的效率提升,本质就是复用,复用就是使用之前做过的东西,不用重新制造。所谓的开发效率高、生产力高,其底层逻辑就是【复用】逻辑,所谓站在前人的肩膀上,就是这个意思。

本教程是新手向的,这些框架对他们而言,其实会遇到使用上的困难。如果没有人教或者讲解,对他们而言,使用门槛还是很高的。

所以,我这边会以演变的形式,慢慢的抽象出一套 Framework,让新手不用走弯路,至少了解我本人的这套框架是如何演变出来的。

因此从资源管理入手,我们开始我们的抽象工作。

4赞

资源管理器

资源管理器 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赞

大佬上点强度,把资源释放管理也分享了吧

资源管理要先有资源加载器,循序渐进

背景音乐

为了丰富游戏的内容,同时也完善资源管理器,接下来我为游戏添加背景音乐。

资源添加

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 这个包中做好了准备;

本小节就到此结束了。

1赞

手动点赞:+1:

手动点赞 + 催更 :+1:

手动点赞 :+1:

手动催更,点赞:+1:

手动催更,点赞:+1:

明确首场景任务,传递责任链

上节我们加载背景音频资源后,播放了背景音乐。但这并不是首场景的主要任务。
(顺便明确一下,这里的首场景就是指 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)
    })
}

编辑器效果图

最近忙着上线羊了个羊,后续再补充教程。
先上编辑器效果图:

请教一下,为什么要一定替换到自己的资源下

最主要是为了合批