创建自定义弹射物

通读并理解本教程是很重要的,因为这将有助于你理解本教程中包含的大部分内容。

本教程将教你如何创建你自己的自定义弹射物,包括 ProjectileEntity 以及弹射物物品本身。本指南将介绍如何定义弹射物、注册弹射物、渲染弹射物,以及创建弹射物物品本身。

ProjectileEntities 是用来,呃,创建和操作弹射物。一些基本的弹射物包括:

  • 雪球
  • 末影珍珠

我们将创建一个类似雪球的自定义弹射物,对被击中的实体施加一些非常恶心的效果。

如果你想自己看一下源代码,以下的全部的代码在这都是完整。在教程开始之前,我想让你知道,我将使用PascalCase来命名这些方法。你可以随意改变命名方案,但我个人发誓使用PascalCase。

创建&注册弹射物实体

首先,我们需要为ProjectileEntity创建一个新类,继承自ThrownItemEntity

/*
我们将创建一个类似雪球的自定义弹射物,造成一些恶心debuff。
由于这是一个抛射物,我们将继承ThrownItemEntity。
部分弹射物有:
- 雪球
- 末影珍珠
 */
public class PackedSnowballEntity extends ThrownItemEntity {
[. . .]
}

你的IDE应该会提示未实现的方法,所以要实现它。

public class PackedSnowballEntity extends ThrownItemEntity {
    @Override
    protected Item getDefaultItem() {
        return null; // 我们会在后面创建了 ProjectileItem 后再进行配置。
    }
}

你的IDE应该会提示没有所需的构造函数,但我不建议使用默认的构造函数,而是使用代码中显示的以下构造函数,因为它们是从默认的构造函数中大量修改而来的,应该可以正常工作,甚至更好。

public class PackedSnowballEntity extends ThrownItemEntity {
    public PackedSnowballEntity(EntityType<? extends ThrownItemEntity> entityType, World world) {
        super(entityType, world);
    }
 
    public PackedSnowballEntity(World world, LivingEntity owner) {
        super(null, owner, world); // null待会再改
    }
 
    public PackedSnowballEntity(World world, double x, double y, double z) {
        super(null, x, y, z, world); // null待会再改
    }
 
    @Override
    protected Item getDefaultItem() {
        return null; // 我们会在后面创建了 ProjectileItem 后再进行配置。
    }
}

如果你正确地遵循这些指示,你的IDE应该不会再提示任何重大问题。
我们将继续添加与我们的弹射物有关的功能。请记住,下面的代码是完全可以自定义的,我鼓励那些遵循本教程的人在这里发挥创造力。

public class PackedSnowballEntity extends ThrownItemEntity {
    public PackedSnowballEntity(EntityType<? extends ThrownItemEntity> entityType, World world) {
        super(entityType, world);
    }
 
    public PackedSnowballEntity(World world, LivingEntity owner) {
        super(null, owner, world); // null待会再改
    }
 
    public PackedSnowballEntity(World world, double x, double y, double z) {
        super(null, x, y, z, world); // null待会再改
    }
 
    @Override
    protected Item getDefaultItem() {
        return null; // 我们会在后面创建了 ProjectileItem 后再进行配置。
    }
 
    @Environment(EnvType.CLIENT)
    private ParticleEffect getParticleParameters() { // 不完全确定,但可能与雪球的粒子有关。(可选)
        ItemStack itemStack = this.getItem();
        return (ParticleEffect)(itemStack.isEmpty() ? ParticleTypes.ITEM_SNOWBALL : new ItemStackParticleEffect(ParticleTypes.ITEM, itemStack));
    }
 
    @Environment(EnvType.CLIENT)
    public void handleStatus(byte status) { // 也不完全确定,但可能也与粒子有关。这个方法(以及前面的方法)是可选的,所以如果你不明白,就不要包括这个方法。
        if (status == 3) {
            ParticleEffect particleEffect = this.getParticleParameters();
 
            for(int i = 0; i < 8; ++i) {
                this.world.addParticle(particleEffect, this.getX(), this.getY(), this.getZ(), 0.0D, 0.0D, 0.0D);
            }
        }
 
    }
 
    protected void onEntityHit(EntityHitResult entityHitResult) { // called on entity hit.
        super.onEntityHit(entityHitResult);
        Entity entity = entityHitResult.getEntity(); // 设置一个新的实体实例作为EntityHitResult(受害者)。
        int i = entity instanceof BlazeEntity ? 3 : 0; // 如果实体实例是BlazeEntity的一个实例,则将i设为3。
        entity.damage(DamageSource.thrownProjectile(this, this.getOwner()), (float)i); // 处理伤害
 
        if (entity instanceof LivingEntity livingEntity) { // 检查实体是否是LivingEntity的一个实例(说明它不是船或矿车)。
            livingEntity.addStatusEffect((new StatusEffectInstance(StatusEffects.BLINDNESS, 20 * 3, 0))); // 应用状态效果
            livingEntity.addStatusEffect((new StatusEffectInstance(StatusEffects.SLOWNESS, 20 * 3, 2))); // 应用状态效果
            livingEntity.addStatusEffect((new StatusEffectInstance(StatusEffects.POISON, 20 * 3, 1))); // 应用状态效果
            livingEntity.playSound(SoundEvents.AMBIENT_CAVE, 2F, 1F); // 只在打中实体的时候播放声音
        }
    }
 
    protected void onCollision(HitResult hitResult) { // 碰到方块时被调用
        super.onCollision(hitResult);
        if (!this.world.isClient) { // 检查世界是否是客户端
            this.world.sendEntityStatus(this, (byte)3); // 粒子?
            this.kill(); // 清除弹射物
        }
 
    }
}

我们现在已经完成了弹射物的核心代码。然而,一旦我们定义和注册了其他物品,我们就会在弹射物类上进行添加。
我们已经创建了弹射物类,但我们还没有定义和注册它。要注册一个弹射物,你可以按照这个教程或者按照下面的代码。

public class ProjectileTutorialMod implements ModInitializer {
    public static final String ModID = "projectiletutorial"; // 这只是为了让我们能够更容易地参考我们的ModID
 
    public static final EntityType<PackedSnowballEntity> PackedSnowballEntityType = Registry.register(
            Registry.ENTITY_TYPE,
            new Identifier(ModID, "packed_snowball"),
            FabricEntityTypeBuilder.<PackedSnowballEntity>create(SpawnGroup.MISC, PackedSnowballEntity::new)
                    .dimensions(EntityDimensions.fixed(0.25F, 0.25F)) // 用Minecraft单位表示的弹射物尺寸
                    .trackRangeBlocks(4).trackedUpdateRate(10) // 对所有投掷物来说都是必要的(因为它可以防止它被打破,笑)。
                    .build() // VERY IMPORTANT DONT DELETE FOR THE LOVE OF GOD PSLSSSSSS(非常重要不要删除!)
    );
 
    @Override
    public void onInitialize() {
 
    }
}

最后,将EntityType添加到我们类的构造函数中。

public PackedSnowballEntity(EntityType<? extends ThrownItemEntity> entityType, World world) {
        super(entityType, world);
    }
 
    public PackedSnowballEntity(World world, LivingEntity owner) {
        super(ProjectileTutorialMod.PackedSnowballEntityType, owner, world);
    }
 
    public PackedSnowballEntity(World world, double x, double y, double z) {
        super(ProjectileTutorialMod.PackedSnowballEntityType, x, y, z, world);
    }

创建弹射物品

当我们在做这件事时,我们应该快速创建一个弹射物品。创建物品所需的大部分内容都是在本教程中重复的,所以如果你对创建物品不熟悉,请参考该教程。
首先,有必要为该物品创建一个扩展于Item的新类。

public class PackedSnowballItem extends Item {
}
Create the constructor, as shown.

public class PackedSnowballItem extends Item {
    public PackedSnowballItem(Settings settings) {
        super(settings);
    }
}

现在, 我们会创建应该新的 TypedActionResult 方法。 如下:

public class PackedSnowballItem extends Item {
    public PackedSnowballItem(Settings settings) {
        super(settings);
    }
 
    public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
        ItemStack itemStack = user.getStackInHand(hand); // creates a new ItemStack instance of the user's itemStack in-hand
        world.playSound(null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENTITY_SNOWBALL_THROW, SoundCategory.NEUTRAL, 0.5F, 1F); // plays a globalSoundEvent
        /*
        user.getItemCooldownManager().set(this, 5);
        Optionally, you can add a cooldown to your item's right-click use, similar to Ender Pearls.
        */
        if (!world.isClient) {
            PackedSnowballEntity snowballEntity = new PackedSnowballEntity(world, user);
            snowballEntity.setItem(itemStack);
            snowballEntity.setVelocity(user, user.pitch, user.yaw, 0.0F, 1.5F, 0F);
                        /*
                        snowballEntity.setProperties(user, user.getPitch(), user.getYaw(), 0.0F, 1.5F, 1.0F);
                        In 1.17,we will use setProperties instead of setVelocity.                                                        
                        */
            world.spawnEntity(snowballEntity); // spawns entity
        }
 
        user.incrementStat(Stats.USED.getOrCreateStat(this));
        if (!user.abilities.creativeMode) {
            itemStack.decrement(1); // decrements itemStack if user is not in creative mode
        }
 
        return TypedActionResult.success(itemStack, world.isClient());
    }
}

确保你用这个项目发射的弹射物确实是你的自定义ProjectileEntity。通过检查PackedSnowballEntity snowballEntity = new PackedSnowballEntity(world, user);来验证这一点。
现在,我们已经完成了为ProjectileEntity创建一个项目。请记住,如果你不了解如何创建一个物品,请参考 "物品"教程
最后,注册你的项目。

public static final Item PackedSnowballItem = new PackedSnowballItem(new Item.Settings().group(ItemGroup.MISC).maxCount(16));
 
[...]
 
@Override
    public void onInitialize() {
        Registry.register(Registry.ITEM, new Identifier(ModID, "packed_snowball"), PackedSnowballItem);
    }

回到我们的ProjectileEntity类中,我们必须将 getDefaultItem() 添加到我们的方法中。


@Override
    protected Item getDefaultItem() {
        return ProjectileTutorialMod.PackedSnowballItem;
    }

确保你把物品的纹理放在正确的位置,否则实体和物品都不会有纹理。

渲染你的弹射物实体

你的弹射物实体现在已经被定义和注册了,但我们还没有做完。如果没有渲染器,ProjectileEntity会让Minecraft崩溃。为了解决这个问题,我们将为我们的ProjectileEntity定义和注册EntityRenderer。要做到这一点,我们将需要在ClientModInitializer中的EntityRenderer和一个spawn packet来确保纹理被正确渲染。
在我们开始之前,我们将快速定义一个我们将经常使用的标识符:我们的PacketID。

public static final Identifier PacketID = new Identifier(ProjectileTutorialMod.ModID, "spawn_packet");

首先,我们应该把EntityRenderer弄出来。到你的ClientModInitializer,写下以下内容。

@Override
public void onInitializeClient() {
    EntityRendererRegistry.register(ProjectileTutorialMod.PackedSnowballEntityType, (context) ->
             new FlyingItemEntityRenderer(context));
    // older versions may have to use
    /* EntityRendererRegistry.INSTANCE.register(ProjectileTutorialMod.PackedSnowballEntityType, (context) ->
             new FlyingItemEntityRenderer(context)); */
    [. . .]
}

为了让projectileEntity被注册,我们将需要一个spawn packet。创建一个名为EntitySpawnPacket的新类,并将其放在该类中。

public class EntitySpawnPacket {
    public static Packet<?> create(Entity e, Identifier packetID) {
        if (e.world.isClient)
            throw new IllegalStateException("SpawnPacketUtil.create called on the logical client!");
        PacketByteBuf byteBuf = new PacketByteBuf(Unpooled.buffer());
        byteBuf.writeVarInt(Registry.ENTITY_TYPE.getRawId(e.getType()));
        byteBuf.writeUuid(e.getUuid());
        byteBuf.writeVarInt(e.getEntityId());
 
        PacketBufUtil.writeVec3d(byteBuf, e.getPos());
        PacketBufUtil.writeAngle(byteBuf, e.pitch);
        PacketBufUtil.writeAngle(byteBuf, e.yaw);
                /*
                In 1.17,we use these.
                byteBuf.writeVarInt(e.getId());
                
                PacketBufUtil.writeVec3d(byteBuf, e.getPos());
                PacketBufUtil.writeAngle(byteBuf, e.getPitch());
                PacketBufUtil.writeAngle(byteBuf, e.getYaw());              
                */
 
        return ServerPlayNetworking.createS2CPacket(packetID, byteBuf);
    }
    public static final class PacketBufUtil {
 
        /**
         * Packs a floating-point angle into a {@code byte}.
         *
         * @param angle
         *         angle
         * @return packed angle
         */
        public static byte packAngle(float angle) {
            return (byte) MathHelper.floor(angle * 256 / 360);
        }
 
        /**
         * Unpacks a floating-point angle from a {@code byte}.
         *
         * @param angleByte
         *         packed angle
         * @return angle
         */
        public static float unpackAngle(byte angleByte) {
            return (angleByte * 360) / 256f;
        }
 
        /**
         * Writes an angle to a {@link PacketByteBuf}.
         *
         * @param byteBuf
         *         destination buffer
         * @param angle
         *         angle
         */
        public static void writeAngle(PacketByteBuf byteBuf, float angle) {
            byteBuf.writeByte(packAngle(angle));
        }
 
        /**
         * Reads an angle from a {@link PacketByteBuf}.
         *
         * @param byteBuf
         *         source buffer
         * @return angle
         */
        public static float readAngle(PacketByteBuf byteBuf) {
            return unpackAngle(byteBuf.readByte());
        }
 
        /**
         * Writes a {@link Vec3d} to a {@link PacketByteBuf}.
         *
         * @param byteBuf
         *         destination buffer
         * @param vec3d
         *         vector
         */
        public static void writeVec3d(PacketByteBuf byteBuf, Vec3d vec3d) {
            byteBuf.writeDouble(vec3d.x);
            byteBuf.writeDouble(vec3d.y);
            byteBuf.writeDouble(vec3d.z);
        }
 
        /**
         * Reads a {@link Vec3d} from a {@link PacketByteBuf}.
         *
         * @param byteBuf
         *         source buffer
         * @return vector
         */
        public static Vec3d readVec3d(PacketByteBuf byteBuf) {
            double x = byteBuf.readDouble();
            double y = byteBuf.readDouble();
            double z = byteBuf.readDouble();
            return new Vec3d(x, y, z);
        }
    }

}

这基本上会读取和写入矢量和角度,从而使实体的纹理得到正确渲染。我不会在这里深入介绍spawn packets,但你可以阅读一下它们的作用和功能。现在,我们可以把这个包括进去,然后继续前进。
回到我们的ClientModInitializer,我们将创建一个新方法并在该方法中加入以下内容。

public void receiveEntityPacket() {
        ClientSidePacketRegistry.INSTANCE.register(PacketID, (ctx, byteBuf) -> {
            EntityType<?> et = Registry.ENTITY_TYPE.get(byteBuf.readVarInt());
            UUID uuid = byteBuf.readUuid();
            int entityId = byteBuf.readVarInt();
            Vec3d pos = EntitySpawnPacket.PacketBufUtil.readVec3d(byteBuf);
            float pitch = EntitySpawnPacket.PacketBufUtil.readAngle(byteBuf);
            float yaw = EntitySpawnPacket.PacketBufUtil.readAngle(byteBuf);
            ctx.getTaskQueue().execute(() -> {
                if (MinecraftClient.getInstance().world == null)
                    throw new IllegalStateException("Tried to spawn entity in a null world!");
                Entity e = et.create(MinecraftClient.getInstance().world);
                if (e == null)
                    throw new IllegalStateException("Failed to create instance of entity \"" + Registry.ENTITY_TYPE.getId(et) + "\"!");
                e.updateTrackedPosition(pos);
                e.setPos(pos.x, pos.y, pos.z);
                e.pitch = pitch;
                e.yaw = yaw;
                e.setEntityId(entityId);
                e.setUuid(uuid);
                MinecraftClient.getInstance().world.addEntity(entityId, e);
            });
        });
    }

*译者:这里高版本其实是这样写(只在1.19版本试过):

public void receiveEntityPacket() {
        ClientPlayNetworking.registerGlobalReceiver(PacketID, ((client, handler, buf, responseSender) -> {
            EntityType<?> et = Registry.ENTITY_TYPE.get(buf.readVarInt());
            UUID uuid = buf.readUuid();
            int entityId = buf.readVarInt();
            Vec3d pos = EntitySpawnPacket.PacketBufUtil.readVec3d(buf);
            float pitch = EntitySpawnPacket.PacketBufUtil.readAngle(buf);
            float yaw = EntitySpawnPacket.PacketBufUtil.readAngle(buf);
            client.execute(()->{
                if (MinecraftClient.getInstance().world == null)
                    throw new IllegalStateException("Tried to spawn entity in a null world!");
                Entity e = et.create(MinecraftClient.getInstance().world);
                if (e == null)
                    throw new IllegalStateException("Failed to create instance of entity \"" + Registry.ENTITY_TYPE.getId(et) + "\"!");
                e.updateTrackedPosition(pos.x, pos.y, pos.z);
                e.setPos(pos.x, pos.y, pos.z);
                e.setPitch(pitch);
                e.setYaw(yaw);
                e.setId(entityId);
                e.setUuid(uuid);
                MinecraftClient.getInstance().world.addEntity(entityId, e);
            });
        }));
    }*

回到我们的ProjectileEntity类中,我们必须添加一个方法,以确保一切正常工作。



    @Override
    public Packet createSpawnPacket() {
        return EntitySpawnPacket.create(this, ProjectileTutorialClient.PacketID);
    }
Finally, make sure to call this method in the onInitializeClient() method.

@Override
    public void onInitializeClient() {
        EntityRendererRegistry.INSTANCE.register(ProjectileTutorialMod.PackedSnowballEntityType, (context) ->
                new FlyingItemEntityRenderer(context));
        receiveEntityPacket();
    }

希望上帝保佑它能发挥作用

现在,你的弹射物应该在游戏中运行了! 只要确保你的纹理在正确的位置,你的物品和弹射物就可以运行了。

如果你想试试这个弹射物,请在这里:Github下载。

Last modification:August 19, 2022
如果喜欢,打赏几分钱呗:)