Skip to content

Latest commit

 

History

History
549 lines (421 loc) · 18.5 KB

EX-2-1.md

File metadata and controls

549 lines (421 loc) · 18.5 KB

EX-2-1 编写菜单插件

不知道为什么,笔者觉得第三章过得很快,又到了演习时间了。

!> 需要 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
这里的 returnelse 好用,我们的思路是:这种情况不符合,处理结束;那种情况不符合,处理结束……直到全都符合,才进行处理。使用 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;
    }
}

plugin.yml

最后一步!

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」,按下绿色锤子——

一切,就完成了。