本帖最后由 童鞋鞋 于 2020-3-5 09:00 编辑



我正在开发imiPet宠物插件,是运用NMS自定义实体,花了一段时间学习NMS自定义实体
该视频有坐骑演示

想必很多人看过莫老的自定义实体教程:https://www.mcbbs.net/thread-811096-1-1.html
那么我为何要还要发表这样自定义实体教程呢?
理由诸如:
  • 我发现其不适用于Minecraft-1.13+
  • 其坐骑需要等待一段时间才能驾驶

那么通过我的教程能学到什么呢
可以学到:
  • 注册实体,并生成实体(包括强制生成)
  • 在Minecraft1.12-1.15版本中自定义实体
  • 玩一把坐骑,到处跳,到处飞
  • 在Minecraft-1.14+利用纹理包实现自定义方块



Minecraft-1.12-总汇
我们开始创建一个类:TestEntity
继承EntityArmorStand (也可以是其它BukkitNMS中EntityXXX)
  1. public class TestEntity extends EntityArmorStand {

  2. }
复制代码

接下来实现构造方法
  1. public TestEntity(Player player) {
  2.     super(((CraftWorld) player.getLocation().getWorld()).getHandle());
  3.     this.setPosition(location.getX(), location.getY(), location.getZ()); // 生成到这个位置
  4. }
复制代码

然后可以生成实体了吗?
并不,我们需要对实体进行注册,否则是不能生成实体
注册方法可以使用莫老的自定义实体教程:https://www.mcbbs.net/thread-811096-1-1.html
  1.     static {
  2.         // 给我们的自定义实体做一个MinecraftKey
  3.         MinecraftKey minecraftKey = new MinecraftKey("custom_armorstand"); // minecraft:xxx
  4.         // 实体注册
  5.         EntityTypes.d.add(minecraftKey); // 将此key添加至EntityTypes的列表里
  6.         EntityTypes.b.a(54, minecraftKey, TestEntity.class); // 对其注册
  7.         // 如果想取消对该实体的注册那么可以执行下方的代码
  8.         EntityTypes.d.remove(minecraftKey);
  9.         MinecraftKey oldKey = EntityTypes.b.b(EntityArmorStand.class); // minecraft:armorstand
  10.         EntityTypes.b.a(54, oldKey, EntityArmorStand.class); // 把54号id所对应的minecraft:key和entity class进行覆盖
  11.     }
复制代码

好啦,我们可以生成实体了
  1.     // 生成实体
  2.     public void spawnEntity() {
  3.         WorldServer world = ((CraftWorld) this.player.getWorld()).getHandle();
  4.         
  5.         // 如果你想对NMS实体进行简单的修改,可以转换为Bukkit实体
  6.         ArmorStand armorStand = (ArmorStand) this.getBukkitEntity();
  7.         armorStand.setSmall(isSmall);

  8.         
  9.         // 注意这里,CraftWolrd的addEntity第二个参数
  10.         // 写入 CreatureSpawnEvent.SpawnReason.CUSTOM 可以强制生成实体了
  11.         // 没看到实体?因为没设置具体生成位置(1.12中),别忘了让实体传送到你身边
  12.         world.addEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM);
  13.     }
复制代码

别走,WASD操控实体呢?
咳咳,喝杯奶茶后,咱们继续吧
莫老的跳跃反射真方便,只要修改不同版本的字段,皆可在不同版本使用
  1.     private static Field jump = null; // 跳跃字段
  2.     static {
  3.         // 反射一下EntityLiving里的跳跃字段
  4.         try {
  5.             jump = EntityLiving.class.getDeclaredField("bd"); // 就算这里字段
  6.             jump.setAccessible(true);
  7.         } catch (NoSuchFieldException | SecurityException e1) {
  8.             e1.printStackTrace();
  9.         }
  10.     }
复制代码
那么字段"bd"怎么看的,我们可以反编译翻阅NMS中EntityLiving类看看
悄悄告诉你:
IDEA快捷键 Ctrl+鼠标左键 jump = EntityLiving.class..... 中的 EntityLiving 即可快速跳到该类
该字段是有规律的,一般在 public float lastDamage; 下面


重点来了,实现坐骑代码

其中 public void a(float motionSideways, float motionForward, float f) {} 怎么知道呢
通过反编译翻阅EntityLiving类,很明显找到我们需要的东西,别忘了,不同版本不是相同的,尽量多自行查看


Minecraft-1.12-创建实体
我们开始创建一个类:TestEntity
继承EntityArmorStand (也可以是其它BukkitNMS中EntityXXX)
  1. public class TestEntity extends EntityArmorStand {

  2. }
复制代码

接下来实现构造方法
  1. public TestEntity(Player player) {
  2.     super(((CraftWorld) player.getLocation().getWorld()).getHandle());
  3. }
复制代码

Minecraft-1.12-注册实体
然后可以生成实体了吗?
并不,我们需要对实体进行注册,否则是不能生成实体
注册方法可以使用莫老的自定义实体教程:https://www.mcbbs.net/thread-811096-1-1.html
  1.     static {
  2.         // 给我们的自定义实体做一个MinecraftKey
  3.         MinecraftKey minecraftKey = new MinecraftKey("custom_armorstand"); // minecraft:xxx
  4.         // 实体注册
  5.         EntityTypes.d.add(minecraftKey); // 将此key添加至EntityTypes的列表里
  6.         EntityTypes.b.a(54, minecraftKey, TestEntity.class); // 对其注册
  7.         // 如果想取消对该实体的注册那么可以执行下方的代码
  8.         EntityTypes.d.remove(minecraftKey);
  9.         MinecraftKey oldKey = EntityTypes.b.b(EntityArmorStand.class); // minecraft:armorstand
  10.         EntityTypes.b.a(54, oldKey, EntityArmorStand.class); // 把54号id所对应的minecraft:key和entity class进行覆盖
  11.     }
复制代码

Minecraft-1.12-生成实体
好啦,我们可以生成实体了
  1.     // 生成实体
  2.     public void spawnEntity() {
  3.         WorldServer world = ((CraftWorld) this.player.getWorld()).getHandle();
  4.         
  5.         // 如果你想对NMS实体进行简单的修改,可以转换为Bukkit实体
  6.         ArmorStand armorStand = (ArmorStand) this.getBukkitEntity();
  7.         armorStand.setSmall(isSmall);

  8.         
  9.         // 注意这里,CraftWolrd的addEntity第二个参数
  10.         // 写入 CreatureSpawnEvent.SpawnReason.CUSTOM 可以强制生成实体了
  11.         // 没看到实体?因为没设置具体生成位置(1.12中),别忘了让实体传送到你身边
  12.         world.addEntity(this, CreatureSpawnEvent.SpawnReason.CUSTOM);
  13.     }
复制代码
Minecraft-1.12-骑行实体
别走,WASD操控实体呢?
咳咳,喝杯奶茶后,咱们继续吧
莫老的跳跃反射真方便,只要修改不同版本的字段,皆可在不同版本使用
  1.     private static Field jump = null; // 跳跃字段
  2.     static {
  3.         // 反射一下EntityLiving里的跳跃字段
  4.         try {
  5.             jump = EntityLiving.class.getDeclaredField("bd"); // 就是这里字段
  6.             jump.setAccessible(true);
  7.         } catch (NoSuchFieldException | SecurityException e1) {
  8.             e1.printStackTrace();
  9.         }
  10.     }
复制代码
那么字段"bd"怎么看的,我们可以反编译翻阅NMS中EntityLiving类看看
悄悄告诉你:
IDEA快捷键 Ctrl+鼠标左键 jump = EntityLiving.class..... 中的 EntityLiving 即可快速跳到该类
该字段是有规律的,一般在 public float lastDamage; 下面


重点来了,实现坐骑代码

其中 public void a(float motionSideways, float motionForward, float f) {} 怎么知道呢
通过反编译翻阅EntityLiving类,很明显找到我们需要的东西,别忘了,不同版本不是相同的,尽量多自行查看

Minecraft-1.13-1.13.X
本版本 TestEntityMinecraft-1.12的创建继承构造步骤一样,这里不重复讲了
我们直奔注册实体
TestEntity 类型写入
  1.     public static EntityTypes entityTypes; // 存储自定义实体类型
  2.     public static EntityTypes registerEntity
  3.     (String name, String from,
  4.     Class<? extends net.minecraft.server.v1_13_R1.Entity> clazz,
  5.     Function<? super World, ? extends Entity> function) {
  6.         // 获取服务器的类型数据
  7.         Map<Object, Type<?>> dataTypes =
  8.         (Map<Object, Type<?>>)
  9.         DataConverterRegistry
  10.         .a()
  11.         .getSchema(15190
  12.         ).findChoiceType(DataConverterTypes.n).types();
  13.         // 写入自定义实体(name是自定义名,from是目标实体诸如minecraft:armorstand)
  14.         dataTypes.put("minecraft:" + name, dataTypes.get("minecraft:" + from));
  15.         // 创建自定义实体并返回EntityTypes
  16.         return EntityTypes.a(name, EntityTypes.a.a(clazz, function));
  17.     }
复制代码

生成实体
  1.     public static void spawnEntity(Player player, Location location) {
  2.         World world = ((CraftWorld) player.getWorld()).getHandle();
  3.         Entity entity = entityTypes.a( // 从NMS的entityTypes,a是创造实体方法
  4.                 world, // 引出NMS世界
  5.                 null, // Entity Tag
  6.                 null, // 实体自定义名
  7.                 null, // 不清楚是什么
  8.                 new BlockPosition(location.getBlockX(),
  9.                        location.getBlockY(),
  10.                        location.getBlockZ()), // 生成于BlockPosition
  11.                 false, // 不清楚是什么
  12.                 false);
  13.         world.addEntity(entity, CreatureSpawnEvent.SpawnReason.CUSTOM); // 强制生成
  14.     }
复制代码


并且在主类中注册实体(onEnableonLoad也行)
  1.     @Override
  2.     public void onEnable() {
  3.         TestEntity.entityTypes = TestEntity.registerEntity(
  4.                                 "custom_armorstand",
  5.                                 "armor_stand",
  6.                                 TestEntity.class,
  7.                                 TestEntity::new);
  8.     }
复制代码

顺便说下跳跃字段,是"bg"
同样可以翻阅MyPet开源项目得到坐骑关键代码




Minecraft-1.14
创建 TestEntity 类并继承,然后完成构造方法
  1. public class Entity extends EntityArmorStand {
  2.     public Entity(EntityTypes<? extends EntityMonster> entitytypes, World world) {
  3.         super(EntityTypes.ARMOR_STAND, world);
  4.     }
  5. }
复制代码

对的,这时候你会发现,和此前版本注册不一样
  1.     private static EntityTypes entityTypes; // 存储自定义实体类型

  2.     public static void registerEntity() {
  3.         // 获取服务器的类型数据
  4.         Map<String, Type<?>> types =
  5.         (Map<String, Type<?>>)
  6.         DataConverterRegistry
  7.         .a()
  8.         .getSchema(DataFixUtils.makeKey(SharedConstants.a().getWorldVersion()))
  9.         .findChoiceType(DataConverterTypes.ENTITY).types();
复制代码

然后知道怎么在主类注册实体了吧?什么?不知道?!
不认真看的挨批傲,快去看v1_13_R1 & v1_13_R2的内容

在1.14生成实体方法是这样的,我们可以注意到,1.14的entityTypes.b是创造实体
在1.13的entityTypes.a是创造实体,b则是生成实体
(1.15也差不多,1.15中生成实体方法是 entityTypes.createCreature(... ,从这单词上容易看出,是创造实体)
  1. public static void spawnEntity(Player player) {
  2.         World world = ((CraftWorld) player.getWorld()).getHandle();
  3.         Location location = player.getLocation();
  4.         Entity entity = entityTypes.b( // 从NMS的entityTypes,b是创造实体方法
  5.                 world, // 引出NMS世界
  6.                 null, // Entity Tag
  7.                 null, // 实体自定义名
  8.                 null, // 不清楚是什么
  9.                 new BlockPosition(location.getX(), location.getY(), location.getZ()), // 生成于BlockPosition
  10.                 null, // 不清楚是什么
  11.                 false,
  12.                 false);
  13.         // 强制生成
  14.         world.addEntity(entity, CreatureSpawnEvent.SpawnReason.CUSTOM);
  15.     }
复制代码

骑行实体,通过前面教程内容,我们容易找到跳跃字段了
请自行反编译找出1.14的跳跃字段,再折叠打开看正确答案

那么找到实现骑行实体方法了吗?
也许你这时候感到疑惑

是的,就是这里,但是呢,它的参数类型变成了Vec3D了!
好在呢,MyPet开源项目已经做了很好的坐骑算法,在这里我简化一下吧







Minecraft-1.15
令人欣慰的是,1.15与1.14的NMS实体创造注册差不多,只是这里不同
  • 获得服务器数据类型,和Minecraft-1.14用法一样的
  1.         Map<String, Type<?>> types = (Map<String, Type<?>>)DataConverterRegistry.a()
  2.                 .getSchema(DataFixUtils.makeKey(SharedConstants
  3.                         .getGameVersion().getWorldVersion()))
  4.                 .findChoiceType(DataConverterTypes.ENTITY_TREE).types();
复制代码



自定义方块


适用范围
  • 需要纹理包
  • 需要编程
  • 仅支持Minecraft-1.14+版本



为何不在材质版发答
  • 根据它的原理是需要编程配合的实现



我们也许在Minecraft-1.13及以下版本知道了方块不能添加,从而无法自定义方块
虽然盔甲架可以实现,但是它对服务器性能有影响的,大量集成放置会导致卡顿
而我们又知道,方块与物品不同之处在于,不能将新模型方块直接加入Minecraft原版中
当Minecraft-1.14正式版问世后,少数人发现了在此版本实现自定义方块
当时我为此花费了几天才研究好,鉴于MCBBS没有相关教程,那么我来讲讲吧



实现原理
首先,我要指出重要的角色:蘑菇块
Minecraft-1.14-1.15中,有三种这样的蘑菇块:蘑菇柄(mushroom_stem),红色蘑菇块(red_mushroom_block),棕色蘑菇块(brown_mushroom_block)

讲出原理之前,我们先放置蘑菇块来看看先,将两个方块放在一起

然后再拆快一个块,我们会发现它的接触面改变了

那么请问,你领悟到了什么呢
接触后块发生了改变,我们称之为块状态
主要应用到了纹理包,Minecraft渲染该块所需内容的Json中,对块状态进行属性操控
方块不同的面显示不同贴图,注意,我的意思并不是说仅仅某个面不同就显示不同纹理贴图
我的正确意思是:方块上有6个面,显示贴图与这些有关:不同方向的面,不同的面的数量

按数学排列来说吧
一共有 A B C D E F G 六个不同角色
  • A角色变色,有5个面相同,则显示"xxx"贴图
  • A E角色变色,有4个面相同,则显示"vvv"贴图
  • A G角色变色,那么有4个面相同,则显示"kaka"贴图
  • 依次推类...

最终,蘑菇柄可以提供给我们64-1=63个自定义方块,有人也许说,怎么扣去 1 了,那个 1 是留给蘑菇柄的贴图啊
当然再加上红色蘑菇块,棕色蘑菇块,我们最终一共有 189 个自定义方块

189个自定义方块应该足够了,不用灰心,Minecraft-1.16快照版中我们可以看到有新的蘑菇块
悄悄告诉你,那个Minecraft-1.16发光蘑菇块可能是可发光的自定义方块



关于纹理包
请具备纹理包相关知识,此处我仅讲块状态
向纹理包添加模型之类的MCBBS材质版有相关教程

我们需要这三个角色:块状态(blockstates),模型(models),贴图(textures)


打开 minecraft\blockstates\mushroom_stem.json 写入(没有就自行创建)
  1. {
  2.     "multipart": [
  3.         {
  4.             "apply": {
  5.                 "model": "mushroom_stem" #这个留给蘑菇柄
  6.             },
  7.             "when": { # 它的块所有面是开放的(true)
  8.                 "east": true, # 东
  9.                 "west": true, # 西
  10.                 "south": true, # 南
  11.                 "north": true, # 北
  12.                 "down": true, # 底
  13.                 "up": true # 顶
  14.             }
  15.         },
  16.         {
  17.             "apply": {
  18.                 "model": "inewrx_block_one/0" # 模型路径
  19.             },
  20.             # 它的意思是
  21.             # 当该块不同面开放(true)与关闭(false)状态是这样时
  22.             # 就引用"inewrx_block_one/0"模型
  23.             "when": {
  24.                 "east": false,
  25.                 "west": true,
  26.                 "south": false,
  27.                 "north": false,
  28.                 "down": false,
  29.                 "up": false
  30.             }
  31.         }
  32.     ]
  33. }
复制代码



自定义方块的应用
请具备Java基础能力,有BukkitAPI相关知识

一开始,考虑其拓展性,我选择用 PlayerInteractEvent 监听,而不是 BlockPlaceEvent
  • 触发该事件时,判断是否是右手,否则返回
  1. if (event.getAction() != Action.RIGHT_CLICK_BLOCK) return;
复制代码
  • 判断右手物品是否为空,否则返回
  1. ItemStack handItemStack = event.getItem();
  2. if (handItemStack==null) return;
复制代码
  • 实现放置自定义方块的代码
翻阅BukkitAPI,方块有这样的方法MultipleFacing,里面有我们需要i的
设置指定的面开放(true)还是关闭(false)状态

  1. // 为放置方块做准备,取点击的块的面
  2. Block newBlock = clickBlock.getRelative(event.getBlockFace());

  3. // 为触发放置方块事件准备
  4. BlockPlaceEvent blockPlaceEvent = new BlockPlaceEvent(newBlock,
  5. newBlock.getState(), clickBlock, handItemStack, player, true, Objects.requireNonNull(event.getHand()));

  6. // 实例蘑菇块的MultipleFacing
  7. MultipleFacing newBlockDataStem = (MultipleFacing) Bukkit.createBlockData(Material.MUSHROOM_STEM);

  8. int id = 1; // 可以写0~63,得到63个不同的面

  9. ItemMeta handItemMeta = handItemStack.getItemMeta();
  10. if (Xxxx.isCustomBlock(handItemMeta)) { // 判断是否是自定义方块(这里不讲了,可以是元数据等)
  11.     FastHandle.setBlockFacing(newBlockDataStem, id); // 设置自定义方块的面,从而实现显示某个贴图
  12.     newBlock.setBlockData(newBlockDataStem); // 放置自定义方块
  13.     event.setCancelled(true); // 取消事件
  14.     Bukkit.getPluginManager().callEvent(blockPlaceEvent); // 告知服务器该玩家触发了blockPlaceEvent事件
  15.     takeItem(player, handItemStack); // 取走玩家物品
  16. }

  17. // 取走玩家物品
  18. private void takeItem(Player player, ItemStack itemStack) {
  19.     if (!player.getGameMode().equals(GameMode.CREATIVE)) { // 如果不是创造模式
  20.         itemStack.setAmount(itemStack.getAmount() - 1);
  21.     }
  22. }
复制代码

其中是 setBlockFacing 设置方块的面方法
Oraxen作者编写了这样的函数方法
该作者与我讨论中,得知该方法的原理:
原话:
In fact the "code" is an integer between 0 and 2^6 because a block has 6 faces and each one can be disabled or enabled.
This function I wrote concerts a code to a list of faces enabled or disabled.
In binary 000000, which is 0 in base 10 too,
means every faces are disabled while 111111,
which is equals to 63 in base 10, means that every faces are enabled.
One of these combinations is reserved for the default minecraft block but the others ones are automatically generated when you try to create a new block.

其中重要意思是:在二进制000000中(基数10中也为0)意味着关闭所有面,而111111(基数10中等于63)则意味着开放了所有面
如果我们写入 int id = 30; 则二进制是:11110
在循环中,通过 int id 二进制已经为不同面设置开放或关闭状态的面
这是关键的代码
  1. /**
  2. * @param blockData
  3. * @param id
  4. */
  5. public static void setBlockFacing(MultipleFacing blockData, int id) {
  6.     final BlockFace[] properties = new BlockFace[]{BlockFace.EAST, BlockFace.WEST,
  7. BlockFace.SOUTH, BlockFace.NORTH, BlockFace.DOWN, BlockFace.UP};
  8.     for (int i = 0; i < properties.length; i++) {
  9.         blockData.setFace(properties[i], (id & 0x1 << i) != 0);
  10.     }
  11. }
复制代码












[groupid=1511]Server CT[/groupid]