# 作业

要求:依赖客户端Mod制作一个简易的菜单插件。

  • 进入游戏自动生成一个在Hud界面上的菜单
  • 服务器可以配置每个菜单按钮:
    • 索引位置
    • 图片、文本
    • 点击后以玩家身份执行指令

示例图:

# Spigot插件

Spigot插件的逻辑较为简单,在玩家UI加载完成之后,向玩家客户端发送提前加载好的菜单配置文件即可。

配置文件格式:

button1:
  idx: 1
  # 该按钮在菜单中的位置
  texture: 'textures/items/apple'
  # 该按钮显示的贴图
  text: '按钮1'
  # 该按钮下方的提示文本
  commands:
    - 'say 点击了按钮1'
  # 点击后执行的玩家指令
button2:
  idx: 2
  texture: 'textures/items/diamond_sword'
  text: '按钮2'
  commands:
    - 'say 点击了按钮2'

读取配置文件的代码不过多介绍,需要参考可以下载源码进行查看。

主类中包含了与客户端通讯的关键代码,供参考:

package me.zhanshi123.tutorialmenu;

import com.neteasemc.spigotmaster.SpigotMaster;
import me.zhanshi123.tutorialmenu.config.ConfigManager;
import me.zhanshi123.tutorialmenu.config.MenuButtonConfig;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;

public final class TutorialMenu extends JavaPlugin {
    private static TutorialMenu instance;
    private SpigotMaster spigotMaster;

    private final String NAMESPACE = "testMenu";
    private final String SERVER_SYSTEM_NAME = "testMenuDev";
    private final String CLIENT_SYSTEM_NAME = "testMenuBeh";
    private final String CLIENT_UI_LOADED_EVENT = "ClientUiLoadedEvent";
    private final String SERVER_MENU_EVENT = "ServerMenuEvent";
    private final String CLIENT_MENU_CLICKED_EVENT = "ClientMenuClickedEvent";


    public static TutorialMenu getInstance() {
        return instance;
    }

    @Override
    public void onEnable() {
        instance = this;
        ConfigManager.getInstance().loadConfig();
        spigotMaster = (SpigotMaster) Bukkit.getPluginManager().getPlugin("SpigotMaster");
        spigotMaster.listenForEvent(NAMESPACE, CLIENT_SYSTEM_NAME, CLIENT_UI_LOADED_EVENT, (player, map) ->
                spigotMaster.notifyToClient(player, NAMESPACE, SERVER_SYSTEM_NAME, SERVER_MENU_EVENT, ConfigManager.getInstance().getClientData()));
        spigotMaster.listenForEvent(NAMESPACE, CLIENT_SYSTEM_NAME, CLIENT_MENU_CLICKED_EVENT, (player, map) -> {
            int index = (int) map.get("index");
            MenuButtonConfig menuButtonConfig = ConfigManager.getInstance().getMenuConfigs().get(index);
            if (menuButtonConfig == null) {
                getLogger().warning("玩家 " + player.getName() + " 发送了一个不正确的菜单数据");
                return;
            }
            menuButtonConfig.dispatchCommand(player);
        });
    }

    @Override
    public void onDisable() {
        
    }
}

# 客户端模组

创建一个命名空间为testMenu的插件,删除developer_mods文件夹之后,就可以先创建一个空白附加包,开始编辑菜单界面。

# 界面编辑

对于这种有序排列的界面,我们可以直接使用布局面板来自动给按钮排版。

  1. 新建一个布局面板,将其锚点都设置在左上方,并设置尺寸X为适应,修改排列方式为水平排布。

  2. 新建一个面板,命名为btn_tpl,作为按钮的模板。自行调整其尺寸,教程中设置为60x60。

  3. btn_tpl下新建一个按钮,这就是按钮本体。自行调整其尺寸,教程中设置为40x40。

  4. 在空间结构中展开界面button,找到button_label,它被用来显示按钮上的文本,将它的父锚点设置到下,子锚点设到上。这样它就会整个显示在按钮图片的下方。

  5. 最后将btn_tpl设置为隐藏。后面我们会把它作为模板来克隆别的按钮,添加到stack_panel中。

除了使用布局面板+克隆模板的方式来制作这种多按钮的界面。

还可以使用网格来实现。需要注意的是,网格的内容在Create生命周期的时候,有可能还没有被生成。

需要监听GridComponentSizeChangedClientEvent (opens new window),在其数量由0变为其他的时候,才能对网格下的控件进行操作。

# 界面逻辑

将刚刚编辑好的文件,复制到对应的客户端资源testMenu/resource_packs/testMenuResource/ui处。

接下来编辑UiDef,修改screen的值为刚刚编辑的json文件。

UIData = {
    UIDef.MenuScreen: {
        "cls": "testMenuScript.ui.test_menu_screen.MenuScreen",
        "screen": "test_menu.main",
        "isHud": 1
    }
}

客户端界面初始化完成后,加载UiMgr,并向服务器通知。

def OnUiInitFinished(self, args):
    logger.info("%s OnUiInitFinished", MenuConst.ClientSystemName)
    self.mUIMgr.Init(self)
    self.NotifyToServer("ClientUiLoadedEvent", {})

监听来自服务器的ServerMenuEvent事件,并通过mUIMgr获取ScreenNode实例,调用SetData方法来设置来自服务端的数据。

def __init__(self, namespace, systemName):
    ClientSystem.__init__(self, namespace, systemName)
    self.mUIMgr = uiMgr.UIMgr()

    self.ListenForEvent(clientApi.GetEngineNamespace(), clientApi.GetEngineSystemName(), MenuConst.UiInitFinishedEvent, self, self.OnUiInitFinished)
    self.ListenForEvent(MenuConst.ModName, MenuConst.ServerSystemName, "ServerMenuEvent", self, self.OnServerMenu)

def OnServerMenu(self, args):
    print "OnServerMenu", args
    if self.mUIMgr:
        self.mUIMgr.GetUI(UIDef.MenuScreen).SetData(args["data"])
    else:
        print "UIMgr还没有加载!"

编写MenuScreen的SetData方法,通过传入的数据来克隆按钮。并给按钮添加回调。

Clone (opens new window)具体参数见文档。

# -*- coding: utf-8 -*-
import mod.client.extraClientApi as clientApi

from testMenuScript.menuConst import ModName, ClientSystemName

ScreenNode = clientApi.GetScreenNodeCls()


class MenuScreen(ScreenNode):
    """
    Menu
    """

    def __init__(self, namespace, name, param):
        ScreenNode.__init__(self, namespace, name, param)
        self.mStackPanel = "/stack_panel"
        self.mTemplate = "/btn_tpl"
        print '==== %s ====' % 'init MenuScreen'

    # Create函数是继承自ScreenNode,会在UI创建完成后被调用
    def Create(self):
        print '==== %s ====' % 'MenuScreen Create'

    def OnBtnClick(self, args):
        print args["ButtonPath"]
        path = args["ButtonPath"]  # 路径为 /stack_panel/btn_x/button
        buttonId = int(path[path.rindex("_") + 1:path.index("/button")])  # 截取路径中的x,它就是按钮的索引
        clientApi.GetSystem(ModName, ClientSystemName).NotifyToServer("ClientMenuClickedEvent", {"index": buttonId})  # 发送给服务器

    def SetData(self, data):
        data = sorted(data, key=lambda x: x["index"])
        # 对按钮顺序进行排序
        for btn in data:
            newName = "btn_{}".format(btn["index"])  # 把按钮对应的index存在路径中,后面按钮点击时,根据路径判断是哪个按钮
            self.Clone(self.mTemplate, self.mStackPanel, newName)
            newPath = self.mStackPanel + "/" + newName
            self.GetBaseUIControl(newPath).SetVisible(True)  # 设置可见
            newPath += "/button"
            btnControl = self.GetBaseUIControl(newPath).asButton()
            btnControl.AddTouchEventParams({"isSwallow": True})  # 先开启按钮回调功能
            btnControl.SetButtonTouchUpCallback(self.OnBtnClick)  # 再设置按钮回调函数
            # 按钮的贴图有3个,分别对应默认、按下、悬浮。这里三个都设置。
            self.GetBaseUIControl(newPath + "/default").asImage().SetSprite(btn["texture"])
            self.GetBaseUIControl(newPath + "/pressed").asImage().SetSprite(btn["texture"])
            self.GetBaseUIControl(newPath + "/hover").asImage().SetSprite(btn["texture"])
            self.GetBaseUIControl(newPath + "/button_label").asLabel().SetText(btn["text"])  # 设置按钮下的文本

# 效果展示

# 代码下载

Spigot插件:点我 (opens new window)

客户端模组:点我 (opens new window)

Spigot插件

客户端模组

界面编辑

界面逻辑

效果展示

代码下载