贺兰兰
本帖最后由 贺兰兰 于 2021-6-16 17:45 编辑

Bukkit NMS 开发实践 —— 创建你自己的自定义实体(适用于 1.16.3 - 1.16.5 版本)

什么是 NMS?

NMS 是 net.minecraft.server 包的简写,是 CraftBukkit 服务端及其下游服务端的底层实现,其代码包含 Mojang 发布的 Vanilla 服务端代码和 SpigotMC 添加的、用于与 BukkitAPI 进行交互的代码。在开发者无法借助 BukkitAPI 完成所需要的功能时,开发者我常常使用 NMS 进行开发。NMS 开发是底层行为,同时跨版本兼容性较差,除非必须使用,否则还请尽量使用 BukkitAPI。NMS 仅存在于编译后的服务端内部,不属于 BukkitAPI 内容。各版本的 NMS 包名一般均为 net.minecraft.server.v版_本_R号,如 net.minecraft.server.v1_16_R3。NMS 包内为扁平结构,没有二级包。NMS 包内类名为 Spigot 定义的反混淆名;方法、字段名一部分为 Spigot 定义的反混淆名,一部分为原混淆名;方法参数名一般为原混淆名。本教程旨在教授 Bukkit 开发者以 NMS 使用方法,拓展 Bukkit 开发者的开发视野。

自 1.17 后,SpigotMC 开始提供 Mojang 混淆表版本的 Spigot 服务端,这意味着大大简化了开发难度 —— 不需要再对照混淆表一个一个看 NMS 方法名,而可以像 Forge 或是 Fabric 开发者一样使用各自的反混淆代码直接进行开发 —— 只需要使用 Spigot 提供的 SpecialSource 工具将 Mojang Mapping 转换回 obf 版本即可发布。

如何使用 NMS?

要想使用 NMS,您必须手动导入编译好的 CraftBukkit/Spigot 服务端核心,这样才能获取其中内置的 NMS。对于 Paper 及其下游服务端来说,不应该直接导入服务端核心本体,而应该导入运行一次服务端后生成的 patched_x.x.x.jar 文件。

教程:创建自定义实体

很显然,BukkitAPI 没有向我们提供自定义实体的功能,甚至,实体的类型是确定的,不能更改的。因此,要想自定义实体,必须使用 NMS。当然,我们并不能创建 Forge 或是 Fabric 意义上完全自定义模型的实体。但是,我们能够通过继承原版存在的实体,创建一个新的实体类型,为这个新的实体类型指定一些交互。本例中,我们将会通过创建一个会在夜间燃烧、不做任何交互、拥有 Boss 血条的巨人僵尸来演示这一过程。

继承已有实体

让我们创建 EntityCustomGiantZombie 类,继承 net.minecraft.server.v1_16_R3.EntityGiantZombie 类:

  1. public class EntityCustomGiantZombie extends EntityGiantZombie {}
复制代码

接下来,初始化该实体,实现超类构造器:

  1. public EntityCustomGiantZombie(EntityTypes<? extends EntityGiantZombie> var0, World var1) {
  2.         super(var0, var1);
  3. }
复制代码

注意,此处的 World 不是我们熟识的 org.bukkit.World 接口,而是 net.minecraft.server.v1_16_R3.World 抽象类,因此不能一概而论。

当然,我们可以通过以下代码实现 Bukkit World 和 NMS World 的互转:

  1. //Bukkit World to NMS World
  2. org.bukkit.World bukkitWorld = nmsWorld.getWorld();
  3. // NMS World to Bukkit World
  4. net.minecraft.server.v1_16_R3.World nmsWorld = ((CraftWorld) bukkitWorld).getHandle();
复制代码

其实,调用 net.minecraft.server.v1_16_R3.World#getHandle() 返回的并非 org.bukkit.World 接口,而是 org.bukkit.craftbukkit.v1_16_R3.CraftWorld 类,其为 org.bukkit.World 在 CraftBukkit 服务端中的内部实现,因此可以直接转换到 World 接口。事实上,nmsWorld#getWorld() 方法返回的也是 CraftWorld 类。

要想生成该实体,则应该调用 WorldServer#addEntity(Entity, SpawnReason) 方法初始化实体,然后使用 Entity#setPositionRotation(double, double, double, float, float) 传送实体到出生位置。为了简便流程,我们可以创建一个可传入 Bukkit Location,并可以自动设置实体出生位置的构造函数:

  1. public EntityCustomGiantZombie(Location loc) {
  2.         this(EntityTypes.GIANT, ((CraftWorld) loc.getWorld()).getHandle());
  3.         setPositionRotation(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
  4.     }
复制代码

然后,在适当的位置初始化该实体,比如,某一个 Bukkit EventListener 中:

  1. ((CraftWorld) e.getPlayer().getWorld()).getHandle().addEntity(new EntityCustomGiantZombie(e.getPlayer().getLocation()), CreatureSpawnEvent.SpawnReason.CUSTOM);
复制代码

这样,你就能看到一个由你自定义的巨人僵尸实体了!

添加 Boss 血条

接下来,我们尝试向这个自定义实体添加 Boss 血条。

添加 Boss 血条大概需要有三步操作:

  • 当玩家进入追踪视野时显示 Boss 血条
  • 当玩家离开追踪视野时隐藏 Boss 血条
  • 当怪物受到攻击时令 Boss 血条相应减少血量

首先,我们需要定义一个 Boss 血条。在 EntityCustomGiantZombie 类中添加以下字段:

  1. private final BossBattleServer bossBar;
复制代码

并在底层构造器中初始化这个 Boss 血条:

  1. bossBar = new BossBattleServer(new ChatComponentText("Boss 血条示例").a(EnumChatFormat.GOLD), BossBattle.BarColor.BLUE, BossBattle.BarStyle.NOTCHED_12);
  2.         bossBar.setDarkenSky(true);
  3.     }
复制代码

这初始化了一个血条名为金色的 "Boss 血条示例",血条颜色为蓝色的,1/12 比例风格的,在玩家显示 Boss 血条时时天空变暗的 Boss 血条。

然后,我们需要覆盖 void b()void c() 两个方法,这两个方法在 MCP 中描述如下:

  1. /**
  2.     * Add the given player to the list of players tracking this entity. For instance, a player may track a boss in order
  3.     * to view its associated boss bar.
  4.     */
  5.    public void addTrackingPlayer(ServerPlayerEntity player) {
  6.    }

  7.    /**
  8.     * Removes the given player from the list of players tracking this entity. See {[url=home.php?mod=space&uid=41191]@link[/url] Entity#addTrackingPlayer} for
  9.     * more information on tracking.
  10.     */
  11.    public void removeTrackingPlayer(ServerPlayerEntity player) {
  12.    }
复制代码

这正是我们需要的,可以动态显示和隐藏 Boss 血条的方法。覆盖这些方法,并添加一些内容:

  1.     @Override
  2.     public void b(EntityPlayer entityplayer) {
  3.         super.b(entityplayer);
  4.         this.bossBar.addPlayer(entityplayer);
  5.     }

  6.     @Override
  7.     public void c(EntityPlayer entityPlayer) {
  8.         super.c(entityPlayer);
  9.         this.bossBar.removePlayer(entityPlayer);
  10.     }
复制代码

最后,覆盖 void tick() 方法,该方法一看名字就知道是干什么的了:

  1.     @Override
  2.     public void tick() {
  3.         super.tick();
  4.         this.bossBar.setProgress(getHealth() / getMaxHealth());
  5.     }
复制代码

其中 bossBar.setProgress(float) 接受一个单精度浮点数,为血条剩余的血量百分比。

需要注意的是,一定要调用 super.tick(),否则该怪物完全被冻结,不会产生任何交互。

让怪物在夜间燃烧

要想让怪物在夜间燃烧,则需要在每 tick 检测怪物是否处于夜间环境,如果是,则使怪物燃烧。因此,在 tick() 方法键入以下代码:

  1. if (world.isNight()) setOnFire(1, false);
复制代码

其中 setOnFire(int, boolean) 的第一个参数为燃烧的 tick 数,由于是 1 tick 检测一次,因此我们在这里填写 1;第二个参数为是否触发 BukkitAPI 的 EntityCombustEvent 事件,为了避免事件被多次调用,这里我们填写 false

这样,怪物就会在夜间燃烧了。

自定义怪物行为

要想自定义怪物行为,我们需要为怪物添加 PathfinderGoal,因为我们不希望保留怪物原本的行为,因此我们需要刷新怪物的 goalSelector(行为选择器) 和 targetSelector(攻击目标选择器)。他们均为 PathfinderGoalSelector 类型的对象。经过研究该对象我们可以发现,PathfinderGoal 对象被包装为 PathfinderGoalWrapped 对象后,存储于private final Set<PathfinderGoalWrapped> d 字段中,由于该字段是 private final 的,因此我们需要通过反射修改该字段。在怪物的底层构造器中键入以下代码:

  1. try {
  2.             Field dField = PathfinderGoalSelector.class.getDeclaredField("d");
  3.             dField.setAccessible(true);
  4.             dField.set(goalSelector, Sets.newLinkedHashSet());
  5.             dField.set(targetSelector, Sets.newLinkedHashSet());
  6.         } catch (NoSuchFieldException | IllegalAccessException noSuchFieldException) {
  7.             noSuchFieldException.printStackTrace();
  8.         }
复制代码

这样,我们便得到了一个没有任何行为的怪物。如果我们还需要为怪物添加行为,只需要为 goalSelector 或是 targetSelector 添加继承了 PathfinderGoal 类的对象即可。NMS 中本身就包含了大量的 PathfinderGoal,大家可自行探索。

来自群组: Server CT

鬼畜畜
是时候出个1.17的教程了,1.17的NMS变化挺大的
以及,你他喵不是发烧了吗,怎么还有空来发教程?

Lidocaine
?发烧好了?
真好,考完就开始活跃了,贴贴

贺兰兰
鬼畜畜 发表于 2021-6-16 17:56
是时候出个1.17的教程了,1.17的NMS变化挺大的
以及,你他喵不是发烧了吗,怎么还有空来发教程?
...

昨天就开始写了,今天发烧本来不写了的
执念让我把这玩意写完发上来

1659418257
666666666666

zycurry
神乎其技!6的飞起!

小六子鸭.
为贺兰大大打Call!!!

MiaoLio
那,有办法实现处理mod新增的生物吗?

比如原版spigot处理暮色森林的娜迦,并且在mod客户端内弄出来

123wwwww正版
谢谢大佬!!!

kqn3
请问,为什么没有注册,就是调用IRegistry.a()。

贺兰兰
kqn3 发表于 2021-6-22 18:50
请问,为什么没有注册,就是调用IRegistry.a()。

自 1.14 后,不再需要注册实体类型。

是林墨然本人
看起来好强,但是根本没看懂(bushi

kqn3
贺兰兰 发表于 2021-6-22 19:11
自 1.14 后,不再需要注册实体类型。

watch out,还就内个孤陋寡闻

何奈vr
666666666666

yyeerai
1.17没有那个类了

贺兰兰
yyeerai 发表于 2021-7-9 20:08
1.17没有那个类了

1.17 没有 NMS 了

a540909034

1.17没有那个类了

EasterFG
如何才能保存实体?

e_mirai
spigot说的那个remap的调用方法,1.16也能用是吧

煮雨gg
我刚学java不知道学到后面能不能学到这种程度

951967696
666666666666666666666666

xiao丶小希
有点6,没系统学习过java看不懂

916881120
#在这里快速回复#12312312321

曦-执念
讲的很好verygood

渣渣鍙
感谢楼主的帮助

χ﹏°失心∝
此时一位瓜友路过此处cwc

caiwenyuan
感谢分享,非常好用

caiwenyuan
还行,挺好用的

1909瓜
大佬能讲讲1.17+的specialsource怎么用吗qwq

Nicole1024
这讲的真不错啊。

随便小小林
NMS无敌无敌,我爱NMS

第一页 上一页 下一页 最后一页