手动催更,点赞
手动催更,点赞
明确首场景任务,传递责任链
上节我们加载背景音频资源后,播放了背景音乐。但这并不是首场景的主要任务。
(顺便明确一下,这里的首场景就是指 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)
})
}
请教一下,为什么要一定替换到自己的资源下
最主要是为了合批
感谢解答
scheduleOnce在这里的作用是什么呢?
只是一个延时调用,没什么特别的
为什么要延时呢?
模拟登录,后面会移除的,不用纠结。
2025年1月1日,本游戏同步集成到抖音小游戏《经典休闲移植单机合集》中,欢迎使用抖音、头条等字节系应用扫码体验:
音频管理器(初步)
现在我们来抽象出音频管理器 AudioManager.
AudioManager 单例
1、在 assets/fw/
文件夹下新建 audio
文件夹,audio 文件夹下新建 AudioManager.ts 文件
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 中。
重新运行,一切照旧!
背景音乐资源移动到登录模块
现在我们先做一个优化,因为在登录界面,我们就加载了背景音乐,但是这个背景音乐其实是 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 文件,用以定义和创建资源的通用格式
修改内容如下:
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.
,靠着代码提示,就能补全所缺的内容,真是太棒了!
重新运行,表现如初!
自动化脚本-生成音频配置文件
这个自动化脚本对应的需求已经在上一节中阐述清楚。
后续会以插件的形式完成实现的部分,然后发布到 Cocos 商店,当然,有时间和能力的开发可以自行实现。
// TODO