Skip to content

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"

顶层字段

字段类型必填说明
idStringGUI 唯一标识符
titleString界面标题(支持 MiniMessage)
rowsint箱子行数(1-6)
slotsMap槽位定义映射

槽位定义字段

字段类型必填说明
keyString映射键名,代码里通过这个名字引用槽位
slotsString槽位索引,支持单个 "4"、范围 "0-8" 或组合 "0-8,18-26"
typeString槽位类型(不写的话系统会根据键名自动推断)
itemMap物品配置
soundsMap声音配置

物品配置

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纯装饰,不响应点击decorationborderfiller
DISPLAY信息展示,点击无效果displayinfostatus
BUTTON可点击按钮buttonbtnconfirmaccept
CLOSE关闭界面closeexitback
TOGGLE开关切换toggleswitch
INPUT输入槽位(允许放入物品)inputslot
OUTPUT输出槽位(允许取出物品)outputresult
PAGINATE_PREV上一页prevprevious
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玩家悬浮到槽位时(仅客户端支持时生效)
openGUI 打开时(定义在顶层)
closeGUI 关闭时(定义在顶层)

openclose 是界面级别的声音,写在顶层而不是某个槽位下面:

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();
        });
});

注意

refreshSlotrefreshAll 会自动调度回主线程执行,你不需要手动切线程。但要注意:更新占位符之后必须调用刷新方法,否则界面上看不到变化。

提示

简单的静态界面直接用同步的 guiService.open(request) 就行。异步渲染主要是给那些需要等外部数据的场景准备的,没必要所有界面都用异步。

Released under the GPL-3.0 License