30 KiB
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 模板,以怪物手册为例,模板如下,直接复制粘贴即可:
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
,增加如下代码:
export * from './myUI';
添加一些内容
新的 UI 使用 tsx 编写,即 TypeScript JSX
,可以直接在 ts 文件中编写 XML,非常适合编写 UI。例如,我们想要把 UI 的位置设为水平竖直居中,位置在 240, 240,长宽为 480, 480,并显示一个文字,可以这么写:
// ... 其他内容
// 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
这个根组件中添加一句话:
// 在这添加引入
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 的状态:
export const MyBook = defineComponent<MyBookProps>(props => {
// 例如,我们可以让它在打开 10 秒钟后关闭:
setTimeout(() => props.controller.close(props.instance), 10000);
return () => (
// ... UI 内容
);
}, myBookProps);
除此之外,我们还可以在任意渲染端模块中引入 ui/controller
来获取到根组件的 UI 控制器,注意跨文件夹引入时需要引入 @user/client-modules
。例如,我们可以在其他文件中控制这个 UI 的开启与关闭:
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
引入:
const { mainUIController } = Mota.require('@user/client-modules');
也可以通过 UIController
的接口获取其实例:
import { UIController } from '@motajs/system-ui';
const mainUIController = UIController.getController('main-ui');
更多的 UI 控制功能可以参考后续文档以及相关的 UI 系统指南 或 API 文档。
添加更多内容
既然我们要编写一个简易怪物手册,那么仅靠上面这些内容当然不够,我们需要更多的元素和组件才行,下面我们来介绍一些常用的元素及组件。
图标
既然是怪物手册,那么图标必然不能少,图标是 <icon>
元素,需要传入 icon
参数,例如:
return () => (
<container>
{/* 显示绿史莱姆图标,位置在 (32, 32),循环播放动画 */}
<icon icon="greenSlime" loc={[32, 32]} animate />
</container>
);
字体
我们很多时候也会想要自定义字体,可以通过 Font
类来实现这个功能:
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 文档
圆角矩形
我们可以为怪物手册的一栏添加圆角矩形,写法如下:
return () => (
<container>
<g-rectr
// 圆角矩形的位置
loc={[16, 16, 480 - 32, 480 - 32]}
// 圆角矩形为仅描边
stroke
// 圆角半径,可以设置四个,具体参考圆角矩形的文档
circle={[8]}
// 描边样式,这里设为了金色
strokeStyle="gold"
/>
</container>
);
线段
我们也可以添加线段,作为怪物列表之间的分割线:
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
组件:
// 从 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
组件,用法如下:
// 从 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
方法来遍历怪物列表,然后返回一个元素,组成元素数组,实现循环遍历的功能。示例如下:
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);
条件判断
可以在表达式中使用三元表达式或者立即执行函数来实现条件判断:
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 的参数,首先我们先定义这个参数:
import { computed } from 'vue';
export interface MyBookProps extends UIComponentProps {
// 定义 floorId 参数
floorId: FloorIds;
}
const myBookProps = {
// 这里也要修改
props: ['controller', 'instance', 'floorId']
} satisfies SetupComponentOptions<MyBookProps>;
然后我们需要在这个参数发生变动时修改怪物列表,可以这么写:
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);
什么样的变量能使用响应式
其实,我们用一般的方式编写的变量或常量都是不能使用响应式的,例如这些都不行:
let num = 10;
let str = '123';
const num2 = computed(() => num * 2);
const str2 = computed(() => parseInt(str));
这么写的话,是没有响应式效果的,这是因为 num
和 str
并不是响应式变量,不能追踪到。对于 string
number
boolean
这些字面量类型的变量,我们需要使用 ref
函数包裹才可以:
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
函数包裹,这个函数会把对象变成深层响应式,任何一级发生更改都会触发响应式更新,例如:
const obj = reactive({ obj1: { num: 10 } });
// 这个就不需要使用 value 属性了,只有 ref 函数包裹的需要
obj.obj1.num = 20;
// 直接调用即可,当值更改时内容也会自动更新
<text text={obj.obj1.num.toString()} />;
数组也可以使用 reactive
方法来实现响应式:
// 传入一个泛型来指定这个变量的类型,这里使用数字数组作为示例
const array = reactive<number[]>([]);
// 可以使用数组自身的方法添加或修改元素
array.push(100);
<container>
{/* 直接对数组遍历,数组修改后这段内容也会自动更新 */}
{array.map(v => (
<text text={v.toString()} />
))}
</container>;
如果对象比较大,只想让第一层变为响应式,深层的不变,可以使用 shallowReactive
或 shallowRef
,或使用 markRaw
手动标记不需要响应式的部分:
// 这样的话,当 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
const num1 = ref(10);
const num2 = ref(20);
watch(num1, (newValue, oldValue) => {
// 当 num1 的值发生变化时,在控制台输出新值和旧值
console.log(newValue, oldValue);
// 这里就不是组件顶层,不要使用 watch。如果需要条件判断的话,可以在监听函数内部判断,而不是外部
watch(num2, () => {});
});
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 官方文档
鼠标与触摸交互事件
监听鼠标或触摸
通过上面这些内容,我们已经可以搭出来一个完整的怪物手册页面了,不过现在这个页面是死的,还没办法交互,我们需要让它有办法交互,允许用户点击和按键操作。UI 系统提供了丰富方便的接口来实现交互动作的监听,例如监听点击可以使用 onClick
:
const click = () => {
console.log('clicked!');
};
// 直接将函数传入 onClick 属性即可
<container onClick={click}>{/* 渲染内容 */}</container>;
可以使用 cursor
属性来指定鼠标移动到该元素上时的指针样式,如下例所示,鼠标移动到这个容器上时就会变成小手的形状:
<container cursor="pointer" />
鼠标与触摸事件的触发包括两个阶段,从根节点捕获,然后一路传递到最下层,然后从最下层冒泡,然后一路再传递回根节点,一般情况下我们使用冒泡阶段的监听即可,也就是 onXxx
,例如 onClick
等,不过如果我们需要监听捕获阶段的事件,也可以使用 onXxxCapture
的方法来监听:
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
: 同上,捕获阶段
触发顺序如下,滚轮单独列出,不在下述顺序中:
downCapture
,按下捕获down
: 按下冒泡moveCapture
: 移动捕获move
: 移动冒泡leave
: 离开元素enter
: 进入元素upCapture
: 抬起捕获up
: 抬起冒泡clickCapture
: 点击捕获click
: 点击冒泡
阻止事件传播
有时候我们需要阻止交互事件的继续传播,例如按钮套按钮时,我们不希望点击内部按钮时也触发外部按钮,这时候我们需要在内部按钮中阻止冒泡的继续传播。每个交互事件都可以接受一个参数,调用这个参数的 stopPropagation
方法即可阻止冒泡或捕获的继续传播:
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
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;
}
export interface IActionEvent extends IActionEventBase {
/** 这次操作的标识符,在按下、移动、抬起阶段中保持不变 */
identifier: number;
/** 相对于触发元素左上角的横坐标 */
offsetX: number;
/** 相对于触发元素左上角的纵坐标 */
offsetY: number;
/** 相对于整个画布左上角的横坐标 */
absoluteX: number;
/** 相对于整个画布左上角的纵坐标 */
absoluteY: number;
/**
* 调用后将停止事件的继续传播。
* 在捕获阶段,将会阻止捕获的进一步进行,在冒泡阶段,将会阻止冒泡的进一步进行。
* 如果当前元素有很多监听器,该方法并不会阻止其他监听器的执行。
*/
stopPropagation(): void;
}
export interface IWheelEvent extends IActionEvent {
/** 滚轮事件的鼠标横向滚动量 */
wheelX: number;
/** 滚轮事件的鼠标纵向滚动量 */
wheelY: number;
/** 滚轮事件的鼠标垂直屏幕的滚动量 */
wheelZ: number;
/** 滚轮事件的滚轮类型,表示了对应值的单位 */
wheelType: WheelType;
}
:::
需要特别说明的是 identifier
属性,这个属性在移动端的表现没有异议,但是在电脑端,我们完全可以按下鼠标左键后,再按下鼠标右键,再按下鼠标侧键,抬起鼠标右键,抬起鼠标左键,再抬起鼠标侧键,这种情况下,我们必须单独定义 identifier
应该指代的是哪个。它遵循如下原则:
- 按下、抬起、点击永远保持为同一个
identifier
- 移动过程中,使用最后一个按下的按键的
identifier
作为移动事件的identifier
- 如果移动过程中,最后一个按下的按键抬起,那么依然会维持原先的
identifer
,不会回退至上一个按下的按键
除此之外,滚轮事件中的 identifier
永远为 -1。
监听按键操作
注册按键命令
首先,我们应该注册一个按键命令,我们从 @motajs/system-action
中引入 gameKey
常量,在模块顶层注册一个按键命令:
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
函数,然后在组件顶层这么使用:
import { useKey } from '@motajs/render';
export const MyBook = defineComponent<MyBookProps>(props => {
// 第一个参数是按键实例,第二个参数是按键作用域,一般用不到
const [key, scope] = useKey();
return () => <container />;
});
最后,实现按键操作,使用 key.realize
方法:
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
圆角矩形元素作为选择框:
<Scroll>
<g-rectr loc={[16, 16, 480 - 32, 480 - 32]} stroke strokeStyle="gold" />
</Scroll>
接下来,我们需要让它能够移动,当用户按下按键时,选择框会平滑移动到目标位置。这时候,我们可以使用动画接口 transitioned
来实现平滑移动。我们需要先用它定义一个动画对象:
// 这个函数在用户代码里面,直接引入
import { transitioned } from '../use';
// 从高级动画库中引入双曲速率曲线,该曲线视角效果相对较好
import { hyper } from 'mutate-animate';
// 创建一个纵坐标动画对象,初始值为 0(第一个参数),动画时长 150ms(第二个参数)
// 曲线为 慢-快-慢 的双曲正弦曲线(第三个参数)
const rectY = transitioned(0, 150, hyper('sin', 'in-out'));
然后,我们需要通过 computed
方法来动态生成圆角矩形的位置:
const rectLoc = computed(() => [
16,
// 使用 rectY.ref.value 获取到动画对象的响应式变量
rectY.ref.value,
480 - 32,
480 - 32
]);
最后,我们把圆角矩形的 loc
属性设为 computed
值:
<Scroll>
<g-rectr loc={rectLoc.value} stroke strokeStyle="gold" />
</Scroll>
执行动画
接下来,我们需要监听当前选中怪物,然后根据当前怪物来设置元素位置,使用 watch
监听 selected
变量:
watch(selected, value => {
// 使用 set 方法来动画至目标值
rectY.set(16 + value * 80);
});
除此之外,我们还可以添加当鼠标移动至怪物元素上时,选择框也移动至目标,我们需要监听 onEnter
事件:
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
标识,即可让交互事件不会受到此元素的影响,不过相应地,这个元素上的交互事件也将会无法触发。示例如下:
<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
属性来获取其接口:
import { ScrollExpose } from './components';
const scrollExpose = ref<ScrollExpose>();
<Scroll ref={scrollExpose}></Scroll>;
然后,我们可以调用其 scrollTo
方法来滚动至目标位置:
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 文档。除此之外,我们还为这个自定义怪物手册添加了 floorId
参数,它也可以在打开 UI 时传入。如果需要打开的 UI 参数具有响应式,例如可以动态修改楼层 id,可以使用 reactive
方法。示例如下:
import { MyBookProps, MyBookUI } from './myUI';
const props = reactive<MyBookProps>({
floorId: 'MT0',
zIndex: 100
});
mainUIController.open(MyBookUI, props);
我们可以监听状态栏更新来实时更新参数:
import { hook } from '@user/data-base';
// 监听状态栏更新事件
hook.on('updateStatusBar', () => {
// 状态栏更新时,修改怪物手册的楼层为当前楼层 id
props.floorId = core.status.floorId,
});
总结
通过以上的学习,你已经可以做出一个自己的怪物手册了!试着做一下吧!