你肯定在服务器中不止一次看到过它们的菜单,有的服务器采用右键一个物品,有的采用命令。
下面是一个简陋的菜单示例。
你应该发现,GUI 和 物品栏长得很像(根本就是一个东西好吧),这并非有意为之,而是因为物品栏是显示可交互 GUI 的方法。
另外两种 GUI 分别是聊天栏和计分板,但是它们的功能都没有物品栏 GUI 强大。
物品栏 GUI 的本质就是一个摆放了不同物品的物品栏。还记得吗,我们说客户端并不知道插件的存在。在 Minecraft 客户端中,物品栏不一定要和什么东西关联起来,物品栏的显示是单独的。也就是说,不是因为你按了箱子导致物品栏打开,而是你按箱子的动作发送到服务端,服务端处理完成后再指示客户端打开物品栏的。
那我们当然也可以通过插件直接指示客户端打开一个物品栏,这是可以做到的。
Minecraft 使用一个名叫物品堆(ItemStack
)的类来描述物品堆。所谓物品堆,基本上就是物品 + 数量,摆在物品栏中的也不是物品(Item
),而是 ItemStack
。
一个基本的 GUI 包含以下内容:
- 基于哪种物品栏制作?多数插件都使用箱子,因为箱子 GUI 的大小可以自定义,而且编号简单易记。
- 标题是什么?标题是区分 GUI 的最方便的方法。
- 包含哪些物品?这应当在实例化
Inventory
类后进行设置。
这部分不方便用语言说明,我们用一段代码来表述:
Inventory inv = Bukkit.createInventory(Bukkit.getPlayer("RarityEG"), 3 * 9, "GUI 标题");
ItemStack exitItem = new ItemStack(Material.BARRIER);
inv.setItem(0, exitItem);
Bukkit.getPlayer("RarityEG").openInventory(inv);
Bukkit.createInventory
用于创建一个物品栏(Inventory
对象),默认的物品栏就是箱子 GUI,所以无需修改。
括号内的三个参数分别是所属实体(这里通过服务端获取了一个名为「RarityEG」的玩家),大小(必须是 9 的倍数,从 1 * 9 到 6 * 9),标题。
接下来我们实例化了一个 ItemStack
,并将它放到了 GUI 的 0 号槽,0 号就是第一个格子(从左往右,从上往下)。Material
枚举里面指出了所有可能的材料,材料可以认为就是物品的实质。
?> 什么是枚举?
假如你需要描述一个数据,它有「大」、「中」、「小」三种状态。使用 String
或者 int
?那就要判断它们是否在取值范围以内。使用 boolean
?对不起,差一个。
枚举(enum
)就是用来解决这种尴尬问题的。enum
规定了一个变量的几种可能取值。要使用枚举的值,可以通过像访问类(静态)变量一样的方法,即 <枚举名>.<值>
。
最后用 openInventory
为玩家打开这个 GUI。这样 GUI 就展现在玩家面前,并且在玩家把它关闭前它会一直存在。
通过设置其中的 ItemStack
,可以赋予菜单各种各样的信息,那究竟有哪些可以设置呢?这就是我们下一章要说到的内容了……不过在下一章之前,我们还有一些要讲明白……
这里打开的 GUI 和通过箱子打开的 GUI 是完全一样的,没有做限制,也就是说,玩家是可以把里面的东西拿走的!
要避免这种情况,我们又需要进行事件处理了。监听 InventoryClickEvent
:
@EventHandler
public void onClick(InventoryClickEvent e) {
Player player = (Player) e.getWhoClicked();
// 只有玩家可以触发 InventoryClickEvent,可以强制转换
InventoryView inv = player.getOpenInventory();
if (inv.getTitle().equals("GUI 标题")) {
// 通过标题区分 GUI
e.setCancelled(true);
}
}
这里有一点值得注意的地方:我们没有使用 e.getInventory
获得物品栏,而是使用 e.getWhoClicked
获取了点击的玩家(能够触发该事件的只有玩家),再通过 getOpenInventory
获得物品栏,绕了好大一个弯子。笔者也不知道为什么是这样,可能是因为内部机制吧。(getInventory
返回 Inventory
,而 getOpenInventory
返回的是 InventoryView
)
要获取被点击的物品,可以通过调用 InventoryClickEvent
的各个方法:
if (e.getRawSlot() < 0 || e.getRawSlot() > e.getInventory().getSize()) {
// 这个方法来源于 Bukkit Development Note
// 如果在合理的范围内,getRawSlot 会返回一个合适的编号(0 ~ 物品栏大小-1)
return;
// 结束处理,使用 return 避免了多余的 else
}
ItemStack clickedItem = e.getCurrentItem();
// 获取被点的物品
if (clickedItem == null){
// 确保不是 null
return;
}
// 后续处理
这样我们就得到了被点击物品的 ItemStack
,可以由此来判断玩家按下了哪个「按钮」。
如果要获得点击的格子,可以使用 getSlot
,该方法返回被点击格子的编号。
这里创建的 GUI 在玩家打开后就无法再修改(如果你做过前端,就像 React 一样,只能渲染一个新的 GUI 传进去),而且还需要进行保护(GUI 里的物品可都是真的)。更重要的是,服务端有这个 GUI 的实例,它在内存里,这种 GUI 被称为经典 GUI。
与经典 GUI 相对的是数据包 GUI,数据包 GUI 在使用时,服务端发送一个数据包到客户端,客户端便会渲染 GUI,此后服务端就可以将这个 GUI 的实例删除。数据包 GUI 也能比经典 GUI 做出更美观的效果,然而,数据包 GUI 需要依赖其它插件(ProtocolLib),这超出了我们的讲解范畴,因此只能向大家说声对不起了。