创建项目
本项目采用 IDEA 开发,在插件市场下载Minecraft Development,来快速初始化项目.
安装完插件后, 在新建项目时可以看到Minecraft
选项, 选择Plugin-Bukkit-Paper
并耐心等待项目初始化.
为了在IDE中直接运行服务器, 在build.gradle
中添加以下内容
1 2 3 4 5 6 7 8 9
| plugins { id("xyz.jpenilla.run-paper") version "2.1.0" }
tasks { runServer { minecraftVersion("1.20.1") } }
|
只需要执行runServer
, 就会自动下载服务端并运行, 插件将会被映射到plugins
文件夹中而无需手动放入.
自动生成的代码中, 可以看到有一个继承了JavaPlugin
的入口点.
main.java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public final class main extends JavaPlugin {
public static Logger logger;
@Override public void onEnable() { logger = getLogger(); logger.info("Hello PaperMC!"); }
@Override public void onDisable() { } }
|
推荐使用Logger.info()
而非System.out.println()
来输出日志. 运行runServer
后, 可以看到控制台成功输出.
由于游戏中有非常多的事件, 无法逐一介绍, 这里只介绍我在开发过程中实际使用过的事件. 并实现一些小功能.
文本
在学习事件之前需要先了解文本输出, 游戏中使用Component
来显示文字, 而非字符串.
将字符串转化为Component
的方法如下:
1 2 3
| import net.kyori.adventure.text.Component;
Component welcome = Component.text("欢迎")
|
Component
可以让你轻松地修改文本样式, 方法名也通俗易懂, 此处不再详细描述.
玩家加入事件
创建一个监听器类, 并监听玩家进入事件. 当玩家加入服务器后, 显示欢迎语, 并标红玩家昵称.
PlayerEvent.java1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.dearxuan.event;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.entity.PlayerDeathEvent; import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.PlayerInventory;
public class PlayerEvent implements Listener {
@EventHandler public void onPlayerJoin(PlayerJoinEvent playerJoinEvent){ String name = playerJoinEvent.getPlayer().getName(); Component welcome = Component.text("热烈欢迎 ") .append(Component.text(name, NamedTextColor.RED)) .append(Component.text(" 进入服务器!")); playerJoinEvent.joinMessage(welcome); } }
|
@EventHandler
表示该方法是一个监听器, 方法名可以随意取, 里面的变量类型决定了这个监听器监听的事件类型.
一个监听类中可以有多个@EventHandler
注解.
现在修改入口点函数, 在onEnable()
里注册该监听器.
main.java1 2 3 4 5
| @Override public void onEnable() { getServer().getPluginManager().registerEvents(new PlayerEvent(), this); }
|
怪物事件
CreatureSpawnEvent
事件在怪物生成时触发, 例如想要禁止恶魂在地狱基岩层上方生成, 以及禁止传送门生成僵尸猪人, 只需要以下代码
1 2 3 4 5 6 7 8 9 10 11
| @EventHandler(ignoreCancelled = true) public void onGhastSpawn(CreatureSpawnEvent event){ if(event.getEntityType() == EntityType.GHAST){ if(event.getLocation().getY() >= 128){ event.setCancelled(true); } } if(event.getEntityType() == EntityType.ZOMBIFIED_PIGLIN && event.getSpawnReason() == CreatureSpawnEvent.SpawnReason.NETHER_PORTAL){ event.setCancelled(true); } }
|
此处我使用了ignoreCancelled = true
, 当事件被取消时, 显然不可能再生成恶魂和僵尸猪人了, 因此加上此注解可以让事件取消后不再执行监听代码, 节约性能.
交易配方
当村民尝试生成交易配方时, 会触发事件VillagerAcquireTradeEvent
, 监听此事件, 可以实现自定义村民交易的功能.
使用以下代码获取触发事件的村民和对应的交易配方
1 2 3 4 5 6 7
| Villager villager = (Villager) event.getEntity() MerchantRecipe recipe = event.getRecipe();
MerchantRecipe newRecipe;
event.setRecipe(newRecipe);
|
使用setIngredients
可以修改配方所需材料, 例如, 在1.20.1及之前版本绿宝石换钻石套的配方修改为1.20.2的绿宝石+钻石换钻石套的配方, 来平衡村民交易.
1
| newRecipe.setIngredients(List.of(recipe.getIngredients().get(0), new ItemStack(Material.DIAMOND, 1)));
|
配方不支持直接修改结果, 因此我写了一个函数实现仅修改产物的功能, 并生成新的配方
1 2 3 4 5 6 7 8 9 10
| private MerchantRecipe copyRecipe(MerchantRecipe recipe, ItemStack result){ MerchantRecipe newRecipe = new MerchantRecipe(result, recipe.getMaxUses()); newRecipe.setUses(recipe.getUses()); newRecipe.setDemand(recipe.getDemand()); newRecipe.setVillagerExperience(recipe.getVillagerExperience()); newRecipe.setExperienceReward(recipe.hasExperienceReward()); newRecipe.setIgnoreDiscounts(recipe.shouldIgnoreDiscounts()); newRecipe.setPriceMultiplier(recipe.getPriceMultiplier()); return newRecipe; }
|
如果想要创建附魔书, 需要按照以下方式进行附魔
1 2 3 4 5 6 7 8 9 10
| Enchantment enchantment = Enchantment.DURABILITY;
ItemStack enchantedBook = new ItemStack(Material.ENCHANTED_BOOK);
EnchantmentStorageMeta meta = (EnchantmentStorageMeta) enchantedBook.getItemMeta();
meta.addStoredEnchant(enchantment, 1, true);
enchantedBook.setItemMeta(meta);
|
附魔经验
附魔所需经验会持续叠加, 直到显示"过于昂贵", 修改铁砧的代码, 可以修改附魔所需经验值
1 2 3 4 5 6
| @EventHandler(ignoreCancelled = true) public void onEnchanting(PrepareAnvilEvent event){ if(event.getInventory().getRepairCost() > 39){ event.getInventory().setRepairCost(39); } }
|
event.getInventory().setMaximumRepairCost(100)
可以直接修改经验值上限, 使得玩家能够使用更多的经验来附魔. 但是此修改仅限服务端, 当客户端发现经验值超过39时, 仍然会显示"过于昂贵", 但是可以正常附魔.
苦力怕爆炸
爆炸事件归于EntityExplodeEvent
事件管理, 通过该事件判断爆炸源, 如果是苦力怕, 则移除全部待损坏方块列表, 就不会有方块被破坏.
同理, 我们还可以判断爆炸源是否是恶魂射出的火球, 来避免恶魂破坏方块.
1 2 3 4 5 6 7 8 9 10 11 12
| @EventHandler(ignoreCancelled = true) public void onExplosion(EntityExplodeEvent event){ if(event.getEntityType() == EntityType.CREEPER){ event.blockList().clear(); } if(event.getEntityType() == EntityType.FIREBALL){ Fireball fireball = (Fireball) event.getEntity(); if(fireball.getShooter() instanceof Ghast){ event.blockList().clear(); } } }
|
末影人搬运
生物尝试改变方块状态时, 会触发EntityChangeBlockEvent
事件, 通过事件源来避免末影人搬运方块.
1 2 3 4 5 6
| @EventHandler(ignoreCancelled = true) public void onEndermanPickup(EntityChangeBlockEvent event){ if(event.getEntityType() == EntityType.ENDERMAN){ event.setCancelled(true); } }
|
指令
在YML文件中配置指令已失效, 现在的指令需开发者手动注册. 本教程将制作一个强加载区块管理指令, 来覆盖游戏原有指令.
创建ForceLoad
类, 并继承Command
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class ForceLoad extends Command {
private static final int MAX_FORCELOAD_COUNT = 10;
public ForceLoad() { super("forceload"); this.setDescription("添加, 删除与查看强加载区块"); String usage = "\n/<command> add - 添加强加载区块\n" + "/<command> remove [x] [z] - 移除强加载区块\n" + "/<command> check [x] [z] - 查询当前区块是否为强加载\n" + "/<command> query - 查询全部强加载区块\n" + "/<command> clear - 移除全部强加载区块\n"; this.setUsage(usage); } }
|
在插件主函数中注册指令, 其中的""
是前缀, 一般是自己插件名, 如果注册的指令较少可以不加.
1
| getServer().getCommandMap().register("", new ForceLoad());
|
重写指令的处理函数, 其中args
是参数数组, 不包括指令本身, 由于我的指令只可能是1个或3个参数, 因此在代码里检查参数数量并返回结果.
sender
表示指令来源, 一般是控制台ConsoleCommandSender
或玩家Player
1 2 3 4 5 6 7 8 9 10 11
| @Override public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { if(args.length != 1 && args.length != 3){ sender.sendMessage("指令不完整, 请输入/help forceload"); return true; } if(!(sender instanceof Player player)){ sender.sendMessage("指令必须由玩家发送"); } return true; }
|
根据玩家坐标或输入的参数来获取区块
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Chunk chunk; if(args.length == 1){ chunk = player.getChunk(); }else{ try{ int chunkX, chunkZ; chunkX = Integer.parseInt(args[1]); chunkZ = Integer.parseInt(args[2]); chunk = player.getWorld().getChunkAt(chunkX, chunkZ); }catch (Exception ignored){ sender.sendMessage("区块坐标有误"); return true; } }
|