创建 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,5 @@
{
"label": "使用指南",
"position": 4,
"collapsed": false
}

View File

@@ -0,0 +1,9 @@
{
"label": "动态合图",
"position": 3,
"collapsed": true,
"link": {
"type": "doc",
"id": "dynamic-batcher-intro"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -0,0 +1,95 @@
---
sidebar_position: 1
description: "了解调校动态合图的方式。"
---
# 调整合图设置
---
## 动态图集最大数量为什么是 7
在前面的文档中有提到动态图集的最大数量默认为 **设备能同时采样纹理数 - Char 缓存模式自动合批图集数**
因为设备能同时采样纹理数固定为 `8`,而 Char 缓存模式自动合批图集数默认为 `1`,所以动态合图的最大数量默认值为 `7`
这样就只需要使用 1 个材质,也就是能在 1 Draw Call 里完成所有参与动态合图的纹理(包括 Bitmap 缓存模式 Label与 Char 缓存模式 Label 的渲染。
一般情况下不推荐直接修改 `maxAtlasCount`,请参考 [Char 缓存模式](../text-render/text-char-mode.md#与动态图集合批的注意事项) 文档。
---
## 控制纹理是否参与动态合图
可以在编辑器上调整纹理的 `packable` 属性,或者用代码控制:
```js
texture.packable = false;
```
---
## 控制组件是否参与动态合图
使用下面的代码控制组件是否默认激活动态合图机制,默认为开启:
```js
cc.sp.allowDynamicAtlas = false;
```
也可以控制单组件是否激活动态合图机制:
![dynamic-batch-settings](./assets/dynamic-batch-settings.png)
除了在编辑器调整,也可以通过代码控制:
```js
// cc.RenderComponent.EnableType
// GLOBAL: 全局默认值
// ENABLE: 开启
// DISABLE: 关闭
label.allowDynamicAtlas = cc.RenderComponent.EnableType.ENABLE;
```
如果一个纹理参与动态合图但是组件不参与,那么使用该组件进行渲染时就不会参与,但如果同时在其它参与的组件上渲染,那么依然会被打入动态图集。
:::caution 注意
组件有脏检查标记,修改后可能需要对渲染组件调用 `comp.setVertsDirty()` 才会生效。
:::
---
## 是否自动多纹理合批
控制图集纹理是否会自动添加到多纹理合批管理器,默认为开启状态,如果关闭也就意味着**失去了自动进行多个图集纹理合批的特性**。
```js
cc.dynamicAtlasManager.autoMultiBatch = false;
```
---
## 在场景切换时清空所有图集
控制在场景切换时是否会清空所有的动态图集,默认为开启状态。
```js
cc.dynamicAtlasManager.autoResetBeforeSceneLoad = false;
```
:::tip 提示
在引擎原来的设计中,该机制不可被关闭,由于旧动态合图不支持复用废弃的空间,图集终究会被用完,所以引擎加入了这个治标不治本的功能。
但现在,我们认为该机制可以关闭,你只需要管理好纹理资源的释放即可,因为纹理资源释放的同时会释放使用的动态图集空间。
:::
---
## 不进行复用的区域空间大小
在实际的测试中,我们发现废弃空间出现碎片化的现象,比如尺寸 5 * 2 这样的非常小的废弃空间,当碎图尝试加入图集的时候会在这些废弃空间中寻找,这些数量多的小废弃空间无法被复用,却要在每次加入时遍历判断一次。
所以我们加入了 `ignoreRectSize` 设置,当废弃空间尺寸小于这个值就不会被遍历到(但是能合并为大的废弃空间时还是会合并),这能提升大约 50% 的理论性能。
这个值默认为 `10`,如果你的项目有很多小于 10 * 10 的纹理,可以考虑进行调整:
```js
cc.dynamicAtlasManager.Atlas.ignoreRectSize = 2;
```

View File

@@ -0,0 +1,10 @@
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
# 动态合图
动态合图是一个非常好的合批手段,但是在之前引擎实现的比较简陋,所以社区版重构了动态合图系统,在原有功能基础上增加了许多重要特性。
比如支持复用废弃碎图空间,优化了图集装箱算法,所有图集作为一个整体进行管理等等,你可以阅读下面的文档了解详情:
<DocCardList items={useCurrentSidebarCategory().items}/>

View File

@@ -0,0 +1,59 @@
---
sidebar_position: 2
description: "随心所欲地控制动态合图的使用。"
---
# 手动管理合图
有时候你可能需要更细致地去控制哪些纹理加入动态图集,考虑到这一点,社区版在保留原来所有接口的基础上完全开放了动态图集相关的所有接口。
---
## 访问图集数组与已用空间集合
你可以通过
```js
cc.dynamicAtlasManager.atlases
```
```js
cc.dynamicAtlasManager.rects
```
分别访问到图集数组与所有图集的已用空间集合。
---
## 添加 SpriteFrame 到动态图集
```js
cc.dynamicAtlasManager.insertSpriteFrame(spriteFrame);
```
可以将 SpriteFrame 所使用的纹理添加到动态图集,这是引擎原有接口,但社区版对其做了一点修改,这个接口不再会检查纹理的 `packable` 属性,也就是变成了一个强制添加的接口。
这样设计的原因是你可以将所有纹理的 `packable` 都设为 `false`,或者直接将 `maxFrameSize` 设为 `0`,然后完全手动地进行动态合图。
---
## 从动态图集删除 SpriteFrame
```js
cc.dynamicAtlasManager.deleteSpriteFrame(spriteFrame);
```
可以使 SpriteFrame 取消使用动态图集纹理,这不一定会将 SpriteFrame 的纹理从动态图集删除,因为可能还会有其它 SpriteFrame 在使用,只有没有 SpriteFrame 在使用时才会删除纹理。
---
## 从动态图集删除 Texture
```js
cc.dynamicAtlasManager.deleteTexture(texture);
```
这个接口与 `deleteSpriteFrame` 相似,但是它会直接删除纹理,并且会使使用该纹理的 SpriteFrame 全部恢复。
---
## 更多接口
虽然还暴露了其它接口出来,但因为太过于底层所以不推荐使用,如果你想了解可以阅读原理文档。

View File

@@ -0,0 +1,9 @@
{
"label": "多纹理渲染",
"position": 1,
"collapsed": true,
"link": {
"type": "doc",
"id": "multi-render-intro"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,112 @@
---
sidebar_position: 2
description: "了解如何手动进行多纹理合批。"
---
# 多纹理合批
使用 [多纹理材质](./multi-material.md) 文档中提到的 `MultiHandler` 的接口来动态设置材质的纹理插槽可能会比较麻烦,比如你需要使用一个纹理时,还得找到该纹理所在的材质并设置到渲染组件上。
为了能更方便地进行多纹理合批,社区版为组件增加了自动切换多纹理材质的机制,并且封装了一个多纹理合批管理类 `cc.sp.MultiBatcher`
动态图集与字符图集使用的是全局多纹理合批管理器实例,可以通过 `cc.sp.multiBatcher` 访问。
## 开关自动切换多纹理材质
可以通过全局开关来控制所有组件的默认值:
```js
cc.sp.autoSwitchMaterial = false;
```
默认情况下组件会使用全局值,你可以控制单个组件是否强制启用/禁用该机制:
![autoswitchsettings](./assets/autoswitch-settings.png)
除了在编辑器调整,也可以通过代码控制:
```js
// cc.RenderComponent.EnableType
// GLOBAL: 全局默认值
// ENABLE: 开启
// DISABLE: 关闭
sprite.autoSwitchMaterial = cc.RenderComponent.EnableType.ENABLE;
```
:::caution 注意
组件有脏检查标记,如果修改全局开关或者修改纹理关联的材质,需要对所有使用该纹理的渲染组件调用 `comp.setVertsDirty()` 重新检查。
:::
:::caution 特别注意
如果 Spine 组件所使用的 `SkeletonData` 同时使用了多个纹理,那么只会遍历数据以找到的第一个纹理为主执行自动切换机制。
:::
## 设置纹理的关联材质
我们为纹理增加了一个关联材质的概念,在支持的组件内对纹理进行渲染时,会提前切换为该纹理关联的材质。
:::info
每个纹理只能关联一个材质,如果同一个纹理,不同的渲染组件需要使用不同材质就需要手动设置。
:::
关联材质的接口有两种用法:
```js
const bool = texture.linkMaterial(material);
const bool = texture.linkMaterial(material, index);
```
第一句代码会自动将纹理设置到材质的空插槽中,然后将该材质设置为该纹理的关联材质,如果没有空插槽会返回 `false`
第二句则是强制将纹理设置到指定的插槽中,并将该材质设置为该纹理的关联材质。
想要解除两者的关联可以使用:
```js
texture.unlinkMaterial();
```
获取关联的材质可以使用:
```js
const material = texture.getLinkedMaterial();
```
## 多纹理合批管理器
手动关联材质就意味着你需要手动创建并管理创建的所有材质,如果是大量纹理需要关联材质就会比较麻烦。
所以我们封装了一个小巧的多纹理合批管理器 `cc.sp.MultiBatcher`
这个管理器有点像动态合图管理器,它会持有一个材质数组,初始化后会使用内置的多纹理 Effect 着色器创建一个材质并放在数组中。
你可以传给管理器一个纹理,它会查找所有材质的空插槽,如果没有材质有空插槽则会创建一个新材质,然后把纹理与材质关联。
### 如何使用
创建管理器并初始化可以使用:
```js
const batcher = new cc.sp.MultiBatcher();
batcher.init();
```
传入纹理可以使用:
```js
const material = batcher.requsetMaterial(texture);
```
会返回关联的材质,如果纹理本来就已经有关联的材质,则会直接返回已关联的材质。
清空内部数组可以使用(这不会取消纹理的关联):
```js
batcher.reset();
```

View File

@@ -0,0 +1,137 @@
---
sidebar_position: 1
description: "了解如何手动管理多纹理材质。"
toc_max_heading_level: 5
---
# 多纹理材质
社区版新增了一个用于多纹理渲染的内置着色器资源,你可以直接使用该材质。
---
## 创建多纹理材质
创建一个材质Effect 选择内置的多纹理 Effect 着色器 `multi-2d-universal`
![material-settings](./assets/material-settings.png)
勾选 `USE_MULTI_TEXTURE` 后可以看到上面有 `texture` - `texture8` 一共 8 个纹理插槽,将需要使用的纹理拖到上面的插槽即可完成多纹理材质的配置。
---
## 在组件中使用多纹理材质
直接拖到组件的 `Materials` 属性上即可。
![material-comp](./assets/material-comp.png)
注意上图中已经在 `cc.Sprite` 组件拖入了刚刚创建的多纹理材质,并且其 `SpriteFrame` 属性设置的是材质中 `texture3` 插槽中的纹理。
![material-final](./assets/material-final.png)
你不需要指定组件要使用的纹理插槽 id组件内部在渲染前会自动查找纹理在材质中的 id。
:::caution 警告
如果组件使用的纹理在材质中找不到,为了保证正常渲染,会在组件的材质变体中将纹理设置到 `texture` 插槽中。
这会导致这个组件的多纹理材质变体之后都不能按预期进行合批,就像 “退化” 成了普通材质。
所以使用时必须要确保纹理在材质中,假如材质变体已经 “退化” 了,那你可以通过重新设置材质的方式使组件持有一个新的材质变体。
:::
:::caution 特别注意
Spine 组件使用多纹理材质时会强制勾选 `enableBatch`,因为不开启就不能合批,那也就没必要使用多纹理材质。
:::
---
## 自定义多纹理材质
上面介绍的多纹理材质都是使用的内置的多纹理 Effect 着色器,你可以直接在内置多纹理 Effect 着色器的基础上修改。
除了直接在内置着色器的基础上修改之外,任何着色器中如果存在一个宏 `USE_MULTI_TEXTURE = true`,则会被认为是多纹理材质。
演示项目中有自定义材质的示范代码可以参考。
:::tip 提示
是否为多纹理材质的判断逻辑流程:
1. 获取材质当前使用的 Technique 中的第一个 Pass
2. 判断这个 Pass 是否 `USE_MULTI_TEXTURE = true`
3. 是的话,这个材质即为多纹理材质
在某些情况下通过代码修改材质可能需要调用 `material.updateMultiSupport()` 来触发这个流程。
:::
---
## 通过代码设置纹理插槽
每个多纹理材质都对应着一个多纹理材质管理器,这是社区版新增的一个工具类,其主要用处是便捷、高性能地管理多纹理材质上面的纹理插槽。
通过 `material.getMultiHandler()` 可以获取到管理器实例,请使用这个实例来操作多纹理材质的纹理插槽。
比如:
```js
// 获取管理器实例
const handler = material.getMultiHandler();
// 设置 `texture` 纹理插槽
handler.setTexture(0, texture);
// 置空 `texture2` 纹理插槽
handler.setTexture(1, null);
// 直接移除指定纹理
handler.removeTexture(texture.getImpl());
// 将纹理自动设置到材质的空插槽
handler.autoSetTexture(texture);
```
从上面的代码中可以看出操作纹理插槽的时候并不是传入插槽的名称,而是需要提供下标。
下标 `0` - `7` 分别对应着名称为 `texture` - `texture8` 的插槽。
可以使用这两个函数进行转换:
```js
// index to name
cc.sp.propertyIndex2Name(0); // return: "texture"
// name to index
cc.sp.propertyName2Index("texture"); // return: 0
```
:::caution 注意
注意区分材质 `cc.Material` 与材质变体 `cc.MaterialVariant`
:::
:::caution 警告
请勿直接通过材质原始的 `setProperty` 接口修改多纹理材质的纹理插槽。
如果你必须这么做,需要调用 `material.getMultiHandler().syncTextures()` 来同步插槽数据到 `MultiHandler` 上。
:::
---
## 强制设置材质的类型
如果你想将某个材质强制视为多纹理材质或非多纹理材质,可以:
```js
// 视为多纹理材质
material.setMultiSupport(true);
// 视为非多纹理材质
material.setMultiSupport(false);
```
没有特殊情况不需要这么做。

View File

@@ -0,0 +1,50 @@
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
# 多纹理渲染
:::tip 提示
多纹理渲染属于底层设施,若你不准备手动使用多纹理材质或者多纹理合批管理器的话,请跳过本特性文档。
:::
## 什么是多纹理渲染?
在以前的认识里我们知道相邻的节点使用不同的纹理Texture会导致不能合并批次。
其根本原因是纹理是使用 uniform 变量传给着色器的,而需要合并批次的话不允许每次渲染都拥有不同的 uniform 变量值。
社区版现在的实现是先设置多个 uniform 变量,比如将 8 张纹理写入到 "texture1" "texture2" "texture3"... 的 8 个 uniform 变量中,然后在着色器里再判断应该在渲染时使用哪个 uniform 变量。
这样的话如果所有渲染都只用这 8 张纹理,就都能合并为 1 个批次。
这要求设备支持采样多个纹理,而在现代绝大多数设备中都至少支持采样 8 张纹理,所以这不是问题。
当然除了这种方法,还有另外几种进行多纹理合批的方法,例如 "Texture Array" 和 "Bindless",但都有实用性与兼容性的问题。
:::info 那么,代价是什么?
因为会多传递一个顶点属性,并且需要在着色器中去判断该使用哪个纹理,导致**合并批次并不一定会提升性能**。
所以我们建议在多个档次设备中实际测试项目是否使用多纹理渲染的性能差距。
:::
为了让多纹理渲染能以最简单的方式在引擎中使用,社区版已经在内部对其做好了一些封装:
<DocCardList items={useCurrentSidebarCategory().items}/>
:::caution 注意
- **支持的渲染组件**
cc.Sprite、cc.Label、cc.RichText、cc.MotionSteak、Spine 组件。
- **不支持的渲染组件**
cc.ParticleSystem、TiledMap 组件:这两个组件当前的引擎实现会强制打断合批,暂时不支持。
DragonBones 组件:因人力有限,并且这个组件与 Spine 组件可以相互代替,所以暂时不支持该组件。
:::

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")`,即可完成修改正常导入。

View File

@@ -0,0 +1,9 @@
{
"label": "Profiler",
"position": 7,
"collapsed": true,
"link": {
"type": "doc",
"id": "profiler-intro"
}
}

View File

@@ -0,0 +1,32 @@
---
sidebar_position: 1
description: "监测更多性能数据。"
---
# 新性能指标
通过宏启用后,如下图所示:
![demo6](/demo-imgs/demo6.png)
会新增以下几个非常有用的指标:
**Label CanvasLabel 组件的 Canvas 数量)**
- 指标:(用量) / (总量)
- 用量:指正在使用的 Canvas 对象数量,一般每个非 CHAR 模式的 Label 都会使用一个 Canvas 对象,直到被销毁。
- 总量:指引擎内部维护的 Canvas 池中 Canvas 对象的总数量。
**Char AtlasChar 字符图集使用情况)**
- 指标:(用量) / (缓存数量) / (总量)
- 用量:指正在占用的像素数量,注意这是通过每块区域的像素占比进行估算得出,会存在小数点,比如 2.8 则表示占用了 2.8 张图集大小。
- 缓存数量:计算方式与用量相同,但包括无引用的字符区域,这意味着随时可能被释放。
- 总量:指引擎已创建的字符图集数量。
**Dynamic Atlas动态图集使用情况**
- 指标:(用量) / (当前数量) / (最大数量)
- 用量:指正在占用的像素数量,注意这是通过每块区域的像素占比进行估算得出,会存在小数点,比如 2.8 则表示占用了 2.8 张图集大小。
- 当前数量:指引擎已创建的字符图集数量。
- 最大数量:指当前设置的 `maxAtlasCount` 值。

View File

@@ -0,0 +1,14 @@
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
# Profiler
社区版对引擎自带的 Profiler 进行了一些改进,默认情况下是禁用的,可通过宏来开启:
```
cc.macro.ENABLE_CUSTOM_PROFILER = true;
```
你可以阅读下面的文档了解详情:
<DocCardList items={useCurrentSidebarCategory().items}/>

View File

@@ -0,0 +1,9 @@
{
"label": "Spine",
"position": 4,
"collapsed": true,
"link": {
"type": "doc",
"id": "spine-intro"
}
}

View File

@@ -0,0 +1,14 @@
---
sidebar_position: 2
description: "像其它渲染组件一样在 Spine 组件上使用动态合图。"
---
# 动态合图
你可以像其它渲染组件一样在 Spine 组件上使用动态合图,如果想了解有关动态合图的更多详情,可以阅读 [动态合图](../dynamic-batcher/dynamic-batcher-intro) 文档。
:::caution 注意
由于引擎实现差异,在原生平台上 Spine 暂不支持与其它组件合批。
:::

View File

@@ -0,0 +1,14 @@
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
# Spine
社区版解决了 Spine 在之前的三大痛点:
- 不参与动态合图
- 无法与其它组件合批
- 不能使用 SpriteFrame 换装
你可以阅读下面的文档了解详情:
<DocCardList items={useCurrentSidebarCategory().items}/>

View File

@@ -0,0 +1,91 @@
---
sidebar_position: 2
description: "随心所欲地更换 Spine 动画的部份纹理。"
---
# SpriteFrame 换装
引擎提供了一个替换插槽的 attachment 对象进行换装的方法,可以在 [Spine 组件参考](https://docs.cocos.com/creator/2.4/manual/zh/components/spine.html) 官方文档进行了解。
但是使用替换插槽的 attachment 对象这种方法比较绕,并且 Spine 动画中有切换 attachment 的关键帧时这种方法就没用了。
而 attachment 对象持有一个 region 对象,这个对象类似引擎的 SpriteFrame所以我们可以通过修改 region 对象的数据来进行换装。
---
## 认识 RegionData
请勿混淆 RegionData 与 regionRegionData 主要作为 SpriteFrame 和 region 对象之间的桥梁,实现两者的相互转换。
将 SpriteFrame 转换为 RegionData
```js
const regionData = new sp.RegionData(spriteFrame);
```
将 RegionData 转换为 SpriteFrame
```js
const spriteFrame = regionData.toSpriteFrame();
```
将 attachment 对象的 region 数据转换为 RegionData
```js
const regionData = new sp.RegionData(attachment);
```
将 RegionData 数据更新到 attachment 对象上:
```js
regionData.assignToAttachment(attachment);
```
:::caution 注意
Spine 的 Region 支持 `0``90``180``270` 四种旋转角度,而 Cocos Creator 的 SpriteFrame 只支持 `0``270` 两种旋转角度,所以如果是 RegionData 转为 SpriteFrame 则可能导致方向不同的问题。
**要进行换装的话使用的是 SpriteFrame 转为 RegionData所以不用担心这个问题。**
:::
---
## 使用 SpriteFrame 修改 Region 数据
虽然使用上面的 RegionData 即可实现使用 SpriteFrame 换装的需求,但我们还在 Spine 组件上提供了两个更方便的接口:
只使用 `regionData.assignToAttachment(attachment)` 只会修改 SkeletonData 的数据,但不会触发 Spine 组件的渲染更新。
推荐直接使用:
```js
skeletonComponent.setRegionData('Head', 'Head', new sp.RegionData(spriteFrame));
```
在修改的同时刷新组件的渲染实现换装,并且不会打断当前正在播放的动画。
还提供了一个通过 attachment 名称获取 RegionData 的接口:
```js
a.getRegion(slotName, attachmentName);
```
---
## 注意事项
### 多实例问题
由于是直接修改 Spine 组件所使用 SkeletonData 的 attachment 数据,所以所有 Spine 组件都会受到影响。
如果你只想替换其中一个组件,那么就可以克隆这个 SkeletonData 让每个组件都使用不同的 SkeletonData 实例进行渲染。
社区版提供了一个克隆数据的接口来实现这个需求:
```js
const clonedSkeletonData = skeletonData.clone();
```
使用以上代码克隆 SkeletonData 后再进行换装,赋值给 Spine 组件,那么替换操作就只会对这个 Spine 组件生效。
---
以上所有用法你可以在演示项目中找到示范代码。

View File

@@ -0,0 +1,9 @@
{
"label": "文本渲染",
"position": 2,
"collapsed": true,
"link": {
"type": "doc",
"id": "text-render-intro"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,187 @@
---
sidebar_position: 4
description: "详细了解如何通过烘焙提升运行时文本渲染性能。"
---
# 文本渲染烘焙
社区版为引擎增加了文本渲染烘焙的能力。
以某个项目的界面打开操作为例,通过分析 CPU Profile 可以看到当界面节点激活时绝大部分的耗时都来自于文本渲染函数_applyFontTexture
![before baking](./assets/before-baking.png)
但这个界面的文本量并不多,在低端机上的耗时却有 1.06s。
通过文本渲染烘焙优化后,耗时降至 60ms
![after baking](./assets/after-baking.png)
文本渲染烘焙分为三个部分:
- **预加载 Label Canvas**
- **烘焙文本测量值**
- **预加载 `Char` 图集**
我们将逐个说明。
## 预加载 Label Canvas
引擎通过离屏 Canvas 渲染文本,内部维护着一个 Canvas 池以作复用。
每个非 Char 模式的 Label 都会占用一个 Canvas 对象,直到 Label 销毁才会将 Canvas 对象放回池中。
默认情况下Canvas 池不预先创建对象,而是在使用时进行创建,在部分低端设备上,这会增加文本渲染时的卡顿。
并且该 Canvas 池默认最大缓存数量为 32超过该数量的对象不会放入池中而是直接销毁这也导致大量使用 Label 的项目可能会因为持续创建 Canvas 而造成卡顿。
现在,社区版开放了这部分的接口,可通过 `cc.Label._canvasPool` 访问该对象池。
可以调整最大缓存数量:
```ts
cc.Label._canvasPool.max = 64;
```
可以预先缓存 Canvas 对象:
```ts
cc.Label._canvasPool.cache(Number.MAX_SAFE_INTEGER);
```
预先缓存接口需传入要缓存的对象数量,无论如何,实际缓存的对象数量都不会超过设置的最大数量。
## 烘焙文本测量值
引擎内部在渲染文本前需要先测量字符的宽高,这是通过 Canvas 上的 `measureText` 接口实现的。
如果你对性能进行了分析,那么可以看出,该接口占据单次文本渲染总耗时的 80% 以上。
引擎会对此接口的结果进行缓存,相同样式的文本会从缓存中读取,避免了部分性能消耗。
但默认情况下,该缓存仅会保留最后 100 次的测量值,也就是说,如果所有 Label 使用 Char 模式渲染,在渲染 100 个不同的字符后,缓存就会被逐个移除,测量值又会有大量性能消耗。
所以,社区版对这部分接口进行了优化,开放了 `cc.textUtils` 的部分接口来控制测量值缓存。
首先,你可以调整缓存容量,容量越大,就能避免更多测量值的性能消耗,但内存占用会上升,请视情况而定:
```ts
cc.textUtils.measureCache.limit = Number.MAX_SAFE_INTEGER;
```
其次,我们开放了操作缓存的接口,意味着你可以烘焙测量值,将测量值数据保存到项目中,完全避免测量值的性能消耗!
获取与应用缓存对象:
```ts
// 获取
const data = cc.textUtils.getMeasureCache();
// 应用
cc.textUtils.applyMeasureCache(data);
```
我们推荐的用法是:
- 在开发测试的版本中收集缓存数据并序列化到项目的 JSON 文件中(因为收集会有性能消耗,所以线上版本不进行收集)
- 在游戏启动前应用缓存数据
**注意**
经过我们的测试,同个字符在微信小游戏平台与 Web 端得出的测量值是一致的。
但可能会出现其它不同平台或者设备对同个字符的测量值是不一致的,建议针对这种情况烘焙多个 JSON 文件并分别应用即可。
## 预加载 `Char` 图集
建议先阅读 [烘焙文本测量值](#烘焙文本测量值),再阅读本节内容。
如果你对性能进行了分析,那么可以看出,即使我们烘焙了文本测量值,依旧会有将文本绘制到纹理上的性能消耗,那么这剩下 20% 的性能消耗如果无法避免,能控制它消耗的时机也是很有意义的。
所以,社区版提供了 Char 图集的预加载接口,我们可以在特定时机(比如游戏启动时)就加载好会用到的所有字符,避免游戏中的卡顿。
引擎的 Char 图集只会在用到时才会创建,所以我们在调用任何接口前需要初始化:
```ts
cc.Label.LetterAtlases.init();
```
由于引擎并不会持有每个字符信息的缓存,所以需要通过社区版新增的开启缓存接口来保留字符信息:
```ts
cc.Label._shareAtlas.enableLetterCache = true;
```
和烘焙文本测量值一样,我们只推荐在开发测试版本收集缓存,因为保留缓存和收集操作都会有性能消耗。
以下两个接口可以获取与应用字符缓存:
```ts
// 获取
const data = cc.Label._shareAtlas.getLetterCache();
// 应用
cc.Label._shareAtlas.applyLetterCache(data);
```
需要注意的是,我们只是将渲染过的字符信息缓存下来,而不是纹理本身,然后在特定时机调用接口来批量渲染字符到纹理上。
并且,这里的缓存信息已经包括了每个字符的测量值,所以如果你的项目只使用 `CHAR` 模式的 Label 组件,那么你不用再单独烘焙文本测量值。
## 实际应用
你可以参考以下代码,在游戏开发测试版本中将缓存数据上传至一个后端服务器。
```ts
// 上报 Label 烘焙信息
if (!CC_EDITOR && game.TEST) {
cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, () => {
// 启用缓存
cc.Label._shareAtlas.enableLetterCache = true;
setInterval(() => {
const unCaches = {};
for (const key in cc.textUtils.measureCache.datas) {
if (!(key in _measureCache)) {
unCaches[key] = cc.textUtils.measureCache.datas[key].value;
_measureCache[key] = unCaches[key];
console.warn("未收集的测量值:", key, unCaches[key]);
}
}
if (Object.keys(unCaches).length > 0) {
sendToServer(JSON.stringify(unCaches));
}
const unLetters = {};
for (const key in cc.Label._shareAtlas.letterCache) {
if (!(key in _letterCache)) {
unLetters[key] = cc.Label._shareAtlas.letterCache[key];
_letterCache[key] = unLetters[key];
console.warn("未收集的 Char 字符:", key, unLetters[key]);
}
}
if (Object.keys(unLetters).length > 0) {
sendToServer(JSON.stringify(unLetters));
}
}, 10000);
});
}
```
然后在每个版本上线前,从后端下载缓存数据到项目中,并用代码读取并在游戏开始前应用:
```ts
cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, () => {
// 提前缓存 Label Canvas
cc.Label._canvasPool.cache(Number.MAX_SAFE_INTEGER);
// 提高文本测量值缓存容量
cc.textUtils.measureCache.limit = Number.MAX_SAFE_INTEGER;
cc.textUtils.applyMeasureCache(_measureCache);
// 烘焙 CHAR 字符
cc.Label.LetterAtlases.init();
cc.Label._shareAtlas.applyLetterCache(_letterCache);
});
```

View File

@@ -0,0 +1,97 @@
---
sidebar_position: 1
description: "详细了解该缓存模式重构后的所有新特性。"
---
# Char 缓存模式
在 [提升游戏性能](../../best-practices/performance-guide) 中,我们提到了社区版使 Bitmap 与 Char 缓存模式都支持了废弃字符空间复用的特性。
除此之外Char 缓存模式还提供了一些可调整的设置。
## 在场景切换时清空所有字符图集
控制在场景切换时是否会清空所有的字符图集,考虑到旧项目兼容,默认为开启状态。
```js
cc.sp.charAtlasAutoResetBeforeSceneLoad = false;
```
在引擎原来的设计中,该机制不可被关闭,现在推荐关闭该机制。
## 字符图集的数量与内置材质
现在在内部最多会创建 8 张字符图集与多纹理材质的最大纹理插槽数一致如果合理使用缓存模式8 张应该对所有项目都是足够的。
Char 缓存模式会在内部维护一个使用内置多纹理 Effect 着色器的材质Label 在渲染时会先判断当前所使用的是否为多纹理材质,是的话则判断是否能在材质纹理插槽中找到字符图集纹理。
若任何判断不满足则会将组件的材质设为内部维护的材质。
如果你有特殊的用途,可以通过以下代码获取:
```js
cc.Label._shareAtlas.material;
```
需注意 Char 字符图集是运行时才创建的,所以现在**暂时无法在 Char 缓存模式下设置组件的自定义材质**。
要解决这个问题,你可以通过代码创建含有当前所有字符图集纹理的自定义材质来解决这个问题,但内部会不断创建新的字符图集(直到 8 张),所以需注意同步更新这个自定义材质,建议有自定义材质需求时使用 Bitmap 缓存模式。
## 与动态图集合批的注意事项
多纹理材质只有 8 个纹理插槽,默认情况下字符图集自动多纹理合批的数量为 1也就是说会将第 1 张字符图集纹理放入材质。
**你可以自己调整这个分配值,但请注意调整的时机。**
下面举一个例子,假设有一个脚本 `example.ts` 的部分内容是:
```js
class Example extends cc.Component {
onLoad() {
// 1
}
}
// 2
```
一般情况下,代码位置 2 是当用户脚本被加载时就会被执行,而代码位置 1 可能需要等到引擎首场景加载后的某个时间执行。
社区版会自动调整动态图集的最大数量,这个调整的时机是在代码位置 2 之后的,所以比如你的项目对 Char 缓存模式使用量比较大时,想尝试将动态图集最大数量调整为 6自动合批的字符图集数量调整为 2那么你只需要在代码位置 2 修改字符图集自动多纹理合批的数量:
```js
cc.sp.charAtlasAutoBatchCount = 2;
```
之后社区版会自动将动态图集的最大数量调整为 `8 - 2`,即 6。
这个自动调整的时机并不意味着你在代码位置 2 修改动态图集的最大数量是无效的,因为一开始动态图集的最大数量为 `-1`,你打印一下可以看到
```js
console.log(cc.dynamicAtlasManager.maxAtlasCount); // -1
```
如果你在代码位置 2 修改了动态图集的最大数量,社区版就不会调整该值了。
```js
cc.dynamicAtlasManager.maxAtlasCount = 5;
```
这时候动态图集的最大数量是 5字符图集自动多纹理合批的数量依旧默认为 1多纹理材质会有 2 个空纹理插槽。
如果这两个数量加起来超过 8 就会使用更多的材质进行渲染,这会导致项目的 Draw Call 数量升高,建议保持加起来的数量不超过 8 张,能保持 1 Draw Call。
```js
cc.dynamicAtlasManager.maxAtlasCount = 13;
cc.sp.charAtlasAutoBatchCount = 3;
```
比如上面这个设置,这会使得引擎需要用 2 个材质进行渲染,但是可用的动态图集扩充到了 13 张Char 能自动合批的图集数量扩充到了 3 张,对于某些项目来说可能并不是一件坏事。
社区版使用这个 “7 + 1” 的默认值有以下几点原因:
- 引擎原本就只有 1 张 Char 字符图集
- 大多数项目使用 1 张 Char 字符图集是足够的

View File

@@ -0,0 +1,37 @@
---
sidebar_position: 2
description: "一行代码开启高清文本渲染。"
---
# 高 DPI 支持
可阅读 [提升游戏质量](../../best-practices/quality-guide) 了解基本的使用方法。
## 调整全局开关
使用下面的代码控制所有组件是否默认开启高 DPI 支持:
```js
cc.sp.enableLabelRetina = false;
```
## 调整渲染缩放比例
使用下面的代码调整内部渲染的缩放倍数:
```js
cc.sp.labelRetinaScale = 2;
```
## 控制单个组件开关
![reinasettings](./assets/reina-settings.png)
除了在编辑器调整,也可以通过代码控制:
```js
// cc.RenderComponent.EnableType
// GLOBAL: 全局默认值
// ENABLE: 开启
// DISABLE: 关闭
label.enableRetina = cc.RenderComponent.EnableType.ENABLE;
```

View File

@@ -0,0 +1,8 @@
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
# 文本渲染
文本渲染一般是游戏性能优化需要重点关注的地方,并且其显示效果也非常重要,所以社区版提供了以下新特性:
<DocCardList items={useCurrentSidebarCategory().items}/>

View File

@@ -0,0 +1,9 @@
---
sidebar_position: 3
description: "就像在其它组件里一样使用自定义材质。"
---
# RichText 自定义材质
![richtextsettings](./assets/richtext-settings.png)
像往常一样使用即可。

View File

@@ -0,0 +1,9 @@
{
"label": "TiledMap",
"position": 5,
"collapsed": true,
"link": {
"type": "doc",
"id": "tiledmap-intro"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@@ -0,0 +1,37 @@
---
sidebar_position: 2
description: "像其它渲染组件一样在 Spine 组件上使用动态合图。"
---
# 复用 Culling 数据
一个 TiledMap 可能会有很多 TiledLayer如果开启了 Culling那这些 Layer 都需要单独计算 Culling 数据,社区版新增了在满足一定条件的情况下可以复用 Culling 数据的特性,以减少 CPU 的性能消耗。
## 大致原理
通过为 TiledLayer 指定另一个 TiledLayer来复用指定的 TiledLayer 的 Culling 数据。
## 前提条件
- 两者的世界矩阵(也是大小、位置、缩放等属性)一致
- 两者所用的 Camera 一致
- 两者的 TiledLayer 类型、属性一致
- 确保被复用的 Layer 会更早进行渲染(否则 Culling 数据同步可能会延迟一帧)
:::tip 注意
复用可以是不同 TiledMap 的 TiledLayer但一定要满足上面几个条件否则 Culling 结果可能不正确。
:::
## 使用方式
假设项目中有 TiledMap 由 5 个 TiledLayer 组合成一张地图。
![](./assets/tiledmap-nodetree.png)
这种情况如果需要复用 Culling 数据只需要多选后四个节点,然后将第一个节点拖入这四个节点的 `Culling Layer` 属性中即可。
![](./assets/tiledmap-inspector.png)
这样就使后四个 Layer 复用第一个 Layer 的 Culling 数据了。

View File

@@ -0,0 +1,12 @@
import DocCardList from '@theme/DocCardList';
import {useCurrentSidebarCategory} from '@docusaurus/theme-common';
# TiledMap
社区版对 TiledMap 进行了优化:
- 复用 TiledLayer 的 Culling 数据
你可以阅读下面的文档了解详情:
<DocCardList items={useCurrentSidebarCategory().items}/>