User Tools

Site Tools


tutorial:projectiles

This is an old revision of the document!


Creating a Custom Projectile

It is important to read through and understand this tutorial as well as this tutorial, as this will help you understand most of the elements included in this tutorial.

This tutorial will teach you how to create your own custom projectile, including the ProjectileEntity, as well as the projectile item itself. This guide will go over how to define the projectile, register the projectile, rendering the projectile, as well as creating the projectile item itself.

ProjectileEntities are used to, well, create and operate projectiles. Some basic projectiles include:

  • Snowballs
  • Ender Pearls

We will be creating a custom snowball-like projectile that applies some very nasty effects to the entity that has been hit.

If you would like to look over the source code yourself, all of the following code was done here. Before the tutorial begins, I would like to let you know that I would be using PascalCase to name the methods. Feel free to change the naming scheme to however you like, but I personally swear by PascalCase.

Creating & Registering a Projectile Entity

To start, we will need to create a new class for the ProjectileEntity, extending ThrownItemEntity.

  1. /*
  2. We will be creating a custom snowball-like projectile that deals some nasty debuffs.
  3. Since this is a thrown projectile, we will extending ThrownItemEntity.
  4. Some ThrownItemEntities include:
  5. - Snowballs
  6. - Ender Pearls
  7.  */
  8. public class PackedSnowballEntity extends ThrownItemEntity {
  9. [. . .]
  10. }

Your IDE should complain about unimplemented methods, so implement that.

  1. public class PackedSnowballEntity extends ThrownItemEntity {
  2. @Override
  3. protected Item getDefaultItem() {
  4. return null; // We will configure this later, once we have created the ProjectileItem.
  5. }
  6. }

Your IDE should complain about not having the required constructors, but I would not recommend using the default constructors, but instead using the following constructors shown in the code, as they were heavily modified from the default constructors and should work fine, if not better.

  1. public class PackedSnowballEntity extends ThrownItemEntity {
  2. public PackedSnowballEntity(EntityType<? extends ThrownItemEntity> entityType, World world) {
  3. super(entityType, world);
  4. }
  5.  
  6. public PackedSnowballEntity(World world, LivingEntity owner) {
  7. super(null, owner, world); // null will be changed later
  8. }
  9.  
  10. public PackedSnowballEntity(World world, double x, double y, double z) {
  11. super(null, x, y, z, world); // null will be changed later
  12. }
  13.  
  14. @Override
  15. protected Item getDefaultItem() {
  16. return null; // We will configure this later, once we have created the ProjectileItem.
  17. }
  18. }

Your IDE should not complain about any more major issues if you followed those instructions correctly.
We will continue adding features related to our projectile. Keep in mind that the following code is entirely customizable, and I am encouraging those who follow this tutorial to be creative here.

  1. public class PackedSnowballEntity extends ThrownItemEntity {
  2. public PackedSnowballEntity(EntityType<? extends ThrownItemEntity> entityType, World world) {
  3. super(entityType, world);
  4. }
  5.  
  6. public PackedSnowballEntity(World world, LivingEntity owner) {
  7. super(null, owner, world); // null will be changed later
  8. }
  9.  
  10. public PackedSnowballEntity(World world, double x, double y, double z) {
  11. super(null, x, y, z, world); // null will be changed later
  12. }
  13.  
  14. @Override
  15. protected Item getDefaultItem() {
  16. return null; // We will configure this later, once we have created the ProjectileItem.
  17. }
  18.  
  19. @Environment(EnvType.CLIENT)
  20. private ParticleEffect getParticleParameters() { // Not entirely sure, but probably has do to with the snowball's particles. (OPTIONAL)
  21. ItemStack itemStack = this.getItem();
  22. return (ParticleEffect)(itemStack.isEmpty() ? ParticleTypes.ITEM_SNOWBALL : new ItemStackParticleEffect(ParticleTypes.ITEM, itemStack));
  23. }
  24.  
  25. @Environment(EnvType.CLIENT)
  26. public void handleStatus(byte status) { // Also not entirely sure, but probably also has to do with the particles. This method (as well as the previous one) are optional, so if you don't understand, don't include this one.
  27. if (status == 3) {
  28. ParticleEffect particleEffect = this.getParticleParameters();
  29.  
  30. for(int i = 0; i < 8; ++i) {
  31. this.world.addParticle(particleEffect, this.getX(), this.getY(), this.getZ(), 0.0D, 0.0D, 0.0D);
  32. }
  33. }
  34.  
  35. }
  36.  
  37. protected void onEntityHit(EntityHitResult entityHitResult) { // called on entity hit.
  38. super.onEntityHit(entityHitResult);
  39. Entity entity = entityHitResult.getEntity(); // sets a new Entity instance as the EntityHitResult (victim)
  40. int i = entity instanceof BlazeEntity ? 3 : 0; // sets i to 3 if the Entity instance is an instance of BlazeEntity
  41. entity.damage(DamageSource.thrownProjectile(this, this.getOwner()), (float)i); // deals damage
  42.  
  43. if (entity instanceof LivingEntity) { // checks if entity is an instance of LivingEntity (meaning it is not a boat or minecart)
  44. ((LivingEntity) entity).addStatusEffect((new StatusEffectInstance(StatusEffects.BLINDNESS, 20 * 3, 0))); // applies a status effect
  45. ((LivingEntity) entity).addStatusEffect((new StatusEffectInstance(StatusEffects.SLOWNESS, 20 * 3, 2))); // applies a status effect
  46. ((LivingEntity) entity).addStatusEffect((new StatusEffectInstance(StatusEffects.POISON, 20 * 3, 1))); // applies a status effect
  47. entity.playSound(SoundEvents.AMBIENT_CAVE, 2F, 1F); // plays a sound for the entity hit only
  48. }
  49. }
  50.  
  51. protected void onCollision(HitResult hitResult) { // called on collision with a block
  52. super.onCollision(hitResult);
  53. if (!this.world.isClient) { // checks if the world is client
  54. this.world.sendEntityStatus(this, (byte)3); // particle?
  55. this.remove(); // kills the projectile
  56. }
  57.  
  58. }
  59. }

We are now finished with the core code of the projectile. We will be adding on to the projectile class, however, once we have defined and registered other items.
We have created the projectile class, but we haven't defined and registered it yet. To register a projectile, you can follow this tutorial or you can follow the code below.

  1. public class ProjectileTutorialMod implements ModInitializer {
  2. public static final String ModID = "projectiletutorial"; // This is just so we can refer to our ModID easier.
  3.  
  4. public static final EntityType<PackedSnowballEntity> PackedSnowballEntityType = Registry.register(
  5. Registry.ENTITY_TYPE,
  6. new Identifier(ModID, "packed_snowball"),
  7. FabricEntityTypeBuilder.<PackedSnowballEntity>create(SpawnGroup.MISC, PackedSnowballEntity::new)
  8. .dimensions(EntityDimensions.fixed(0.25F, 0.25F)) // dimensions in Minecraft units of the projectile
  9. .trackRangeBlocks(4).trackedUpdateRate(10) // necessary for all thrown projectiles (as it prevents it from breaking, lol)
  10. .build() // VERY IMPORTANT DONT DELETE FOR THE LOVE OF GOD PSLSSSSSS
  11. );
  12.  
  13. @Override
  14. public void onInitialize() {
  15.  
  16. }
  17. }

Finally, add the EntityType to our class' constructors.

  1. public PackedSnowballEntity(EntityType<? extends ThrownItemEntity> entityType, World world) {
  2. super(entityType, world);
  3. }
  4.  
  5. public PackedSnowballEntity(World world, LivingEntity owner) {
  6. super(ProjectileTutorialMod.PackedSnowballEntityType, owner, world);
  7. }
  8.  
  9. public PackedSnowballEntity(World world, double x, double y, double z) {
  10. super(ProjectileTutorialMod.PackedSnowballEntityType, x, y, z, world);
  11. }

Creating a Projectile Item

While we're at it, we should quickly create a projectile item. Most of the things that are required to create an item are repeated from this tutorial, so if you're unfamiliar with creating an item, refer to that tutorial.
First, it is necessary to create a new class for the item that extends Item.

  1. public class PackedSnowballItem extends Item {
  2. }

Create the constructor, as shown.

  1. public class PackedSnowballItem extends Item {
  2. public PackedSnowballItem(Settings settings) {
  3. super(settings);
  4. }
  5. }

Now, we will create a new TypedActionResult method. Follow along:

  1. public class PackedSnowballItem extends Item {
  2. public PackedSnowballItem(Settings settings) {
  3. super(settings);
  4. }
  5.  
  6. public TypedActionResult<ItemStack> use(World world, PlayerEntity user, Hand hand) {
  7. ItemStack itemStack = user.getStackInHand(hand); // creates a new ItemStack instance of the user's itemStack in-hand
  8. world.playSound(null, user.getX(), user.getY(), user.getZ(), SoundEvents.ENTITY_SNOWBALL_THROW, SoundCategory.NEUTRAL, 0.5F, 1F); // plays a globalSoundEvent
  9. /*
  10. user.getItemCooldownManager().set(this, 5);
  11. Optionally, you can add a cooldown to your item's right-click use, similar to Ender Pearls.
  12. */
  13. if (!world.isClient) {
  14. PackedSnowballEntity snowballEntity = new PackedSnowballEntity(world, user);
  15. snowballEntity.setItem(itemStack);
  16. snowballEntity.setProperties(user, user.pitch, user.yaw, 0.0F, 1.5F, 0F);
  17. world.spawnEntity(snowballEntity); // spawns entity
  18. }
  19.  
  20. user.incrementStat(Stats.USED.getOrCreateStat(this));
  21. if (!user.abilities.creativeMode) {
  22. itemStack.decrement(1); // decrements itemStack if user is not in creative mode
  23. }
  24.  
  25. return TypedActionResult.success(itemStack, world.isClient());
  26. }
  27. }

Make sure that the projectile that you are launching with this item is indeed your custom ProjectileEntity. Verify this by checking PackedSnowballEntity snowballEntity = new PackedSnowballEntity(world, user);.
Now, we are finished with creating an item for the ProjectileEntity. Keep in mind that if you do not understand how to create an item, refer to the "Item" tutorial.
Finally, register your item.

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

Back in our ProjectileEntity class, we must add the getDefaultItem() into our method.

  1. @Override
  2. protected Item getDefaultItem() {
  3. return ProjectileTutorialMod.PackedSnowballItem;
  4. }

Make sure you have the texture for the item in the correct spot, or else neither the entity or the item will have a texture.

Rendering your Projectile Entity

Your projectile entity is now defined and registered, but we are not done. Without a renderer, the ProjectileEntity will crash Minecraft. To fix this, we will define and register the EntityRenderer for our ProjectileEntity. To do this, we will need a EntityRenderer in the ClientModInitializer and a spawn packet to make sure the texture is rendered correctly.
Before we start, we will quickly define an Identifier that we will be using a lot: our PacketID.

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


First on the list, we should get the EntityRenderer out of the way. Go into your ClientModInitializer and write the following:

  1. @Override
  2. public void onInitializeClient() {
  3. EntityRendererRegistry.INSTANCE.register(ProjectileTutorialMod.PackedSnowballEntityType, (context) ->
  4. new FlyingItemEntityRenderer(context));
  5. [. . .]
  6. }

In order for the projectileEntity to be registered, we will need a spawn packet. Create a new class named EntitySpawnPacket, and put this in that class.

  1. public class EntitySpawnPacket {
  2. public static Packet<?> create(Entity e, Identifier packetID) {
  3. if (e.world.isClient)
  4. throw new IllegalStateException("SpawnPacketUtil.create called on the logical client!");
  5. PacketByteBuf byteBuf = new PacketByteBuf(Unpooled.buffer());
  6. byteBuf.writeVarInt(Registry.ENTITY_TYPE.getRawId(e.getType()));
  7. byteBuf.writeUuid(e.getUuid());
  8. byteBuf.writeVarInt(e.getEntityId());
  9.  
  10. PacketBufUtil.writeVec3d(byteBuf, e.getPos());
  11. PacketBufUtil.writeAngle(byteBuf, e.pitch);
  12. PacketBufUtil.writeAngle(byteBuf, e.yaw);
  13. return ServerSidePacketRegistry.INSTANCE.toPacket(packetID, byteBuf);
  14. }
  15. public static final class PacketBufUtil {
  16.  
  17. /**
  18. * Packs a floating-point angle into a {@code byte}.
  19. *
  20. * @param angle
  21. * angle
  22. * @return packed angle
  23. */
  24. public static byte packAngle(float angle) {
  25. return (byte) MathHelper.floor(angle * 256 / 360);
  26. }
  27.  
  28. /**
  29. * Unpacks a floating-point angle from a {@code byte}.
  30. *
  31. * @param angleByte
  32. * packed angle
  33. * @return angle
  34. */
  35. public static float unpackAngle(byte angleByte) {
  36. return (angleByte * 360) / 256f;
  37. }
  38.  
  39. /**
  40. * Writes an angle to a {@link PacketByteBuf}.
  41. *
  42. * @param byteBuf
  43. * destination buffer
  44. * @param angle
  45. * angle
  46. */
  47. public static void writeAngle(PacketByteBuf byteBuf, float angle) {
  48. byteBuf.writeByte(packAngle(angle));
  49. }
  50.  
  51. /**
  52. * Reads an angle from a {@link PacketByteBuf}.
  53. *
  54. * @param byteBuf
  55. * source buffer
  56. * @return angle
  57. */
  58. public static float readAngle(PacketByteBuf byteBuf) {
  59. return unpackAngle(byteBuf.readByte());
  60. }
  61.  
  62. /**
  63. * Writes a {@link Vec3d} to a {@link PacketByteBuf}.
  64. *
  65. * @param byteBuf
  66. * destination buffer
  67. * @param vec3d
  68. * vector
  69. */
  70. public static void writeVec3d(PacketByteBuf byteBuf, Vec3d vec3d) {
  71. byteBuf.writeDouble(vec3d.x);
  72. byteBuf.writeDouble(vec3d.y);
  73. byteBuf.writeDouble(vec3d.z);
  74. }
  75.  
  76. /**
  77. * Reads a {@link Vec3d} from a {@link PacketByteBuf}.
  78. *
  79. * @param byteBuf
  80. * source buffer
  81. * @return vector
  82. */
  83. public static Vec3d readVec3d(PacketByteBuf byteBuf) {
  84. double x = byteBuf.readDouble();
  85. double y = byteBuf.readDouble();
  86. double z = byteBuf.readDouble();
  87. return new Vec3d(x, y, z);
  88. }
  89. }
  90. }

This will basically read and write vectors and angles that will allow the entity's texture to be rendered correctly. I will not go in-depth about spawn packets here, but you could read up on what they do and how they function. For now, we can include this and move on.
Back to our ClientModInitializer, we will create a new method and put the following in that method.

  1. public void receiveEntityPacket() {
  2. ClientSidePacketRegistry.INSTANCE.register(PacketID, (ctx, byteBuf) -> {
  3. EntityType<?> et = Registry.ENTITY_TYPE.get(byteBuf.readVarInt());
  4. UUID uuid = byteBuf.readUuid();
  5. int entityId = byteBuf.readVarInt();
  6. Vec3d pos = EntitySpawnPacket.PacketBufUtil.readVec3d(byteBuf);
  7. float pitch = EntitySpawnPacket.PacketBufUtil.readAngle(byteBuf);
  8. float yaw = EntitySpawnPacket.PacketBufUtil.readAngle(byteBuf);
  9. ctx.getTaskQueue().execute(() -> {
  10. if (MinecraftClient.getInstance().world == null)
  11. throw new IllegalStateException("Tried to spawn entity in a null world!");
  12. Entity e = et.create(MinecraftClient.getInstance().world);
  13. if (e == null)
  14. throw new IllegalStateException("Failed to create instance of entity \"" + Registry.ENTITY_TYPE.getId(et) + "\"!");
  15. e.updateTrackedPosition(pos);
  16. e.setPos(pos.x, pos.y, pos.z);
  17. e.pitch = pitch;
  18. e.yaw = yaw;
  19. e.setEntityId(entityId);
  20. e.setUuid(uuid);
  21. MinecraftClient.getInstance().world.addEntity(entityId, e);
  22. });
  23. });
  24. }

Back in our ProjectileEntity class, we must add a method to make sure everything works correctly.

  1. @Override
  2. public Packet createSpawnPacket() {
  3. return EntitySpawnPacket.create(this, ProjectileTutorialClient.PacketID);
  4. }

Finally, make sure to call this method in the onInitializeClient() method.

  1. @Override
  2. public void onInitializeClient() {
  3. EntityRendererRegistry.INSTANCE.register(ProjectileTutorialMod.PackedSnowballEntityType, (dispatcher, context) ->
  4. new FlyingItemEntityRenderer(dispatcher, context.getItemRenderer()));
  5. receiveEntityPacket();
  6. }

Hoping to God It Works

Now, your projectile should be working in-game! Just make sure your textures are in the right place, and your item and projectile should be working.

If you would like to try out this projectile, download here.

[INSERT USABLE PICTURE HERE]

tutorial/projectiles.1624018222.txt.gz · Last modified: 2021/06/18 12:10 by redgrapefruit