GUI 系统
EmakiCoreLib 的 GUI 系统让你用 YAML 模板来定义箱子界面(Chest GUI)。写好模板文件,代码里调一下打开接口,界面就出来了——不需要手动操作 Inventory API。系统还内置了声音配置和异步渲染,适合从简单菜单到需要加载远程数据的复杂界面。
YAML 模板结构
每个 GUI 对应一个 YAML 文件,下面是一个典型的模板:
yaml
id: "example_menu"
title: "<gradient:gold:yellow>示例菜单</gradient>"
rows: 3
slots:
decoration_border:
slots: "0-8,18-26"
type: DECORATION
item:
source: "vanilla-gray_stained_glass_pane"
name: " "
info_display:
slots: "12"
type: DISPLAY
item:
source: "vanilla-book"
name: "<gold>服务器信息"
lore:
- "<gray>在线玩家:<white>%server_online%"
- "<gray>服务器 TPS:<white>%server_tps%"
- ""
- "<yellow>点击刷新"
confirm_button:
slots: "14"
type: BUTTON
item:
source: "vanilla-lime_wool"
name: "<green>确认"
lore:
- "<gray>点击确认操作"
sounds:
click: "ui.button.click"
close_button:
slots: "22"
type: CLOSE
item:
source: "vanilla-barrier"
name: "<red>关闭"
sounds:
click: "block.chest.close"顶层字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
id | String | 是 | GUI 唯一标识符 |
title | String | 是 | 界面标题(支持 MiniMessage) |
rows | int | 是 | 箱子行数(1-6) |
slots | Map | 是 | 槽位定义映射 |
槽位定义字段
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
key | String | 是 | 映射键名,代码里通过这个名字引用槽位 |
slots | String | 是 | 槽位索引,支持单个 "4"、范围 "0-8" 或组合 "0-8,18-26" |
type | String | 否 | 槽位类型(不写的话系统会根据键名自动推断) |
item | Map | 是 | 物品配置 |
sounds | Map | 否 | 声音配置 |
物品配置
yaml
item:
source: "vanilla-diamond_sword" # 物品来源(见物品来源系统)
name: "<gold>传说之剑" # 自定义名称(MiniMessage)
lore: # 自定义 Lore
- "<gray>一把充满力量的剑"
- ""
- "<yellow>攻击力:<red>+15"
amount: 1 # 数量
glow: true # 是否附魔光效
custom_model_data: 10001 # 自定义模型数据槽位类型与自动推断
如果你省略了 type 字段,系统会根据槽位的键名猜测类型。这是个便利功能,但建议在正式配置里还是显式写明 type,免得改了键名之后行为跟着变。
| 类型 | 说明 | 自动推断条件(键名包含) |
|---|---|---|
DECORATION | 纯装饰,不响应点击 | decoration、border、filler |
DISPLAY | 信息展示,点击无效果 | display、info、status |
BUTTON | 可点击按钮 | button、btn、confirm、accept |
CLOSE | 关闭界面 | close、exit、back |
TOGGLE | 开关切换 | toggle、switch |
INPUT | 输入槽位(允许放入物品) | input、slot |
OUTPUT | 输出槽位(允许取出物品) | output、result |
PAGINATE_PREV | 上一页 | prev、previous |
PAGINATE_NEXT | 下一页 | next |
提示
自动推断是按键名做子串匹配的。如果你的键名恰好包含了上面的关键词但实际用途不同,就会推断错误。所以正式环境下建议显式指定 type。
GuiOpenRequest
用 GuiOpenRequest 打开一个 GUI 界面:
java
GuiService guiService = EmakiServiceRegistry.get(GuiService.class);
GuiOpenRequest request = GuiOpenRequest.builder()
.templateId("example_menu")
.player(player)
.handler(new MyGuiHandler())
.placeholder("server_online", String.valueOf(Bukkit.getOnlinePlayers().size()))
.placeholder("server_tps", String.valueOf(Bukkit.getTPS()[0]))
.build();
guiService.open(request);| 方法 | 说明 |
|---|---|
templateId(String) | 指定 GUI 模板 ID |
player(Player) | 目标玩家 |
handler(GuiSessionHandler) | 会话处理器 |
placeholder(String, String) | 添加占位符 |
placeholders(Map) | 批量添加占位符 |
attribute(String, Object) | 附加属性 |
GuiSessionHandler 接口
实现 GuiSessionHandler 来处理 GUI 的交互事件。每个回调方法对应一种交互行为:
java
public class MyGuiHandler implements GuiSessionHandler {
@Override
public void onSlotClick(GuiClickContext ctx) {
String key = ctx.getSlotKey();
switch (key) {
case "confirm_button" -> {
ctx.getPlayer().sendMessage("已确认!");
ctx.close();
}
case "info_display" -> {
// 刷新显示
ctx.refreshSlot("info_display");
}
}
}
@Override
public void onPlayerInventoryClick(GuiClickContext ctx) {
// 玩家点击自己背包中的物品时触发
// 大多数情况下你会想取消这个操作,防止物品被移走
ctx.cancel();
}
@Override
public void onDrag(GuiDragContext ctx) {
// 玩家拖拽物品时触发
ctx.cancel();
}
@Override
public void onClose(GuiCloseContext ctx) {
// 界面关闭时触发,适合做数据保存、归还物品等收尾工作
Player player = ctx.getPlayer();
player.sendMessage("菜单已关闭");
}
}GuiClickContext 常用方法
| 方法 | 说明 |
|---|---|
getPlayer() | 获取点击的玩家 |
getSlotKey() | 获取被点击槽位的键名 |
getSlotIndex() | 获取被点击的槽位索引 |
getClickType() | 获取点击类型(LEFT, RIGHT, SHIFT_LEFT 等) |
getCurrentItem() | 获取当前槽位的物品 |
cancel() | 取消本次点击事件 |
close() | 关闭 GUI |
refreshSlot(String) | 刷新指定槽位的显示 |
refreshAll() | 刷新所有槽位 |
声音配置
每个槽位都可以配置交互声音,支持简写和完整两种格式。
简写格式
直接写声音 ID,音量和音高都用默认值 1.0:
yaml
sounds:
click: "ui.button.click"完整格式
需要调音量或音高时用这种写法:
yaml
sounds:
click:
sound: "entity.experience_orb.pickup" # 声音 ID
volume: 1.0 # 音量 (0.0 ~ 2.0)
pitch: 1.5 # 音高 (0.5 ~ 2.0)支持的声音事件
| 事件 | 触发时机 |
|---|---|
click | 玩家点击槽位时 |
hover | 玩家悬浮到槽位时(仅客户端支持时生效) |
open | GUI 打开时(定义在顶层) |
close | GUI 关闭时(定义在顶层) |
open 和 close 是界面级别的声音,写在顶层而不是某个槽位下面:
yaml
id: "shop_menu"
title: "<gold>商店"
rows: 6
sounds:
open: "block.chest.open"
close: "block.chest.close"
slots:
# ...异步渲染
如果界面内容需要从数据库或远程 API 加载,可以用异步渲染。先把界面打开(可以显示加载中的占位内容),数据到了再刷新:
java
GuiOpenRequest request = GuiOpenRequest.builder()
.templateId("player_stats")
.player(player)
.handler(new StatsHandler())
.build();
guiService.openAsync(request).thenAccept(session -> {
// GUI 已打开,异步加载数据
CompletableFuture.supplyAsync(() -> fetchPlayerStats(player))
.thenAccept(stats -> {
session.updatePlaceholder("kills", String.valueOf(stats.kills()));
session.updatePlaceholder("deaths", String.valueOf(stats.deaths()));
session.refreshAll();
});
});注意
refreshSlot 和 refreshAll 会自动调度回主线程执行,你不需要手动切线程。但要注意:更新占位符之后必须调用刷新方法,否则界面上看不到变化。
提示
简单的静态界面直接用同步的 guiService.open(request) 就行。异步渲染主要是给那些需要等外部数据的场景准备的,没必要所有界面都用异步。