创建 v3.0.0 文档

This commit is contained in:
SmallMain
2024-12-20 14:47:49 +08:00
parent 0155b219ab
commit 7254c95883
75 changed files with 2352 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
{
"label": "多线程支持",
"position": 6,
"collapsed": true,
"link": {
"type": "doc",
"id": "thread-intro"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

View File

@@ -0,0 +1,18 @@
---
sidebar_position: 1
description: "在多线程中执行资源管线。"
---
# 资源管线
依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **多线程驱动资源管线**,即可启用这一特性。
启用后,资源的下载和缓存部分都会在 Worker 线程中执行,完全释放对主线程的占用。
![analysis](./assets/tap-a.png)
![analysis-2](./assets/tap-a2.png)
这是在 Android 设备上,对游戏帧耗时的分析图,可以看到红框部分的消耗消失了,降低了每帧的耗时。
在启用资源管线的多线程支持后,会有一些接口差异,请前往 [破坏性变更](../../breaking-change#资源管线) 查看详情。

View File

@@ -0,0 +1,32 @@
---
sidebar_position: 2
description: "在多线程中操作音频。"
---
# 音频系统
依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **多线程驱动音频系统**,即可启用这一特性。
启用后,针对音频的所有操作都会在 Worker 线程中执行,完全释放对主线程的占用。
下面是在 Android 设备上,在开启前对游戏帧耗时的分析图:
![analysis](./assets/tas-a.png)
下面是开启多线程支持后:
![analysis-2](./assets/tas-a2.png)
可以看到每次播放音频的耗时从 7.5ms 降低至 0.6ms。
## 调整属性同步间隔
启用多线程支持后,音频实例运行在 Worker 线程中,所以音频属性是定时同步更新到主线程的。
默认情况下,间隔时间为 `500` 毫秒,其实大部分项目都不会读取音频属性,而是直接监听播放开始、播放结束等音频事件(无论如何,事件是立即发出的)。
所以我们可以适当地降低同步频率,优化项目的性能。
依次点击编辑器的菜单项 **项目 - 社区版设置**,然后修改 **属性同步间隔** 的值即可。
在启用音频系统的多线程支持后,会有一些接口差异,请前往 [破坏性变更](../../breaking-change#音频系统) 查看详情。

View File

@@ -0,0 +1,254 @@
---
sidebar_position: 5
description: "轻松地将项目逻辑多线程化。"
---
# 自定义多线程扩展
在之前,如果想为项目编写任何的多线程代码,因为 Worker 本身的易用性差,加上平台之间实现的差异,都会非常麻烦。
为此,社区版新增了自定义多线程扩展的支持,以简化多线程代码的编写,下面以计算斐波那契数列函数为例演示如何轻松编写多线程代码。
## 启用多线程扩展
依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **项目多线程扩展** 即可。
## 创建多线程扩展
依次点击编辑器的菜单项 **扩展 - 创建新扩展插件... - 项目多线程扩展**
这会在项目内的 worker 目录中以默认模板创建一个多线程扩展。
![custom worker struct](./assets/custom_worker_struct.png)
- `src` 多线程源码目录
- `index.js` 入口脚本
- `math.js` 含有 `add` 加法函数的示范脚本
- `creator-worker.d.ts` 提供代码类型提示
- `jsconfig.json` JavaScript 语言服务器配置
## 初识多线程架构
在编写多线程代码时,你需要时刻清楚**多线程扩展内的脚本会在 Worker 线程中执行**,所以不能直接导入项目内的脚本文件。
以下为 `math.js` 的内容,虽然只有几句代码,但这就已经是一个多线程函数的完整实现!
```js
const { registerHandler } = require("ipc-worker.js");
export function add(x, y, callback) {
callback(x + y);
}
registerHandler("math", {
add,
});
```
你已经可以在项目中直接调用这个函数:
```js
worker.math.add([1, 2], ([v]) => {
console.log('Worker math add result:', v);
});
```
这个函数将在 Worker 线程内执行,而不会阻塞主线程。
让我们来编写一个在 Worker 线程中计算斐波那契数列的函数,来深入了解多线程代码的编写!
## 编写多线程脚本
### 创建脚本
我们先在 `src` 目录创建一个 `fibonacci.js` 脚本。
然后在 `index.js` 添加新的一行来导入它:
```js
require("fibonacci.js");
```
这样引擎在创建 Worker 时才会执行这个新脚本。
### 编写函数
`fibonacci.js` 脚本中实现计算斐波那契的函数:
```js
function _fibonacci(n) {
if (n <= 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
```
### 导出函数到主线程
现在,虽然在 Worker 线程中我们有了这个函数,但是我们无法在主线程调用它。
与 Worker 线程的通信通常使用 `postMessage``onMessage` 进行,但是需要处理很多边缘情况,而且这样的开发体验也较差,所以社区版提供了一个封装。
我们需要导入 `registerHandler` 函数:
```js
const { registerHandler } = require("ipc-worker.js");
```
该函数的签名是:
```ts
export function registerHandler(name: string, handler: object): void;
```
调用函数时,函数会执行以下操作:
- 在全局变量 `worker` 的对象上增加一个与传入 `name` 一样的对象属性。
- 遍历传入 `handler` 对象上的所有属性,按规则在 `worker.<name>` 对象上创建对应的函数。
也就是说,我们只需要将 `fibonacci` 函数传入到 `registerHandler` 函数并调用,函数就可以在主线程中调用了!
以下是完整的 `fibonacci.js` 内容:
```js
const { registerHandler } = require("ipc-worker.js");
function _fibonacci(n) {
if (n <= 0) return 0;
if (n === 1) return 1;
return fibonacci(n - 1) + fibonacci(n - 2);
}
function fibonacci(n, callback) {
callback(_fibonacci(n));
}
registerHandler("utils", {
fibonacci,
});
```
### 导出的内部原理
你可能注意到了我们导出的是另一个函数,而不是直接导出 `_fibonacci` 函数。
因为这是实现一个跨线程调用函数时需要遵循的规范:
-`postMessage` 的要求一样,函数的所有参数必须是可序列化的。
- 当函数被调用时,会在函数最后一个参数传入一个回调函数,当需要返回到主线程时,请调用该函数。
像上面 `fibonacci` 函数的实现,在调用 `_fibonacci` 拿到计算结果后,通过调用 `callback(v)` 将值返回到主线程。
而在主线程中,我们需要像这样从主线程调用 Worker 线程中的这个函数:
```js
worker.utils.fibonacci([10], ([v]) => {
console.log('Worker fibonacci result:', v);
});
```
你可能注意到了,第一个参数是数组,而第二个参数的回调的第一个参数也是数组,这也是规范。
为了提高跨线程通信的性能,减少垃圾回收的频率,所以选择了这种调用的方式。
你可以这样理解 `worker.xxx.xxx()` 的调用签名:
```
worker.utils.fibonacci(args, (values) => ...);
// utils: 要调用的 handler 名称
// fibonacci: 要调用 handler 中的 Worker 函数名称
// args: 传入到 Worker 函数的所有参数
// values: Worker 函数返回时的回调,参数是返回值数组
```
这很好理解,我们再举个多参数调用的例子:
```js
// Worker 线程的函数
function handle(a, b, c, callback) {
// a = "ye.", b = {}, c = 1000
callback(1, "text", { prop: 2 });
}
// 主线程的调用方式
worker.utils.handle(["ye.", {}, 1000], ([v1, v2, v3]) => {
// v1 = 1, v2 = "text", v3 = { prop: 2 }
});
```
### 更多导出场景
无参数函数的实现与调用:
```js
// Worker 线程的函数
function setValue(callback) {
// ...
callback();
}
// 主线程的调用方式
worker.utils.setValue(() => {
// ok.
});
```
无需返回的函数的实现与调用(这同时能节省 Worker 的通信开销,因为只需要单向通信!):
```js
// Worker 线程的函数
function setValue(v) {
// ...
// 执行完成之后不调用 callback甚至不用声明
}
// 主线程的调用方式
worker.utils.setValue(["ye."]);
```
无参数也无返回的函数的实现与调用(这同时能节省 Worker 的通信开销,因为只需要单向通信!):
```js
// Worker 线程的函数
function setValue() {
// ...
// 执行完成之后不调用 callback甚至不用声明
}
// 主线程的调用方式
worker.utils.setValue();
```
除了函数之外你还可以导出值、getter/setter 属性,但需要注意需通过 `get_xxx``set_xxx``write_xxx` 三个代理函数进行访问与修改:
```js
// Worker 线程中:
registerHandler("Date", {
time: 1,
});
// 主线程中:
// 获取值
worker.Date.get_time(([v]) => {
// v is 1.
});
// 修改值,不会回调,性能比 write 更高
worker.Date.set_time([100]);
// 修改值,会回调以通知操作已执行完毕
worker.Date.write_time([100], () => {
// finish.
});
```
## 编译多线程扩展
每次修改扩展代码之后,需要手动点击 **项目 - 重新编译多线程扩展** 以生效。
特别注意:**就像修改多线程的设置会影响到所有项目一样,多线程扩展的编译结果也是所有项目共用的!**
所以当你**构建某个项目之前,必须确保最后一次编译是当前项目的多线程扩展**

View File

@@ -0,0 +1,20 @@
---
sidebar_position: 3
description: "在多线程中使用 XMLHttpRequest。"
---
# XMLHttpRequest
依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **多线程驱动 XMLHttpRequest**,即可启用这一特性。
启用后,有关于 XMLHttpRequest 的操作将会在 Worker 线程中执行,完全释放对主线程的占用。
下面是在 Android 设备上,在开启前对游戏帧耗时的分析图:
![alt text](./assets/th-a.png)
下面是开启多线程支持后:
![alt text](./assets/th-b.png)
可以看到每次发起网络请求的耗时从 15.2ms 降低至 0.5ms。

View File

@@ -0,0 +1,84 @@
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
# 多线程支持
:::caution 注意
本章节所有多线程特性暂时仅适用于微信小游戏平台。
并且在微信小游戏平台下还有以下改进:
- 默认启用网络接口和音频接口的高性能模式
- 网络接口支持 HTTP/2、HTTP/3(QUIC) 协议
:::
社区版为引擎的部分系统增加了多线程支持,启用后可以释放其对主线程的占用,减少卡顿现象。
你可以在社区版的设置面板启用多线程支持:
![thread-settings](./assets/thread-settings.png)
## 使用代码调整设置
你可以使用 [构建模板](https://docs.cocos.com/creator/2.4/manual/zh/publish/custom-project-build-template.html) 在 `game.js` 的 `__globalAdapter.init();` 语句执行之前声明宏来调整设置:
- CC_WORKER_ASSET_PIPELINE是否启用 Worker 驱动资源管线)
- CC_WORKER_AUDIO_SYSTEM是否启用 Worker 驱动音频系统)
- CC_WORKER_DEBUG是否启用 Worker 调试模式)
- CC_CUSTOM_WORKER是否启用自定义 Worker
- CC_WORKER_AUDIO_SYSTEM_SYNC_INTERVALWorker 音频系统同步音频属性的间隔时间(单位:毫秒))
- CC_WORKER_WEBSOCKET是否启用 Worker 驱动 WebSocket
- CC_WORKER_HTTP_REQUEST是否启用 Worker 驱动 HTTP 请求)
例如这样:
```js
// game.js
require('adapter-js-path');
// --- 在 init 执行之前设置 ---
globalThis.CC_WORKER_ASSET_PIPELINE = isAndroid;
// --------------------------
__globalAdapter.init();
```
当 `init` 执行之后,由于引擎已经初始化完毕,就不能再对设置进行修改了。
## 将 workers 代码目录设为子包
依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **设为小游戏子包**,即可启用这一特性。
## 注意事项
当你重启编辑器,或者启用/禁用多线程支持时,可能出现以下几种情况:
**禁用多线程支持时输出的包体大小警告:**
由于要实现多线程支持,社区版在包体内增加了一个 `workers` 目录,用于存放 Worker 线程的代码。
大小有 `30-50KB` 左右,当你禁用多线程时,这个目录可以被删除以减少包体。
具体流程如下:
- 点击编辑器界面右上角的 **编辑器** 按钮,并跳转到 `Resources/builtin/adapters/platforms/wechat/res` 目录。
- 删除该目录下的 `workers` 目录。
- 打开该目录下的 `game.json`,删除 `workers` 字段并保存。
**启用多线程支持时输出的检测错误:**
这是由于你按照上面的步骤删除了多线程所需的文件和配置,所以无法启用多线程支持,只需恢复或者重新安装完整的社区版即可。
:::tip 注意
**如果你正在使用付费扩展,则无需手动删除或重新安装,也不会输出任何警告或者错误。**
因为付费扩展会存储已安装版本的备份文件,所以能够自动删除或重新安装。
如果付费扩展依然输出错误则可能是文件损坏,重新一键安装即可。
:::
阅读下面的文档了解详情:
<DocCardList items={useCurrentSidebarCategory().items}/>

View File

@@ -0,0 +1,171 @@
---
sidebar_position: 4
description: "在多线程中使用 WebSocket。"
---
# WebSocket
依次点击编辑器的菜单项 **项目 - 社区版设置**,然后勾选 **多线程驱动 WebSocket**,即可启用这一特性。
启用后,有关于 WebSocket 的操作将会在 Worker 线程中执行,完全释放对主线程的占用。
并且,通过 [自定义多线程扩展](./thread-custom) 你还可以将项目本身的 WebSocket 数据解析操作转移至线程内执行,接下来我们会用一个例子来详细介绍。
下面是在 Android 设备上,在优化前对游戏帧耗时的分析图:
![alt text](./assets/th-a.png)
下面是优化后:
![alt text](./assets/th-b.png)
可以看到网络请求的耗时从 ms 降低至 ms。
:::caution 注意
需注意,不是任何情况下启用多线程支持都能得到性能提升,因为线程之间有通信成本,如果收发大量数据可能导致卡顿,请实际测试性能是否有提升!
:::
## 自定义数据解析
接下来我们以一个使用 Protobuf + WebSocket 进行网络通信的游戏为例子来介绍如何将所有网络层逻辑都移至线程中执行。
我们首先启用 **多线程驱动 WebSocket**这时候项目无需任何改动WebSocket 实际操作即已在线程中进行。
但是在发送数据到 WebSocket 前;或者从 WebSocket 接收到数据后,都需要首先使用 Protobuf 进行编解码,这部分的逻辑也应该移至线程中进行。
首先创建 **自定义多线程扩展**,然后我们可以新建一个 `ws-parser.js` 脚本文件,先编写下面的代码:
```js
globalThis.hookWSSend = function (data) {
return data;
}
globalThis.hookWSRecv = function (data) {
return data;
}
```
:::tip
不要忘记在扩展的 `index.js` 入口脚本中导入该文件
:::
`hookWSSend``hookWSRecv` 是社区版增加的两个特殊接口。
WebSocket 在发送时会尝试调用 `hookWSSend` 函数,并传入即将发送的数据,并实际发送函数的返回值。
WebSocket 在接收时会尝试调用 `hookWSRecv` 函数,并传入收到的数据,并实际返回函数的返回值到主线程中。
有了这两个接口,我们可以很轻松地将数据解析移至线程中实现。
假设以下是主线程中的 `net.ts` 文件:
```ts
import { protocol } from './proto';
export function login(obj) {
const buffer = protocol.LoginRequest.encode(obj);
webSocket.send(buffer);
}
export function onMessage(data) {
const obj = protocol.LoginRespone.decode(data);
console.log("login result:", obj);
}
```
那么我们可以将解析移至刚刚的 `ws-parser.js` 文件,并注释掉原来的解析代码:
`ws-parser.js`
```js
const protocol = require("./proto");
globalThis.hookWSSend = function (data) {
return protocol.LoginRequest.encode(data);
}
globalThis.hookWSRecv = function (data) {
return protocol.LoginRespone.decode(data);
}
```
`net.ts`
```ts
// import { protocol } from './proto';
export function login(obj) {
// 直接发送对象即可,会直接发送给 hookWSSend 函数进行编码
// const buffer = protocol.LoginRequest.encode(obj);
webSocket.send(obj);
}
export function onMessage(data) {
// WebSocket onmessage 回调的参数即是 hookWSRecv 函数的返回值,所以可以直接使用
// const obj = protocol.LoginRespone.decode(data);
console.log("login result:", data);
}
```
这样我们就已经将数据解析的操作移至 Worker 线程中执行,彻底释放主线程了!
## 解决细节问题
当然,以上是情况简单,比较理想的伪代码,如果你也是使用 [protobufjs](https://www.npmjs.com/package/protobufjs) 库,实际上还有以下细节问题需要解决:
- 将 Protobuf 从 node_modules 中抽离
- 将 Protobuf 引用的 Long 库从 node_modules 中抽离
- 既要兼容不支持 Worker 的设备,也要避免加载两份代码
首先如果直接将从 proto 文件编译出来的 `.js` 文件放到 worker 目录中引用,会因为该文件引用 npm 库而报错。
所以每次编译出来的 `.js` 文件,我们需要将里面的
```js
var $protobuf = require("protobufjs/minimal");
```
修改为
```js
var $protobuf = require("./protobuf.js");
```
为了减少麻烦,可以写一个自动脚本进行编译并修改。
然后我们把项目中的 `node_modules/protobufjs/dist/minimal/protobuf.min.js` 文件复制一份到 worker 目录中,并重命名为 `protobuf.js`
需在 `protobuf.js` 的首行插入:
```js
export let protobuf;
```
然后在大段代码中查找 `"object"==typeof module&&module&&module.exports&&(module.exports=n)`,在末尾加上 `,protobuf=n`,这样 `protobuf.js` 就可以作为单独的脚本文件进行导入了。
为了不同时加载两份 Protobuf 库和协议文件,可以先将 workers 设为子包,方法是在设置面板开启 **设为小游戏子包**
然后将 Protobuf 库移至项目的子包中,并修改项目的协议文件以引用 `protobuf.js` 而不是 npm 库。
你还可以使用宏来实现支持 Worker 时不在主线程加载 Protobuf 子包,不支持时则回退到原逻辑:
```ts
if(cc.sys.platform === cc.sys.WECHAT_GAME && CC_WORKER_WEBSOCKET) {
webSocket.send(obj);
} else {
cc.assetManager.loadBundle("protobuf", async (err, bundle) => {
const { protocol } = await import("./proto");
webSocket.send(protocol.LoginRequest.encode(obj));
});
}
```
最后,我们处理 long 库,这是 protobufjs 依赖的大数库。
所幸 long 库的编写比较现代化,我们可以直接将 `node_modules/long/index.js` 文件复制一份到 worker 目录中,并重命名为 `long.js`
然后在 `protobuf.js` 文件中找到 `inquire("long")`,改为 `inquire("./long.js")`,即可完成修改正常导入。