mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-11 15:47:06 +08:00
docs: UI 指南文档
This commit is contained in:
parent
e282d3f111
commit
5e1ea25d6d
@ -5,12 +5,17 @@ export default defineConfig({
|
||||
title: 'HTML5 魔塔样板 V2.B',
|
||||
description: 'HTML5 魔塔样板 V2.B 帮助文档',
|
||||
base: '/_docs/',
|
||||
markdown: {
|
||||
math: true
|
||||
},
|
||||
themeConfig: {
|
||||
// https://vitepress.dev/reference/default-theme-config
|
||||
outline: [2, 3],
|
||||
nav: [
|
||||
{ text: '主页', link: '/' },
|
||||
{ text: '指南', link: '/guide/diff' },
|
||||
{ text: 'API', link: '/api/' }
|
||||
{ text: 'API', link: '/api/' },
|
||||
{ text: '错误代码', link: '/logger/' }
|
||||
],
|
||||
sidebar: {
|
||||
'/guide/': [
|
||||
@ -18,7 +23,12 @@ export default defineConfig({
|
||||
text: '深度指南',
|
||||
items: [
|
||||
{ text: '差异说明', link: '/guide/diff' },
|
||||
{ text: '系统说明', link: '/guide/system' }
|
||||
{ text: '系统说明', link: '/guide/system' },
|
||||
{ text: 'UI 编写', link: '/guide/ui' },
|
||||
{ text: 'UI 优化', link: '/guide/ui-perf' },
|
||||
{ text: 'UI 系统', link: '/guide/ui-system' },
|
||||
{ text: 'UI 元素', link: '/guide/ui-elements' },
|
||||
{ text: 'UI 常见问题', link: '/guide/ui-faq' }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
@ -6,7 +6,7 @@ lang: zh-CN
|
||||
|
||||
本文档暂时只会对新样板新增内容进行说明,其余请查看[旧样板文档](https://h5mota.com/games/template/_docs/#/)。
|
||||
|
||||
本指南建立在你已经大致了解 js 的基础语法的基础上。如果还不了解可以尝试对指南内容进行模仿,或者查看[人类塔解析](https://h5mota.com/bbs/thread/?tid=1018&p=1)
|
||||
本指南建立在你已经大致了解 js 的基础语法的基础上。如果还不了解 js 语法可以尝试对指南内容进行模仿,或者查看[人类塔解析](https://h5mota.com/bbs/thread/?tid=1018&p=1)
|
||||
|
||||
如果你有能力直接使用源码版样板进行创作,也可以直接 fork 或 clone 2.B 样板[存储库](https://github.com/unanmed/HumanBreak/tree/template-v2.B)。2.B 样板使用了 vite 作为了构建工具,同时使用了 ts 等作为了开发语言。
|
||||
|
||||
@ -22,7 +22,7 @@ lang: zh-CN
|
||||
- 使用全新的 UI 编写方式,速度快,效率高
|
||||
- 模块化,可以使用 ES6 模块化语法
|
||||
- 移除插件系统,可以自定义代码目录结构,更加自由
|
||||
- 优化渲染端(client 端)与数据端(data 端)的通讯,渲染段现在可以直接引用数据端,不过数据端还不能直接引用渲染端
|
||||
- 优化渲染端(client 端)与数据端(data 端)的通讯,渲染端现在可以直接引用数据端,不过数据端还不能直接引用渲染端
|
||||
|
||||
## 差异内容
|
||||
|
||||
|
BIN
docs/guide/img/image.png
Normal file
BIN
docs/guide/img/image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 22 KiB |
3
docs/guide/img/mermaid-diagram-2025-03-12-210212.svg
Normal file
3
docs/guide/img/mermaid-diagram-2025-03-12-210212.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 35 KiB |
@ -26,7 +26,7 @@ lang: zh-CN
|
||||
- [@motajs/system](../api/motajs-system)
|
||||
- [@motajs/system-action](../api/motajs-system-action)
|
||||
- [@motajs/system-ui](../api/motajs-system-ui)
|
||||
- [@motajs/types](../api/types)
|
||||
- [@motajs/types](../api/motajs-types)
|
||||
- [@user/client-modules](../api/user-client-modules)
|
||||
- [@user/data-base](../api/user-data-base)
|
||||
- [@user/data-fallback](../api/user-data-fallback)
|
||||
@ -97,13 +97,11 @@ hook.on('afterBattle', enemy => {
|
||||
|
||||
1. 加载渲染端入口
|
||||
2. 加载数据端入口
|
||||
3. 并行初始化数据端,写入 `Mota` 全局变量
|
||||
4. 初始化完毕后执行 `loading.emit('dataRegistered')` 钩子
|
||||
5. 并行初始化渲染端
|
||||
6. 初始化完毕后执行 `loading.emit('clientRegistered')` 钩子
|
||||
7. 二者都初始化完毕后执行 `loading.emit('registered')` 钩子
|
||||
8. 执行数据端各个模块的初始化函数
|
||||
9. 执行渲染段各个模块的初始化函数
|
||||
3. 并行初始化数据端与渲染端,在数据端写入 `Mota` 全局变量
|
||||
4. 数据端初始化完毕后执行 `loading.emit('dataRegistered')` 钩子,渲染端初始化完毕后执行 `loading.emit('clientRegistered')` 钩子
|
||||
5. 二者都初始化完毕后执行 `loading.emit('registered')` 钩子
|
||||
6. 执行数据端各个模块的初始化函数
|
||||
7. 执行渲染端各个模块的初始化函数
|
||||
|
||||
4. 如果是录像验证中:
|
||||
|
||||
@ -121,6 +119,10 @@ hook.on('afterBattle', enemy => {
|
||||
11. 资源加载完毕后执行 `loading.emit('loaded')` 钩子
|
||||
12. 进入标题界面
|
||||
|
||||
使用流程图表示如下:
|
||||
|
||||

|
||||
|
||||
## 函数重写
|
||||
|
||||
在 2.B 模式下,如果想改 `libs` 的内容,如果直接在里面改会很麻烦,而且两端通讯也不方便,因此我们建议在 `package-user` 中对函数重写,这样的话就可以使用模块化语法,更加方便。同时,2.B 也提供了函数重写接口,他在 `@motajs/legacy-common` 模块中,我们可以这么使用它:
|
||||
@ -146,7 +148,7 @@ export function patchMyFunctions() {
|
||||
}
|
||||
```
|
||||
|
||||
然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中调用 `patchMyFunctions`,这样我们的函数重写就完成了。
|
||||
然后,我们找到 `client-modules` 文件夹下的 `index.ts` 文件,然后在 `create` 函数中引入并调用 `patchMyFunctions`,这样我们的函数重写就完成了。**注意**,如果两个重写冲突,会在控制台弹出警告,并使用最后一次重写的内容。
|
||||
|
||||
::: warning
|
||||
**注意**,在渲染端重写的函数在录像验证中将无效,因为录像验证不会执行任何渲染端内容!
|
||||
@ -155,3 +157,32 @@ export function patchMyFunctions() {
|
||||
## 目录结构
|
||||
|
||||
我们建议每个文件夹中都有一个 `index.ts` 文件,将本文件夹中的其他文件经由此文件导出,这样方便管理,同时结构清晰。可以参考 `packages-user/client-modules` 文件夹中是如何做的。
|
||||
|
||||
## ES6 模块化语法
|
||||
|
||||
我们推荐使用 ES6 模块化语法来编写代码,这会大大提高开发效率。下面来简单说明一下模块化语法的用法,首先是引入其他模块:
|
||||
|
||||
```ts
|
||||
import { Patch } from '@motajs/legacy-common'; // 从样板库中引入接口
|
||||
// 引入本地文件,注意不要填写后缀名,只可以在同一个 packages-user 子文件夹下使用
|
||||
// 不可以跨文件夹使用,例如 packages-user/client-modules 就不能直接引用 packages-user/data-base 文件夹
|
||||
// 需要使用 import { ... } from '@user/data-base'
|
||||
import { patchMyFunctions } from './override';
|
||||
```
|
||||
|
||||
然后是从当前模块导出内容:
|
||||
|
||||
```ts
|
||||
// 导出函数
|
||||
export function myFunc() { ... }
|
||||
// 导出变量/常量
|
||||
export const num = 100;
|
||||
// 导出类
|
||||
export class MyClass { ... }
|
||||
// 从另一个模块中导出全部内容,即将另一个模块的内容转发为当前模块
|
||||
export * from './xxx';
|
||||
```
|
||||
|
||||
更多模块化语法内容请查看[这个文档](https://h5mota.com/bbs/thread/?tid=1018&p=3#p33)
|
||||
|
||||
与 TypeScript 相关语法请查看[这个文档](https://h5mota.com/bbs/thread/?tid=1018&p=3#p41)
|
||||
|
754
docs/guide/ui-elements.md
Normal file
754
docs/guide/ui-elements.md
Normal file
@ -0,0 +1,754 @@
|
||||
# UI 元素
|
||||
|
||||
本节将会讲解 UI 系统中常用的渲染元素以及基础使用。
|
||||
|
||||
## 通用属性
|
||||
|
||||
UI 元素包含很多通用属性,我们先来介绍这些属性,它们可以用在任何渲染元素和 UI 组件中。
|
||||
|
||||
### 定位属性
|
||||
|
||||
元素包含若干定位属性,其中最常用的是 `loc` 属性,我们也推荐全部使用这个属性来修改元素定位。其类型声明如下:
|
||||
|
||||
```ts
|
||||
type ElementLocator = [
|
||||
x?: number,
|
||||
y?: number,
|
||||
w?: number,
|
||||
h?: number,
|
||||
ax?: number,
|
||||
ay?: number
|
||||
];
|
||||
```
|
||||
|
||||
这些属性两两组成一组(`x, y` 一组,`w, h` 一组,`ax, ay` 一组),每组可选填,也就是说 `x` 和 `y` 要么都填,要么都不填,以此类推。
|
||||
|
||||
- `x` `y`: 元素的位置,描述了在没有旋转时元素的锚点位置,例如 `[32, 32]` 就表示这个元素锚点在 `32, 32` 的位置,默认锚点在元素左上角,也就表示元素左上角在 `32, 32`。
|
||||
- `w` `h`: 元素的长宽,描述了在没有缩放时元素的矩形长宽,默认是没有放缩的。
|
||||
- `ax` `ay`: 元素的锚点位置,描述了元素参考点的位置,所有位置变换等将以此点作为参考点。0 表示元素最左侧或最上侧,1 表示最右侧或最下侧,可以填不在 0-1 范围内的值,例如 `[-1, 1]` 表示锚点横坐标在元素左侧一个元素宽度的位置,纵坐标在元素下边缘的位置。
|
||||
|
||||

|
||||
|
||||
示例如下:
|
||||
|
||||
```tsx
|
||||
// 元素相对于 32, 32 位置居中(锚点在元素正中间),宽高为 64
|
||||
<sprite loc={[32, 32, 64, 64, 0.5, 0.5]} />
|
||||
```
|
||||
|
||||
除了 `loc` 属性之外,还可以通过设置 `anc` 属性来修改锚点位置,示例如下:
|
||||
|
||||
```tsx
|
||||
// 设置锚点,效果为靠右对齐,上下居中对齐
|
||||
<sprite anc={[1, 0.5]} />
|
||||
```
|
||||
|
||||
你还可以手动指定 `x` `y` `width` `height` `anchorX` `anchorY` 属性,但是这种方式比较啰嗦,并不建议使用:
|
||||
|
||||
```tsx
|
||||
<sprite x={32} y={32} width={64} height={64} anchorX={0.5} anchorY={0.5} />
|
||||
```
|
||||
|
||||
最后说明一下元素的 `type` 属性,此属性描述了元素的定位模式,默认为 `static` 常规定位,此定位模式下元素位置会按照上述内容更改,而在 `absolute` 模式下,不论怎么修改定位属性,它都会保持在左上角的位置,可能会在一些特殊场景下使用(极度不建议使用此属性,很可能在 2.B.1 版本就会将其删除)。
|
||||
|
||||
### 纵深属性
|
||||
|
||||
可以通过 `zIndex` 属性来调整一个元素的纵深。纵深描述了元素之间的重叠关系,纵深高的会处在纵深低的元素上方,同时也会阻碍交互事件向纵深低的元素传播。必要的时候,需要通过设置纵深属性来调整层级关系。未设置时,后面的元素会处在前面的元素之上。
|
||||
|
||||
```tsx
|
||||
// 这个元素会处在上层
|
||||
<sprite zIndex={10} />
|
||||
// 这个元素会处在下层
|
||||
<sprite zIndex={5} />
|
||||
```
|
||||
|
||||
### 效果属性
|
||||
|
||||
效果属性包含 `filter` `composite` 及 `alpha` 三个属性。
|
||||
|
||||
`filter` 表示此元素的滤镜,参考 [CanvasRenderingContext2D.filter](https://developer.mozilla.org/zh-CN/docs/Web/CSS/filter),可以填写内置函数或 svg 滤镜。默认不包含任何滤镜。示例如下:
|
||||
|
||||
```tsx
|
||||
// 亮度变为 150%,对比度变为 120%
|
||||
<sprite filter="brightness(150%) contrast(120%)" />
|
||||
```
|
||||
|
||||
`composite` 属性描述了当前元素与在此之前渲染的元素之间的混合模式,参考 [CanvasRenderingContext2D.globalCompositeOperation](https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation),可以填写 26 个值。默认使用简单 `alpha` 混合,即 `source-over`。例如:
|
||||
|
||||
```tsx
|
||||
// 使用加算方式叠加,两个颜色的 RGB 值分别相加得到最终结果
|
||||
<sprite composite="lighter" />
|
||||
```
|
||||
|
||||
`alpha` 属性描述了此元素的不透明度,1 表示完全不透明,0 表示完全透明。在叠加时,所有颜色都会乘以此不透明度后叠加。默认值是完全不透明,即 1。但需要注意的是,虽然 1 表示完全不透明,但是如果画布内容本身包含透明内容(例如一个半透明矩形),即使是 1 也会表现为透明,因为叠加时会乘以 1,不透明度不变。示例如下:
|
||||
|
||||
```tsx
|
||||
// 一个半透明元素
|
||||
<sprite alpha={0.5} />
|
||||
```
|
||||
|
||||
### 缓存属性
|
||||
|
||||
可以通过 `cache` 和 `nocache` 属性来指定这个元素的缓存行为,其中 `nocache` 表示禁用此元素的缓存机制,优先级最高,设置后必然不使用缓存。`cache` 表示启用此元素的缓存行为,常用于一些默认不启用缓存的元素,优先级低于 `cache`。这两个元素都不能动态设置,也就是说不能使用响应式来修改其值。示例如下:
|
||||
|
||||
```tsx
|
||||
// 内部渲染内容比较简单,不需要启用缓存
|
||||
<container nocache>
|
||||
<text />
|
||||
</container>
|
||||
// 路径较为复杂,因此启用 g-path 的缓存行为
|
||||
<g-path path={veryComplexPath} cache />
|
||||
```
|
||||
|
||||
### 元素溢出行为
|
||||
|
||||
溢出行为是指,当其子元素超出父元素的大小时,执行的行为。例如,假如父元素大小为 `200 * 200`,里面有一个子元素,大小为 `100 * 100`,位于 `(150, 50)` 的位置,这时候子元素的一部分就会超出父元素的范围。
|
||||
|
||||
在本渲染系统中,所有元素的默认溢出行为是裁剪,即不会显示任何溢出内容,注意调整容器的宽高。在 `nocache` 模式下,由于不受到缓存的约束,溢出内容依然会显示,不过不建议利用此特性来编写 UI,因为这种行为可能会在后续的更新中修改。
|
||||
|
||||
### 隐藏元素
|
||||
|
||||
可以使用 `hidden` 属性来隐藏元素:
|
||||
|
||||
```tsx
|
||||
const hidden = ref(false);
|
||||
// 一般使用一个响应式变量来控制隐藏行为,因为设置成常量没有任何意义
|
||||
<sprite hidden={hidden.value} />;
|
||||
```
|
||||
|
||||
### 交互属性
|
||||
|
||||
交互属性包括 `cursor` 和 `noevent`。前者描述了鼠标覆盖在当前元素上时的指针样式,参考 [CSS: cursor](https://developer.mozilla.org/zh-CN/docs/Web/CSS/cursor)。示例如下:
|
||||
|
||||
```tsx
|
||||
// 鼠标放置在该元素上时使用小手样式
|
||||
<sprite cursor="pointer" />
|
||||
```
|
||||
|
||||
`noevent` 表明当前元素将不会触发任何事件,事件将会下穿至纵深更低的元素。示例如下:
|
||||
|
||||
```tsx
|
||||
// 设置为 noevent 模式
|
||||
<sprite zIndex={100} noevent />
|
||||
// 这样的话这个 onClick 就可以正常触发了
|
||||
<sprite zIndex={10} onClick={click} />
|
||||
```
|
||||
|
||||
### 高清与抗锯齿
|
||||
|
||||
包含 `hd` `anti` `noanti` 三个属性,`hd` 表示是否启用高清,大部分元素是默认启用的,除了几个像素风为主的元素(地图渲染等);`anti` 表示手动启用画布的抗锯齿行为,一般用于默认不启用抗锯齿的元素;`noanti` 表示手动关闭元素的抗锯齿行为,优先级高于 `anti`,一般用于像素风图片展示、图标显示等,同时也有助于提高渲染性能。
|
||||
|
||||
```tsx
|
||||
// 关闭高清
|
||||
<sprite hd={false} />
|
||||
// 关闭抗锯齿
|
||||
<sprite noanti />
|
||||
// 启用地图渲染的抗锯齿
|
||||
<layer anti />
|
||||
```
|
||||
|
||||
### 元素变换属性
|
||||
|
||||
可以通过调整 `transform` 属性来修改元素的线性变换,包括平移、旋转、缩放。如果是简易的变换,可以使用 `rotate` `scale` 属性来修改旋转、缩放,使用 `loc` 来修改位置:
|
||||
|
||||
```tsx
|
||||
// 旋转 90 度,横向放缩为 1.5 倍,纵向不放缩
|
||||
<sprite rotate={Math.PI / 2} scale={[1.5, 1]} loc={[32, 32]} />
|
||||
```
|
||||
|
||||
我们没有设置锚点属性,那么需要注意旋转后,`loc` 属性所标明的位置将不再是左上角的那个点,因为旋转后,原本在左上角的点将会变成右上角的点。旋转时,顺时针为正,逆时针为负。
|
||||
|
||||
使用上面这种方式时,没有办法指定变换的顺序。一般情况下,变换的顺序将会影响结果,例如先旋转,再放缩,和先放缩,再旋转,其结果是不同的。下面我们来讲解一下图形学中的 2D 矩阵变换的基本概念,以及如何使用 `transform` 属性。
|
||||
|
||||
### Transform 矩阵变换
|
||||
|
||||
大部分情况下用不到此属性,此属性理解难度较大,如果不是必须使用此属性,可以不看此小节。以下矩阵变换内容由 `DeepSeek R1` 模型生成,并稍作修改。
|
||||
|
||||
#### 为什么需要变换矩阵?
|
||||
|
||||
在 2D 图形学中,变换矩阵(3x3)可以统一表示以下基本变换操作:
|
||||
|
||||
- 平移(Translation)
|
||||
- 旋转(Rotation)
|
||||
- 缩放(Scale)
|
||||
- 错切(Skew)
|
||||
|
||||
通过矩阵乘法可以将多个变换组合为单个矩阵运算,其通用数学表示为(列主序):
|
||||
|
||||
$Transform=\begin{bmatrix} a & b & 0 \\ c & d & 0 \\ e & f & 1 \end{bmatrix}$
|
||||
|
||||
其中:
|
||||
|
||||
- `a,d` 控制缩放和旋转
|
||||
- `b,c` 控制错切
|
||||
- `e,f` 控制平移
|
||||
|
||||
#### 变换组合原理
|
||||
|
||||
矩阵乘法具有结合性但不具有交换性,变换顺序会影响最终效果,矩阵按从右到左的顺序应用变换:
|
||||
|
||||
最终矩阵 = 平移矩阵 × 旋转矩阵 × 缩放矩阵 × 原始坐标
|
||||
|
||||
#### Transform 类核心功能
|
||||
|
||||
首先创建其实例:
|
||||
|
||||
```ts
|
||||
import { Transform } from '@motajs/render';
|
||||
|
||||
const trans = new Transform();
|
||||
```
|
||||
|
||||
之后可以链式调用来修改矩阵:
|
||||
|
||||
```ts
|
||||
// 链式调用示例
|
||||
trans
|
||||
.setTranslate(100, 50)
|
||||
.rotate(Math.PI / 4)
|
||||
.scale(2, 1.5);
|
||||
```
|
||||
|
||||
方法对比
|
||||
|
||||
| 方法类型 | 特点 | 函数 |
|
||||
| -------- | -------------------------- | ---------------------------------------------------- |
|
||||
| 叠加变换 | 在现有变换基础上叠加新变换 | `translate` `rotate` `scale` `transform` |
|
||||
| 直接设置 | 覆盖当前变换参数 | `setTranslate` `setRotate` `setScale` `setTransform` |
|
||||
|
||||
#### 关键方法详解
|
||||
|
||||
平移变换:
|
||||
|
||||
```ts
|
||||
// 相对移动(叠加)
|
||||
trans.translate(20, -10);
|
||||
// 绝对定位(覆盖)
|
||||
trans.setTranslate(200, 150);
|
||||
```
|
||||
|
||||
旋转变换:
|
||||
|
||||
```ts
|
||||
// 叠加旋转 45 度
|
||||
trans.rotate(Math.PI / 4);
|
||||
// 设置绝对旋转角度
|
||||
trans.setRotate(Math.PI / 2);
|
||||
```
|
||||
|
||||
缩放变换:
|
||||
|
||||
```ts
|
||||
// X 轴放大 2 倍,Y 轴不变(叠加)
|
||||
trans.scale(2, 1);
|
||||
// 设置绝对缩放比例
|
||||
trans.setScale(0.5, 0.8);
|
||||
```
|
||||
|
||||
#### 高级功能
|
||||
|
||||
矩阵操作:
|
||||
|
||||
```ts
|
||||
// 手动设置变换矩阵
|
||||
trans.setTransform(
|
||||
1,
|
||||
0, // 缩放部分
|
||||
0,
|
||||
1, // 旋转部分
|
||||
100,
|
||||
50 // 平移部分
|
||||
);
|
||||
// 矩阵相乘(组合变换)
|
||||
const combined = trans.multiply(otherTransform);
|
||||
```
|
||||
|
||||
坐标变换:
|
||||
|
||||
```ts
|
||||
// 将局部坐标转换为世界坐标,即计算一个坐标经过此变换矩阵计算后的位置
|
||||
const worldPos = trans.transformed(10, 20);
|
||||
// 将世界坐标转换回局部坐标,即计算一个坐标经过此变换矩阵逆转换后的位置
|
||||
const localPos = trans.untransformed(150, 80);
|
||||
```
|
||||
|
||||
#### 性能优化技巧
|
||||
|
||||
使用自动更新机制:
|
||||
|
||||
```ts
|
||||
import { ITransformUpdatable } from '@motajs/render';
|
||||
|
||||
// 绑定可更新对象
|
||||
class MyElement implements ITransformUpdatable {
|
||||
updateTransform() {
|
||||
console.log('变换已更新!');
|
||||
}
|
||||
}
|
||||
|
||||
const element = new MyElement();
|
||||
trans.bind(element); // 变换修改时自动触发 updateTransform
|
||||
```
|
||||
|
||||
#### 最佳实践
|
||||
|
||||
推荐变换执行顺序:
|
||||
|
||||
1. 缩放(Scale)
|
||||
2. 旋转(Rotate)
|
||||
3. 平移(Translate)
|
||||
|
||||
```ts
|
||||
// 正确顺序示例
|
||||
trans
|
||||
.setScale(2)
|
||||
.rotate(Math.PI / 3)
|
||||
.setTranslate(100, 50);
|
||||
```
|
||||
|
||||
组合复杂变换:
|
||||
|
||||
```ts
|
||||
// 创建子变换
|
||||
const childTrans = trans
|
||||
.clone() // 从 trans 复制一个相同的变换出来,以防止修改原变换
|
||||
.rotate(-Math.PI / 6)
|
||||
.translate(30, 0);
|
||||
|
||||
// 应用组合变换
|
||||
const finalTrans = trans.multiply(childTrans);
|
||||
```
|
||||
|
||||
#### 应用到元素
|
||||
|
||||
赋值给 `transform` 属性即可
|
||||
|
||||
```tsx
|
||||
<sprite transform={finalTrans} />
|
||||
```
|
||||
|
||||
#### 常见问题排查
|
||||
|
||||
1. 变换不生效?
|
||||
|
||||
- 验证绑定的对象是否实现 `updateTransform`
|
||||
- 检查有没有把 `trans` 对象赋值给元素的 `transform` 属性
|
||||
|
||||
2. 性能问题
|
||||
|
||||
- 避免高频调用 `setTransform`
|
||||
- 优先使用叠加方法代替矩阵直接操作
|
||||
- 利用 `clone()` 复用已有变换
|
||||
|
||||
## `sprite` 标签
|
||||
|
||||
`sprite` 标签是一个允许你自定义渲染内容的标签,它新增了一个属性 `render` 属性,允许你传入一个函数来执行自定义渲染。函数定义如下:
|
||||
|
||||
```ts
|
||||
type RenderFn = (canvas: MotaOffscreenCanvas2D, transform: Transform) => void;
|
||||
```
|
||||
|
||||
- `canvas`: 要渲染至的画布,一般直接将内容渲染至这个画布上
|
||||
- `transform`: 当前元素的变换矩阵,相对于父元素,不常用
|
||||
|
||||
多数情况下,我们只会使用到第一个参数,`MotaOffscreenCanvas2D` 接口请参考 [API 文档](../api/motajs-render-core)。下面是一个典型案例:
|
||||
|
||||
```tsx
|
||||
const render = (canvas: MotaOffscreenCanvas2D) => {
|
||||
const { ctx, width, height } = canvas; // 获取画布上下文以及长宽
|
||||
ctx.fillStyle = '#d84'; // 设置填充样式
|
||||
ctx.fillRect(0, 0, width, height); // 绘制一个矩形
|
||||
};
|
||||
```
|
||||
|
||||
`sprite` 元素的应用场景并不算多,因为样板内置的各种元素已经足够丰富,此元素一般只会在一些特殊情况下或性能敏感情况下使用。
|
||||
|
||||
## `container` 标签
|
||||
|
||||
`container` 表示一个容器,它可以将一系列元素作为子元素并渲染。它并没有新增任何属性。如果你想渲染子元素,请务必使用此元素包裹,除此之外的大部分元素是不能渲染子元素的。
|
||||
|
||||
## `container-custom` 标签
|
||||
|
||||
`container-custom` 是另一种容器,它允许你自定义对子元素的渲染方案,对于特殊场景下有一定的作用,例如样板自带的 `Scroll` 组件就使用此标签实现。它新增了一个 `render` 参数,定义如下:
|
||||
|
||||
```ts
|
||||
type CustomContainerRenderFn = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
transform: Transform
|
||||
) => void;
|
||||
```
|
||||
|
||||
- `canvas`: 要渲染至的画布,一般直接将内容渲染至这个画布上
|
||||
- `children`: 要渲染的子元素,按 `zIndex` 升序排列
|
||||
- `transform`: 当前元素的变换矩阵,相对于父元素,不常用
|
||||
|
||||
典型案例如下:
|
||||
|
||||
```tsx
|
||||
const render = (
|
||||
canvas: MotaOffscreenCanvas2D,
|
||||
children: RenderItem[],
|
||||
transform: Transform
|
||||
) => {
|
||||
// 顺序遍历子元素,保证纵深关系正确
|
||||
children.forEach(v => {
|
||||
if (v.hidden) return; // 如果元素隐藏,则不渲染
|
||||
// 调用子元素的渲染函数,传入 canvas 和 transform 作为参数
|
||||
v.renderContent(canvas, transform);
|
||||
});
|
||||
};
|
||||
|
||||
<container-custom render={render} />;
|
||||
```
|
||||
|
||||
## `text` 标签
|
||||
|
||||
`text` 标签用于显示文字,它会自动计算文字的宽高并设置为元素宽高,因此不要手动指定宽高,否则可能会引起位置错误。它新增了这些属性:
|
||||
|
||||
```ts
|
||||
interface TextProps extends BaseProps {
|
||||
/** 要渲染的文字 */
|
||||
text?: string;
|
||||
/** 文字的填充样式 */
|
||||
fillStyle?: CanvasStyle;
|
||||
/** 文字的描边样式 */
|
||||
strokeStyle?: CanvasStyle;
|
||||
/** 文字的字体 */
|
||||
font?: Font;
|
||||
/** 文字的描边粗细 */
|
||||
strokeWidth?: number;
|
||||
}
|
||||
```
|
||||
|
||||
典型案例如下:
|
||||
|
||||
```tsx
|
||||
import { Font } from '@motajs/render';
|
||||
|
||||
<text
|
||||
// 文字内容
|
||||
text="这是一段文字"
|
||||
// 文字定位,不要填写宽高,如果需要填写锚点,可以使用 anc 属性或宽高填 void 0
|
||||
loc={[32, 32]}
|
||||
// 填充样式,纯白色
|
||||
fillStyle="#fff"
|
||||
// 描边样式,红色
|
||||
strokeStyle="#d54"
|
||||
// 字体,使用大小为 24px 的默认字体
|
||||
font={new Font(24)}
|
||||
// 描边宽度 3px,默认为 2px
|
||||
strokeWidth={3}
|
||||
/>;
|
||||
```
|
||||
|
||||
## `image` 标签
|
||||
|
||||
`image` 标签允许你显示一张图片,包含一个 `image` 属性,传入图片对象(注意不是注册图片名称)。用例如下:
|
||||
|
||||
```tsx
|
||||
// 获取注册的图片
|
||||
const img = core.material.images.images['myImage.png'];
|
||||
// 显示图片
|
||||
<image image={img} />;
|
||||
```
|
||||
|
||||
## `icon` 标签
|
||||
|
||||
`icon` 标签用于显示一个图标,可以包含动画帧。它有如下参数:
|
||||
|
||||
```ts
|
||||
export interface IconProps extends BaseProps {
|
||||
/** 图标 id 或数字 */
|
||||
icon: AllNumbers | AllIds;
|
||||
/** 显示图标的第几帧 */
|
||||
frame?: number;
|
||||
/** 是否开启动画,开启后 frame 参数无效 */
|
||||
animate?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
使用案例如下:
|
||||
|
||||
```tsx
|
||||
// 显示绿史莱姆,开启动画
|
||||
<icon icon="greenSlime" animate />
|
||||
```
|
||||
|
||||
## `winskin` 标签
|
||||
|
||||
`winskin` 标签允许你显示一个 rpg maker 风格的背景图(window skin),它有如下参数:
|
||||
|
||||
```ts
|
||||
export interface WinskinProps extends BaseProps {
|
||||
/** winskin 的图片 id */
|
||||
image: ImageIds;
|
||||
/** 边框大小 */
|
||||
borderSize?: number;
|
||||
}
|
||||
```
|
||||
|
||||
其中边框大小默认为 32,表示上边框和下边框加起来共 32 像素,即四周边框 16 像素宽。用例如下:
|
||||
|
||||
```tsx
|
||||
// 使用 winskin.png 作为图片,四周边框宽度为 24 像素
|
||||
<winskin image="winskin.png" borderSize={48} />
|
||||
```
|
||||
|
||||
## 图形标签
|
||||
|
||||
本小节讲解图形相关的标签,以下内容由 `DeepSeek R1` 模型生成并稍作修改。
|
||||
|
||||
### 通用属性说明
|
||||
|
||||
所有图形元素均支持以下核心属性:
|
||||
|
||||
| 属性分类 | 关键参数 | 说明 |
|
||||
| -------------- | ------------------------- | -------------------------------------------------------- |
|
||||
| **填充与描边** | `fill` `stroke` | 控制是否填充/描边(不同元素默认值不同) |
|
||||
| **样式控制** | `fillStyle` `strokeStyle` | 填充和描边样式,支持颜色/渐变等(如 `'#f00'`) |
|
||||
| **线型设置** | `lineWidth` `lineDash` | 线宽、虚线模式(如 `[5, 3]` 表示 5 像素实线+3 像素间隙) |
|
||||
| **高级控制** | `fillRule` `actionStroke` | 填充规则(非零/奇偶)、是否仅在描边区域响应交互 |
|
||||
|
||||
### 矩形 `<g-rect>`
|
||||
|
||||
矩形的定位直接使用 `loc` 即可,示例如下:
|
||||
|
||||
```tsx
|
||||
// 基础矩形,矩形默认仅填充模式,因此如果需要描边的话需要手动指定 fill 和 stroke 参数
|
||||
// 注意如果仅指定 stroke 参数的话,会变为仅描边形式
|
||||
<g-rect loc={[100, 100, 200, 150]} fill stroke fillStyle="#f0f" lineWidth={2} />
|
||||
```
|
||||
|
||||
### 圆形和扇形 `<g-circle>`
|
||||
|
||||
参数如下:
|
||||
|
||||
```ts
|
||||
interface CirclesProps {
|
||||
radius: number; // 半径
|
||||
start?: number; // 起始弧度(默认0)
|
||||
end?: number; // 结束弧度(默认2π)
|
||||
/**
|
||||
* 圆属性参数,可以填 `[圆心 x 坐标,圆心 y 坐标,半径,起始角度,终止角度]`,是 x, y, radius, start, end 的简写,
|
||||
* 其中半径可选,后两项要么都填,要么都不填
|
||||
*/
|
||||
circle?: CircleParams;
|
||||
}
|
||||
```
|
||||
|
||||
示例如下:
|
||||
|
||||
```tsx
|
||||
// 完整圆形
|
||||
<g-circle circle={[300, 200, 10]} fillStyle="skyblue" />
|
||||
// 扇形(60度到180度)
|
||||
<g-circle circle={[400, 300, 40, Math.PI/3, Math.PI]} />
|
||||
```
|
||||
|
||||
### 直线 `<g-line>`
|
||||
|
||||
核心参数:
|
||||
|
||||
```ts
|
||||
interface LineProps {
|
||||
x1: number; // 起点X
|
||||
y1: number; // 起点Y
|
||||
x2: number; // 终点X
|
||||
y2: number; // 终点Y
|
||||
/** 直线属性简写参数,可以填 `[x1, y1, x2, y2]`,都是必填 */
|
||||
line: [number, number, number, number];
|
||||
}
|
||||
```
|
||||
|
||||
示例如下:
|
||||
|
||||
```tsx
|
||||
// 普通直线
|
||||
<g-line
|
||||
line={[50, 50, 200, 150]}
|
||||
strokeStyle="red"
|
||||
lineDash={[10, 5]} // 虚线样式
|
||||
/>
|
||||
|
||||
// 带箭头的参考线
|
||||
<g-line
|
||||
// 不使用简写形式
|
||||
x1={300} y1={80}
|
||||
x2={450} y2={220}
|
||||
lineCap="round" // 端点圆形
|
||||
lineWidth={4}
|
||||
/>
|
||||
```
|
||||
|
||||
### 三次贝塞尔曲线 `<g-bezier>`
|
||||
|
||||
核心参数:
|
||||
|
||||
```ts
|
||||
interface BezierProps {
|
||||
sx: number; // 起点X
|
||||
sy: number; // 起点Y
|
||||
cp1x: number; // 控制点1X
|
||||
cp1y: number; // 控制点1Y
|
||||
cp2x: number; // 控制点2X(三次贝塞尔)
|
||||
cp2y: number; // 控制点2Y
|
||||
ex: number; // 终点X
|
||||
ey: number; // 终点Y
|
||||
/** 三次贝塞尔曲线参数简写,可以填 `[sx, sy, cp1x, cp1y, cp2x, cp2y, ex, ey]`,都是必填 */
|
||||
curve: BezierParams;
|
||||
}
|
||||
```
|
||||
|
||||
示例如下:
|
||||
|
||||
```tsx
|
||||
// 三次贝塞尔曲线
|
||||
<g-bezier
|
||||
curve={[100, 100, 150, 50, 250, 200, 300, 100]}
|
||||
strokeStyle="purple"
|
||||
lineWidth={3}
|
||||
/>;
|
||||
|
||||
// 动态路径
|
||||
const path = computed(() => [
|
||||
startX.value,
|
||||
startY.value,
|
||||
control1X.value,
|
||||
control1Y.value,
|
||||
control2X.value,
|
||||
control2Y.value,
|
||||
endX.value,
|
||||
endY.value
|
||||
]);
|
||||
<g-bezier curve={path.value} />;
|
||||
```
|
||||
|
||||
### 二次贝塞尔曲线 `<g-quad>`
|
||||
|
||||
核心参数:
|
||||
|
||||
```ts
|
||||
interface BezierProps {
|
||||
sx: number; // 起点X
|
||||
sy: number; // 起点Y
|
||||
cpx: number; // 控制点X
|
||||
cpy: number; // 控制点Y
|
||||
ex: number; // 终点X
|
||||
ey: number; // 终点Y
|
||||
/** 二次贝塞尔曲线参数,可以填 `[sx, sy, cpx, cpy, ex, ey]`,都是必填 */
|
||||
curve: QuadParams;
|
||||
}
|
||||
```
|
||||
|
||||
示例如下:
|
||||
|
||||
```tsx
|
||||
// 二次贝塞尔曲线
|
||||
<g-bezier
|
||||
curve={[100, 100, 250, 200, 300, 100]}
|
||||
strokeStyle="purple"
|
||||
lineWidth={3}
|
||||
/>;
|
||||
|
||||
// 动态路径
|
||||
const path = computed(() => [
|
||||
startX.value,
|
||||
startY.value,
|
||||
controlX.value,
|
||||
controlY.value,
|
||||
endX.value,
|
||||
endY.value
|
||||
]);
|
||||
<g-bezier curve={path.value} />;
|
||||
```
|
||||
|
||||
### 圆角矩形 `<g-rectr>`
|
||||
|
||||
圆角矩形的核心参数与 CSS 的 border-radius 类似,如下:
|
||||
|
||||
```ts
|
||||
interface RectRProps extends GraphicPropsBase {
|
||||
/**
|
||||
* 圆形圆角参数,可以填 `[r1, r2, r3, r4]`,后三项可选。填写不同数量下的表现:
|
||||
* - 1个:每个角都是 `r1` 半径的圆
|
||||
* - 2个:左上和右下是 `r1` 半径的圆,右上和左下是 `r2` 半径的圆
|
||||
* - 3个:左上是 `r1` 半径的圆,右上和左下是 `r2` 半径的圆,右下是 `r3` 半径的圆
|
||||
* - 4个:左上、右上、左下、右下 分别是 `r1, r2, r3, r4` 半径的圆
|
||||
*/
|
||||
circle?: RectRCircleParams;
|
||||
/**
|
||||
* 椭圆圆角参数,可以填 `[rx1, ry1, rx2, ry2, rx3, ry3, rx4, ry4]`,
|
||||
* 两两一组,后三组可选,填写不同数量下的表现:
|
||||
* - 1组:每个角都是 `[rx1, ry1]` 半径的椭圆
|
||||
* - 2组:左上和右下是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ry2]` 半径的椭圆
|
||||
* - 3组:左上是 `[rx1, ry1]` 半径的椭圆,右上和左下是 `[rx2, ey2]` 半径的椭圆,右下是 `[rx3, ry3]` 半径的椭圆
|
||||
* - 4组:左上、右上、左下、右下 分别是 `[rx1, ry1], [rx2, ry2], [rx3, ry3], [rx4, ry4]` 半径的椭圆
|
||||
*/
|
||||
ellipse?: RectREllipseParams;
|
||||
}
|
||||
```
|
||||
|
||||
示例如下:
|
||||
|
||||
```tsx
|
||||
// 四角圆角半径都为 10 的圆角矩形
|
||||
<g-rectr loc={[0, 0, 200, 200]} circle={[10]} />
|
||||
// 每个角都是横向半径为 10,纵向半径为 5 的椭圆
|
||||
<g-rectr loc={[0, 0, 200, 200]} ellipse={[10, 5]} />
|
||||
// 左上和右下是半径为 10 的圆角,左下和右上是半径为 25 的圆角
|
||||
<g-rectr loc={[0, 0, 200, 200]} circle={[10, 25]} />
|
||||
```
|
||||
|
||||
### 自定义路径 `<g-path>`
|
||||
|
||||
核心参数:
|
||||
|
||||
```ts
|
||||
interface PathProps {
|
||||
path?: Path2D; // 自定义路径对象
|
||||
}
|
||||
```
|
||||
|
||||
示例:
|
||||
|
||||
```tsx
|
||||
// 创建五角星
|
||||
const starPath = new Path2D();
|
||||
// ...路径绘制逻辑
|
||||
<g-path
|
||||
path={starPath}
|
||||
fill
|
||||
stroke
|
||||
fillStyle="orange"
|
||||
strokeStyle="#c00"
|
||||
lineWidth={2}
|
||||
/>;
|
||||
```
|
||||
|
||||
### 最佳实践建议
|
||||
|
||||
1. 交互增强:
|
||||
|
||||
```tsx
|
||||
<g-rect
|
||||
fill
|
||||
stroke
|
||||
actionStroke // 仅在描边区域响应点击
|
||||
onClick={handleSelect}
|
||||
/>
|
||||
```
|
||||
|
||||
2. 样式复用:
|
||||
|
||||
```tsx
|
||||
// 创建样式对象
|
||||
const themeStyle = {
|
||||
fillStyle: '#2c3e50',
|
||||
strokeStyle: '#ecf0f1',
|
||||
lineWidth: 2
|
||||
};
|
||||
|
||||
<g-rect fill {...themeStyle} />
|
||||
<g-circle stroke {...themeStyle} />
|
||||
```
|
61
docs/guide/ui-faq.md
Normal file
61
docs/guide/ui-faq.md
Normal file
@ -0,0 +1,61 @@
|
||||
# UI 常见问题
|
||||
|
||||
## 为什么我的 UI 不显示?
|
||||
|
||||
检查 UI 的纵深(zIndex)是否符合预期,有没有被其他元素遮挡;检查当前元素是否处于 `hidden` 状态。可以在控制台输入 `logTagTree()` 来输出当前的渲染树 `xml` 标签结构,会包含一些重要信息。
|
||||
|
||||
第二种可能性是你的元素处在了父元素范围之外,导致被裁剪掉。注意,`transform` 属性是对元素本身的变换,这也会导致元素本身的矩形范围发生变化,如果你的元素设置了缩放、旋转等,需要考虑此属性对位置的影响。
|
||||
|
||||
## 为什么我的元素 onClick 事件没办法触发?
|
||||
|
||||
可能你的元素被纵深更高的元素覆盖,导致事件无法传播至你的元素,考虑在纵深更高的元素上添加 `noevent` 标识来禁用它的事件传播。注意,一个纯透明的元素也可能会覆盖你的元素,仔细查看你的渲染树结构。
|
||||
|
||||
第二种可能性是其子元素拦截了冒泡或是其父元素拦截了捕获,检查 `e.stopPropagation` 调用情况。
|
||||
|
||||
## 我的数据更新后,为什么渲染内容没有更新?
|
||||
|
||||
你可能在使用 `sprite` 元素,然后在渲染函数里面调用了外部数据,这样的话当外部数据更新时,你的 `sprite` 元素并不会自动更新,需要手动更新。手动更新参考代码:
|
||||
|
||||
```tsx
|
||||
import { Sprite } from '@motajs/render';
|
||||
|
||||
const mySprite = ref<Sprite>();
|
||||
// 数据更新时,同时更新 sprite 元素
|
||||
watch(data, () => mySprite.value?.update());
|
||||
|
||||
// 将 mySprite 传入 ref 参数,这样当挂载完毕后就会将 mySprite.value 设置为该元素
|
||||
<sprite ref={mySprite} render={render} />;
|
||||
```
|
||||
|
||||
除此之外还可能你的数据不是响应式数据,确保你的数据经过了 `reactive` 或 `ref` 包裹。
|
||||
|
||||
## 我的 UI 很卡
|
||||
|
||||
可能使用了平铺式布局,建议使用 `Scroll` 组件或者 `Page` 组件来对平铺内容分割,从而提高渲染效率。可以参考对应的 [API 文档](../api/user-client-modules/Scroll)。
|
||||
|
||||
## 玩着玩着突然黑屏了一下,然后画面就不显示了
|
||||
|
||||
你应该遇到了内存泄漏问题,当一个元素被卸载后,它应该会被销毁,但是如果没有被预期销毁,那么会导致内存泄漏,最终导致爆显存,就会导致画面黑屏一下,然后内容就会不显示。样板本身已经针对这个问题进行了处理,一般情况下不会出现问题,出现这个问题时大概率是你自己的组件或 UI 有问题。可能原因有很多,例如你声明了一个列表,当组件挂载时将元素放入列表,但是当组件卸载时,你却没有将元素移除,这时候就会导致这个元素无法正确被垃圾回收,从而引起内存泄漏。
|
||||
|
||||
关于这个问题的最佳实践:
|
||||
|
||||
- 如果你手动存储了一些元素,确保在卸载时将它们删除
|
||||
- 在删除它们的同时,调用它们的 `destroy` 方法,来确保可以被垃圾回收
|
||||
- 在控制台输入 `Mota.require('@motajs/render').MotaOffscreenCanvas2D.list` 来查看当前还有哪些画布正在使用,游玩一段时间后再次输入,检查数量是否增长,如果增长,说明发生了内存泄漏
|
||||
- 确保组件卸载时已经清空了定时器等内容
|
||||
- 如果需要每帧执行函数,请使用 `onTick` 接口,而非其他方法
|
||||
|
||||
如果你直接使用 `MotaOffscreenCanvas2D` 接口,请确保:
|
||||
|
||||
- 在使用前调用了 `activate` 方法
|
||||
- 在使用后调用了 `deactivate` 方法
|
||||
- 如果不需要再修改画布属性,只需要绘制,请调用 `freeze` 方法
|
||||
- 如果之后不再使用该画布,请调用 `destroy` 方法
|
||||
|
||||
## 为什么我的滤镜不显示?
|
||||
|
||||
很遗憾,截止目前(2.B 发布日期),IOS 依然没有支持 `CanvasRenderingContext2D` 上的 `filter` 方法,所有滤镜属性在 IOS 上将不会显示。不过,我们提供了 `Shader` 元素,它使用 `WebGL2` 接口,允许你制作自己的滤镜,如果滤镜是必要的,请考虑使用此元素,但是需要一定的图形学基础,可以在造塔群询问我或造塔辅助 AI。
|
||||
|
||||
## 不同设备的显示内容会不一样吗?
|
||||
|
||||
从理论上来讲,除了上面那个问题提到的滤镜,其他的所有内容的渲染结果应该完全一致,如果出现了不一致的情况,请上报样板 bug。
|
142
docs/guide/ui-perf.md
Normal file
142
docs/guide/ui-perf.md
Normal file
@ -0,0 +1,142 @@
|
||||
---
|
||||
lang: zh-CN
|
||||
---
|
||||
|
||||
# UI 优化指南
|
||||
|
||||
多数情况下,我们编写的简单 UI 并不需要特别的性能优化,渲染系统的懒更新及缓存机制已经可以有很优秀的性能表现,不过我们还是可能会遇到一些需要特殊优化的场景,本节将会讲述如何优化 UI 的性能表现,优化建议包括:避免元素平铺;使用 `Scroll` 或 `Page` 组件优化平铺性能;避免元素自我更新;使用 `cache` 和 `nocache` 标识;特殊场景禁用抗锯齿和高清;在合适场景下隐藏一些元素等。
|
||||
|
||||
## 避免元素平铺
|
||||
|
||||
在不使用 `Scroll` 组件时,我们需要尽量避免元素平铺,因为这会导致更新时渲染次数上升,从而引起性能下降。我们建议善用树形结构的缓存特性,将可以作为一个整体的元素使用一个容器 `container` 包裹起来,减少更新时的渲染次数,尤其是对于那些不常更新的元素来说,更应该使用容器包裹。不过我们也不建议嵌套过深,这可能导致浪费在递归渲染上的时间过长,渲染效率变低。
|
||||
|
||||
画布渲染树的深度遍历特性使得:
|
||||
|
||||
- 每个独立容器的更新会触发子树的重新渲染
|
||||
- 容器层级过深会增加递归调用栈开销
|
||||
- 合理分组可将高频/低频更新元素隔离
|
||||
|
||||
下面是代码示例:
|
||||
|
||||
```tsx
|
||||
// ❌ 差的写法,全部平铺在一个容器里
|
||||
<container>
|
||||
<text text="1" />
|
||||
{/* 中间省略 998 个元素 */}
|
||||
<text text="1000" />
|
||||
</container>
|
||||
|
||||
// ✅ 好的写法
|
||||
<container>
|
||||
{/* 把不常更新的单独放到一个容器里面 */}
|
||||
<container>
|
||||
<text text="1" />
|
||||
{/* 中间省略 988 个元素 */}
|
||||
<text text="990" />
|
||||
</container>
|
||||
{/* 把常更新的单独放到一个容器里面 */}
|
||||
<container>
|
||||
<text text="991" />
|
||||
{/* 中间省略 8 个元素 */}
|
||||
<text text="1000" />
|
||||
</container>
|
||||
</container>
|
||||
```
|
||||
|
||||
## 使用 `Scroll` 或 `Page` 组件优化平铺性能
|
||||
|
||||
在一些特殊情况下,我们不得不使用平铺布局,例如上一节提到的怪物手册,或是展示一个列表等,这时候必须平铺元素。这时候我们可以使用 `Scroll` 组件或 `Page` 组件来优化性能表现。`Scroll` 组件中,只有在画面内的元素会被渲染,而画面外的不会被渲染,这会大大提高渲染效率;`Page` 组件允许你把列表拆分成多个部分,然后把内容放在不同页中,从而提高渲染性能。极端情况下,`Page` 组件的渲染效率要明显高于 `Scroll` 组件,但是滚动条对于交互更友好,我们推荐在简单场景下使用 `Scroll` 组件,而对于复杂场景,换为 `Page` 组件。两个组件的使用方式可以参考 [API 文档](../api/motajs-render-elements/)。
|
||||
|
||||
我们建议:
|
||||
|
||||
1. **优先使用 Scroll**:
|
||||
- 元素数量 < 500
|
||||
- 需要流畅滚动交互
|
||||
- 元素高度不固定
|
||||
2. **切换至 Page**:
|
||||
- 元素数量 > 1000
|
||||
- 需要支持快速跳转
|
||||
- 存在复杂子组件(如嵌套动画)
|
||||
|
||||
下面是代码示例:
|
||||
|
||||
```tsx
|
||||
// ❌ 差的写法,全部平铺在一个容器里
|
||||
<container>
|
||||
<text text="1" />
|
||||
{/* 中间省略 998 个元素 */}
|
||||
<text text="1000" />
|
||||
</container>
|
||||
|
||||
// ✅ 好的写法,使用 Scroll 组件优化
|
||||
<Scroll>
|
||||
<text text="1" />
|
||||
{/* 中间省略 998 个元素 */}
|
||||
<text text="1000" />
|
||||
</Scroll>
|
||||
|
||||
// ✅ 好的写法,使用 Page 组件优化
|
||||
<Page>
|
||||
{(page: number) => {
|
||||
return list.slice(page * 10, (page + 1) * 10).map(v => <text text={v.toString()} />)
|
||||
}}
|
||||
</Page>
|
||||
```
|
||||
|
||||
## 避免元素自我更新
|
||||
|
||||
元素自我更新是指,在元素的渲染函数内,触发了元素的冒泡更新,这会导致更新无限循环,而且难以察觉。为了解决难以察觉的问题,我们使用了一种方式来专门探测这种情况。常见的触发元素自我更新的场景就是使用 `sprite` 元素,例如:
|
||||
|
||||
```tsx
|
||||
const element = ref<Sprite>();
|
||||
const render = () => {
|
||||
element.value?.update();
|
||||
};
|
||||
|
||||
<sprite render={render} ref={element} />;
|
||||
```
|
||||
|
||||
在上面这段渲染代码中,`sprite` 元素的渲染函数又再次触发了自我更新,这会导致更新无限循环。在开发环境下,这种情况会在控制台抛出警告:`Unexpected recursive call of Sprite.update?uid in render function. Please ensure you have to do this, if you do, ignore this warn.`,这会告诉你是哪个类型的元素触发了循环更新,以及对应元素的 `uid`,从而帮助你寻找问题所在。不过,样板还是留出了一个口子,如果你必须使用循环更新,那么你可以忽略此条警告,在网站上游玩时这条警告将不会被触发,游戏会正常运行。
|
||||
|
||||
## 使用 `cache` 和 `nocache` 标识
|
||||
|
||||
`cache` 和 `nocache` 表示可以让你更加精确地控制渲染树的缓存行为,从而更好地优化渲染性能。默认情况下,这些元素是会被缓存的:`container` `container-custom` `template` `sprite` `image` `icon` `layer` `layer-group` `animation`,对于这些元素,你可以使用 `nocache` 标识来禁用它们的缓存,对于其本身或其子元素的渲染较为简单的场景,禁用缓存后渲染效率可能会更高。其他元素默认是禁用缓存的,如果你的渲染内容比较复杂,例如 `g-path` 元素的路径很复杂,可以使用 `cache` 表示来启用缓存,从而提高渲染效率。示例代码如下:
|
||||
|
||||
```tsx
|
||||
const render = (canvas: MotaOffscreenCanvas2D) => {
|
||||
canvas.ctx.fillRect(0, 0, 200, 200);
|
||||
};
|
||||
// ❌ 差的写法,一个简单的矩形绘制,但是 sprite 默认启用缓存,可能会拉低渲染效率
|
||||
<sprite render={render} />;
|
||||
|
||||
// ✅ 好的写法,使用 nocache 标识禁用 sprite 的缓存机制
|
||||
<sprite render={render} nocache />;
|
||||
```
|
||||
|
||||
## 特殊场景禁用抗锯齿和高清
|
||||
|
||||
默认情况下,大部分元素都是默认启用高清即抗锯齿的(`layer` 和 `layer-group` `icon` 不启用),这可能会导致一些不必要的计算出现,从而拉低渲染性能。对于一些需要保持像素风的内容,我们建议关闭抗锯齿和高清画布。代码示例如下:
|
||||
|
||||
```tsx
|
||||
// ❌ 差的写法,像素风图片使用默认设置,启用了抗锯齿和高清
|
||||
<image image="pixel.png" />
|
||||
// ✅ 好的写法,关闭了默认的抗锯齿和高清
|
||||
<image image="pixel.png" noanti hd={false} />
|
||||
```
|
||||
|
||||
## 在合适场景下隐藏一些元素
|
||||
|
||||
如果一个元素在某些场景下需要隐藏,另一些场景下需要显示,我们建议使用 `hidden` 属性来设置,而不是通过把它移动到画面外、调成透明颜色、使用 `if` 或三元表达式判断等方式。示例代码如下:
|
||||
|
||||
```tsx
|
||||
// ❌ 差的写法,使用条件表达式切换元素显示与否
|
||||
{
|
||||
!hidden.value && <sprite />;
|
||||
}
|
||||
// ✅ 好的写法,使用 hidden 属性
|
||||
<sprite hidden={hidden.value} />;
|
||||
```
|
||||
|
||||
## 后续计划
|
||||
|
||||
我们后续计划推出渲染树调试工具,届时可以更加细致方便地查看渲染树的渲染情况以及性能问题。
|
@ -0,0 +1,243 @@
|
||||
# UI 系统
|
||||
|
||||
本节将会讲解 2.B 的渲染树与 UI 系统的工作原理,以及一些常用 API。
|
||||
|
||||
## 创建一个自己的 UI 管理器
|
||||
|
||||
样板提供 `UIController` 类,允许你在自己的一个 UI 中创建自己的 UI 管理器,例如在样板中,游戏画面本身包含一个 UI 管理器,分为了封面、加载界面、游戏界面三种,其中游戏界面里面还有一个游戏 UI 管理器,我们常用的就是最后一个游戏 UI 管理器。
|
||||
|
||||
### 创建 UIController 实例
|
||||
|
||||
我们从 `@motajs/system-ui` 引入 `UIController` 类,然后对其实例化:
|
||||
|
||||
```ts
|
||||
import { UIController } from '@motajs/system-ui';
|
||||
|
||||
// 传入一个字符串来表示这个控制器的 id
|
||||
export const myController = new UIController('my-controller');
|
||||
```
|
||||
|
||||
### 获取 UI 控制器
|
||||
|
||||
可以通过 id 来获取到这个控制器,或者直接引入对应文件中的控制器:
|
||||
|
||||
```ts
|
||||
import { UIController } from '@motajs/system-ui';
|
||||
import { myController } from './myController';
|
||||
|
||||
const myController = UIController.get('my-controller');
|
||||
```
|
||||
|
||||
### 添加到渲染树
|
||||
|
||||
接下来,可以直接调用 `myController.render` 方法来添加到你自己的 UI 中:
|
||||
|
||||
```tsx
|
||||
<container>{myController.render()}</container>
|
||||
```
|
||||
|
||||
## UI 显示模式
|
||||
|
||||
### 内置显示模式
|
||||
|
||||
UI 管理器内置了两种显示模式,只显示最后一个以及显示所有。其中前者常用于级联式 UI,例如 `设置 -> 系统设置 -> 快捷键设置`,这时候只会显示最后一个 UI,前面的 UI 不会显示。后者常用于展示信息类的 UI,例如在地图上展示怪物信息等。我们可以通过下面这两个方法来设置 UI 显示模式,立即生效,但不推荐频繁切换,建议一个控制器只使用**一种**显示模式:
|
||||
|
||||
```ts
|
||||
// 设置为只显示最后一个
|
||||
myController.lastOnly();
|
||||
// 设置为显示所有
|
||||
myController.showAll();
|
||||
```
|
||||
|
||||
### 栈模式
|
||||
|
||||
对于级联式 UI,我们希望在关闭一个 UI 时,在其之后的 UI 也能关闭,例如对于上面提到的 `设置 -> 系统设置 -> 快捷键设置` 级联 UI,当我们关闭设置界面时,我们会希望系统设置和快捷键设置也一并关闭,而不是需要手动关闭。这时候,栈模式就可以做到这一点,启用栈模式时,关闭一个 UI 后,在其之后的 UI 也会全部关闭。我们依然可以使用上面两个方法来设置是否启用栈模式:
|
||||
|
||||
```ts
|
||||
// 设置为显示最后一个,启用栈模式,不过 lastOnly 默认启用栈模式,因此参数可不填
|
||||
myController.lastOnly(true);
|
||||
// 设置为显示最后一个,不启用栈模式
|
||||
myController.lastOnly(false);
|
||||
```
|
||||
|
||||
### 自定义显示模式
|
||||
|
||||
::: info
|
||||
这一小节内容不重要,没有特殊需求的可以不看。
|
||||
:::
|
||||
|
||||
样板内置的两个显示模式以及栈模式已经能够满足绝大多数情况,不过可能还会有一些非常特殊的情况满足不了,这时候我们可以使用 `showCustom` 方法来自定义一个显示模式。这个方法要求传入一个参数,参数需要是 `IUICustomConfig` 对象,对象要求实现 `open` `close` `hide` `show` `update` 五个方法,我们来介绍一下如何做出一个自定义显示模式。
|
||||
|
||||
方法说明如下:
|
||||
|
||||
- `open` 方法会在一个 UI 打开时调用,例如默认的 `lastOnly` 模式其实就是在打开 UI 时将 UI 添加至栈末尾,然后隐藏在其之前的所有 UI
|
||||
- `close` 方法会在一个 UI 关闭时调用,例如默认的 `lastOnly` 模式就会在这个时候把在传入 UI 之后的所有 UI 一并关闭
|
||||
- `hide` 方法会在一个 UI 隐藏时调用,默认的 `lastOnly` 模式会在这个时候把 UI 隐藏显示
|
||||
- `show` 方法会在一个 UI 显示时调用,默认的 `lastOnly` 模式会在这个时候把 UI 启用显示
|
||||
- `update` 方法会在切换显示模式时调用,默认的 `lastOnly` 模式会在这个时候把最后一个 UI 显示,之前的隐藏
|
||||
|
||||
那么,假如我们要做一个反向 `lastOnly`,即只显示第一个,添加 UI 时添加至队列开头,我们可以这么写:
|
||||
|
||||
```ts
|
||||
import { IUICustomConfig, IUIInstance } from '@motajs/system-ui';
|
||||
|
||||
const myCustomMode: IUICustomConfig = {
|
||||
open(ins: IUIInstance, stack: IUIInstance[]) {
|
||||
stack.forEach(v => v.hide()); // 隐藏当前所有 UI
|
||||
stack.unshift(ins); // 将要打开的 UI 添加至队列开头
|
||||
ins.show(); // 显示要打开的 UI
|
||||
},
|
||||
close(ins: IUIInstance, stack: IUIInstance[], index: number) {
|
||||
stack.splice(0, index + 1); // 关闭传入 UI 及其之前的所有内容
|
||||
stack[0]?.show(); // 显示第一个 UI
|
||||
},
|
||||
hide(ins: IUIInstance, stack: IUIInstance[], index: number) {
|
||||
ins.hide(); // 直接隐藏
|
||||
},
|
||||
show(ins: IUIInstance, stack: IUIInstance[], index: number) {
|
||||
ins.show(); // 直接显示
|
||||
},
|
||||
update(stack: IUIInstance[]) {
|
||||
stack.forEach(v => v.hide()); // 先隐藏所有 UI
|
||||
stack[0]?.show(); // 然后显示第一个 UI
|
||||
}
|
||||
};
|
||||
|
||||
myController.showCustom(myCustomMode); // 应用自己的显示模式
|
||||
```
|
||||
|
||||
## 设置 UI 背景
|
||||
|
||||
我们可以为 UI 设置背景组件,背景组件在 UI 打开时常亮。我们推荐使用此方法来为 UI 设置背景,因为它可以搭配 `keep` 防抖动来使用,避免出现 UI 闪烁的问题。现在,我们使用样板内置的 `Background` 背景组件作为例子,来展示如何设置背景:
|
||||
|
||||
```ts
|
||||
import { Background } from '@user/client-modules';
|
||||
|
||||
// 传入背景组件作为背景,然后设置参数,使用 winskin.png 作为背景
|
||||
myController.setBackground(Background, { winskin: 'winskin.png' });
|
||||
```
|
||||
|
||||
默认情况下,当我们打开 UI 时,背景组件将会自动展示,不过我们也可以手动控制背景组件是否显示,它的优先级高于系统优先级:
|
||||
|
||||
```ts
|
||||
myController.hideBackground(); // 隐藏背景组件,即使有 UI 已经打开,也不会显示背景
|
||||
myController.showBackground(); // 显示背景组件,在 UI 已经打开的情况下展示,没有 UI 打开时不显示
|
||||
```
|
||||
|
||||
## 背景维持防抖动
|
||||
|
||||
有时候,我们需要关闭当前 UI 然后立刻打开下一个 UI,例如使用一个道具时可能会打开一个新的页面,这时候会先关闭道具背包界面,再打开道具的页面,这时候可能会出现短暂的“背景丢失”,这是因为 UI 的挂载需要时间,在极短的时间内如果没有挂载上,那么就会在屏幕上什么都不显示,上面设置的背景 UI 也不会显示,会引起一次闪烁,观感很差。为了解决这个问题,我们提供了背景维持防抖动的功能,使用 `keep` 方法来实现:
|
||||
|
||||
```ts
|
||||
const keep = myController.keep();
|
||||
```
|
||||
|
||||
调用此方法后,在下一次 UI 全部关闭时,背景会暂时维持,直到有 UI 打开,也就是说它会维持一次 UI 背景不会关闭,下一次就失效了。这样的话,如果我们去使用一个打开页面的道具,就不会出现闪烁的问题了。不过,假如我们使用了一个没有打开页面的道具,会有什么表现?答案是背景一直显示着,用户就什么也干不了了,这显然不是我们希望的,因此 `keep` 函数的返回值提供了一些能力来让你关闭背景,它们包括:
|
||||
|
||||
```ts
|
||||
// 推荐方法,使用 safelyUnload 安全地卸载背景,这样如果有 UI 已经打开,不会将其关闭
|
||||
keep.safelyUnload();
|
||||
// 不推荐方法,调用后立刻关闭所有 UI,不常用
|
||||
keep.unload();
|
||||
```
|
||||
|
||||
## 打开与关闭 UI
|
||||
|
||||
在 UI 编写章节已经提到了打开和关闭 UI 使用 `open` 和 `close` 方法,现在我们更细致地讲解一下如何打开与关闭 UI。打开 UI 使用 `open` 方法,定义如下:
|
||||
|
||||
```ts
|
||||
function open<T extends UIComponent>(
|
||||
ui: IGameUI<T>,
|
||||
props: UIProps<T>,
|
||||
alwaysShow?: boolean
|
||||
): IUIInstance;
|
||||
```
|
||||
|
||||
其中第一个参数表示要打开的 UI,第二个表示传给 UI 的参数,第三个表示 UI 是否永远保持显示状态(除非被关闭),不受到显示模式的影响。同种 UI 可以打开多个,也可以在不同的控制器上同时打开多个相同的 UI。例如,如果我们想在主 UI 控制器中添加一个常量的返回游戏按钮,就可以这么写:
|
||||
|
||||
```ts
|
||||
// BackToGame 是自定义 UI,第三个参数传 true 来保证它一直显示在画面上
|
||||
myController.open(BackToGame, {}, true);
|
||||
```
|
||||
|
||||
关闭 UI 使用 `close` 方法,传入 UI 实例,即 `open` 方法的返回值,没有其他参数。例如:
|
||||
|
||||
```ts
|
||||
const MyUI = defineComponent(props => {
|
||||
// 所有通过 UI 控制器打开的,同时按照 UI 模板填写了 props 的 UI 都包含 controller 和 instance 属性
|
||||
props.controller.close(props.instance);
|
||||
}, myUIProps);
|
||||
```
|
||||
|
||||
除此之外,还提供了一个关闭所有 UI 的:
|
||||
|
||||
```ts
|
||||
function closeAll(ui?: IGameUI): void;
|
||||
```
|
||||
|
||||
其中参数表示要关闭的 UI 类型,不填时表示关闭所有 UI,填写时表示关闭所有指定类型的 UI。例如我想关闭所有 `EnemyInfo` UI,可以这么写:
|
||||
|
||||
```ts
|
||||
// EnemyInfo 是自定义 UI
|
||||
myController.closeAll(EnemyInfo);
|
||||
```
|
||||
|
||||
## 渲染系统的树结构
|
||||
|
||||
接下来我们来讲解一下渲染系统的一些工作原理。下面的部分由 `DeepSeek R1` 模型生成并稍作修改。
|
||||
|
||||
### 结构原理
|
||||
|
||||
想象一棵倒着生长的树:
|
||||
|
||||
- 根节点:相当于画布本身,是所有元素的起点
|
||||
- 枝干节点:类似文件夹,可以包含其他元素
|
||||
- 叶子节点:实际显示的内容,如图片、文字等
|
||||
|
||||
### 运作特点
|
||||
|
||||
- 层级管理:子元素永远在父元素的"内部"显示
|
||||
- 自动排序:像叠扑克牌一样,后添加的元素默认盖在之前元素上方,不过也可以通过参数来调整顺序
|
||||
- 智能裁剪:父元素就像相框,超出范围的内容自动隐藏
|
||||
|
||||
## 渲染系统的事件系统
|
||||
|
||||
### 事件传递三阶段
|
||||
|
||||
1. 收件扫描(捕获阶段):从根部开始层层扫描,寻找可能接收事件的元素,类似快递分拣中心扫描包裹目的地
|
||||
2. 精准投递(目标阶段):找到实际触发事件的元素进行处理,就像快递员将包裹送到收件人手中
|
||||
3. 回执确认(冒泡阶段):处理结果沿着原路返回汇报,如同收件人签收后系统更新物流状态
|
||||
|
||||
将事件分为三个阶段,是为了让交互更加符合直觉,你也不想点击内层按钮的时候外层按钮也被触发吧)
|
||||
|
||||
### 特殊处理机制
|
||||
|
||||
- 紧急拦截:任何环节都可以标记"无需继续传递"
|
||||
- 批量处理:多个事件自动合并减少处理次数
|
||||
- 智能过滤:自动忽略不可见区域的事件
|
||||
|
||||
## 冒泡更新
|
||||
|
||||
### 工作原理
|
||||
|
||||
当某个元素发生变化时:自动通知直系父元素,父元素检查自身是否需要调整,继续向上传递直到根部,最终统一计算所有需要改变的位置,并在下一帧执行更新。
|
||||
|
||||
### 设计优势
|
||||
|
||||
- 精准定位:只更新受影响的部分画面
|
||||
- 避免重复:多个子元素变化只需一次整体计算
|
||||
- 顺序保障:始终从最深层开始逐层处理
|
||||
|
||||
## 懒更新机制
|
||||
|
||||
### 工作模式
|
||||
|
||||
1. 收集阶段:记录所有需要改变的内容(如颜色变化、文字修改)
|
||||
2. 等待时机:一般是等待到下一帧
|
||||
3. 批量处理:一次性完成所有修改
|
||||
|
||||
### 实际效益
|
||||
|
||||
- 性能优化:减少像频繁开关灯的资源浪费
|
||||
- 流畅保障:避免连续小改动导致的画面闪烁
|
||||
- 智能调度:优先处理用户可见区域的变化
|
850
docs/guide/ui.md
850
docs/guide/ui.md
@ -0,0 +1,850 @@
|
||||
---
|
||||
lang: zh-CN
|
||||
---
|
||||
|
||||
# UI 编写
|
||||
|
||||
本文将介绍如何在 2.B 样板中编写 UI,以及如何优化 UI 性能。
|
||||
|
||||
## 创建 UI 文件
|
||||
|
||||
首先,我们打开 `packages-user/client-modules/render` 文件夹,这里是目前样板的 UI 目录(之后可能会修改),我们可以看到 `components` `legacy` `ui` 三个文件夹,其中 `component` 是组件文件夹,也就是所有 UI 都可能用到的组件,例如滚动条、分页、图标等,这些东西不会单独组成一个 UI,但是可以方便 UI 开发。`legacy` 文件夹是将要删除或重构的内容,不建议使用里面的内容。`ui` 就是 UI 文件夹,这里面存放了所有的 UI,我们在这里创建一个文件 `myUI.tsx`。
|
||||
|
||||
## 编写 UI 模板
|
||||
|
||||
下面,我们需要编写 UI 模板,以怪物手册为例,模板如下,直接复制粘贴即可:
|
||||
|
||||
```tsx
|
||||
import { defineComponent } from 'vue';
|
||||
import { GameUI, UIComponentProps } from '@motajs/system-ui';
|
||||
import { SetupComponentOptions } from '../components';
|
||||
|
||||
export interface MyBookProps extends UIComponentProps {}
|
||||
|
||||
const myBookProps = {
|
||||
props: ['controller', 'instance']
|
||||
} satisfies SetupComponentOptions<MyBookProps>;
|
||||
|
||||
export const MyBook = defineComponent<MyBookProps>(props => {
|
||||
return () => <container></container>;
|
||||
}, myBookProps);
|
||||
|
||||
export const MyBookUI = new GameUI('my-book', MyBook);
|
||||
```
|
||||
|
||||
然后打开 `index.ts`,增加如下代码:
|
||||
|
||||
```ts
|
||||
export * from './myUI';
|
||||
```
|
||||
|
||||
## 添加一些内容
|
||||
|
||||
新的 UI 使用 tsx 编写,即 `TypeScript JSX`,可以直接在 ts 文件中编写 XML,非常适合编写 UI。例如,我们想要把 UI 的位置设为水平竖直居中,位置在 240, 240,长宽为 480, 480,并显示一个文字,可以这么写:
|
||||
|
||||
```tsx
|
||||
// ... 其他内容
|
||||
// loc 参数表示这个元素的位置,六个数分别表示:
|
||||
// 横纵坐标;长宽;水平竖直锚点,0.5 表示居中,1 表示靠右或靠下对齐,可以填不在 0-1 范围的数
|
||||
// 每两项组成一组,这两项要么都填,要么都不填,例如长宽可以都不填,横纵坐标可以都不填
|
||||
// 不填时会使用默认值,或是组件内部计算出的值
|
||||
return () => (
|
||||
<container loc={[240, 240, 480, 480, 0.5, 0.5]}>
|
||||
{/* 文字元素会自动计算长宽,因此不能手动指定 */}
|
||||
<text text="这是一段文字" loc={[240, 240, void 0, void 0, 0.5, 0.5]} />
|
||||
</container>
|
||||
);
|
||||
```
|
||||
|
||||
## 显示 UI
|
||||
|
||||
我们编写完 UI 之后,这个 UI 并不会自己显示,需要手动打开。我们找到 `ui/main.tsx`,在 `MainScene` 这个根组件中添加一句话:
|
||||
|
||||
```ts
|
||||
// 在这添加引入
|
||||
import { MyBookUI } from './ui';
|
||||
// ... 其他内容
|
||||
const MainScene = defineComponent(() => {
|
||||
// ... 其他内容
|
||||
// 在这添加一句话,打开 UI,第二个参数为传入 UI 的参数,后面会有讲解
|
||||
// 纵深设为 100 以保证可以显示出来,纵深越大,元素越靠上,会覆盖纵深低的元素
|
||||
mainUIController.open(MyBookUI, { zIndex: 100 });
|
||||
return () => (
|
||||
// ... 其他内容
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
这样的话,我们就会在页面上显示一个新的 UI 了!不过这个 UI 会是常亮的 UI,没办法关闭,我们需要更精细的控制。我们可以在内部使用 `props.controller` 来获取到 UI 控制器实例,使用 `props.instance` 获取到当前 UI 实例,从而控制当前 UI 的状态:
|
||||
|
||||
```tsx
|
||||
export const MyBook = defineComponent<MyBookProps>(props => {
|
||||
// 例如,我们可以让它在打开 10 秒钟后关闭:
|
||||
setTimeout(() => props.controller.close(props.instance), 10000);
|
||||
return () => (
|
||||
// ... UI 内容
|
||||
);
|
||||
}, myBookProps);
|
||||
```
|
||||
|
||||
除此之外,我们还可以在任意渲染端模块中引入 `ui/controller` 来获取到根组件的 UI 控制器,注意跨文件夹引入时需要引入 `@user/client-modules`。例如,我们可以在其他文件中控制这个 UI 的开启与关闭:
|
||||
|
||||
```ts
|
||||
import { mainUIController, MyBookUI } from './ui';
|
||||
import { IUIInstance } from '@motajs/system-ui';
|
||||
|
||||
let myBookInstance: IUIInstance;
|
||||
export function openMyBook() {
|
||||
// 使用一个变量来记录打开的 UI 实例
|
||||
myBookInstance = mainUIController.open(MyBookUI, {});
|
||||
}
|
||||
|
||||
export function closeMyBook() {
|
||||
// 传入 UI 实例,将会关闭此 UI 及其之后的 UI
|
||||
mainUIController.close(myBookInstance);
|
||||
}
|
||||
```
|
||||
|
||||
也可以使用 `Mota.require` 引入:
|
||||
|
||||
```ts
|
||||
const { mainUIController } = Mota.require('@user/client-modules');
|
||||
```
|
||||
|
||||
也可以通过 `UIController` 的接口获取其实例:
|
||||
|
||||
```ts
|
||||
import { UIController } from '@motajs/system-ui';
|
||||
|
||||
const mainUIController = UIController.getController('main-ui');
|
||||
```
|
||||
|
||||
更多的 UI 控制功能可以参考后续文档以及相关的 [UI 系统指南](./ui-system.md) 或 [API 文档](../api/motajs-system-ui/UIController)。
|
||||
|
||||
## 添加更多内容
|
||||
|
||||
既然我们要编写一个简易怪物手册,那么仅靠上面这些内容当然不够,我们需要更多的元素和组件才行,下面我们来介绍一些常用的元素及组件。
|
||||
|
||||
### 图标
|
||||
|
||||
既然是怪物手册,那么图标必然不能少,图标是 `<icon>` 元素,需要传入 `icon` 参数,例如:
|
||||
|
||||
```tsx
|
||||
return () => (
|
||||
<container>
|
||||
{/* 显示绿史莱姆图标,位置在 (32, 32),循环播放动画 */}
|
||||
<icon icon="greenSlime" loc={[32, 32]} animate />
|
||||
</container>
|
||||
);
|
||||
```
|
||||
|
||||
### 字体
|
||||
|
||||
我们很多时候也会想要自定义字体,可以通过 `Font` 类来实现这个功能:
|
||||
|
||||
```tsx
|
||||
import { Font, FontWeight } from '@motajs/render';
|
||||
|
||||
// 创建一个字体,包含五个参数,第一个是字体名称,第二个是字体大小,第三个是字体大小的单位,一般是 'px'
|
||||
// 第四个是字体粗细,默认是 400,可以填 FontWeight.Bold,FontWeight.Light 或是数字,范围在 1-1000 之间
|
||||
// 第五个是是否斜体。每个参数都是可选,不填则使用默认字体的样式。
|
||||
const font = new Font('myFont', 24, 'px', FontWeight.Bold, false);
|
||||
// 可以将这个字体设置为默认字体,之后的所有没有指定的都会使用此字体
|
||||
Font.setDefaults(font);
|
||||
// 如果需要使用默认字体,有两种写法
|
||||
const font = new Font();
|
||||
const font = Font.defaults();
|
||||
|
||||
return () => (
|
||||
<container>
|
||||
<icon icon="greenSlime" loc={[32, 32]} animate />
|
||||
<text
|
||||
text="绿史莱姆"
|
||||
// 使用上面定义的字体
|
||||
font={font}
|
||||
// 靠左对齐,上下居中对齐
|
||||
loc={[64, 48, void 0, void 0, 0, 0.5]}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
```
|
||||
|
||||
更多的字体使用方法可以参考 [API 文档](../api/motajs-render-style/Font)
|
||||
|
||||
### 圆角矩形
|
||||
|
||||
我们可以为怪物手册的一栏添加圆角矩形,写法如下:
|
||||
|
||||
```tsx
|
||||
return () => (
|
||||
<container>
|
||||
<g-rectr
|
||||
// 圆角矩形的位置
|
||||
loc={[16, 16, 480 - 32, 480 - 32]}
|
||||
// 圆角矩形为仅描边
|
||||
stroke
|
||||
// 圆角半径,可以设置四个,具体参考圆角矩形的文档
|
||||
circle={[8]}
|
||||
// 描边样式,这里设为了金色
|
||||
strokeStyle="gold"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
```
|
||||
|
||||
### 线段
|
||||
|
||||
我们也可以添加线段,作为怪物列表之间的分割线:
|
||||
|
||||
```tsx
|
||||
return () => (
|
||||
<container>
|
||||
<g-line
|
||||
// 线段的起始位置和终止位置,不需要指定 loc 属性
|
||||
line={[16, 80, 480 - 16, 80]}
|
||||
// 线的端点为圆形
|
||||
lineCap="round"
|
||||
// 线宽为 1
|
||||
lineWidth={1}
|
||||
// 虚线样式,5 个像素为实,5 个像素为虚
|
||||
lineDash={[5, 5]}
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
```
|
||||
|
||||
### winskin 背景
|
||||
|
||||
我们可以为手册添加一个 winskin 背景,可以使用 `Background` 组件:
|
||||
|
||||
```tsx
|
||||
// 从 components 文件夹中引入这个组件
|
||||
import { Background } from '../components';
|
||||
|
||||
return () => (
|
||||
<container loc={[240, 240, 480, 480, 0.5, 0.5]}>
|
||||
<Background
|
||||
// 位置是相对于父元素的,因此从 (0, 0) 开始
|
||||
loc={[0, 0, 480, 480]}
|
||||
// 设置 winskin 的图片名称
|
||||
winskin="winskin.png"
|
||||
/>
|
||||
</container>
|
||||
);
|
||||
```
|
||||
|
||||
### 滚动条
|
||||
|
||||
怪物多的话一页肯定显示不完,因此我们可以添加一个滚动条 `Scroll` 组件,用法如下:
|
||||
|
||||
```tsx
|
||||
// 从 components 文件夹中引入这个组件
|
||||
import { Scroll } from '../components';
|
||||
|
||||
return () => (
|
||||
// 使用滚动条组件替换 container 元素
|
||||
<Scroll loc={[240, 240, 480, 480, 0.5, 0.5]}> // [!code ++]
|
||||
<Background
|
||||
// 位置是相对于父元素的,因此从 (0, 0) 开始
|
||||
loc={[0, 0, 480, 480]}
|
||||
// 设置 winskin 的图片名称
|
||||
winskin="winskin.png"
|
||||
/>
|
||||
{/* 其他内容 */}
|
||||
</Srcoll> // [!code ++]
|
||||
);
|
||||
```
|
||||
|
||||
在使用滚动条时,建议使用平铺式布局,将每个独立的内容平铺显示,而不是整体包裹为一个 `container`,这有助于提高性能表现。
|
||||
|
||||
### 循环
|
||||
|
||||
编写怪物手册的话,我们就必须用到循环,因为我们需要遍历当前怪物列表,然后每个怪物生成一个 `container`,在这个 `container` 里面显示内容。tsx 为我们提供了嵌入表达式的功能,因此我们可以通过 `map` 方法来遍历怪物列表,然后返回一个元素,组成元素数组,实现循环遍历的功能。示例如下:
|
||||
|
||||
```tsx
|
||||
export const MyBook = defineComponent<MyBookProps>(props => {
|
||||
// 获取怪物列表,enemys 为 CurrenEnemy 数组,可以查看 package-user/data-fallback/src/battle.ts
|
||||
const enemys = core.getCurrentEnemys();
|
||||
// 工具函数,居中,靠右,靠左对齐文字
|
||||
const central = (x: number, y: number) => [x, y, void 0, void 0, 0.5, 0.5];
|
||||
const right = (x: number, y: number) => [x, y, void 0, void 0, 1, 0.5];
|
||||
const left = (x: number, y: number) => [x, y, void 0, void 0, 0, 0.5];
|
||||
|
||||
return () => (
|
||||
<Scroll>
|
||||
{/* 写一个 map 循环,将一个容器元素返回,就可以显示了 */}
|
||||
{enemys.map((v, i) => {
|
||||
return (
|
||||
<container loc={[0, 80 * i, 480, 80]}>
|
||||
{/* 怪物图标与怪物名称 */}
|
||||
<icon icon={v.enemy.id} loc={[32, 16, 32, 32]} />
|
||||
<text text={v.enemy.enemy.name} loc={central(48, 64)} />
|
||||
{/* 显示怪物的属性 */}
|
||||
<text text="生命" loc={right(96, 20)} />
|
||||
<text text={v.enemy.info.hp} loc={left(108, 20)} />
|
||||
{/* 其他的属性,例如攻击,防御等 */}
|
||||
</container>
|
||||
);
|
||||
})}
|
||||
</Scroll>
|
||||
);
|
||||
}, myBookProps);
|
||||
```
|
||||
|
||||
### 条件判断
|
||||
|
||||
可以在表达式中使用三元表达式或者立即执行函数来实现条件判断:
|
||||
|
||||
```tsx
|
||||
return () => (
|
||||
<Scroll>
|
||||
{enemys.length === 0 ? (
|
||||
// 无怪物时,显示没有剩余怪物
|
||||
<text text="没有剩余怪物" loc={central(240. 240)} font={new Font('Verdana', 48)} /> // [!code ++]
|
||||
) : (
|
||||
enemys.map(v => {
|
||||
// 有怪物时
|
||||
})
|
||||
)}
|
||||
</Scroll>
|
||||
);
|
||||
```
|
||||
|
||||
## 响应式
|
||||
|
||||
使用新的 UI 系统时,最大的优势就是响应式了,它可以让 UI 在数据发生变动时自动更改显示内容,而不需要手动重绘。本 UI 系统完全兼容 `vue` 的响应式系统,非常方便。
|
||||
|
||||
### 基础用法
|
||||
|
||||
例如,我想要给我的怪物手册添加一个楼层 id 的参数,首先我们先定义这个参数:
|
||||
|
||||
```tsx
|
||||
import { computed } from 'vue';
|
||||
|
||||
export interface MyBookProps extends UIComponentProps {
|
||||
// 定义 floorId 参数
|
||||
floorId: FloorIds;
|
||||
}
|
||||
|
||||
const myBookProps = {
|
||||
// 这里也要修改
|
||||
props: ['controller', 'instance', 'floorId']
|
||||
} satisfies SetupComponentOptions<MyBookProps>;
|
||||
```
|
||||
|
||||
然后我们需要在这个参数发生变动时修改怪物列表,可以这么写:
|
||||
|
||||
```tsx
|
||||
export const MyBook = defineComponent<MyBookProps>(props => {
|
||||
// 使用 computed,这样的话就会自动追踪到 props.floorId 参数,更新怪物列表,并更新显示内容
|
||||
const enemys = computed(() => core.getCurrentEnemys(props.floorId)); // [!code ++]
|
||||
|
||||
return () => (
|
||||
<Scroll>
|
||||
{/* 需要使用 enemys.value 属性,不能直接使用 enemys.length */}
|
||||
{enemys.value.length === 0 ? ( // [!code ++]
|
||||
<text text="没有剩余怪物" loc={central(240. 240)} font={new Font('Verdana', 48)} />
|
||||
) : (
|
||||
// 同上,需要 value 属性
|
||||
enemys.value.map(v => {}) // [!code ++]
|
||||
)}
|
||||
</Scroll>
|
||||
);
|
||||
}, myBookProps);
|
||||
```
|
||||
|
||||
### 什么样的变量能使用响应式
|
||||
|
||||
其实,我们用一般的方式编写的变量或常量都是不能使用响应式的,例如这些都不行:
|
||||
|
||||
```ts
|
||||
let num = 10;
|
||||
let str = '123';
|
||||
|
||||
const num2 = computed(() => num * 2);
|
||||
const str2 = computed(() => parseInt(str));
|
||||
```
|
||||
|
||||
这么写的话,是没有响应式效果的,这是因为 `num` 和 `str` 并不是响应式变量,不能追踪到。对于 `string` `number` `boolean` 这些字面量类型的变量,我们需要使用 `ref` 函数包裹才可以:
|
||||
|
||||
```tsx
|
||||
import { ref } from 'vue';
|
||||
|
||||
// 使用 ref 函数包裹
|
||||
const num = ref(10);
|
||||
// 使用 num.value 属性调用
|
||||
const num2 = computed(() => num.value * 2);
|
||||
// 使用 num.value 修改值
|
||||
num.value = 20;
|
||||
|
||||
// 这样的话就有响应式效果了
|
||||
<text text={num2.value.toString()} />;
|
||||
```
|
||||
|
||||
对于对象类型来说,需要使用 `reactive` 函数包裹,这个函数会把对象变成深层响应式,任何一级发生更改都会触发响应式更新,例如:
|
||||
|
||||
```tsx
|
||||
const obj = reactive({ obj1: { num: 10 } });
|
||||
|
||||
// 这个就不需要使用 value 属性了,只有 ref 函数包裹的需要
|
||||
obj.obj1.num = 20;
|
||||
|
||||
// 直接调用即可,当值更改时内容也会自动更新
|
||||
<text text={obj.obj1.num.toString()} />;
|
||||
```
|
||||
|
||||
数组也可以使用 `reactive` 方法来实现响应式:
|
||||
|
||||
```tsx
|
||||
// 传入一个泛型来指定这个变量的类型,这里使用数字数组作为示例
|
||||
const array = reactive<number[]>([]);
|
||||
|
||||
// 可以使用数组自身的方法添加或修改元素
|
||||
array.push(100);
|
||||
|
||||
<container>
|
||||
{/* 直接对数组遍历,数组修改后这段内容也会自动更新 */}
|
||||
{array.map(v => (
|
||||
<text text={v.toString()} />
|
||||
))}
|
||||
</container>;
|
||||
```
|
||||
|
||||
如果对象比较大,只想让第一层变为响应式,深层的不变,可以使用 `shallowReactive` 或 `shallowRef`,或使用 `markRaw` 手动标记不需要响应式的部分:
|
||||
|
||||
```ts
|
||||
// 这样的话,当 obj1.obj1.num 修改时,就不会触发响应式,而 obj1.obj1 修改时会触发
|
||||
const obj1 = shallowReactive({ obj1: { num: 10 } });
|
||||
// 使用 shallowRef,也可以变成浅层响应式
|
||||
const obj2 = shallowRef({ obj1: { num: 10 } });
|
||||
// 或者手动标记为不需要响应式
|
||||
const obj3 = reactive({ obj1: markRaw({ num: 10 }) });
|
||||
```
|
||||
|
||||
响应式不仅可以用在 `computed` 或者是渲染元素中,还可以使用 `watch` 监听。不过该方法有一定的限制,那就是尽量不要在组件顶层之外使用。下面是一些例子:
|
||||
|
||||
::: code-group
|
||||
|
||||
```ts [ref]
|
||||
const num1 = ref(10);
|
||||
const num2 = ref(20);
|
||||
|
||||
watch(num1, (newValue, oldValue) => {
|
||||
// 当 num1 的值发生变化时,在控制台输出新值和旧值
|
||||
console.log(newValue, oldValue);
|
||||
|
||||
// 这里就不是组件顶层,不要使用 watch。如果需要条件判断的话,可以在监听函数内部判断,而不是外部
|
||||
watch(num2, () => {});
|
||||
});
|
||||
```
|
||||
|
||||
```ts [reactive]
|
||||
const obj = reactive({
|
||||
num: 10,
|
||||
obj1: {
|
||||
num2: 20
|
||||
}
|
||||
});
|
||||
|
||||
// 监听 obj.num
|
||||
watch(
|
||||
() => obj.num,
|
||||
(newValue, oldValue) => {
|
||||
console.log(newValue, oldValue);
|
||||
}
|
||||
);
|
||||
// 监听 obj 整体
|
||||
watch(obj, () => {
|
||||
console.log(obj.num);
|
||||
});
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: info
|
||||
传入组件的 `props` 参数也是响应式的,可以通过 `watch` 监听,或使用 `computed` 追踪。
|
||||
:::
|
||||
|
||||
关于更多 `vue` 响应式的知识,可以查看 [Vue 官方文档](https://cn.vuejs.org/)
|
||||
|
||||
## 鼠标与触摸交互事件
|
||||
|
||||
### 监听鼠标或触摸
|
||||
|
||||
通过上面这些内容,我们已经可以搭出来一个完整的怪物手册页面了,不过现在这个页面是死的,还没办法交互,我们需要让它有办法交互,允许用户点击和按键操作。UI 系统提供了丰富方便的接口来实现交互动作的监听,例如监听点击可以使用 `onClick`:
|
||||
|
||||
```tsx
|
||||
const click = () => {
|
||||
console.log('clicked!');
|
||||
};
|
||||
|
||||
// 直接将函数传入 onClick 属性即可
|
||||
<container onClick={click}>{/* 渲染内容 */}</container>;
|
||||
```
|
||||
|
||||
可以使用 `cursor` 属性来指定鼠标移动到该元素上时的指针样式,如下例所示,鼠标移动到这个容器上时就会变成小手的形状:
|
||||
|
||||
```tsx
|
||||
<container cursor="pointer" />
|
||||
```
|
||||
|
||||
鼠标与触摸事件的触发包括两个阶段,从根节点捕获,然后一路传递到最下层,然后从最下层冒泡,然后一路再传递回根节点,一般情况下我们使用冒泡阶段的监听即可,也就是 `onXxx`,例如 `onClick` 等,不过如果我们需要监听捕获阶段的事件,也可以使用 `onXxxCapture` 的方法来监听:
|
||||
|
||||
```tsx
|
||||
const clickCapture = () => {
|
||||
console.log('click capture.');
|
||||
};
|
||||
const click = () => {
|
||||
console.log('click bubble.');
|
||||
};
|
||||
|
||||
<container onClick={click} onClickCapture={clickCapture} />;
|
||||
```
|
||||
|
||||
当点击这个容器时,就会先触发 `clickCapture` 事件,再触发 `click` 事件。
|
||||
|
||||
### 监听事件的类型
|
||||
|
||||
鼠标和触摸交互包含如下类型:
|
||||
|
||||
- `click`: 当按下与抬起都发生在这个元素上时触发,冒泡阶段
|
||||
- `clickCapture`: 同上,捕获阶段
|
||||
- `down`: 当在这个元素上按下时触发,冒泡阶段
|
||||
- `downCapture`: 同上,捕获阶段
|
||||
- `up`: 当在这个元素上抬起时触发,冒泡阶段
|
||||
- `upCapture`: 同上,捕获阶段
|
||||
- `move`: 当在这个元素上移动时触发,冒泡阶段
|
||||
- `moveCapture`: 同上,捕获阶段
|
||||
- `enter`: 当进入这个元素时触发,顺序不固定,没有捕获阶段与冒泡阶段的分类
|
||||
- `leave`: 当离开这个元素时触发,顺序不固定,没有捕获阶段与冒泡阶段的分类
|
||||
- `wheel`: 当在这个元素上滚轮时触发,冒泡阶段
|
||||
- `wheelCapture`: 同上,捕获阶段
|
||||
|
||||
触发顺序如下,滚轮单独列出,不在下述顺序中:
|
||||
|
||||
1. `downCapture`,按下捕获
|
||||
2. `down`: 按下冒泡
|
||||
3. `moveCapture`: 移动捕获
|
||||
4. `move`: 移动冒泡
|
||||
5. `leave`: 离开元素
|
||||
6. `enter`: 进入元素
|
||||
7. `upCapture`: 抬起捕获
|
||||
8. `up`: 抬起冒泡
|
||||
9. `clickCapture`: 点击捕获
|
||||
10. `click`: 点击冒泡
|
||||
|
||||
### 阻止事件传播
|
||||
|
||||
有时候我们需要阻止交互事件的继续传播,例如按钮套按钮时,我们不希望点击内部按钮时也触发外部按钮,这时候我们需要在内部按钮中阻止冒泡的继续传播。每个交互事件都可以接受一个参数,调用这个参数的 `stopPropagation` 方法即可阻止冒泡或捕获的继续传播:
|
||||
|
||||
```tsx
|
||||
import { IActionEvent } from '@motajs/render';
|
||||
|
||||
const click1 = (e: IActionEvent) => {
|
||||
// 调用以阻止冒泡的继续传播
|
||||
e.stopPropagation();
|
||||
console.log('click1');
|
||||
};
|
||||
const click2 = () => {
|
||||
console.log('click2');
|
||||
};
|
||||
|
||||
<container onClick={click2}>
|
||||
<container onClick={click1}></container>
|
||||
</container>;
|
||||
```
|
||||
|
||||
在上面这个例子中,当我们点击内层的容器时,只会触发 `click1`,而不会触发 `click2`,只有当我们点击外层容器时,才会触发 `click2`,这样就成功避免了内外两个按钮同时触发的场景。
|
||||
|
||||
### 事件对象的属性
|
||||
|
||||
事件包含很多属性,它们定义如下,其中 `IActionEventBase` 是 `enter` `leave` 的事件对象,`IActionEvent` 是按下、抬起、移动、点击的事件对象,`IWheelEvent` 是滚轮的事件对象。
|
||||
|
||||
::: code-group
|
||||
|
||||
```ts [IActionEventBase]
|
||||
interface IActionEventBase {
|
||||
/** 当前事件是监听的哪个元素 */
|
||||
target: RenderItem;
|
||||
/** 是触摸操作还是鼠标操作 */
|
||||
touch: boolean;
|
||||
/**
|
||||
* 触发的按键种类,会出现在点击、按下、抬起三个事件中,而其他的如移动等该值只会是 {@link MouseType.None},
|
||||
* 电脑端可以有左键、中键、右键等,手机只会触发左键,每一项的值参考 {@link MouseType}
|
||||
*/
|
||||
type: MouseType;
|
||||
/**
|
||||
* 当前按下了哪些按键。该值是一个数字,可以通过位运算判断是否按下了某个按键。
|
||||
* 例如通过 `buttons & MouseType.Left` 来判断是否按下了左键。
|
||||
* 注意在鼠标抬起或鼠标点击事件中,并不会包含触发的那个按键
|
||||
*/
|
||||
buttons: number;
|
||||
/** 触发时是否按下了 alt 键 */
|
||||
altKey: boolean;
|
||||
/** 触发时是否按下了 shift 键 */
|
||||
shiftKey: boolean;
|
||||
/** 触发时是否按下了 ctrl 键 */
|
||||
ctrlKey: boolean;
|
||||
/** 触发时是否按下了 Windows(Windows) / Command(Mac) 键 */
|
||||
metaKey: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
```ts [IActionEvent]
|
||||
export interface IActionEvent extends IActionEventBase {
|
||||
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
|
||||
identifier: number;
|
||||
/** 相对于触发元素左上角的横坐标 */
|
||||
offsetX: number;
|
||||
/** 相对于触发元素左上角的纵坐标 */
|
||||
offsetY: number;
|
||||
/** 相对于整个画布左上角的横坐标 */
|
||||
absoluteX: number;
|
||||
/** 相对于整个画布左上角的纵坐标 */
|
||||
absoluteY: number;
|
||||
|
||||
/**
|
||||
* 调用后将停止事件的继续传播。
|
||||
* 在捕获阶段,将会阻止捕获的进一步进行,在冒泡阶段,将会阻止冒泡的进一步进行。
|
||||
* 如果当前元素有很多监听器,该方法并不会阻止其他监听器的执行。
|
||||
*/
|
||||
stopPropagation(): void;
|
||||
}
|
||||
```
|
||||
|
||||
```ts [IWheelEvent]
|
||||
export interface IWheelEvent extends IActionEvent {
|
||||
/** 滚轮事件的鼠标横向滚动量 */
|
||||
wheelX: number;
|
||||
/** 滚轮事件的鼠标纵向滚动量 */
|
||||
wheelY: number;
|
||||
/** 滚轮事件的鼠标垂直屏幕的滚动量 */
|
||||
wheelZ: number;
|
||||
/** 滚轮事件的滚轮类型,表示了对应值的单位 */
|
||||
wheelType: WheelType;
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
需要特别说明的是 `identifier` 属性,这个属性在移动端的表现没有异议,但是在电脑端,我们完全可以按下鼠标左键后,再按下鼠标右键,再按下鼠标侧键,抬起鼠标右键,抬起鼠标左键,再抬起鼠标侧键,这种情况下,我们必须单独定义 `identifier` 应该指代的是哪个。它遵循如下原则:
|
||||
|
||||
1. 按下、抬起、点击**永远**保持为同一个 `identifier`
|
||||
2. 移动过程中,使用最后一个按下的按键的 `identifier` 作为移动事件的 `identifier`
|
||||
3. 如果移动过程中,最后一个按下的按键抬起,那么依然会维持**原先的** `identifer`,**不会**回退至上一个按下的按键
|
||||
|
||||
除此之外,滚轮事件中的 `identifier` 永远为 -1。
|
||||
|
||||
## 监听按键操作
|
||||
|
||||
### 注册按键命令
|
||||
|
||||
首先,我们应该注册一个按键命令,我们从 `@motajs/system-action` 中引入 `gameKey` 常量,在模块顶层注册一个按键命令:
|
||||
|
||||
```ts
|
||||
import { gameKey } from '@motajs/system-action';
|
||||
import { KeyCode } from '@motajs/client-base';
|
||||
|
||||
gameKey
|
||||
// 将后面注册的内容形成一个组,在修改快捷键时比较直观
|
||||
// 命名建议为 @ui_[UI 名称]
|
||||
.group('@ui_mybook', '示例怪物手册')
|
||||
.register({
|
||||
// 命名时,建议使用 @ui_[UI 名称]_[按键名称] 的格式
|
||||
id: '@ui_mybook_moveUp',
|
||||
// 在自定义快捷键界面显示的名称
|
||||
name: '上移一个怪物',
|
||||
// 默认按键
|
||||
defaults: KeyCode.ArrowUp
|
||||
})
|
||||
// 可以继续注册其他的,这里不再演示
|
||||
.register({});
|
||||
```
|
||||
|
||||
### 实现按键操作
|
||||
|
||||
然后,我们需要从 `@motajs/render` 中引入 `useKey` 函数,然后在组件顶层这么使用:
|
||||
|
||||
```tsx
|
||||
import { useKey } from '@motajs/render';
|
||||
|
||||
export const MyBook = defineComponent<MyBookProps>(props => {
|
||||
// 第一个参数是按键实例,第二个参数是按键作用域,一般用不到
|
||||
const [key, scope] = useKey();
|
||||
|
||||
return () => <container />;
|
||||
});
|
||||
```
|
||||
|
||||
最后,实现按键操作,使用 `key.realize` 方法:
|
||||
|
||||
```tsx
|
||||
import { clamp } from 'lodash-es';
|
||||
|
||||
export const MyBook = defineComponent<MyBookProps>(props => {
|
||||
const selected = ref(0); // [!code ++]
|
||||
const [key, scope] = useKey();
|
||||
|
||||
// 实现按键操作,让选中的怪物索引减一 // [!code ++]
|
||||
key.realize('@ui_mybook_moveUp', () => {
|
||||
// clamp 函数是 lodash 库中的函数,可以将值限定在指定范围内 // [!code ++]
|
||||
selected.value = clamp(0, enemys.value.length - 1, selected.value - 1); // [!code ++]
|
||||
});
|
||||
|
||||
return () => <container />;
|
||||
});
|
||||
```
|
||||
|
||||
## 绘制选择框与动画
|
||||
|
||||
### 定义选择框动画
|
||||
|
||||
下面我们来把选择框加上,当按下方向键时,选择框会移动,当按下确定键时,会打开这个怪物的详细信息。首先,我们使用一个描边格式的 `g-rectr` 圆角矩形元素作为选择框:
|
||||
|
||||
```tsx
|
||||
<Scroll>
|
||||
<g-rectr loc={[16, 16, 480 - 32, 480 - 32]} stroke strokeStyle="gold" />
|
||||
</Scroll>
|
||||
```
|
||||
|
||||
接下来,我们需要让它能够移动,当用户按下按键时,选择框会平滑移动到目标位置。这时候,我们可以使用动画接口 `transitioned` 来实现平滑移动。我们需要先用它定义一个动画对象:
|
||||
|
||||
```ts
|
||||
// 这个函数在用户代码里面,直接引入
|
||||
import { transitioned } from '../use';
|
||||
// 从高级动画库中引入双曲速率曲线,该曲线视角效果相对较好
|
||||
import { hyper } from 'mutate-animate';
|
||||
|
||||
// 创建一个纵坐标动画对象,初始值为 0(第一个参数),动画时长 150ms(第二个参数)
|
||||
// 曲线为 慢-快-慢 的双曲正弦曲线(第三个参数)
|
||||
const rectY = transitioned(0, 150, hyper('sin', 'in-out'));
|
||||
```
|
||||
|
||||
然后,我们需要通过 `computed` 方法来动态生成圆角矩形的位置:
|
||||
|
||||
```ts
|
||||
const rectLoc = computed(() => [
|
||||
16,
|
||||
// 使用 rectY.ref.value 获取到动画对象的响应式变量
|
||||
rectY.ref.value,
|
||||
480 - 32,
|
||||
480 - 32
|
||||
]);
|
||||
```
|
||||
|
||||
最后,我们把圆角矩形的 `loc` 属性设为 `computed` 值:
|
||||
|
||||
```tsx
|
||||
<Scroll>
|
||||
<g-rectr loc={rectLoc.value} stroke strokeStyle="gold" />
|
||||
</Scroll>
|
||||
```
|
||||
|
||||
### 执行动画
|
||||
|
||||
接下来,我们需要监听当前选中怪物,然后根据当前怪物来设置元素位置,使用 `watch` 监听 `selected` 变量:
|
||||
|
||||
```ts
|
||||
watch(selected, value => {
|
||||
// 使用 set 方法来动画至目标值
|
||||
rectY.set(16 + value * 80);
|
||||
});
|
||||
```
|
||||
|
||||
除此之外,我们还可以添加当鼠标移动至怪物元素上时,选择框也移动至目标,我们需要监听 `onEnter` 事件:
|
||||
|
||||
```tsx
|
||||
const onEnter = (index: number) => {
|
||||
// 前面已经监听过 selected 了,这里直接设置即可,不需要再调用 rectY.set
|
||||
// 不过调用了也不会有什么影响,动画会智能处理这种情况
|
||||
selected.value = index;
|
||||
};
|
||||
|
||||
<Scroll>
|
||||
{/* 把圆角矩形的纵深调大,防止被怪物容器遮挡 */}
|
||||
<g-rectr loc={rectLoc.value} stroke strokeStyle="gold" zIndex={10} />
|
||||
{enemys.map((v, i) => {
|
||||
// 元素内容不再展示。监听时,需要传入一个函数,因此需要使用匿名箭头函数包裹,
|
||||
// 添加 void 关键字是为了防止返回值泄漏,不过在这里并不是必要,因为 onEnter 没有返回值
|
||||
return <container onEnter={() => void onEnter(i)}></container>;
|
||||
})}
|
||||
</Scroll>;
|
||||
```
|
||||
|
||||
### 处理重叠
|
||||
|
||||
如果你去尝试着使用上面这个方法来实现动画,并给每个怪物添加了一个点击事件,你会发现你可能无法触发选中怪物的点击事件,这是因为 `g-rectr` 的纵深 `zIndex` 较高,交互事件会传播至此元素,而不会传播至下层元素,于是就不会触发点击事件。样板自然也考虑到了这种情况,我们只需要给圆角矩形添加一个 `noevent` 标识,即可让交互事件不会受到此元素的影响,不过相应地,这个元素上的交互事件也将会无法触发。示例如下:
|
||||
|
||||
```tsx
|
||||
<Scroll>
|
||||
<g-rectr
|
||||
loc={rectLoc.value}
|
||||
stroke
|
||||
strokeStyle="gold"
|
||||
zIndex={10}
|
||||
// 添加 noevent 标识,事件就不会传播至此元素
|
||||
noevent // [!code ++]
|
||||
/>
|
||||
{enemys.map((v, i) => {
|
||||
return <container onEnter={() => void onEnter(i)}></container>;
|
||||
})}
|
||||
</Scroll>
|
||||
```
|
||||
|
||||
## 调用 Scroll 组件接口
|
||||
|
||||
我们现在已经实现了按键操作,但是移动时并不能同时修改滚动条的位置,这会导致当前选中的怪物跑到画面之外,这时候我们需要自动滚动到目标位置,可以使用 `Scroll` 组件暴露出的接口来实现。我们使用 `ref` 属性来获取其接口:
|
||||
|
||||
```tsx
|
||||
import { ScrollExpose } from './components';
|
||||
|
||||
const scrollExpose = ref<ScrollExpose>();
|
||||
|
||||
<Scroll ref={scrollExpose}></Scroll>;
|
||||
```
|
||||
|
||||
然后,我们可以调用其 `scrollTo` 方法来滚动至目标位置:
|
||||
|
||||
```tsx
|
||||
import { ScrollExpose } from './components';
|
||||
|
||||
const scrollExpose = ref<ScrollExpose>();
|
||||
|
||||
watch(selected, () => {
|
||||
// 滚动到选中怪物上下居中的位置,组件内部会自动处理滚动条边缘,因此不需要担心为负值
|
||||
scrollExpose.value.scrollTo(selected.value * 80 - 240);
|
||||
});
|
||||
|
||||
<Scroll ref={scrollExpose}></Scroll>;
|
||||
```
|
||||
|
||||
## 修改 UI 参数
|
||||
|
||||
在打开 UI 时,我们可以传入参数,默认情况下,可以传入所有的 `BaseProps`,也就是所有元素通用属性,以及自己定义的 UI 参数。`BaseProps` 内容较多,可以参考 [API 文档](../api/motajs-render-vue/RenderItem.md)。除此之外,我们还为这个自定义怪物手册添加了 `floorId` 参数,它也可以在打开 UI 时传入。如果需要打开的 UI 参数具有响应式,例如可以动态修改楼层 id,可以使用 `reactive` 方法。示例如下:
|
||||
|
||||
```ts
|
||||
import { MyBookProps, MyBookUI } from './myUI';
|
||||
|
||||
const props = reactive<MyBookProps>({
|
||||
floorId: 'MT0',
|
||||
zIndex: 100
|
||||
});
|
||||
|
||||
mainUIController.open(MyBookUI, props);
|
||||
```
|
||||
|
||||
我们可以监听状态栏更新来实时更新参数:
|
||||
|
||||
```ts
|
||||
import { hook } from '@user/data-base';
|
||||
|
||||
// 监听状态栏更新事件
|
||||
hook.on('updateStatusBar', () => {
|
||||
// 状态栏更新时,修改怪物手册的楼层为当前楼层 id
|
||||
props.floorId = core.status.floorId,
|
||||
});
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
通过以上的学习,你已经可以做出一个自己的怪物手册了!试着做一下吧!
|
Loading…
Reference in New Issue
Block a user