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
类:
public class EntityCustomGiantZombie extends EntityGiantZombie {}
接下来,初始化该实体,实现超类构造器:
public EntityCustomGiantZombie(EntityTypes<? extends EntityGiantZombie> var0, World var1) {
super(var0, var1);
}
注意,此处的 World
不是我们熟识的 org.bukkit.World
接口,而是 net.minecraft.server.v1_16_R3.World
抽象类,因此不能一概而论。
当然,我们可以通过以下代码实现 Bukkit World 和 NMS World 的互转:
//Bukkit World to NMS World
org.bukkit.World bukkitWorld = nmsWorld.getWorld();
// NMS World to Bukkit World
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,并可以自动设置实体出生位置的构造函数:
public EntityCustomGiantZombie(Location loc) {
this(EntityTypes.GIANT, ((CraftWorld) loc.getWorld()).getHandle());
setPositionRotation(loc.getX(), loc.getY(), loc.getZ(), loc.getYaw(), loc.getPitch());
}
然后,在适当的位置初始化该实体,比如,某一个 Bukkit EventListener 中:
((CraftWorld) e.getPlayer().getWorld()).getHandle().addEntity(new EntityCustomGiantZombie(e.getPlayer().getLocation()), CreatureSpawnEvent.SpawnReason.CUSTOM);
这样,你就能看到一个由你自定义的巨人僵尸实体了!
添加 Boss 血条
接下来,我们尝试向这个自定义实体添加 Boss 血条。
添加 Boss 血条大概需要有三步操作:
- 当玩家进入追踪视野时显示 Boss 血条
- 当玩家离开追踪视野时隐藏 Boss 血条
- 当怪物受到攻击时令 Boss 血条相应减少血量
首先,我们需要定义一个 Boss 血条。在 EntityCustomGiantZombie 类中添加以下字段:
private final BossBattleServer bossBar;
并在底层构造器中初始化这个 Boss 血条:
bossBar = new BossBattleServer(new ChatComponentText("Boss 血条示例").a(EnumChatFormat.GOLD), BossBattle.BarColor.BLUE, BossBattle.BarStyle.NOTCHED_12);
bossBar.setDarkenSky(true);
}
这初始化了一个血条名为金色的 "Boss 血条示例",血条颜色为蓝色的,1/12 比例风格的,在玩家显示 Boss 血条时时天空变暗的 Boss 血条。
然后,我们需要覆盖 void b()
和 void c()
两个方法,这两个方法在 MCP 中描述如下:
/**
* Add the given player to the list of players tracking this entity. For instance, a player may track a boss in order
* to view its associated boss bar.
*/
public void addTrackingPlayer(ServerPlayerEntity player) {
}
/**
* Removes the given player from the list of players tracking this entity. See {@link Entity#addTrackingPlayer} for
* more information on tracking.
*/
public void removeTrackingPlayer(ServerPlayerEntity player) {
}
这正是我们需要的,可以动态显示和隐藏 Boss 血条的方法。覆盖这些方法,并添加一些内容:
@Override
public void b(EntityPlayer entityplayer) {
super.b(entityplayer);
this.bossBar.addPlayer(entityplayer);
}
@Override
public void c(EntityPlayer entityPlayer) {
super.c(entityPlayer);
this.bossBar.removePlayer(entityPlayer);
}
最后,覆盖 void tick()
方法,该方法一看名字就知道是干什么的了:
@Override
public void tick() {
super.tick();
this.bossBar.setProgress(getHealth() / getMaxHealth());
}
其中 bossBar.setProgress(float)
接受一个单精度浮点数,为血条剩余的血量百分比。
需要注意的是,一定要调用 super.tick()
,否则该怪物完全被冻结,不会产生任何交互。
让怪物在夜间燃烧
要想让怪物在夜间燃烧,则需要在每 tick 检测怪物是否处于夜间环境,如果是,则使怪物燃烧。因此,在 tick()
方法键入以下代码:
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
的,因此我们需要通过反射修改该字段。在怪物的底层构造器中键入以下代码:
try {
Field dField = PathfinderGoalSelector.class.getDeclaredField("d");
dField.setAccessible(true);
dField.set(goalSelector, Sets.newLinkedHashSet());
dField.set(targetSelector, Sets.newLinkedHashSet());
} catch (NoSuchFieldException | IllegalAccessException noSuchFieldException) {
noSuchFieldException.printStackTrace();
}
这样,我们便得到了一个没有任何行为的怪物。如果我们还需要为怪物添加行为,只需要为 goalSelector
或是 targetSelector
添加继承了 PathfinderGoal
类的对象即可。NMS 中本身就包含了大量的 PathfinderGoal
,大家可自行探索。
谢谢大佬的分享!!省了我看源码的时间!!!!
为什么1.12+版本这样生成实体却什么都没有,生成出来但有怪物的声音没有贴图实体