教程
容器GUI,俗称箱子菜单,是Minecraft人机交互的重要渠道。在GUI的帮助下,玩家无需记忆复杂的命令,给予其直观的交互体验,并因此成为时下精品插件的主要交互手段。
原理
要了解玩家与一般容器交互所需的类,我们从生成容器的方法Bukkit.createInventory()开始。
| static Inventory createInventory(InventoryHolder owner, int size)
static Inventory createInventory(InventoryHolder owner, int size, String title)
static Inventory createInventory(InventoryHolder owner, InventoryType type)
static Inventory createInventory(InventoryHolder owner, InventoryType type, String title)
|
其中,InventoryHolder是代表着容器的持有者,所有具有容器的实体和方块都是它的子接口。同理,我们只需继承InventoryHolder,并通过isinstance进行判断,即可筛选出由本脚本控制的容器。这就是本库实现玩家交互处理的基本原理。开发者只需要监听所需要的InventoryEvent并且获取InventoryHolder进行判断即可,本库已封装isGUI函数来实现这一点。
除了确认容器是本脚本控制的以外,还需要确定具体要使用什么函数来处理点击事件,因为大部分的脚本都不只有一个菜单。在过去时通过菜单名进行索引,而在Util2则是采用GUIManager来存储玩家当前交互的GUIController来实现的。与过去相比,这样不仅能更好的存储临时变量,还可以增强内存安全性。
库结构
| 库名 |
中文名 |
描述 |
| bugfixer |
安全保障 |
阻止容器GUI的常见漏洞 |
| builder |
构建器 |
容器GUI绘制和判别的基础 |
| manager |
管理器 |
进行渲染、点击事件处理、菜单生成与展示等关键功能 |
| legacy |
传统构建 |
便于旧脚本初步迁移,新脚本请勿使用 |
监听事件
我们出于不同目的,需要监听不同的事件。一般的,至少需要监听点击、拖动、关闭事件。
点击事件
点击事件用于实现GUI的按钮功能,需要正确判断是否为本脚本的容器,并将事件传递给GUIController来实现其他相关操作。一般情况下,还需避免通过“Shift + 左键”将物品放入菜单导致无法取出。以下是一个一般示例:(出自DC邮电第三代系统)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | import pyspigot as ps
from org.bukkit.event.inventory import InventoryClickEvent
from util2.develop.gui.builder import isGUI
from util2.develop.gui.bugfixer import cancelShiftClickFromPlayerInventory
from dcpt3.gui.index import guiManager # 导入GuiManager的对象
# 监听容器点击
def onInventoryClick(e):
inv = e.getClickedInventory()
if inv is None: # 点击Inventory界面外部时为None,应排除
return
invHolder = inv.getHolder()
if isGUI(invHolder): # 是否为本脚本控制
guiManager.get(e.getWhoClicked(), e.getClickedInventory().getHolder().getName()).onClick(e) # 菜单控制
return
cancelShiftClickFromPlayerInventory(e) # 避免通过 Shift + 左键 将物品放入菜单导致无法取出
ps.listener.registerListener(onInventoryClick, InventoryClickEvent, True) # 注册命令
|
拖动事件
拖动是指玩家将物品右键分配至格子的行为,一般会使物品放入菜单无法取出,除特殊用途外直接禁止即可。由于一般无需通过该事件处理任何内容,直接传入denyAllInventoryDrag即可。
| import pyspigot as ps
from org.bukkit.event.inventory import InventoryDragEvent
from util2.develop.gui.bugfixer import denyAllInventoryDrag
ps.listener.registerListener(denyAllInventoryDrag, InventoryDragEvent, True)
|
关闭事件
关闭事件用于当玩家关闭容器时,及时清空GUIManager的对象关于玩家交互的GUIController内容,利于内存安全。以下是一个一般示例:(出自DC邮电第三代系统)
| import pyspigot as ps
from org.bukkit.event.inventory import InventoryCloseEvent
from dcpt3.gui.index import guiManager # 导入GuiManager的对象
# 监听容器关闭
def onInventoryClose(e):
inv = e.getInventory()
if isGUI(inv.getHolder()): # 是否为本脚本控制
guiManager.remove(e.getPlayer()) # 关闭菜单时清空存储的控制器
ps.listener.registerListener(onInventoryClose, InventoryCloseEvent, True)
|
构造管理器
在原理部分我们已经介绍了GUIManager的作用。在使用菜单以前,我们需要首先构造为它的对象。
| from util2.develop.gui.manager import GUIManager
guiManager = GUIManager()
|
在此后的菜单中我们都必须引用guiManager对象。如果是多文本编程,可以在gui的目录下新建一个文件来方便导入GUIManager的对象。
编写GUI
简单GUI
我们需要把自定义GUI类设置为GUIController的派生类,并至少对“render”和“onClick”方法进行重写。下边是一个简单的索引GUI:(出自DC邮电第三代系统)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 | #coding: utf-8
#柜台主菜单
from org.bukkit import Material
from util2.develop.gui.manager import GUIController
from dcpt3.gui.index import guiManager # GUIManager的对象
from dcpt3.gui.send import Menu_Send
from dcpt3.gui.poc.special import Menu_poc_SpecialSend
# 邮局柜台主菜单
class Menu_poc_main(GUIController):
def __init__(self, player):
super(Menu_poc_main, self).__init__(player) # 运行基类的构造函数
self.setInfo("dcpt.poc.main", u"§2§lDC邮电 §3§l柜台") # 设置基本信息,自动行数
self.setGUIManager(guiManager) # 向GUIManager的对象存储玩家对应的GUI类
# 渲染器:构造菜单时运行,负责生成菜单内容
def render(self):
self.set(2, Material.OAK_CHEST_BOAT, u"§6§l交寄邮件", "", u"§f邮快件交寄", u"§f点击进入菜单")
self.set(4, Material.HOPPER, u"§a§l特殊寄递", "", u"§f收购/电商/退件", u"§f点击进入菜单")
self.set(6, Material.CARTOGRAPHY_TABLE, u"§e§l集邮服务", "", u"§f邮资/邮戳卡册/集戳", u"§f点击进入菜单")
# 点击处理:负责处理InventoryClickEvent,在“监听事件”中已介绍引入方法
def onClick(self, e):
e.setCancelled(True) # 一般的,都需要取消点击事件
clickInt = e.getSlot()
if clickInt == 2:
Menu_Send(self._player, "pob").openWithRender() # 为玩家打开菜单
elif clickInt == 4:
Menu_poc_SpecialSend(self._player).openWithRender() # 打开不同的菜单
|
分区渲染的GUI
对于复杂的GUI,分区生成利用模块化方法,不仅使结构更清晰而且节省重复渲染资源。下边是自助服务机的聚落玩委会投票系统代码节选:(效果可前往任一自助服务机查看)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52 | #coding: utf-8
#聚落玩家委员会投票GUI
from org.bukkit import Material
from util2.basic.string import StrUtil
from util2.develop.gui.builder import GUIConstructor
from util2.develop.gui.manager import GUIController
from servicemachine.townvote import townvote, TownVotePlayer
from servicemachine.gui import index
class Menu_Vote(GUIController):
def __init__(self, player):
super(Menu_Vote, self).__init__(player)
self.setInfo("servicemachine.townvote", u"§e§lDC§2§l自助服务机 §r第{}届聚落玩委会投票".format(townvote.getSession()))
self.setGUIManager(index.guiManager) # 向GUIManager的对象存储玩家对应的GUI类
self._townvotePlayer = TownVotePlayer(player)
# 渲染器
def render(self):
self._spawnHeaderSection()
self._spawnCandidateSection()
# 头部按钮区域
def _spawnHeaderSection(self):
self.set(0, Material.BIRCH_SIGN, u"§b投票信息", "", townvote.getInfoMsg(), u"§f届别: {}".format(townvote.getSession()), u"§f区域: {}".format(self._townvotePlayer.getArenaName()), u"§f投票: {}/{}".format(self._townvotePlayer.getVoteCastedHere(), self._townvotePlayer.getMaxVoteTimeHere()), u"§f得票: {}".format(self._townvotePlayer.getVoteObtainedHere()))
self.set(2, Material.ANVIL, u"§6进行投票", "", u"§7点击输入拟投票玩家ID", u"§7也可直接点选下方玩家")
self.set(4, Material.SPYGLASS, u"§a参选资格预审", "", u"§f核验您的参选资格", u"§f通过后加入候选表")
self.set(6, Material.PLAYER_HEAD, u"§a投票资格验证", "", u"§f核验您的投票资格", u"§7核验结果仅供参考")
self.set(8, Material.RED_WOOL, u"§c返回")
# 候选人区域
def _spawnCandidateSection(self): # 使用GUIConstructor构建,然后与主内容通过替换的方式合并
gui = GUIConstructor() # 注:GUIManager是GUIConstructor的派生类
verified = self._townvotePlayer.getVerifiedCandidateShuffled() # 乱序候选人
for candidate in verified:
gui.add(1, verified[candidate], u"§e"+candidate, "", u"§b已为其投票" if self._townvotePlayer.isAlreadyVoteFor(candidate) else u"§f点击为其投票")
self.replace(gui, 9, self.getSize() - 1) # 从9到头都为候选人区
# 点击处理
def onClick(self, e):
e.setCancelled(True)
clickInt = e.getSlot()
if clickInt == 2: # 进行投票
pass # 省略
elif clickInt == 4: # 参选资格预审
pass # 省略
elif clickInt == 6: # 投票资格验证
pass # 省略
elif clickInt == 8: # 返回上一级
index.Menu_Main(self._player).openWithRender() index.Menu_Main(self._player).openWithRender()
elif 9 <= clickInt: # 为列表投票
currentItem = e.getCurrentItem()
if currentItem is None or currentItem.getType() != Material.PLAYER_HEAD: # 对点击物判断
return
# 处理过程省略
self._spawnCandidateSection() # 仅需刷新区域:投票后,只需刷新候选人区域
self.update() # 刷新后必须update()来应用到玩家的界面上
|
使用GUI
只需将GUI导入,然后使用openWithRender()方法即可。比如一个打开GUI的命令:
| #coding: utf-8
#DC邮电3 主文件节选
import pyspigot as ps
from dcpt3.gui.command.main import Menu_command_main # 导入
# 命令处理函数
def onCommand(sender, label, args):
Menu_command_main(sender).openWithRender()
return True
ps.command.registerCommand(onCommand, "dcpt") # 注册命令
|
对于希望同步打开(多用于调试时显示错误信息),则可使用openWithRenderSync()方法。但考虑到性能,在生产环境中请尽量使用异步。