不知道为什么,笔者觉得第三章过得很快,又到了演习时间了。
!> 需要 Paper 服务端
由于笔者的失误,本次行动(以及后面章节的一些内容)必须使用 Paper 才能完成,如果发现示例代码在你的开发工具中显示错误,你可在 PaperMC 官网 下载 Paper 并运行一次(自动下载 Mojang 服务端,速度较慢,请耐心等待)。当 Paper 启动成功(出现 「Done!」 字样)后,你可以导入 cache
文件夹中的 patched_1.16.5.jar
以代替 spigot-1.16.5.jar
进行开发。
总是让玩家使用命令行不是很友好,我们需要设计一个菜单。菜单的功能包括:
- 退出服务器
- 获取服务器公告
- 随机传送
我们先实现简单的功能,在没有支援的情况下,要实现那么多功能可能比较困难。我们慢慢来。
行动名称:HoofPower
行动代号:EX-2
行动类别:演习
涉及章节:
- EX-2-1
- EX-2-2
难度:骷髅
这次演习基本上就是复习学过的内容,我们会用到物品介绍、配置文件读取、显示书等功能的。不记得了?再回去读一遍。
创建新模块「HoofPower」并为它添加依赖(参照上一次演习内容)。
「Hoof」是什么意思呢?玩家玩 Minecraft 时右手只用两根手指,所以就算玩家没有手指,一样可以玩 Minecraft(把你的手握成拳试试),就像马蹄一样,所以用它的英文命名了这个插件——也算是提醒玩家不要沉迷游戏吧。(没错这个理由是现编的)
创建包,创建主类,继承 JavaPlugin
,这你应该很熟练了。自己命名!。
package rarityeg.hoofpower;
// 一定要自己为包命名啊
import org.bukkit.plugin.java.JavaPlugin;
public class HoofPower extends JavaPlugin {
public static JavaPlugin instance;
@Override
public void onEnable() {
instance = this;
}
}
我们先不考虑菜单如何打开,先考虑如何把菜单画出来。
一番思考后,我决定:使用一个单独的 Menu
类来制作菜单(面向对象!)。
那么我们赶快创建一个类,并为它创建一个成员变量 components
表示菜单内的组件。
package rarityeg.hoofpower;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
public class Menu {
public Inventory components;
public Player player;
public Menu(Player player) {
}
}
这里我还顺便创建了 player
变量,记录下 GUI 的所属者。
那么我们就在构造方法中,赶紧创建一个 GUI 吧~有了之前的知识,唰唰唰地就写出来了:
package rarityeg.hoofpower;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
public class Menu {
public Inventory components;
public Player owner;
public static final String TITLE = "HoofPower 菜单";
public Menu(Player player) {
components = Bukkit.createInventory(player, 9, TITLE);
owner = player;
}
}
为什么要将标题作为一个常量呢?我们说过标题是区分 GUI 的方法,如果各处都硬编码字符串,不仅不好修改,如果写错了还难发现。使用常量就可很好地规避这些问题。
这里因为我们菜单的功能比较少,所以只做了一行。
按钮基本上是物品,所以都是实例化 ItemStack
,方法都差不多。
退出服务器按钮我们决定使用屏障方块(禁行符号),因为……实在没有更合适的了。
// Menu 节选
public static final String QUIT_SERVER = "退出服务器";
public Menu(Player player) {
components = Bukkit.createInventory(player, 9, TITLE);
ItemStack quitServer = new ItemStack(Material.BARRIER);
// 屏障
ItemMeta quitServerMeta = quitServer.getItemMeta();
quitServerMeta.setDisplayName(QUIT_SERVER);
// 设置「按钮」的名字
quitServerMeta.setLore(Collections.singletonList(ChatColor.GRAY + "" + ChatColor.ITALIC + "离开此服务器"));
quitServer.setItemMeta(quitServerMeta);
// 有借有还,再借不难
}
这里我们同样使用了常量,以免出错。
ChatColor
不能连续拼接(+
),中间需要一个空字符串,没办法,这是规定。此外,Collections.singletonList
方法用于创建单元素 List
,比 Arrays.asList
更快。
这里我们用一本书表示。
// Menu 节选
// Menu 构造方法外:
public static final String SHOW_ANNOUNCEMENT = ChatColor.GOLD + "显示公告";
// Menu 构造方法内:
ItemStack showAnnouncement = new ItemStack(Material.BOOK);
ItemMeta showAnnouncementMeta = showAnnouncement.getItemMeta();
showAnnouncementMeta.setDisplayName(SHOW_ANNOUNCEMENT);
showAnnouncementMeta.setLore(Collections.singletonList(ChatColor.GRAY + "" + ChatColor.ITALIC + "查看公告"));
showAnnouncement.setItemMeta(showAnnouncementMeta);
最后我们来添加随机传送按钮。
大家也许注意到了,这样编写代码比较累,你可以自己编写创建物品的方法。
// Menu 节选
// Menu 构造方法外:
public static final String TELEPORT = ChatColor.GREEN + "随机传送";
// Menu 构造方法内:
ItemStack teleport = new ItemStack(Material.COMPASS);
ItemMeta teleportMeta = teleport.getItemMeta();
teleportMeta.setDisplayName(TELEPORT);
teleportMeta.setLore(Collections.singletonList(ChatColor.GRAY + "" + ChatColor.ITALIC + "在当前世界随机传送"));
teleport.setItemMeta(teleportMeta);
我们还需要将这些物品「粘」到菜单上。
// Menu 节选
components.setItem(0, quitServer); // 左边
components.setItem(4, showAnnouncement); // 中间
components.setItem(8, teleport); // 右边
我们需要一个单独的方法打开菜单:
// Menu 节选
public void open() {
owner.openInventory(components);
}
最终的代码像这样:
package rarityeg.hoofpower;
import org.bukkit.Bukkit;
import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.Inventory;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta;
import java.util.Collections;
public class Menu {
public Inventory components;
public Player owner;
public static final String TITLE = "HoofPower 菜单";
public static final String QUIT_SERVER = "退出服务器";
public static final String SHOW_ANNOUNCEMENT = ChatColor.GOLD + "显示公告";
public static final String TELEPORT = ChatColor.GREEN + "随机传送";
public Menu(Player player) {
components = Bukkit.createInventory(player, 9, TITLE);
owner = player;
ItemStack quitServer = new ItemStack(Material.BARRIER);
ItemMeta quitServerMeta = quitServer.getItemMeta();
quitServerMeta.setDisplayName(QUIT_SERVER);
quitServerMeta.setLore(Collections.singletonList(ChatColor.GRAY + "" + ChatColor.ITALIC + "离开此服务器"));
quitServer.setItemMeta(quitServerMeta);
ItemStack showAnnouncement = new ItemStack(Material.BOOK);
ItemMeta showAnnouncementMeta = showAnnouncement.getItemMeta();
showAnnouncementMeta.setDisplayName(SHOW_ANNOUNCEMENT);
showAnnouncementMeta.setLore(Collections.singletonList(ChatColor.GRAY + "" + ChatColor.ITALIC + "查看公告"));
showAnnouncement.setItemMeta(showAnnouncementMeta);
ItemStack teleport = new ItemStack(Material.COMPASS);
ItemMeta teleportMeta = teleport.getItemMeta();
teleportMeta.setDisplayName(TELEPORT);
teleportMeta.setLore(Collections.singletonList(ChatColor.GRAY + "" + ChatColor.ITALIC + "在当前世界随机传送"));
teleport.setItemMeta(teleportMeta);
components.setItem(0, quitServer);
components.setItem(4, showAnnouncement);
components.setItem(8, teleport);
}
public void open() {
owner.openInventory(components);
}
}
好,这样我们的菜单就画好了。
上面画好的菜单,实际上只是空壳,如果我们打开菜单,除了点击(拿出物品)以外什么也做不了。
我们需要事件监听,创建 EventListener
类。
你不会希望玩家把屏障拿出来吧?
// EventListener 节选
@EventHandler
public void onClick(InventoryClickEvent e) {
Player player = (Player) e.getWhoClicked();
InventoryView inv = player.getOpenInventory();
if (inv.getTitle().equals(Menu.TITLE)) {
e.setCancelled(true);
}
}
正如我们之前所说,监听器的函数名不重要,重要的是参数。
以上代码检查玩家点击的是不是我们的菜单,如果是就阻止本次点击。(防止拿出物品)
和 3-1 一样:
// EventListener 节选
if (inv.getTitle().equals(Menu.TITLE)) {
e.setCancelled(true);
if (e.getRawSlot() < 0 || e.getRawSlot() > e.getInventory().getSize()) {
// 有效取值范围
return;
}
ItemStack clickedItem = e.getCurrentItem();
if (clickedItem == null){
return;
}
// 进一步的代码写在这里
}
如果格子的位置不合法,就不进行进一步处理,直接 return
。
?> 为什么不用 else
?
这里的 return
比 else
好用,我们的思路是:这种情况不符合,处理结束;那种情况不符合,处理结束……直到全都符合,才进行处理。使用 return
可以避免嵌套 if
的繁琐。
退出服务器的实现也很简单:
// EventListener 节选
if (clickedItem.getItemMeta().getDisplayName().equals(Menu.QUIT_SERVER)) {
player.kickPlayer("您已离开服务器");
return; // 处理完了记得结束处理
}
直接使用上一章我们讲到的 kickPlayer
就可以了。
这里我们比较了所点击物品的名称,由于 switch
不允许对变量进行,只能使用 if
了。
随机传送稍微有一点点难度,需要用到 Random
。
我们希望随机传送不是完全的随机,而是在玩家身边十万格之内传送。为了简单起见,我们让玩家传送到主世界。在服务端中,主世界的默认名称是 world
,但是有的腐竹可能会修改这个名称,所以不能用名字查找……怎么办呢?
这个在 Bukkit 的文档中没有记载,但我在 Bukkit 的论坛上找到了解决方法:
World overworld = Bukkit.getWorlds().get(0);
World nether = Bukkit.getWorlds().get(1);
World end = Bukkit.getWorlds().get(2);
因为主世界、下界和末地在游戏的内部编号就是 0、1、2,这个是不会变的,因此可以用这个方法获得各个世界的。那么我们就可以实现随机传送功能啦!
// EventListener 节选
// onClick 方法外:
public static final Random RANDOM = new Random(); // 创建随机数单元
// onClick 方法内:
if (clickedItem.getItemMeta().getDisplayName().equals(Menu.TELEPORT)) {
player.closeInventory();
World playerWorld = Bukkit.getWorlds().get(0); // 获得主世界
double randX = RANDOM.nextInt(200000) - 100000;
double randZ = RANDOM.nextInt(200000) - 100000;
Location offset = new Location(playerWorld, randX, 0, randZ).toHighestLocation(); // 获得最高的非空气方块
player.teleport(player.getLocation().add(offset)); // add 加算距离
player.sendMessage(ChatColor.GREEN + "传送成功!");
return;
}
toHighestLocation
获得最高的非空方块位置——确保玩家脚下有东西并且不被卡到石头里面去(虽然有极小的概率传送到熔岩上)!
nextInt
方法会返回一个 「0 ~ 设定值」之间的整数,因此我们生成 0 ~ 200000 之间的随机数,再减去 100000 就可以得到 -100000 ~ 100000 的随机数啦~
最后我们实现服务器公告。为了服主配置方便,我们这里使用配置文件。默认的 config.yml
(创建在 src
下):
announcement: |
这是一段公告,您可以随意修改。
+++
使用三个加号来换页。
记得使用 UTF-8 编码保存!
服务端如果乱码请加上 Java 参数:
-Dfile.encoding=UTF-8
RarityEG 作品
管道符(|
)是 YAML 多行字符串的语法,加在冒号空格后面,再使用正确的缩进就可以书写多行字符串。
然后就简单啦,创建一个 ItemStack
并为玩家显示:
// EventListener 节选
if (clickedItem.getItemMeta().getDisplayName().equals(Menu.SHOW_ANNOUNCEMENT)) {
ItemStack ann = new ItemStack(Material.WRITTEN_BOOK);
BookMeta annBm = (BookMeta) ann.getItemMeta();
String[] acText = Objects.requireNonNullElse(HoofPower.instance.getConfig().getString("announcement"), "").split("\\+\\+\\+");
// Java 默认使用正则表达式进行查找,在正则表达式中,加号是特殊字符,需要使用 \ 转义;而在 Java 中,\ 本身也是特殊字符,因此就需要输入两个 \
annBm.setPages(acText);
annBm.setAuthor("HoofPower");
annBm.setTitle("服务器公告");
// 三项缺一不可
ann.setItemMeta(annBm);
player.openBook(ann);
}
最后不需要 return
,因为这已经是最后一个处理了。
最终代码:
package rarityeg.hoofpower;
import org.bukkit.*;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.inventory.InventoryClickEvent;
import org.bukkit.inventory.InventoryView;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.BookMeta;
import java.util.Objects;
import java.util.Random;
public class EventListener implements Listener {
public static final Random RANDOM = new Random();
@EventHandler
public void onClick(InventoryClickEvent e) {
Player player = (Player) e.getWhoClicked();
InventoryView inv = player.getOpenInventory();
if (inv.getTitle().equals(Menu.TITLE)) {
e.setCancelled(true);
if (e.getRawSlot() < 0 || e.getRawSlot() > e.getInventory().getSize()) {
return;
}
ItemStack clickedItem = e.getCurrentItem();
if (clickedItem == null) {
return;
}
if (clickedItem.getItemMeta().getDisplayName().equals(Menu.QUIT_SERVER)) {
player.kickPlayer("您已离开服务器");
return;
}
if (clickedItem.getItemMeta().getDisplayName().equals(Menu.TELEPORT)) {
player.closeInventory();
World playerWorld = Bukkit.getWorld("world");
double randX = RANDOM.nextInt(200000) - 100000;
double randZ = RANDOM.nextInt(200000) - 100000;
Location offset = new Location(playerWorld, randX, 0, randZ).toHighestLocation();
player.teleport(offset);
player.sendMessage(ChatColor.GREEN + "传送成功!");
return;
}
if (clickedItem.getItemMeta().getDisplayName().equals(Menu.SHOW_ANNOUNCEMENT)) {
ItemStack ann = new ItemStack(Material.WRITTEN_BOOK);
BookMeta annBm = (BookMeta) ann.getItemMeta();
String[] acText = Objects.requireNonNullElse(HoofPower.instance.getConfig().getString("announcement"), "").split("\\+\\+\\+");
annBm.setPages(acText);
annBm.setAuthor("HoofPower");
annBm.setTitle("服务器公告");
ann.setItemMeta(annBm);
player.openBook(ann);
}
}
}
}
如果只是需要简单的命令处理,可以将 onCommand
写在主类中,这样就不需要注册了:
// HoofPower 节选
@Override
@ParametersAreNonnullByDefault
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!command.getName().equals("cd")) {
// 按道理这里其实不用判断,因为不是 cd 这个命令就不会送到这里处理,但为了以防万一……
return false;
}
if (!(sender instanceof Player)) {
return false;
}
new Menu((Player) sender).open();
return true;
}
能够写在主类中只是因为 JavaPlugin
自己就实现了 CommandExecutor
,不需要注册则是因为 Bukkit 在初始化插件时会默认注册主类。
接下来就是常规操作啦~
package rarityeg.hoofpower;
import org.bukkit.Bukkit;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;
import javax.annotation.ParametersAreNonnullByDefault;
public class HoofPower extends JavaPlugin {
public static JavaPlugin instance;
@Override
public void onLoad() {
saveDefaultConfig();
}
@Override
public void onEnable() {
instance = this;
Bukkit.getPluginManager().registerEvents(new EventListener(), this);
}
@Override
@ParametersAreNonnullByDefault
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!command.getName().equals("cd")) {
return false;
}
if (!(sender instanceof Player)) {
return false;
}
new Menu((Player) sender).open();
return true;
}
}
最后一步!
main: rarityeg.hoofpower.HoofPower
# 改成你对应的包名!
# 改成你对应的包名!
# 改成你对应的包名!
# 改成你对应的包名!
# 改成你对应的包名!
name: HoofPower
version: 1.0
api-version: 1.16
commands:
cd:
usage: "/cd"
description: "Open menu."
我……我还是担心你会把上面的东西直接复制进去……不要……不要啊!你在编写代码时使用的是什么包名(可以通过主类中的 package
语句查到),main
这里就要填写对应的包名 + 主类名!不然 Bukkit 不认的啊!嘤……你什么时候才能让本小马对你放心呢……
至此,所有的代码部分就完成了,呼——
和上一章一样,创建新的「Artifact」,「Include in project build」、「'HoofPower' compile output」,按下绿色锤子——
一切,就完成了。