跳转至

教程

​ 容器GUI,俗称箱子菜单,是Minecraft人机交互的重要渠道。在GUI的帮助下,玩家无需记忆复杂的命令,给予其直观的交互体验,并因此成为时下精品插件的主要交互手段。

原理

​ 要了解玩家与一般容器交互所需的类,我们从生成容器的方法Bukkit.createInventory()开始。

1
2
3
4
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即可。

1
2
3
4
5
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邮电第三代系统)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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的作用。在使用菜单以前,我们需要首先构造为它的对象。

1
2
3
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的命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#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()方法。但考虑到性能,在生产环境中请尽量使用异步。