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

手动点赞 :+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)
    })
}

编辑器效果图

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

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

最主要是为了合批

感谢解答 :+1:

羊了个羊已上线,展示一些关卡:
春:


吉:

福:

田:

1赞

scheduleOnce在这里的作用是什么呢?

只是一个延时调用,没什么特别的

为什么要延时呢? :thinking:

模拟登录,后面会移除的,不用纠结。

2025年1月1日,本游戏同步集成到抖音小游戏《经典休闲移植单机合集》中,欢迎使用抖音、头条等字节系应用扫码体验:
qrcode

音频管理器(初步)

现在我们来抽象出音频管理器 AudioManager.

AudioManager 单例

1、在 assets/fw/ 文件夹下新建 audio 文件夹,audio 文件夹下新建 AudioManager.ts 文件

image

2、AudioManager.ts 实现单例模式

/** 单例模式的音频管理器 */
export class AudioManager {
    private static _instance: AudioManager = null!;
    /** 获取单例的接口 */
    static getInstance() {
        if (this._instance === null) {
            this._instance = new AudioManager();
        }
        return this._instance;
    }

    private constructor() {
        // 私有化的构造函数
    }
}

3、音频管理器提供播放接口

3.1 音频管理器需要 AudioSource 控制播放,因此从正在运行的场景获得 Canvas 节点,并在其上添加 AudioSource 组件

export class AudioManager {
    /** ======== 之前的单例模式实现代码 ======== */

    /** AudioSource 挂载在此节点上 */
    private m_AttachNode: Node = null;
    /** AudioSource 组件 */
    private m_AudioSource: AudioSource = null;
    private constructor() {
        // 私有化的构造函数
        this.m_AttachNode = director.getScene().getChildByName("Canvas");
        this.m_AudioSource = this.m_AttachNode.addComponent(AudioSource);
    }

3.2 添加播放 API

目前我们先提供一个简单的 playMusic 接口,用于播放背景音乐

export class AudioManager {
    /** ======== 其他代码 ======== */

    /** 默认播放背景音乐接口 */
    playMusic(bUrl: {
        /** * 子包名 */ b: string,
        /** * 资源路径 */ l: string,
    }): void {
        // 加载背景音乐后播放
        ResManager.getInstance().loadAudioClip(bUrl.b, bUrl.l, audioClip => {
            let audioSource = this.m_AudioSource;
            audioSource.clip = audioClip;
            audioSource.loop = true;
            audioSource.play();
        })
    }
}

4、改造 LoginEntry 中的播放背景音乐实现

import { _decorator, Component, instantiate, UITransform } from 'cc';
import { G_VIEW_SIZE } from '../../Boost';
import { AudioManager } from '../../fw/audio/AudioManager';
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());
            });
            // 加载背景音乐后播放
            AudioManager.getInstance().playMusic({
                b: "Match3BN",
                l: "Audio/bgm"
            })
        }, 1)
    }
}

点击运行,完美!

当然,音频模块还不是最终版本,这里涉及到多个音频同时播放的问题,背景和音效的区别,音频的音量设置、静音功能、多平台适配、音频中断恢复逻辑等。当然如果对音频有更高的要求,比如做音游的游戏来说,可能需要引入更专业的音频库。

对新手来说,只能先入门,再深入。

添加主游戏场景入口

现在我们打开《羊了个羊》的逻辑是写在登录界面。这个是不正确的。正确的流程是:

graph TD
    A[登录成功] --> B[移交控制权] --> C[打开三消主玩法界面]

因此我们添加一个 Match3Entry 用来接管登录成功后的管理权

1、创建 Match3Entry.ts,并提供 init 方法接口用于接管控制权限

1.1 在 Match3 目录下新建 Script 文件夹,在 Script 文件夹下新建 Match3Entry.ts 脚本:

1.2 Match3Entry.ts 实现如下:

import { _decorator, Component, instantiate, UITransform } from 'cc';
import { G_VIEW_SIZE } from '../../Boost';
import { ResManager } from '../../fw/res/ResManager';
const { ccclass } = _decorator;

@ccclass('Match3Entry')
export class Match3Entry extends Component {
    async init() {
        ResManager.getInstance().loadPrefab("Match3BN", "Match3UI", prefab => {
            let match3Node = instantiate(prefab);
            this.node.addChild(match3Node);
            match3Node.getComponent(UITransform).setContentSize(G_VIEW_SIZE.clone());
        });
    }
}

其中 init 方法用于接管登录成功后的控制权。目前的控制逻辑主要是打开 Match3UI 界面。

1.3 修改 LoginEntry.ts 中的代码

import { _decorator, Component } from 'cc';
import { AudioManager } from '../../fw/audio/AudioManager';
import { ResManager } from '../../fw/res/ResManager';
const { ccclass } = _decorator;

@ccclass('LoginEntry')
export class LoginEntry extends Component {
    start() {
        // 加载背景音乐后播放
        AudioManager.getInstance().playMusic({
            b: "Match3BN",
            l: "Audio/bgm"
        })

        this.scheduleOnce(() => {
            ResManager.getInstance().loadBundle("Match3BN", () => {
                let match3Entry = this.node.addComponent("Match3Entry");
                (match3Entry as any).init();
            })
        }, 1)
    }
}

2、注意事项

2.1 子包的脚本被 require 的时机

子包 Match3BN 在loadBundle 完成的时候,包中的脚本将被 require,Match3Entry 这样的组件就被注册到引擎的组件管理模块中。因此就可以通过 addComponent("Match3Entry") 的形式正确的添加组件。

2.2 注意 Match3Entry 组件的添加方式

在 LoginEntry 中,为什么要用 addComponent("Match3Entry") 的形式,而不是用下面的形式?

import { Match3Entry } from '../../Match3/Script/Match3Entry';
// ...省略
let match3Entry = this.node.addComponent(Match3Entry);
// ...省略

这是因为 LoginEntry.ts 脚本所在的Asset Bundle 包的优先级高于 Match3BN ,因此这样的引用是错误的。
会导致
1、加载依赖关系错乱;
2、微信小游戏、抖音小游戏中 Match3BN (是小游戏分包)的脚本将被错误的打包到 LoginBN 中。

重新运行,一切照旧!

2赞

背景音乐资源移动到登录模块

现在我们先做一个优化,因为在登录界面,我们就加载了背景音乐,但是这个背景音乐其实是 Match3BN 包中的。
这个做法严格意义上是错误的。因为 Match3BN 中的背景音乐,应该由 Match3Entry 负责播放。

换句话说:LoginEntry,你越线了。你把手伸到我的地盘上了。

但实际上,我们更想在登录界面就播放背景音乐,因此我们要将背景音乐调整到 LoginBN 模块中。

1、Login文件夹下新建 Res 文件夹(这里为什么要建这样的一个文件夹,后面会细说,这里简单说一下:每个Asset Bundle 包中,我们区分资源在 Res 文件夹下, 脚本在 Script 文件夹下,这样方便结构化我们的 Asset Bundle,也方便后续脚本自动生成资源配置文件)

2、将 Match3 文件夹中的 Audio 拖到 Login/Res 目录下。

3、修改 LoginEntry.ts 中的背景音乐播放逻辑

        // 加载背景音乐后播放
        AudioManager.getInstance().playMusic({
            b: "LoginBN",
            l: "Res/Audio/bgm"
        })
···

重新运行,表现如初。

手写字符串是很容易出错的-为自动化做准备

在加载音频的时候,我们需要手动输入 Asset Bundle 名称,以及资源路径。

这…这…%#?@¥!…你受得了?

我肯定是受不了。

因此自动生成才是王道。

资源生成格式

首先我们要确定资源生成的格式,因此我们先手动创建我们希望的资源生成的最终格式。

1、在 fw/res/ 中新建 ResConst.ts 文件,用以定义和创建资源的通用格式

image
修改内容如下:

declare global {
    interface IBundleUrl {
        /** * 子包名 */ b: string,
        /** * 资源路径 */ l: string,
        /** 缓存关键字 */ id: string,
    }
}
/**
 * 创建 BundleUrl 对象
 * @param url 
 * @param bundleName Asset Bundle 名称
 * @returns 
 */
export function BL(url: string, bundleName: string, k?: string): IBundleUrl {
    let obj: IBundleUrl = Object.create(null);
    obj.b = bundleName;
    obj.l = url;
    obj.id = `${bundleName}${url}`
    return obj;
}

这里需要补充一点,为什么在 BL 方法中,要定义一个 id 字段,这里其实是为了后续做资源管理用的。用来标记一个资源的唯一ID

2、在 Login/Script/ 文件夹下,创建目标文件 LoginAudio.ts

2、修改 LoginAudio.ts 内容如下

import { BL } from "db://assets/fw/res/ResConst";
const B = (m: string) => BL(`Res/Audio/${m}`, "LoginBN");
export const LoginAudio = {
    bgm: B("bgm"),
}

这样,我们就定义好了 LoginAudio.ts 这个输出文件格式了。只要后续我们在 LoginBN 包中的 Res/Audio/ 目录下新增音频文件,后续自动化脚本就会将这个变化同步到配置中,完美的设计。

3、修改 LoginEntry.ts 的 start 方法中的加载方式

import { LoginAudio } from './LoginAudio';
// ...... 省略
        // 加载背景音乐后播放
        AudioManager.getInstance().playMusic(LoginAudio.bgm)
// ...... 省略

现在我们播放的时候,只需要输入 LoginAudio.,靠着代码提示,就能补全所缺的内容,真是太棒了!

重新运行,表现如初!

1赞