# 作业
要求:依赖客户端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
文件夹之后,就可以先创建一个空白附加包,开始编辑菜单界面。
# 界面编辑
对于这种有序排列的界面,我们可以直接使用布局面板来自动给按钮排版。
新建一个布局面板,将其锚点都设置在左上方,并设置
尺寸X
为适应,修改排列方式为水平排布。新建一个面板,命名为
btn_tpl
,作为按钮的模板。自行调整其尺寸,教程中设置为60x60。在
btn_tpl
下新建一个按钮,这就是按钮本体。自行调整其尺寸,教程中设置为40x40。在空间结构中展开界面
button
,找到button_label
,它被用来显示按钮上的文本,将它的父锚点设置到下,子锚点设到上。这样它就会整个显示在按钮图片的下方。最后将
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)