本帖最后由 dyodyo 于 2026-7-14 11:22 编辑
提示
建议先看看这篇教程。请注意,测试版的API在正式版可能已经不适用。
- 基本的JavaScript/TypeScript知识
- 基岩版ScriptAPI的简单使用。
ScriptAPI为附加包的开发提供了极大的便利,其中表单(Form)是较为常用的功能之一。它的重要功能是用来收集数据,不过各大开发者们都把它玩出了花。你在过去看到的这些东西都是表单的一种:
基岩版创世神的笔刷编辑表单不过,无论是ActionFormData、MessageFormData,还是ModalFormData,它们都是静态的。这意味着我们无法方便地用这些表单做出丰富的交互效果,直到Mojang在基岩版26.30中把@minecraft/server-ui模块的2.1.0正式版给端了上来! 在本次更新中,Mojang为我们带来了一种全新的表单:CustomForm。与之前的表单不同,它的组件支持动态更新。有了动态的表单,我们就可以做出一些过去做不到的交互效果。
这到底是怎么实现的?
Mojang除了更新CustomForm这个表单之外,还新加入了一大堆为它服务的可观测(Observable)数据类型,有字符串ObservableString、数字ObservableNumber、布尔值ObservableBoolean,以及文本组件ObservableUIRawMessage。 所以这有什么用呢?请看下列示例。
第一步当然是更新一下我们的清单文件(manifest.json),不然没法用新的API: - ……
- "dependencies": [
- {
- "module_name": "@minecraft/server",
- "version": "2.8.0"
- },
- {
- "module_name": "@minecraft/server-ui",
- "version": "2.1.0"
- }
- ]
复制代码 之后,创建并打开脚本文件,新实例化一个CustomForm:
- import { system, world } from "@minecraft/server";
- import { CustomForm } from "@minecraft/server-ui";
- const customForm = new CustomForm(
- world.getPlayers()[0], // 玩家
- "一个标题" // 窗口的标题
- );
复制代码 添加一个组件,比如说按钮:
- customForm.button( /* …… */ );
复制代码 我们注意到,添加一个按钮需要2个参数:显示的文本和按下按钮后触发的回调函数。在显示的文本这一参数中,不仅可以填字符串string和文本组件UIRawMessage,还可以填我们刚刚说的可观测字符串ObservableString和可观测文本组件ObservableUIRawMessage。让我们看看它有什么用吧。
新实例化一个可观测字符串ObservableString:
- const observableString = new ObservableString("我是一个字符串");
复制代码 用它作为按钮的文本:
- customForm.button(observableString, /* …… */);
复制代码 还差一个回调函数,写些什么呢……这个observableString有一个方法setData,还能设置数据?让我们来试一试:
- customForm.button(observableString, () => {
- observableString.setData("更改了按钮的文本,耶");
- });
复制代码 根据官方文档,如果要对可观测对象设置数据,在声明它时还要传入一个额外的clientWritable属性。所以,让我们把之前的代码改一下:
- const observableString = new ObservableString("我是一个字符串", { clientWritable: true });
复制代码 这是最后一步了,显示表单: 大功告成!
让表单变得丰富起来
除了最基本的按钮以外,我们还有下拉栏(dropdown)、输入框(textField)、开关(toggle)、滑块(slider)等诸多组件可以使用,下面让我们来逐一认识它们。
下拉栏(dropdown)
让我们在表单中添加一个下拉栏:
- customForm.dropdown( /* …… */ );
复制代码 设置一个标题,这是当然的:
- customForm.dropdown(
- "下拉栏",
- /* …… */
- );
复制代码 根据语法提示,我们必须要给它设置一个值。比如说,来个0怎么样:
- customForm.dropdown(
- "下拉栏",
- 0,
- /* …… */
- );
复制代码 最后是下拉栏的选项们,这需要一个数组……
- customForm.dropdown(
- "下拉栏",
- 0,
- [
- {
- label: "选项1"
- },
- {
- label: "选项2"
- },
- {
- label: "选项3"
- }
- ]
- );
复制代码 别忘了给每个选项分配一个独一无二的值!
- customForm.dropdown(
- "下拉栏",
- 0,
- [
- {
- label: "选项1",
- value: 0 // 这个选项是默认选中的
- },
- {
- label: "选项2",
- value: 1
- },
- {
- label: "选项3",
- value: 114514 // 随便什么值都是可以的,只要你能记住它们……
- }
- ]
- );
复制代码 把表单显示出来: 干得漂亮!
为了使这个表单变得更高级一点,我们还可以让标题动起来。下拉栏的值也是可以使用可观测数字ObservableNumber的,让我们先新实例化一个ObservableNumber和一个ObservableString,后者用来作下拉栏的标题:
- const dropdownIndex = new ObservableNumber(0);
- const dropdownLabel = new ObservableString("下拉栏", { clientWritable: true });
复制代码 使用它们:
- customForm.dropdown(
- dropdownLabel,
- dropdownIndex,
- [{ label: "选项1", value: 0 }, { label: "选项2", value: 1 }, { label: "选项3", value: 114514 }]
- );
复制代码 当下拉栏选中的选项发生变化时,我们希望改变下拉栏的标题,看来这需要使用一点新的方法来监听它的变化了。正巧可观测对象都有一个subscribe方法,这可以让我们像订阅事件一样——当下拉栏的值发生变化时(即它选中的选项发生了变化),我们订阅的回调函数就会被调用。
- dropdownIndex.subscribe(currentIndex => {
- if (dropdownIndex.getData() === 114514) {
- dropdownLabel.setData("你选中了一个十分特别的选项");
- } else {
- dropdownLabel.setData(你选中的值为:${currentIndex});
- }
- });
复制代码 这很酷,不是吗?别着急,还有更酷的。经过前面的了解,我们发现,只要是能填可观测对象的地方都是可以动态变化的。所以,表单的标题也能变成ObservableString!
- const customFormTitle = new ObservableString("一个标题", { clientWritable: true });
- const customForm = new CustomForm(world.getPlayers()[0], customFormTitle);
复制代码 把选项的文字也变成ObservableString!
- const myOptionLabel = new ObservableString("选项2", { clientWritable: true });
- customForm.dropdown(
- dropdownLabel,
- dropdownIndex,
- [
- {
- label: "选项1",
- value: 0
- },
- {
- label: myOptionLabel, // 注意这里!
- value: 1
- },
- {
- label: "选项3",
- value: 114514
- }
- ]
- );
复制代码 在回调中处理它们!
- dropdownIndex.subscribe(currentIndex => {
- if (currentIndex === 114514) {
- dropdownLabel.setData("你选中了一个十分特别的选项");
- customFormTitle.setData("恶臭的表单");
- myOptionLabel.setData("你触发了彩蛋!");
- } else {
- dropdownLabel.setData(你选中的值为:${currentIndex});
- }
- });
复制代码
输入框(textField)
其实剩下的几个组件用起来基本上和下拉栏一样,让我们简单地过一遍。
- customForm.textField("我是标题", "我是输入的文本");
复制代码 如果要监听输入的文本变化,就可以用一个ObservableString代替原来的值。
- const text = new ObservableString("0");
- const message = new ObservableString("", { clientWritable: true });
- customForm.textField("请输入数字", text, {
- description: message
- });
复制代码 然后通过订阅添加一个回调。
- text.subscribe(currentText => {
- if (!isFinite(Number(currentText))) {
- message.setData("非法的数字!");
- } else {
- message.setData("");
- }
- });
复制代码
开关(toggle)
- customForm.toggle("我是标题", new ObservableBoolean(true) /* 值 */);
复制代码 所有的操作依旧适用,只不过要使用布尔类型的可观测对象(ObservableNumber)。
滑块(slider)
这个就有一点不同了,不仅有值,还要有最大值和最小值。
- customForm.slider("我是标题", new ObservableNumber(0) /* 值 */, 0 /* 最小值 */, 100 /* 最大值 */);
复制代码 甚至还有可选的步长:
- customForm.slider("我是标题", new ObservableNumber(0), 0, 100, {
- step: 1 // 步长
- });
复制代码 你注意到了吗?这里的所有参数都可以使用可观测对象,所以能干什么就不必多说了。
除了这些组件外还有……? 手搓动画
让我们看看这是如何实现的。首先,向表单中添加一个label:
- customForm.label( /* …… */ );
复制代码 考虑到文本是动态的,所以这里我们使用ObservableString:
- const label = new ObservableString("", { clientWritable: true });
- customForm.label(label);
复制代码 ……以及要显示的文本:
- const text = "打字机入场效果测试";
复制代码 编写一个辅助函数用来实现打字机入场效果:
- function showAnimation() {
- // 如果动画完成了就停止
- if (label.getData().length >= text.length) return;
- // 增加一个字符
- label.setData(text.substring(0, label.getData().length + 1));
- }
复制代码 别忘了这要循环多次!
- function showAnimation() {
- if (label.getData().length >= text.length) return;
- label.setData(text.substring(0, label.getData().length + 1));
- // 如果表单还在显示的话,在 5 游戏刻之后再次调用自己
- if (customForm.isShowing()) {
- system.runTimeout(showAnimation, 5);
- }
- }
复制代码 回到表单上来。先显示表单: 再显示动画:
- // 为了防止玩家来不及看到动画开头,延迟 10 游戏刻后再调用
- system.runTimeout(showAnimation, 10);
复制代码 太棒了,我们的表单又变得高级了一点。
从外部更新数据
数据不仅仅止于表单内部,还可以从外部获取!比如说做一个可以实时监视掉落物数量的表单。
让我们先添加一个label。
- const label = new ObservableString("", { clientWritable: true });
- customForm.label(label);
复制代码 编写更新文本的函数:
- function update() {
- // 获取玩家所在维度中掉落物的数量
- const itemCount = world.getPlayers()[0].dimension.getEntities({ type: "minecraft:item" }).length;
- // 更新文本
- label.setData(当前维度共有 ${itemCount} 个掉落物);
- // 每 20 游戏刻更新一次
- if (customForm.isShowing()) system.runTimeout(update, 20);
- }
复制代码 显示表单并更新文本:
- customForm.show();
- update();
复制代码 这些简单的例子让我们的表单拥有了高级的交互,但CustomForm能实现的功能可远不止这些。
附:一个可用的完整实例
我使用CustomForm做了一个简单的传送器,附加包可以在这里下载:
transporter.mcpack(7.69 KB, 下载次数: 0)
它主要是为了展示表单的动态效果,支持自定义位置、朝向、维度,还有一些附加的选项。
这个行为包使用TypeScript编写,源代码可以在这里(
scripts.zip(6.82 KB, 下载次数: 0)
)下载到。
由于我嫌创建可观测对象太麻烦了,所以所有的组件都被我被封装成了对象,并附带了一些方便的功能。这个包里的代码的画风大概是这样的:
- export function test() {
- // 窗口实例
- const dialog = new Dialog(world.getPlayers()[0], "标题");
- // 一个输入框
- const textField = new DialogTextField("输入内容");
- // 一个按钮
- const button = new DialogButton("清空内容", () => {
- textField.text = "";
- });
- // 一个文本
- const label = new DialogLabel("");
- // 输入框的回调
- textField.onChange = (text) => {
- label.label = 你输入了:${text};
- };
- // 一个开关
- const toggle = new DialogToggle("隐藏所有组件");
- // 开关的回调
- toggle.onChange = (toggled) => {
- textField.visible = !toggled;
- button.visible = !toggled;
- label.visible = !toggled;
- };
- // 添加所有组件并显示
- dialog.add(textField, button, label, toggle).show();
- }
复制代码 如果你想的话,可以去看看包里的源代码,可能可以让你对CustomForm的理解更加深刻。
你知道吗
- @minecraft/server-ui模块的最新测试版本2.2.0-beta.1.26.40-preview.29中,CustomForm已经支持显示图像了。
- dropdown组件的选项通过数组定义,所以你不能动态添加或减少选项。(~~什么时候Mojang能出个ObservableArray~~)
- 表单最下面的close(关闭)按钮是Mojang的小巧思,目前没办法去除也没办法更改按钮上的文字。
- 附:微软的官方文档(英文)
|