抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

创建项目

本项目采用 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.java
1
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() {
// Plugin shutdown logic
}
}

推荐使用Logger.info()而非System.out.println()来输出日志. 运行runServer后, 可以看到控制台成功输出.

由于游戏中有非常多的事件, 无法逐一介绍, 这里只介绍我在开发过程中实际使用过的事件. 并实现一些小功能.

文本

在学习事件之前需要先了解文本输出, 游戏中使用Component来显示文字, 而非字符串.

将字符串转化为Component的方法如下:

1
2
3
import net.kyori.adventure.text.Component;

Component welcome = Component.text("欢迎")

Component可以让你轻松地修改文本样式, 方法名也通俗易懂, 此处不再详细描述.

玩家加入事件

创建一个监听器类, 并监听玩家进入事件. 当玩家加入服务器后, 显示欢迎语, 并标红玩家昵称.

PlayerEvent.java
1
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.java
1
2
3
4
5
@Override
public void onEnable() {
// 此写法对应API版本1.20, 不同的版本可能有所不同.
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();
// 添加等级为 1 的耐久附魔
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");
// 指令描述
// 输入 /help forceload 即可查看
this.setDescription("添加, 删除与查看强加载区块");
// 指令使用方式
// 其中 <command> 会被自动替换为你的指令内容, 中括号表示可选参数
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;
}
}

评论