创建自定义弹射物
本教程将教你如何创建你自己的自定义弹射物,包括 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下载。