mirror of
https://github.com/unanmed/HumanBreak.git
synced 2025-04-30 11:23:24 +08:00
838 lines
35 KiB
Markdown
838 lines
35 KiB
Markdown
# UI 编写
|
||
|
||
新样板对 UI 系统进行了完全性重构,构建了一种新的 UI 系统。当然,你还可以继续使用旧的`createCanvas`流程进行 UI 编写,但是在新版 UI 的加持下,你可以更方便地管理多级 UI 以及内嵌 UI。同时,新版 UI 系统也提供了画布 API,你依然可以通过画布进行 UI 绘制及交互处理。
|
||
|
||
注意,本页所说内容全部基于[渲染进程](./system.md#进程分离),请注意进程分离。
|
||
|
||
本页面注重于 UI 编写的指引,如果你想了解新版 UI 系统,请查看[UI 系统](./ui-control.md)
|
||
|
||
## VNode
|
||
|
||
新版 UI 系统基于 Vue 的 `h` 函数,它用于生成出 `VNode`,当显示时可以直接渲染到页面上。而对于新版 UI,可以认为是一种 `VNode` 生成器,用于生成出模板化的 `VNode`,从而简化了 API,避免写出冗长的 `h` 函数链。因此,应当注意区分 `VNode` 与新版 UI 系统。
|
||
|
||
## MComponent
|
||
|
||
对于新版 UI,应当使用系统提供的类 `MComponent` 进行编写。它的类型过于复杂,这里不再列出。下面我们将会以怪物手册为例,一步步做出一个属于你自己的怪物手册。
|
||
|
||
## 创建组件
|
||
|
||
想要写出一个 UI,就要先创建一个组件,我们可以直接构造`MComponent`类,也可以通过函数`m`进行创建:
|
||
|
||
```js
|
||
const MComponent = Mota.require('class', 'MComponent');
|
||
const m = Mota.require('fn', 'm');
|
||
|
||
const myUI = new MComponent();
|
||
// 等价于
|
||
const myUI = m();
|
||
```
|
||
|
||
因此,方便起见,我们更多地使用`m`函数创建组件。
|
||
|
||
## 编写 UI
|
||
|
||
创建组件之后,我们就可以编写 UI 了。我们首先来看下面这些基础函数:
|
||
|
||
```ts
|
||
// 在 MComponent 类上,也就是创建的组件上
|
||
function div(children?: any | any[], config?: any): this
|
||
function span(children?: any | any[], config?: any): this
|
||
function canvas(config?: any): this
|
||
function text(text: string | (() => string), config?: any): this
|
||
```
|
||
|
||
这些函数都是渲染单独内容的,分别是渲染 `div` `span` `canvas` 和文字。例如,我想在 UI 上渲染出一个画布,和一句话,可以写么写:
|
||
|
||
```js
|
||
myUI
|
||
.canvas()
|
||
.text('这是要渲染的内容');
|
||
```
|
||
|
||
这便是这四个函数的最基础的用法。除此之外,文字还可以传入一个函数,每次渲染 UI 的时候都会获取其返回值作为渲染内容,也就可以实现单个组件在不同条件下渲染内容不同了。例如:
|
||
|
||
```js
|
||
let cnt = 0;
|
||
core.registerAnimationFrame('example', true, () => {
|
||
cnt++; // 每帧让 cnt 加一
|
||
});
|
||
myUi.text(() => cnt.toString()); // 显示文字,文字是一个函数,返回cnt
|
||
// 其效果便是每次显示这个组件的时候,都会显示出cnt的值,注意并不会实时更新,因为cnt不是一个响应式变量
|
||
// 如果想要实时更新,请了解vue的响应式机制
|
||
```
|
||
|
||
对于 `div` 和 `span`,还可以添加子内容,例如可以在 `div` 里面套一个 `div`,再套一个 `span`,再套一个 `canvas`。每个子内容都可以是一个 `MComponent` 组件,或其数组:
|
||
|
||
```js
|
||
myUi
|
||
.div(
|
||
m()
|
||
.div(
|
||
m()
|
||
.span(
|
||
m().canvas()
|
||
)
|
||
)
|
||
);
|
||
```
|
||
|
||
这样,我们就实现了元素的嵌套功能,布局也能够更加灵活。对于 `config` 参数,由于其非常复杂,请参考本页之后的内容。
|
||
|
||
## 导出并渲染 UI
|
||
|
||
当我们将 UI 编写完毕后,我们并不能直接渲染在页面上,我们需要将它导出,使用 `export` 函数:
|
||
|
||
```js
|
||
const show = myUI.export();
|
||
```
|
||
|
||
然后我们需要将其注册为一个 `GameUi`。这里都是模式化的编写方式,直接照葫芦画瓢即可,一般不需要理解很多。详见[UI 系统](./ui-control.md)
|
||
|
||
```js
|
||
// 这里 mainUi 指的是有优先级的 UI,例如怪物手册及怪物详细信息
|
||
// 而 fixedUi 指的是没有优先级的 UI,例如状态栏与自定义工具栏,多个自定义工具栏等
|
||
const { mainUi, fixedUi } = Mota.requireAll('var');
|
||
const { GameUi } = Mota.requireAll('class');
|
||
|
||
// 注册使用 register 函数
|
||
// 这里的 'myUI' 指的是 UI 名称,而第二个参数指的是 UI 内容。
|
||
mainUi.register(new GameUi('myUI', myUI));
|
||
// fixedUi 注册也是同样,mainUi 与 fixedUi 间可以重名
|
||
fixedUi.register(new GameUi('myUI', myUI));
|
||
```
|
||
|
||
下面我们可以来打开这个 UI 了:
|
||
|
||
```js
|
||
// 直接调用 open 函数即可打开 UI
|
||
mainUi.open('myUI');
|
||
```
|
||
|
||
::: tip
|
||
UI 的导出具有顺序无关性,也就是说,如果导出后继续新增内容,新增的内容不需要重新导出也可以显示。
|
||
:::
|
||
|
||
## 列表渲染
|
||
|
||
我们可以对一个列表进行渲染,例如我们要渲染怪物手册,就需要获取每个怪物信息,这时,我们就需要用到列表渲染了:
|
||
|
||
```ts
|
||
function vfor(items: any[] | (() => any[]), map: (value: any, index: number) => VNode): this
|
||
```
|
||
|
||
函数要求传入两个参数:
|
||
|
||
- `items`: 要渲染的列表,是一个数组,或者是一个返回数组的函数。
|
||
- `map`: 渲染函数,接受列表项与索引作为参数,返回 `VNode`,注意不是 `MComponent` 组件。
|
||
|
||
除此之外,我们还会用到 `MComponent` 上的静态函数 `vNodeS`:
|
||
|
||
```ts
|
||
function vNodeS(mc: MotaComponent, mount?: number): VNode
|
||
```
|
||
|
||
这个函数用于将单个渲染内容生成为单个的 `VNode`。
|
||
|
||
为了生成单个渲染内容,我们需要获取到模块 `MCGenerator`,使用它上面的函数,`MComponent` 上的函数也是基于它的。
|
||
|
||
例如,我获取了所有怪物的信息,需要渲染每个怪物的名称,就可以这么写:
|
||
|
||
```js
|
||
// 首先获取渲染内容生成器,用于生成渲染内容
|
||
const { text, div } = Mota.require('module', 'MCGenerator');
|
||
// 定义渲染列表,例如这里我们就要渲染当前楼层的所有怪物
|
||
// 由于当前楼层id会随着游戏进行而变化,因此是一个动态列表,因此需要函数包裹,这样可以确保每次获取的都是正确的楼层
|
||
const enemys = () => core.getCurrentEnemys();
|
||
|
||
myUI.vfor(enemys, (value, index) => {
|
||
// 要求返回一个 VNode,因此需要经过 MComponent.vNodeS 的包裹
|
||
return MComponent.vNodeS(
|
||
// 输出内容,是一个由div包裹的文字显示
|
||
div(text(value.enemy.enemy.name))
|
||
);
|
||
})
|
||
```
|
||
|
||
这样,我们就把每个怪物的名称渲染出来了!相比于直接使用 `canvas` 绘制,是不是简便了很多。
|
||
|
||
## 渲染组件
|
||
|
||
假如我们在很多组件里面需要共用一部分内容,每个组件里面都写一遍显然很麻烦,因此我们可以把这部分组件单独拿出来,然后做成一个组件,供其他组件使用。而这也是 `Vue` 组件一个相当强大的功能,在 `MComponent` 组件中也有相应的功能。
|
||
|
||
例如,我们先编写一个组件,内容是显示 `lhjnb`:
|
||
|
||
```js
|
||
const lhjnb = m()
|
||
.div(
|
||
m().text('lhjnb')
|
||
)
|
||
.export();
|
||
```
|
||
|
||
然后,我们可以通过调用 `com` 函数来渲染组件。`com` 函数用法如下:
|
||
|
||
```ts
|
||
function com(component: any, config?: any): this
|
||
```
|
||
|
||
我们就可以将这个组件添加到我们的 UI 中了:
|
||
|
||
```js
|
||
// 传入 lbjnb 组件
|
||
myUI.com(lhjnb);
|
||
```
|
||
|
||
对于传入的组件,也可以是一个 `MComponent` 未导出组件,这样的话,这个未导出组件会被视为与当前组件在同一级,对于组件分级,请查看[组件嵌套关系](#组件嵌套关系)
|
||
|
||
## 内置组件
|
||
|
||
样板还内置了部分组件,以及一些包装好的函数可以使用。你甚至还可以把样板自带的 UI 渲染到你的 UI 上,当然由于样式的改变,效果可能会不尽人意。例如,样板就内置了一个渲染图标的组件,它使用 `canvas` 逐帧绘制实现:
|
||
|
||
```ts
|
||
// 在模块 MCGenerator 中
|
||
function icon(
|
||
id: string,
|
||
width?: number,
|
||
height?: number,
|
||
noBoarder?: number // 怪物图标是否设置为无边框无背景形式
|
||
): VNode
|
||
```
|
||
|
||
例如,这个时候我就可以给怪物手册中添加一个图标了:
|
||
|
||
```js {8-9}
|
||
const { text, div, icon } = Mota.require('module', 'MCGenerator');
|
||
const enemys = () => core.getCurrentEnemys();
|
||
|
||
myUI.vfor(enemys, (value, index) => {
|
||
return MComponent.vNodeS(
|
||
// 输出先由一个 div 包裹,参数可以填数组,因此这里直接填入了数组作为 children
|
||
div([
|
||
// 渲染图标
|
||
icon(value.enemy.id),
|
||
// 渲染怪物名称
|
||
text(value.enemy.enemy.name)
|
||
])
|
||
);
|
||
})
|
||
```
|
||
|
||
## 组件嵌套关系
|
||
|
||
对于任何组件,其子组件都不应该影响父组件本身,而在这里也是如此。除非你使用全局变量作为桥梁,任何子组件都不会影响父组件的渲染与参数等。
|
||
|
||
在渲染子组件时,我们可以选择子组件是否经过 `export`,而经不经过导出处理,最终结果是不一样的。具体表现如下:
|
||
|
||
- 如果子组件经过了 `export` 处理,那么子组件会被视为新一级的组件
|
||
- 而如果没有经过 `export` 处理,则会视为当前级组件
|
||
- 组件是否为当前级影响着画布的获取,一个组件在接口中只能获取到当前级的画布,而不能获取其子级画布
|
||
|
||
获取画布接口请参考[钩子](#钩子)
|
||
|
||
如果不使用 `MComponent` 进行渲染,直接调用 `MComponent.vNode` 等函数生成 `VNode`,依然会被视为一级新的组件。
|
||
|
||
## 条件渲染
|
||
|
||
很多时候我们会根据一些条件去判断一个内容是否渲染,样板同样有相关的接口。接口在 `config` 参数中:
|
||
|
||
```ts
|
||
interface Config {
|
||
vif: () => boolean
|
||
velse: boolean
|
||
}
|
||
```
|
||
|
||
其中`vif`表示渲染条件,不填时视为永远满足,当满足这个条件的时候渲染内容,`velse`表示是否是否则选项,当`velse`与`vif`同时出现时,效果等同于`else if`。于是,我们便可以依此来判断怪物的特殊属性,如果没有特殊属性,那么就渲染为`无属性`,否则渲染怪物的属性列表。
|
||
|
||
```js {16-25}
|
||
const { text, div, icon, vfor, span } = Mota.require('module', 'MCGenerator');
|
||
const enemys = () => core.getCurrentEnemys();
|
||
// 用于获取怪物应该显示在手册上的信息的函数
|
||
const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed;
|
||
|
||
myUI.vfor(enemys, (value, index) => {
|
||
// 首先要获取怪物的详细信息
|
||
const detail = getDetailedEnemy(value.enemy);
|
||
return MComponent.vNodeS(
|
||
// 输出先由一个 div 包裹,参数可以填数组,因此这里直接填入了数组作为 children
|
||
div([
|
||
// 渲染图标
|
||
icon(value.enemy.id),
|
||
// 渲染怪物名称
|
||
text(value.enemy.enemy.name),
|
||
// 当怪物拥有特殊属性时
|
||
div([
|
||
// 渲染怪物的特殊属性,还需要一层 for 嵌套
|
||
vfor(detail.special, ([name, desc, color], index) => {
|
||
// 这里的special是一个数组,第一项表示名称,第二项表示说明,第三项表示名称颜色
|
||
// 这里我们只渲染名称
|
||
return span(text(name))
|
||
})
|
||
], { vif: () => detail.special.length > 0 }), // 当怪物拥有特殊属性时
|
||
div(span(text('无属性')), { velse: true }) // 否则渲染无属性
|
||
])
|
||
);
|
||
})
|
||
```
|
||
|
||
## 传递参数
|
||
|
||
我们会发现,`icon` 函数可以将怪物 id 传入组件中并显示出来,这种行为叫做传递`props`,在这里我们也称为传递参数。拥有了传递参数的功能,我们可以接收来自父组件的参数,同时根据父组件的意愿渲染出对应的内容。
|
||
|
||
例如,就拿样板内置的 `BoxAnimate` 组件为例,也就是 `icon` 函数渲染的组件,我们可以通过 `config` 传递参数。对于每个 `props`,都要求是一个函数,其返回值作为真正传递的参数。如果想要传递函数,那么就需要是一个返回函数的函数。如果只想传递一个常量,又不想写一个函数,那么可以使用 `MCGenerator` 提供的 `f` 函数。
|
||
|
||
```js
|
||
const { BoxAnimate } = Mota.require('module', 'UIComponents');
|
||
const { f } = Mota.require('module', 'MCGenerator');
|
||
|
||
myUI.com(BoxAnimate, { props: { id: f('redSlime') } });
|
||
// 这个就等效于使用 icon 函数:
|
||
myUI.h(icon('redSlime'));
|
||
```
|
||
|
||
## 定义参数
|
||
|
||
如果我们想给自己的组件定义一些参数,可以使用 `defineProps` 函数:
|
||
|
||
```ts
|
||
function defineProps(props: Record<string, any>): this
|
||
```
|
||
|
||
这个函数传入一个对象,其键表示参数名称,值表示参数类型,例如:
|
||
|
||
```js
|
||
const com = m().defineProps({
|
||
id: String, // 对于字符串、数字、布尔值,类型表示为String, Number, Boolean
|
||
num: Number,
|
||
ui: GameUi // 对于对象,可以表示为 Object 或者类名
|
||
})
|
||
```
|
||
|
||
这样,我们就可以向这个组件传递参数了,如果参数类型不正确,或者传递的参数并没有定义,依然可以正确运行,不过会在控制台输出警告。例如:
|
||
|
||
```js
|
||
const { f } = Mota.require('module', 'MCGenerator');
|
||
// 传入lhjnb作为参数,但是由于还有两个参数没有传递,会在控制台有报错,不过依然可以正确运行
|
||
myUI.com(com, { props: { id: f('lhjnb') } });
|
||
```
|
||
|
||
对于如何获取传入的参数,请参考[钩子](#钩子)
|
||
|
||
## 传递 Attribute
|
||
|
||
我们还可以向浏览器的 DOM 元素中添加信息,只要我们将值传递给 `props` 即可,例如我们可以给一个 `span` 元素添加 id,设置样式等。就拿上面的怪物特殊属性举例,我们如果想要让不同的属性显示出对应的颜色,那么可以这么写:
|
||
|
||
```js {15-18}
|
||
const { text, div, icon, vfor, span } = Mota.require('module', 'MCGenerator');
|
||
const enemys = () => core.getCurrentEnemys();
|
||
const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed;
|
||
|
||
myUI.vfor(enemys, (value, index) => {
|
||
const detail = getDetailedEnemy(value.enemy);
|
||
return MComponent.vNodeS(
|
||
div([
|
||
// 渲染图标
|
||
icon(value.enemy.id),
|
||
// 渲染怪物名称
|
||
text(value.enemy.enemy.name),
|
||
// 当怪物拥有特殊属性时
|
||
div([
|
||
vfor(detail.special, ([name, desc, color], index) => {
|
||
// 将 style 作为 props 传入即可,这样颜色就能正确显示出来了
|
||
return span(text(name), { props: { style: f(`color: ${color}`) } });
|
||
})
|
||
], { vif: () => detail.special.length > 0 }), // 当怪物拥有特殊属性时
|
||
div(span(text('无属性')), { velse: true }) // 否则渲染无属性
|
||
])
|
||
);
|
||
})
|
||
```
|
||
|
||
## 监听事件与 emits
|
||
|
||
对于 `props`,有一类特殊的属性,由 `on` 开头,然后紧跟着大写字母,这种形式的 `props` 会被视为监听函数。例如,我想监听一个 `div` 的点击事件,当玩家点击这个 `div` 时,在控制台输出 `clicked!`,那么可以这么写:
|
||
|
||
```js
|
||
myUI.div(
|
||
[], // 没有子元素,填空数组
|
||
{
|
||
props: {
|
||
onClick: f((e) => {
|
||
// 当点击时,输出 clicked! 与点击位置相对于浏览器左上角的坐标
|
||
console.log('clicked!', e.clientX, e.clientY);
|
||
})
|
||
}
|
||
}
|
||
)
|
||
```
|
||
|
||
对于自定义组件,依然可以使用同样的方法监听其 `emits`,`emits` 可以认为是组件的事件,可以在组件内部触发。例如,对于样板自带的 `Column` 组件,它有一个 `close` 事件,我们便可以用这种方式进行监听:
|
||
|
||
```js
|
||
const { Column } = Mota.require('module', 'UIComponents');
|
||
|
||
myUI.com(
|
||
Column,
|
||
{
|
||
props: {
|
||
// 监听close事件,所以名为 onClose
|
||
onClose: f(() => {
|
||
// 当 close 事件触发时,会在控制台输出 close emitted!
|
||
console.log('close emitted!');
|
||
})
|
||
}
|
||
}
|
||
)
|
||
```
|
||
|
||
你也可以为自己的组件定义自己的 `emits`,通过 `defineEmits` 函数:
|
||
|
||
```js
|
||
myUI.defineEmits(['myEmit']); // 传入一个字符串数组,表示组件的 emits 名称列表
|
||
```
|
||
|
||
这样,你就可以在其他地方监听这个组件的 `emits` 了
|
||
|
||
## 渲染函数
|
||
|
||
`MComponent` 上也有一个名为 `h` 的渲染函数,但是它与 `Vue` 的渲染函数不同。在这里,`h` 函数的用法如下:
|
||
|
||
```ts
|
||
function h(type: any, children?: any | any[], config?: any): this
|
||
```
|
||
|
||
上面用到的所有函数,包括 `div` `span` `canvas` `text` `com` 在内的函数都基于这个函数。借助于这个函数,我们可以更加灵活地编写我们的 UI。
|
||
|
||
对于第一个参数,它可以填写这些内容:
|
||
|
||
- `text`: 表示渲染文字,等价于 `text` 函数,需要填写 `config` 参数的 `innerText` 属性
|
||
- `component`: 表示渲染组件,需要在 `config` 参数中填写 `component` 属性或 `dComponent` 属性,详见[动态组件](#动态组件)
|
||
- 任何 DOM 元素的名称,例如 `span` `li` `input` 等,表示渲染 DOM 元素
|
||
- 任何 `MComponent` 组件,表示渲染组件,等价于填写 `component`,然后填写对应的 `config` 属性,也等价于 `com` 函数
|
||
- 任何导出后的组件,或者是样板自带组件,与填写 `MComponent` 时类似
|
||
|
||
例如:
|
||
|
||
```js
|
||
myUI
|
||
.h('div')
|
||
// 注意,如果你确保递归调用组件没问题,那么你可以按下面这样递归调用自身作为组件,否则出问题概不负责!
|
||
.h('component', [], { component: myUI })
|
||
.h('text', [], { innerText: 'lhjnb' });
|
||
```
|
||
|
||
## 动态组件
|
||
|
||
当 `h` 函数传入 `component` 作为类型时,可以作为动态组件使用,要求填写 `config` 参数的 `component` 属性或者 `dComponent` 属性。填写 `component` 属性时,等价于 `com` 函数,这里不再赘述,下面我们来看填写 `dComponent` 属性时的情况。
|
||
|
||
这个属性要求传入一个函数,函数返回一个导出组件,注意不能是 `MComponent` 组件,也不能是 `VNode`。例如:
|
||
|
||
```js
|
||
const randomComponent = [BoxAnimate, Box, Column];
|
||
// 随机渲染一个组件上去!
|
||
myUI.h(
|
||
'component',
|
||
[],
|
||
{ dComponent: () => randomComponent[Math.floor(Math.random() * 3)] }
|
||
);
|
||
```
|
||
|
||
## 传递插槽
|
||
|
||
插槽的功能是向子组件中渲染内容,在目前样板中,没有提供定义插槽的方法,只能向子组件中传递插槽。这也意味着,你只能向样板内置组件或者 `Vue` 内置组件中传递插槽。不过,在[自定义 setup](#自定义-setup)中,提供了另一种方式可以渲染插槽(定义插槽)
|
||
|
||
你可以通过 `config` 参数的 `slots` 属性传递插槽,每个插槽都是一个函数,要求返回一个 `VNode` 或其数组用于渲染。例如,对于滚动条组件 `Scroll`,它有一个默认插槽 `default`,你可以这样传递插槽:
|
||
|
||
```js
|
||
const { icon, div } = Mota.require('module', 'MCGenerator');
|
||
myUI.com(Scroll, [], {
|
||
slots: {
|
||
// 传递默认插槽
|
||
default: () => {
|
||
// 返回 VNode,使用 MComponent 上的静态函数 vNode,它可以将 MCGenerator 生成的渲染内容生成为 vNode
|
||
return MComponent.vNode([
|
||
div(icon('greenSlime')),
|
||
div(icon('redSlime'))
|
||
]);
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
## 钩子
|
||
|
||
下面是样板提供的几个钩子接口。什么是钩子呢?就是在组件渲染的不同时刻执行传入的函数。目前样板中,提供了两个接口,分别是:
|
||
|
||
- `onSetup`: 当组件开始渲染时,这时组件内容还没有挂载到页面上
|
||
- `onMounted`: 当组件渲染完毕时,这时组件已经完全渲染到了页面上
|
||
|
||
这两个函数都会接收两个参数,分别是 `props` 和 `ctx`。
|
||
|
||
- `props`: 表示父组件传递过来的参数(`props`)
|
||
- `ctx`: `setup` 上下文,用于触发 `emits`,渲染插槽(定义插槽)等,不过在这里渲染插槽没有用处
|
||
|
||
例如:
|
||
|
||
```js
|
||
myUI.onSetup((props, ctx) => {
|
||
// 输出父组件传递的参数
|
||
console.log(props);
|
||
// 触发名为 close 的 emits,同时将 123 作为数据输出,数据可以由父组件的监听函数的参数中获取
|
||
ctx.emits('close', 123);
|
||
});
|
||
```
|
||
|
||
对于 `onMounted` 函数,还会另外多一个参数,描述的是当前级组件(不包括子组件,但包括 `MComponent` 未导出组件,详见[组件嵌套关系](#组件嵌套关系))中所有的画布。例如:
|
||
|
||
```js
|
||
myUI
|
||
// 创建一个 id 为 my-canvas 的画布
|
||
.canvas({ props: { id: f('my-canvas') } })
|
||
// 当组件渲染完毕时
|
||
.onMounted((props, ctx, canvas) => {
|
||
const myCanvas = canvas[0];
|
||
if (myCanvas?.id === 'my-canvas') {
|
||
// 获取画布的绘制上下文
|
||
const ctx = myCanvas.getContext('2d');
|
||
// 绘制一条线,也可以使用样板的 core.drawLine 等 api 绘制
|
||
ctx.moveTo(0, 0);
|
||
ctx.lineTo(200, 200);
|
||
ctx.stroke();
|
||
}
|
||
});
|
||
```
|
||
|
||
## 自定义 setup
|
||
|
||
自定义 `setup` 允许你完全控制组件的渲染内容,并在组件渲染时执行脚本。它包含两个接口:
|
||
|
||
- `ret`: 设置 `setup` 的返回值,即组件的最终渲染函数
|
||
- `setup`: 完全重写设置组件的 `setup` 函数,让组件完全自定义
|
||
|
||
`setup` 的含义是,这个组件被渲染的时候,执行的函数
|
||
|
||
例如,你可以通过 `ret` 接口来修改你的组件的渲染内容,而由于可以直接操作渲染内容,你便可以做到渲染插槽(定义插槽)
|
||
|
||
```js
|
||
const { icon, div, span, text } = Mota.require('module', 'MCGenerator');
|
||
myUI.ret((props, ctx) => {
|
||
// 函数要求返回 VNode,因此要有 vNode 函数包裹
|
||
// 注意在这里请尽量避免使用导出组件直接作为返回值,这会对性能造成一定的影响
|
||
return MComponent.vNode([
|
||
span(text('lhjnb')),
|
||
div(icon('greenSlime')),
|
||
ctx.slots.defaults(), // 渲染插槽(定义插槽)
|
||
]);
|
||
})
|
||
```
|
||
|
||
还可以通过 `setup` 函数完全重写组件。对于稍微复杂点的组件,我们都需要使用这个功能。
|
||
|
||
::: tip
|
||
重写 setup 函数后,应当重新 export 组件,否则内容不会变化
|
||
:::
|
||
|
||
```js
|
||
// 这里就需要获取 vue 的函数了,还可以使用 ref 实现响应式布局
|
||
const { onMounted, ref } = Mota.Package.require('vue');
|
||
|
||
myUI.setup((props, ctx) => {
|
||
// 当组件渲染完毕时
|
||
onMounted(() => {
|
||
console.log('mounted!');
|
||
});
|
||
|
||
const cnt = ref(0);
|
||
|
||
// setup 函数要返回一个函数,函数返回值为 VNode
|
||
return () => {
|
||
return MComponent.vNode([
|
||
span(text('lhjnb')),
|
||
div(icon('greenSlime')),
|
||
span(text(cnt.value)) // 使用响应式变量cnt,当其更改时,渲染内容也会随之更改
|
||
]);
|
||
}
|
||
})
|
||
```
|
||
|
||
实际上,样板自带的 `setup` 函数也是使用它来实现的:
|
||
|
||
```ts
|
||
const setup = (props, ctx) => {
|
||
const mountNum = MComponent.mountNum++;
|
||
this.onSetupFn?.(props, ctx);
|
||
|
||
onMounted(() => {
|
||
this.onMountedFn?.(
|
||
props,
|
||
ctx,
|
||
Array.from(
|
||
document.getElementsByClassName(
|
||
`--mota-component-canvas-${mountNum}`
|
||
) as HTMLCollectionOf<HTMLCanvasElement>
|
||
)
|
||
);
|
||
});
|
||
|
||
if (this.retFn) return () => this.retFn!(props, ctx);
|
||
else {
|
||
return () => {
|
||
const vNodes = MComponent.vNode(
|
||
this.content,
|
||
mountNum
|
||
);
|
||
return vNodes;
|
||
};
|
||
}
|
||
}
|
||
```
|
||
|
||
## 怪物手册示例
|
||
|
||
下面我们将做一个简易的怪物手册作为 UI 示例。在这里,我们使用样板组件 `Column` 进行编写,它可以让一个 UI 分为两栏,同时还可以设置大小,及左右栏占比,对于制作说明式的 UI 非常有用。示例中包含了按键操作的说明,详见[按键系统](./hotkey.md)
|
||
|
||
内容较长,可以复制到编辑器内查看。
|
||
|
||
```js
|
||
// 首先引入需要的接口,注意以下所有内容均基于渲染进程
|
||
const { MComponent, GameUi } = Mota.requireAll('class');
|
||
const { getDetailedEnemy } = Mota.require('module', 'UITools').fixed;
|
||
const { Column } = Mota.require('module' ,'UIComponents');
|
||
const { com, div, span, text, icon, f, vfor } = Mota.require('module', 'MCGenerator');
|
||
const { mainUi, gameKey } = Mota.requireAll('var');
|
||
const { m } = Mota.requireAll('fn');
|
||
const { ref, computed, onUnmounted } = Mota.Package.require('vue');
|
||
|
||
// 怪物手册属于较为复杂的内容,必须要完全重写 setup
|
||
const myBook = m().setup((props, ctx) => {
|
||
const close = () => {
|
||
// 对于每个直接由 UI 系统打开的 UI,都会有一个 num 参数,表示这个 UI 的标识符
|
||
// 这个标识符可以用于关闭 UI,例如下面就是关闭这个 UI
|
||
// 更多内容请查阅 UI 系统章节
|
||
mainUi.close(props.num);
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
// 当关闭 UI 时,跳出当前层按键作用域
|
||
// 注意由于 UI 可能是被其他原因被关闭的,不会执行 close 函数,因此要使用 onUnmounted 钩子
|
||
gameKey.dispose(props.ui.symbol);
|
||
});
|
||
|
||
const enemys = core.getCurrentEnemys();
|
||
|
||
// 对于 number string boolean,要做响应式需要 ref,对于对象,需要 reactive,更多信息请了解 vue 的响应式
|
||
const selected = ref(0);
|
||
// computed 会自动追踪用到的响应式变量,同时在响应式变量更改时更改 enemy 的值
|
||
const enemy = computed(() => enemys[selected.value]);
|
||
|
||
// 按键系统,首先创建新作用域,然后实现按键操作
|
||
gameKey
|
||
.use(props.ui.symbol)
|
||
// 上移一位
|
||
.realize('@book_up', () => {
|
||
if (selected.value > 0) selected.value--;
|
||
})
|
||
// 下移一位
|
||
.realize('@book_down', () => {
|
||
if (selected.value < enemys.length - 1) selected.value++;
|
||
})
|
||
// 下移5位
|
||
.realize('@book_pageDown', () => {
|
||
if (selected.value >= enemys.length - 5) {
|
||
selected.value = enemys.length - 1;
|
||
} else {
|
||
selected.value += 5;
|
||
}
|
||
})
|
||
// 上移5位
|
||
.realize('@book_pageUp', () => {
|
||
if (selected.value <= 4) {
|
||
selected.value = 0;
|
||
} else {
|
||
selected.value -= 5;
|
||
}
|
||
})
|
||
// 退出
|
||
.realize('exit', () => {
|
||
close();
|
||
});
|
||
|
||
// 要渲染的怪物属性
|
||
const status = [
|
||
['hp', '生命', 'lightgreen']
|
||
['atk', '攻击', 'lightcoral'],
|
||
['def', '防御', 'lightblue'],
|
||
['exp', '经验', 'lightgreen'],
|
||
['gold', '金币', 'lightyellow']
|
||
];
|
||
|
||
// 简写数据格式化
|
||
const format = core.formatBigNumber;
|
||
|
||
// 属性样式,由于多次用到在这里声明为一个常量
|
||
const statusStyle = {
|
||
props: {
|
||
style: f(`
|
||
display: flex;
|
||
flex-direction: row;
|
||
color: ${color};
|
||
`)
|
||
}
|
||
};
|
||
const statusSpanStyle = {
|
||
props: {
|
||
style: f(`
|
||
flex-basis: 30%;
|
||
text-align: right;
|
||
margin-right: 5%;
|
||
`)
|
||
}
|
||
}
|
||
|
||
return () => {
|
||
// 要求返回 VNode。对于大部分情况,如果你不确定是否要包裹 vNode 的话,包裹一层不会有任何副作用
|
||
return MComponent.vNode([
|
||
com(Column, {
|
||
// Column 组件共有四个参数,分别是宽高,与左右占比,单位都是百分比
|
||
// 宽高作为百分比时描述的一般是相对于父组件宽高的占比,对于根组件自然就是屏幕的占比了
|
||
props: {
|
||
width: f(70),
|
||
height: f(70),
|
||
left: f(30),
|
||
right: f(70),
|
||
onClose: f(close) // Column 组件自带关闭的功能,监听 close 事件即可,然后将 close 函数传入
|
||
},
|
||
// Column 组件包含名为 left 和 right 的插槽,向其中传入要渲染的内容即可
|
||
// 在这里,左侧渲染怪物列表,右侧渲染怪物信息
|
||
slots: {
|
||
// 左侧内容,为当前楼层的怪物列表
|
||
left: () => {
|
||
return MComponent.vNode([
|
||
// 列表渲染,遍历函数要返回单个 VNode
|
||
vfor(enemys, (value, index) => {
|
||
return MComponent.vNodeS(
|
||
div(
|
||
[
|
||
// 渲染怪物的图标,设置为无边框与背景
|
||
icon(value.enemy.id, void 0, void 0, true),
|
||
// 然后是怪物名称
|
||
span(text(value.enemy.enemy.name))
|
||
],
|
||
{
|
||
props: {
|
||
// selectable 是样板内置的样式类,选中时会有选中动画
|
||
class: f('selectable'),
|
||
// selected 属性表示是否选中
|
||
selected: () => selected.value === index,
|
||
style: f(`
|
||
margin: 0 0 4px 0;
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
`)
|
||
},
|
||
// 当点击这个怪物时
|
||
onClick: f(() => {
|
||
selected.value = index;
|
||
})
|
||
}
|
||
)
|
||
);
|
||
});
|
||
]);
|
||
},
|
||
// 右侧内容,显示怪物的详细信息
|
||
right: () => {
|
||
return MComponent.vNode([
|
||
// 顶部,渲染怪物图标与名称
|
||
div([
|
||
// 没有边框
|
||
icon(enemy.value.enemy.id, void 0, void 0, true);
|
||
span(text(enemy.value.enemy.enemy.name), {
|
||
props: {
|
||
// 注意所有的文字尽量使用百分比进行大小限定,这样可以被设置所更改
|
||
style: f(`font-size: 200%`)
|
||
}
|
||
})
|
||
],
|
||
{
|
||
props: {
|
||
// 顶部样式
|
||
style: f(`
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-around;
|
||
padding-bottom: 5%;
|
||
border-bottom: dashed 1px #ddd8;
|
||
`)
|
||
}
|
||
}),
|
||
// 然后是下面的部分,包括怪物的详细信息
|
||
// 首先是怪物的基础属性
|
||
div(
|
||
[
|
||
// 使用列表渲染,对上面定义的怪物属性进行渲染
|
||
vfor(status, ([key, name, color]) => {
|
||
return MComponent.vNodeS(
|
||
div([
|
||
// 怪物属性名称,右对齐
|
||
span(text(name), statusSpanStyle),
|
||
span(text(format(enemy.value.enemy.info[key])))
|
||
], statusStyle)
|
||
);
|
||
}),
|
||
// 然后是伤害、临界
|
||
div([
|
||
span(text('伤害'), statusSpanStyle),
|
||
span(text(format(enemy.value.damage)))
|
||
], statusStyle),
|
||
div([
|
||
span(text('临界'), statusSpanStyle),
|
||
span(text(format(enemy.value.critical)))
|
||
], statusStyle),
|
||
div([
|
||
span(text('临界减伤'), statusSpanStyle),
|
||
span(text(format(enemy.value.criticalDam)))
|
||
], statusStyle),
|
||
div([
|
||
span(text(core.status.thisMap.ratio + '防'), statusSpanStyle),
|
||
span(text(format(enemy.value.defDam)))
|
||
], statusStyle),
|
||
// 下面是特殊属性,依然要用列表渲染
|
||
vfor(enemy.value.showSpecial, ([name, desc, color]) => {
|
||
return MComponent.vNodeS(div(
|
||
span(text(name), { props: { styles: f(`color: ${color}`) } }),
|
||
span(text(': ')),
|
||
span(text(desc))
|
||
));
|
||
})
|
||
],
|
||
{
|
||
props: {
|
||
style: f(`
|
||
display: flex;
|
||
flex-direction: column;
|
||
width: 100%;
|
||
`)
|
||
}
|
||
}
|
||
)
|
||
]);
|
||
}
|
||
}
|
||
});
|
||
]);
|
||
}
|
||
})
|
||
// 由 UI 系统直接打开的 UI 都需要包含这两个参数
|
||
.defineProps({
|
||
num: Number,
|
||
ui: GameUi
|
||
})
|
||
.export();
|
||
|
||
// 注册 UI
|
||
mainUi.register(new GameUi('myBook', myBook));
|
||
```
|