如何在Bukkit服务器插件中实现类似马匹菜单的自定义实体GUI?
实现Bukkit服务器自定义Inventory GUI动态渲染实体的方案
完全可行,纯服务端即可实现,无需客户端模组。核心是复刻原版马匹界面的数据包逻辑,通过向客户端发送特定NMS数据包,在自定义GUI内渲染任意实体。
核心逻辑
原版马匹界面并非直接在Inventory里嵌入实体,而是服务端向客户端发送「打开马匹窗口」的数据包,同时在客户端的窗口渲染位置生成一个临时实体。我们可以照搬这个机制,替换成任意目标实体。
具体实现步骤
1. 搭建自定义GUI容器
先用Bukkit API创建基础Inventory框架,比如和原版马匹界面尺寸一致的5行容器:
// 创建5行的Inventory,自定义标题 Inventory customGui = Bukkit.createInventory(null, 45, "劫掠兽信息界面"); // 添加交互元素(比如关闭按钮) customGui.setItem(40, new ItemStack(Material.BARRIER));
这个Inventory仅作为交互操作的容器,实体渲染依赖后续的数据包发送。
2. 发送窗口打开数据包
当玩家打开GUI时,不直接调用player.openInventory(customGui),而是发送NMS的PacketPlayOutOpenWindow数据包,指定窗口类型为minecraft:horse(对应原版带实体渲染位的窗口模板)。这里用ProtocolLib简化跨版本操作:
ProtocolManager manager = ProtocolLibrary.getProtocolManager(); // 构建打开窗口数据包 PacketContainer openWindowPacket = manager.createPacket(PacketType.Play.Server.OPEN_WINDOW); openWindowPacket.getIntegers().write(0, player.getOpenInventory().getTopInventory().getViewers().size()); // 窗口ID openWindowPacket.getWindowTypes().write(0, WindowType.HORSE); // 指定马匹窗口类型 openWindowPacket.getChatComponents().write(0, new ChatComponentText("劫掠兽信息界面")); // 发送数据包给目标玩家 manager.sendServerPacket(player, openWindowPacket);
客户端收到后会自动加载带实体渲染区域的窗口框架。
3. 发送临时实体渲染数据包
创建目标实体的虚拟实例(不加入服务器世界),然后发送实体生成、元数据数据包到客户端:
// 创建虚拟劫掠兽实体(服务端不加入世界,避免影响游戏逻辑) EntityRaider ravager = (EntityRaider) EntityTypes.RAVAGER.create(player.getWorld()); ravager.setLocation(player.getLocation().add(0, 100, 0), 0, 0); // 放在玩家视野外,防止世界中显示 // 发送实体生成数据包 PacketContainer spawnPacket = manager.createPacket(PacketType.Play.Server.SPAWN_ENTITY_LIVING); spawnPacket.getIntegers().write(0, ravager.getId()); // 实体唯一ID spawnPacket.getEntityTypeModifier().write(0, EntityType.RAVAGER); spawnPacket.getDoubles().write(0, 0).write(1, 0).write(2, 0); // 客户端内渲染位置,固定为0即可 manager.sendServerPacket(player, spawnPacket); // 发送实体元数据,同步实体状态(比如生命值、外观) PacketContainer metadataPacket = manager.createPacket(PacketType.Play.Server.ENTITY_METADATA); metadataPacket.getIntegers().write(0, ravager.getId()); metadataPacket.getEntityMetadataModifier().write(0, ravager.getDataWatcher()); manager.sendServerPacket(player, metadataPacket);
客户端会自动将这个实体渲染在窗口的中间位置,而非游戏世界中。
4. 清理临时实体
当玩家关闭GUI时,发送实体销毁数据包,避免客户端残留无效实体:
// 监听Inventory关闭事件 @EventHandler public void onGuiClose(InventoryCloseEvent event) { Player player = (Player) event.getPlayer(); ProtocolManager manager = ProtocolLibrary.getProtocolManager(); // 发送销毁实体数据包(替换为你的虚拟实体ID) PacketContainer destroyPacket = manager.createPacket(PacketType.Play.Server.DESTROY_ENTITIES); destroyPacket.getIntLists().write(0, Arrays.asList(ravager.getId())); manager.sendServerPacket(player, destroyPacket); }
同时要在PlayerQuitEvent中做同样的清理,防止玩家断开连接后客户端残留实体。
5. 全生物兼容
只要替换实体类型(比如EntityType.ZOMBIE、EntityType.CREEPER),发送对应类型的Spawn数据包即可,客户端会自动渲染对应实体,无需额外适配逻辑。
关键注意事项
- 若不使用ProtocolLib,需用反射处理不同MC版本的NMS类名差异(比如1.19的NMS包是
net.minecraft.network.protocol.game,1.18是net.minecraft.server.v1_18_R1)。 - 虚拟实体绝对不能调用
world.addEntity()加入服务器世界,否则会影响游戏逻辑。 - 不同MC版本的窗口类型标识可能有差异,比如1.17+用字符串标识,旧版本用整数ID,需要针对性兼容。
内容的提问来源于stack exchange,提问作者LeyCM
相关产品推荐
相关产品推荐

