Resource 1.15.x Particle Beams, Vectors and Locations, oh my!

Discussion in 'Spigot Plugin Development' started by ZizzyZizzy, Feb 19, 2020.

  1. PART 1 - A Basic Particle Beam Straight Line

    After digging around here and on Bukkit forums for hours, testing, tweaking, researching and pulling hair out, I finally have a good grasp of this topic. Hopefully this will help someone cut through all the chaff, since lots has changed since most of the good tutorials were written. All code here has been tested on 1.15.2.

    ** Spoon feeding? Perhaps. Personally I learn better and faster from taking a working example and then dissecting it to see exactly how it works, so I LOVE IT when people spoon feed examples. **


    This tutorial will cover the basics, and then get right to more advanced particle beams.


    First of all, it's VERY CRUCIAL to understand Location and Vectors. Take the time to learn this, as you will be using the concepts frequently in Minecraft. Seriously. Don't skip this, because you'll end up confused and frustrated if you do.

    • Minecraft uses x,y,z coordinates, where y is elevation instead of the standard z.
    • A Location is a single POINT IN 3D SPACE, but Minecraft adds the world, yaw (left/right angle) and pitch (up/down angle) to the Location class. More on that later.
    • A Vector is used to indicate both direction and speed (AKA Velocity).
    • A Vector is just a point in 3D space, offset from 0,0,0.
    • The longer the Vector, the faster the "speed".
    Here's a picture of a Vector in 3D space. Click the link to view it animated so it's easier to get the idea.
    https://ibb.co/PC0QptH

    upload_2020-2-18_18-24-49.png

    The blue line is a Vector with an offset of 2,2,2 from the origin 0,0,0. That means the "direction" of whatever it describes is going up and away at an angle from the center 0,0,0 location. Its "speed" would be the length of the Vector. Larger offsets means it's moving faster. An offset of 4,4,4 is moving twice as fast as an offset of 2,2,2, but it's moving in the exact same direction.

    So how do we use this information? A particle beam in Minecraft is just a series of particles displayed in a direction over a period of time. Make it go fast enough with short enough distances between particles and it looks like a laser beam.

    To display the particles one after another, we can simply add a Vector offset to the particle's current location and then display another particle at the new location. Repeat this and you have a line of particles. One very helpful Vector for this in Minecraft is Direction.

    A Direction Vector indicates which direction in 3D space an object is facing or moving. One common use for this is the player.getEyeLocation().getDirection() function, which describes which direction in 3D space the player is looking. Another is what we will use it for, which is to correctly display the sequence of particles in the trajectory the player is "shooting".


    To use this in a plugin, we need to create a scheduled task so that the main bukkit processing thread isn't held up while the particles are displayed.

    Here is a very basic particle beam:
    https://ibb.co/dmQrkPy
    upload_2020-2-18_19-3-2.png

    The code to create that is very simple. First we need an EventHandler listener for when a player left-clicks the air with an End Rod. (Search around here if you don't know how to add this to your plugin):

    Code (Text):

        // @EventHandler is required!
        // We also specify that we are MONITORING the event only.
        // Meaning, we are not making any changes to the event. Google for more info.
        @EventHandler (priority = EventPriority.MONITOR)
        // You can name the function anything you want. "clickAction" is appropriate here.
        public void clickAction(PlayerInteractEvent event) {
            // The player that triggered the event
            Player player = event.getPlayer();
            // If the player left-clicked the air, and has an end rod in their hand
            if(event.getAction() == Action.LEFT_CLICK_AIR && player.getInventory().getItemInMainHand().getType() == Material.END_ROD){
                // Display the particle beam
                particleBeam(player);
            }
        }
    "particleBeam" is the name of the function we will create to display the beam of particles. The most basic version works like this:
    1. Get the eye location of the player.
      This will be the particle starting point.
    2. Get the direction the player is facing
      This will determine which direction the particle beam fires.
    3. Create a repeating task that runs every tick (1/20th of a second)
      This is on purpose so we can later change the speed of the particle for advanced effects.
    4. Calculate a Vector offset that is "forward" 0.5 blocks in the direction the player fired the particle beam
    5. Add the Vector to the particle beam location.
    6. Display a new particle at this new location.

    Code (Text):

        public static void particleBeam(Player player){
            // Player's eye location is the starting location for the particle
            Location startLoc = player.getEyeLocation();

            // We need to clone() this location, because we will add() to it later.
            Location particleLoc = startLoc.clone();

            World world = startLoc.getWorld(); // We need this later to show the particle

            // dir is the Vector direction (offset from 0,0,0) the player is facing in 3D space
            Vector dir = startLoc.getDirection();

            /* vecOffset is used to determine where the next particle should appear
            We are taking the direction and multiplying it by 0.5 to make it appear 1/2 block
              in its continuing Vector direction.
            NOTE: We have to clone() because multiply() modifies the original variable!
            For a straight beam, we only need to calculate this once, as the direction does not change.
            */
            Vector vecOffset = dir.clone().multiply(0.5);

            // This can also be done without the extra "dir" variable:
            // Vector vecOffset = startLoc.getDirection().clone().multiply(0.5);

            new BukkitRunnable(){
            // The run() function runs every X number of ticks - see below
                public void run(){
                    // Now we add the direction vector offset to the particle's current location
                    particleLoc.add(vecOffset);

                    // Display the particle in the new location
                    world.spawnParticle(Particle.FIREWORKS_SPARK, particleLoc, 0);
                }
            }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
            // 0 is the delay in ticks before starting this task
            // 1 is the how often to repeat the run() function, in ticks (20 ticks are in one second)
        }

     
    vecOffset is where the magic happens here. We are taking the original starting location direction (the direction the player is looking), multiplying it by 0.5 (1/2 of a block) and then adding that to the particle's last location to make the next particle appear 0.5 block "forward" of the previous particle in same trajectory.

    In simple English:
    1. Look at something in any direction.
    2. Now move 0.5 blocks in the direction you were looking in step 1.
    3. Pause.
    4. Go to step 2.

    Instead of calculating the direction vector once outside the loop, we could instead use the particle's current direction inside the loop like this:

    Code (Text):

                    Vector vecOffset = particleLoc.getDirection().multiply(0.5);

     
    Since there's no reason to change the particle's direction for a straight beam, the extra getDirection() calculation for each loop is pointless here, as it will always be the same as the original starting direction. However, I'll show you how to use that exact change in code to create a "homing beam" that automatically locks on and turns towards a target!

    Pretty simple, huh? Spigot API takes care of all the fancy math formulas required to make it work.


    I added some quick debugging to help explain the location and vector used in this code:

    Code (Text):
    [18:47:46] [Server thread/INFO]: Particle Beam starting location: Location{world=world{name=world},x=-67.086684512604,y=66.79823645538315,z=-88.28188858399895,pitch=-36.45003,yaw=152.41364}

    [18:47:46] [Server thread/INFO]: Vector Offset: -0.1862471199794164,0.2970607510246191,-0.3564644729844225

    [18:47:46] [Server thread/INFO]: New Particle Beam location: Location{world=world{name=world},x=-67.27293163258341,y=67.09529720640776,z=-88.63835305698338,pitch=-36.45003,yaw=152.41364}
     
    So the starting location was
    x=-67.086684512604,y=66.79823645538315,z=-88.28188858399895

    The Vector offset we generated by multiplying the direction by 0.5 was
    -0.1862471199794164,0.2970607510246191,-0.3564644729844225

    Adding that to the starting location gives you the new particle location along the beam:
    x=-67.27293163258341,y=67.09529720640776,z=-88.63835305698338

    For the next loop, the new particle location is already set, so we just calculate the Vector offset again, and add it to the location to get another new location in the same direction.

    You probably noticed the Vector offset wasn't exactly 0.5 for x, y or z. Welcome to 3D Vector distance calculations! I can assure you the offset calculated is exactly 0.5 blocks in 3D space from the original location, it's just not obvious like it would be in 2D space.


    If you run the code as it is written now, the particles will continue to display forever because there is nothing in the runTaskTimer() to make them stop. Let's fix that by adding a max length to the beam by counting how many particles are displayed.

    Code (Text):

            new BukkitRunnable(){
                int maxBeamLength = 30; // Max beam length (number of particles)
                int beamLength = 0; // Number of particles already displayed

                // The run() function runs every X number of ticks - see below
                public void run(){
                    beamLength ++;
                    // Kill this task if the beam length is max
                    if(beamLength >= maxBeamLength){
                        world.spawnParticle(Particle.FLASH, particleLoc, 0);
                        this.cancel();
                        return;
                    }
     
    Since we were using a 0.5 offset for each particle, a maxBeamLength of 30 means the beam will travel 15 blocks (30 * 0.5) and then stop with a big flash at the end.

    Neat! Now how about making the particle actually do something when it collides with a mob or player? I'll continue in the next reply.
     

    Attached Files:

    #1 ZizzyZizzy, Feb 19, 2020
    Last edited: Feb 21, 2020
    • Useful x 9
    • Like x 5
    • Agree x 2
    • Winner x 2
    • Informative x 1
    • Friendly x 1
  2. PART 2 - A Basic Particle Beam Straight Line With Damage

    OK, so now we have this code for a simple particle beam that lasts for 15 blocks and then ends with a large flash:

    Code (Text):

        public static void particleBeam(Player player){
            // Player's eye location is the starting location for the particle
            Location startLoc = player.getEyeLocation();

            // We need to clone() this location, because we will add() to it later.
            Location particleLoc = startLoc.clone();

            World world = startLoc.getWorld(); // We need this later to show the particle

            // dir is the Vector direction (offset from 0,0,0) the player is facing in 3D space
            Vector dir = startLoc.getDirection();

            /* vecOffset is used to determine where the next particle should appear
            We are taking the direction and multiplying it by 0.5 to make it appear 1/2 block
              in its continuing Vector direction.
            NOTE: We have to clone() because multiply() modifies the original variable!
            For a straight beam, we only need to calculate this once, as the direction does not change.
            */
            Vector vecOffset = dir.clone().multiply(0.5);

            new BukkitRunnable(){
                int maxBeamLength = 30; // Max beam length
                int beamLength = 0; // Current beam length

                // The run() function runs every X number of ticks - see below
                public void run(){
                    beamLength ++; // This is the distance between each particle
                    // Kill this task if the beam length is max
                    if(beamLength >= maxBeamLength){
                        world.spawnParticle(Particle.FLASH, particleLoc, 0);
                        this.cancel();
                        return;
                    }

                    // Now we add the direction vector offset to the particle's current location
                    particleLoc.add(vecOffset);

                    // Display the particle in the new location
                    world.spawnParticle(Particle.FIREWORKS_SPARK, particleLoc, 0);
                }
            }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
            // 0 is the delay in ticks before starting this task
            // 1 is the how often to repeat the run() function, in ticks (20 ticks are in one second)
        }

    Now let's make it do something when the particles collide with a mob or player. We'll use BoundingBox in the Spigot API for this. A Bounding Box is a rectangular box that describes the outer edges of something in 3D space. This is also called a HitBox. The white lines in the following picture define the bounding box for each entity:
    https://ibb.co/hfr943D
    upload_2020-2-19_11-5-39.png

    The steps needed to check if the particle is colliding with an entity's BoundingBox will have to happen every loop of the run() inside the
    BukkitRunnable():

    1. Get the current location of the particle
      We use getLocation() for this.
      ** Remember that the particle's location started out as a clone() of the player's getEyeLocation()
    2. Search nearby for any entities.
      We can use getNearbyEntities() for this.
      Five blocks in each direction should be enough to cover all mobs
    3. For each entity found:
      1. Make sure the entity is not the player shooting
      2. Check if the particle is inside the entity's bounding box.
        We can use getBoundingBox().overlaps for this, but we have to define the size of our particle first.
      3. If the particle is inside the bounding box:
        1. Display a flash or explosion at the particle's location
          This is just another spawnParticle() call.
        2. Damage the mob
          This is done using Damageable's damage() function.
        3. Knock-back the mob
          We need use getDirection(), multiply it, and then add that to the entity using setVelocity().
        4. Cancel the BukkitRunnable() task.
      4. If the particle is NOT inside the entity's bounding box, check the next entity found.
    Calculating the knock-back will be tricky if you're new to Vectors, but the rest will be pretty easy.

    Here it is in action:
    https://ibb.co/8Pg4LNT
    upload_2020-2-19_12-12-16.png

    Now for the code. (This runs first in the run() loop, before the beamLength is increased):
    Code (Text):

                    // Search for any entities near the particle's current location
                    for (Entity entity : world.getNearbyEntities(particleLoc, 5, 5, 5)) {
                        // We only care about living entities. Any others will be ignored.
                        if (entity instanceof LivingEntity) {
                            // Ignore player that initiated the shot
                            if (entity == player) {
                                continue;
                            }

                            /* Define the bounding box of the particle.
                            We will use 0.25 here, since the particle is moving 0.5 blocks each time.
                            That means the particle won't miss very small entities like chickens or bats,
                              as the particle bounding box covers 1/2 of the movement distance.
                             */
                            Vector particleMinVector = new Vector(
                                    particleLoc.getX() - 0.25,
                                    particleLoc.getY() - 0.25,
                                    particleLoc.getZ() - 0.25);
                            Vector particleMaxVector = new Vector(
                                    particleLoc.getX() + 0.25,
                                    particleLoc.getY() + 0.25,
                                    particleLoc.getZ() + 0.25);

                            // Now use a spigot API call to determine if the particle is inside the entity's hitbox
                            if(entity.getBoundingBox().overlaps(particleMinVector,particleMaxVector)){
                                // We have a hit!
                                // Display a flash at the location of the particle
                                world.spawnParticle(Particle.FLASH, particleLoc, 0);
                                // Play an explosion sound at the particle location
                                world.playSound(particleLoc,Sound.ENTITY_GENERIC_EXPLODE,2,1);
                                // Damage the target, using the shooter as the damager
                                ((Damageable) entity).damage(5,player);
                                // Cancel the particle beam
                                this.cancel();
                                // We must return here, otherwise the code below will display one more particle.
                                return;
                            }
                        }
                    }
     
    That works pretty well! You can see in the animated gif that the particle beam goes right over the sheep and hits the zombie in the face. Also, since we aren't spawning an actual explosion, only the zombie is damaged by the beam. The only thing missing is the knock-back.

    There are several steps to accomplish this:
    1. Get the current Velocity (directional) Vector of the entity being "hit".
      We can use getVelocity() for this, stored in a new variable.
    2. Get the current directional Vector of the particle.
      This is a call to getDirection() which we've used before
    3. normalize() the particle's vector to reset the "speed" (length) to 1.
    4. multiply() the particle's vector by 1.5 to increase the "speed" by 50%
    5. add() this new velocity to the hit entity's current Velocity from step 1.
    6. Set the entity's new Velocity to the new calculated "knock-back" Velocity
      We can use setVelocity() for this

    Here is the working code to make this happen:
    Code (Text):

                                // Knock-back the entity in the same direction from where the particle is coming.
                                // We need a variable to calculate the new velocity to apply to the entity.
                                Vector entityNewVelocity = entity.getVelocity();

                                // This is the directional vector for the particle
                                Vector particleVelocity = particleLoc.getDirection();

                                // We need to normalize it, setting the "speed" (length) to 1. Remember that a vector
                                //   indicates a direction in 3D space, and the length of the vector indicates speed.
                                particleVelocity.normalize();

                                // Now add 50% to the particle velocity, giving them a temporary speed boost
                                particleVelocity.multiply(1.5);

                                // Add this to the entity's current velocity
                                entityNewVelocity.add(particleVelocity);

                                // Finally set the entity's new velocity:
                                entity.setVelocity(entityNewVelocity);
     
    We can shorten this to make this a one-liner instead, eliminating the need for all the variables:

    Code (Text):
    entity.setVelocity(entity.getVelocity().add(particleLoc.getDirection().normalize().multiply(1.5)));
    The new code now looks like this:
    Code (Text):

    public static void particleBeam(Player player){
        // Player's eye location is the starting location for the particle
        Location startLoc = player.getEyeLocation();

        // We need to clone() this location, because we will add() to it later.
        Location particleLoc = startLoc.clone();

        World world = startLoc.getWorld(); // We need this later to show the particle

        // dir is the Vector direction (offset from 0,0,0) the player is facing in 3D space
        Vector dir = startLoc.getDirection();

        /* vecOffset is used to determine where the next particle should appear
        We are taking the direction and multiplying it by 0.5 to make it appear 1/2 block
          in its continuing Vector direction.
        NOTE: We have to clone() because multiply() modifies the original variable!
        For a straight beam, we only need to calculate this once, as the direction does not change.
        */
        Vector vecOffset = dir.clone().multiply(0.5);

        new BukkitRunnable(){
            int maxBeamLength = 30; // Max beam length
            int beamLength = 0; // Current beam length

            // The run() function runs every X number of ticks - see below
            public void run(){
                // Search for any entities near the particle's current location
                for (Entity entity : world.getNearbyEntities(particleLoc, 5, 5, 5)) {
                    // We only care about living entities. Any others will be ignored.
                    if (entity instanceof LivingEntity) {
                        // Ignore player that initiated the shot
                        if (entity == player) {
                            continue;
                        }

                        /* Define the bounding box of the particle.
                        We will use 0.25 here, since the particle is moving 0.5 blocks each time.
                        That means the particle won't miss very small entities like chickens or bats,
                          as the particle bounding box covers 1/2 of the movement distance.
                         */
                        Vector particleMinVector = new Vector(
                                particleLoc.getX() - 0.25,
                                particleLoc.getY() - 0.25,
                                particleLoc.getZ() - 0.25);
                        Vector particleMaxVector = new Vector(
                                particleLoc.getX() + 0.25,
                                particleLoc.getY() + 0.25,
                                particleLoc.getZ() + 0.25);

                        // Now use a spigot API call to determine if the particle is inside the entity's hitbox
                        if(entity.getBoundingBox().overlaps(particleMinVector,particleMaxVector)){
                            // We have a hit!
                            // Display a flash at the location of the particle
                            world.spawnParticle(Particle.FLASH, particleLoc, 0);
                            // Play an explosion sound at the particle location
                            world.playSound(particleLoc,Sound.ENTITY_GENERIC_EXPLODE,2,1);

                            // Knock-back the entity in the same direction from where the particle is coming.
                            entity.setVelocity(entity.getVelocity().add(particleLoc.getDirection().normalize().multiply(1.5)));

                            // Damage the target, using the shooter as the damager
                            ((Damageable) entity).damage(5,player);
                            // Cancel the particle beam
                            this.cancel();
                            // We must return here, otherwise the code below will display one more particle.
                            return;
                        }
                    }
                }

                beamLength ++; // This is the distance between each particle

                // Kill this task if the beam length is max
                if(beamLength >= maxBeamLength){
                    world.spawnParticle(Particle.FLASH, particleLoc, 0);
                    this.cancel();
                    return;
                }

                // Now we add the direction vector offset to the particle's current location
                particleLoc.add(vecOffset);

                // Display the particle in the new location
                world.spawnParticle(Particle.FIREWORKS_SPARK, particleLoc, 0);
            }
        }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
        // 0 is the delay in ticks before starting this task
        // 1 is the how often to repeat the run() function, in ticks (20 ticks are in one second)
    }
     

    Click the link for the animated gif of what this looks like in-game:
    https://ibb.co/DQh940b
    upload_2020-2-19_13-44-50.png
     
    #2 ZizzyZizzy, Feb 19, 2020
    Last edited: Feb 19, 2020
    • Informative Informative x 1
    • Useful Useful x 1
  3. PART 3 - A Basic Particle Beam With
    Damage and Player-Controlled Tracking

    OK, so that was fun. Now let's add something else to this particle beam - player-controlled tracking!

    This is actually much easier than it sounds. Instead of re-using the original player's getEyeLocation() in each loop, we will get the player's eye location in each loop, and then add that directional Vector to the particle instead. This will create a small but noticeable "curving" ability to the particle beam by simply changing where you look.

    Here it is in action (slowed down a bit so you can see it better):
    https://ibb.co/7nhGS1K
    upload_2020-2-19_14-51-37.png

    Since we need to modify vecOffset, we'll have to move its definition line inside the BukkitRunnable(), but outside the run() loop.

    Code (Text):

            new BukkitRunnable(){
                /* vecOffset is used to determine where the next particle should appear
                We are taking the direction and multiplying it by 0.5 to make it appear 1/2 block
                  in its continuing Vector direction.
                NOTE: We have to clone() because multiply() modifies the original variable!
                For a straight beam, we only need to calculate this once, as the direction does not change.
                */
                Vector vecOffset = dir.clone().multiply(0.5);

                int maxBeamLength = 30; // Max beam length
                int beamLength = 0; // Current beam length
     
    Next we'll insert the code to calculate the vecOffset based on the player's eye direction. This goes after the hit detection and beam length limiter, but before we add the vecOffset to the particle's current location:

    Code (Text):

                    // Kill this task if the beam length is max
                    if(beamLength >= maxBeamLength){
                        world.spawnParticle(Particle.FLASH, particleLoc, 0);
                        this.cancel();
                        return;
                    }

                    // Alter the particle's course based on where the player is looking
                    vecOffset = player.getEyeLocation().getDirection().clone().multiply(0.5);

                    // Now we add the direction vector offset to the particle's current location
                    particleLoc.add(vecOffset);
     

    Here's the entire updated code if that was confusing:

    Code (Text):

    public static void particleBeam(Player player){
        // Player's eye location is the starting location for the particle
        Location startLoc = player.getEyeLocation();

        // We need to clone() this location, because we will add() to it later.
        Location particleLoc = startLoc.clone();

        World world = startLoc.getWorld(); // We need this later to show the particle

        // dir is the Vector direction (offset from 0,0,0) the player is facing in 3D space
        Vector dir = startLoc.getDirection();


        new BukkitRunnable(){
            /* vecOffset is used to determine where the next particle should appear
            We are taking the direction and multiplying it by 0.5 to make it appear 1/2 block
              in its continuing Vector direction.
            NOTE: We have to clone() because multiply() modifies the original variable!
            */
            Vector vecOffset = dir.clone().multiply(0.5);
            int maxBeamLength = 30; // Max beam length
            int beamLength = 0; // Current beam length

            // The run() function runs every X number of ticks - see below
            public void run(){
                // Search for any entities near the particle's current location
                for (Entity entity : world.getNearbyEntities(particleLoc, 5, 5, 5)) {
                    // We only care about living entities. Any others will be ignored.
                    if (entity instanceof LivingEntity) {
                        // Ignore player that initiated the shot
                        if (entity == player) {
                            continue;
                        }

                        /* Define the bounding box of the particle.
                        We will use 0.25 here, since the particle is moving 0.5 blocks each time.
                        That means the particle won't miss very small entities like chickens or bats,
                          as the particle bounding box covers 1/2 of the movement distance.
                         */
                        Vector particleMinVector = new Vector(
                                particleLoc.getX() - 0.25,
                                particleLoc.getY() - 0.25,
                                particleLoc.getZ() - 0.25);
                        Vector particleMaxVector = new Vector(
                                particleLoc.getX() + 0.25,
                                particleLoc.getY() + 0.25,
                                particleLoc.getZ() + 0.25);

                        // Now use a spigot API call to determine if the particle is inside the entity's hitbox
                        if(entity.getBoundingBox().overlaps(particleMinVector,particleMaxVector)){
                            // We have a hit!
                            // Display a flash at the location of the particle
                            world.spawnParticle(Particle.FLASH, particleLoc, 0);
                            // Play an explosion sound at the particle location
                            world.playSound(particleLoc,Sound.ENTITY_GENERIC_EXPLODE,2,1);

                            // Knock-back the hit entity
                            entity.setVelocity(entity.getVelocity().add(particleLoc.getDirection().normalize().multiply(1.5)));

                            // Damage the target, using the shooter as the damager
                            ((Damageable) entity).damage(5,player);
                            // Cancel the particle beam
                            this.cancel();
                            // We must return here, otherwise the code below will display one more particle.
                            return;
                        }
                    }
                }

                beamLength ++; // This is the distance between each particle

                // Kill this task if the beam length is max
                if(beamLength >= maxBeamLength){
                    world.spawnParticle(Particle.FLASH, particleLoc, 0);
                    this.cancel();
                    return;
                }

                /* Direction contains very small numbers to indicate a position in 3D space in relation to 0,0,0
                 Drawing an arrow from 0,0,0 to this Direction indicates the direction in 3D space.
                 We will use the player's current eye direction instead of the starting direction, which will
                 make the beam curve toward where the player is looking.
                */
                vecOffset = player.getEyeLocation().getDirection().clone().multiply(0.5);

                // Now we add the direction vector offset to the particle's current location
                particleLoc.add(vecOffset);

                // Display the particle in the new location
                world.spawnParticle(Particle.FIREWORKS_SPARK, particleLoc, 0);
            }
        }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
        // 0 is the delay in ticks before starting this task
        // 1 is the how often to repeat the run() function, in ticks (20 ticks are in one second)
    }
     


    This is a nice change to the particle beam. It's multiplayer friendly and not very op. If you want a truly op version that tracks exactly where the player is looking, we have to change the code a little bit.

    The changes that need to be make this happen:
    1. Instead of adding a small amount to the particle's current location each loop, we must use the player's current eye location as the starting location and then add() to that instead.
      We can do this by extending vecOffset's length by 0.5 each loop to ensure it is always moving away from the player.
    2. We need to use the player's current eye direction for the particle, instead of the particle's previous direction
      This also means the particles will "jump" much further than the previous tracking beam,since the direction will match the player's eye direction exactly. (Op much?)

    Here's a new function with a basic particle beam I created to make it easier to see the differences. There's minimal comments and no collision detection in this one:

    Code (Text):

        public static void trackingTest(Player p){
            new BukkitRunnable() {
                World world = p.getLocation().getWorld();
                int beamLength = 30;
                double t = 0;
                @Override
                public void run() {
                    Location startLoc = p.getEyeLocation();
                    final Vector dir = p.getLocation().getDirection().normalize();

                    t += 0.5;
                    if(t > beamLength){
                        this.cancel();
                    }
                    // Extend the length of the vector by 0.5 each loop to ensure
                    // the particle trail is always moving away
                    Vector vecOffset = dir.clone().multiply(t);

                    world.spawnParticle(Particle.FIREWORKS_SPARK, startLoc.add(vecOffset), 0);
                }
            }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
        }
    Here's what that looks like in-game:
    https://ibb.co/ncm8zZF
    upload_2020-2-19_15-33-6.png

    Next let's do something even more fun, like an automatic homing beam that will lock onto the closest target and automatically track it until either the beam hits or reaches max length.
     
    #3 ZizzyZizzy, Feb 19, 2020
    Last edited: Feb 22, 2020
    • Useful Useful x 4
    • Informative Informative x 1
  4. PART 3 - A Homing Particle Beam With Damage!
    The player-controlled tracking was pretty neat, but how about an automatic homing beam that will guide itself to the nearest target? Nothing better than fire-and-forget!

    Here are the steps to make this happen:
    1. Search the particle's current location for nearby entities.
      We can use getNearbyEntities() for this, just like before.
    2. Make sure the entity is a living entity and not the shooting player.
    3. Lock on to the first entity found.
    4. Adjust the particle's trajectory so it curves towards the target entity.
      We want to curve by a small amount instead of making a sharp turn.
      For a more advanced homing beam, we can also tighten the turn based on distance to target!
    5. Use the previous collision detection, but add some more features to level the playing field
      Homing beams are a little bit op, so we'll make any hit mobs instantly target the shooting player & make them silent, and give any hit target entity a speed boost potion.
    Don't panic just yet. These additions are not that hard if you take them one at a time. First, let's find entities near the particle beam. How about 5 blocks in every direction? Since we need to "lock" on this target, we'll also need another variable to track the target Entity. If this variable is set to a living entity in the next loop, we already have our target.

    One quick note about the getLocation() function for entities. It returns a point at the bottom of the entity, which is the feet for a living entity. For a more realistic homing beam, we'll instead target the exact center of the entity instead. This is easy using the spigot getHeight() function and dividing by 2, then adding that to the y coordinate. No special maths needed.

    Here are the new variables we'll need. These must go inside BukkitRunnable(), but before the run() loop:

    Code (Text):
                Entity target = null; // This is the target for the beam
                double targetHeight = 0.0; // Height of the target entity. Used for tracking to the exact center.
     
    Now for the tracking code to locate and lock on to a nearby entity:

    Code (Text):

                // Tracking code. First we only activate if there is no target or the previous target has died.
                if(target == null || target.isDead()) {
                    for (Entity entity : world.getNearbyEntities(particleLoc, 5, 5, 5)) {
                        if (entity instanceof LivingEntity) {
                            // Ignore player that initiated the shot
                            if (entity instanceof Player && entity == player) {
                                continue;
                            }
                            // Found a target entity. Stop searching
                            target = entity; // Save/lock the target
                            targetHeight = target.getHeight(); // We need this to target the exact center
                            String targetName = "unknown";
                            if (target instanceof Player) {
                                targetName = target.getName();
                            } else {
                                targetName = target.getType().toString();
                            }
                            player.sendMessage("Target Locked! (" + targetName + ")");
                            if(target instanceof Player) {
                                target.sendMessage(player.getName() + "'s magic beam has locked on. Run, Forest, Run!");
                            }
                            break;
                        }
                    }
                }
     
    This is very similar to the collision detection code. I've added some simple code to notify the player that fired the shot when a lock happens, and a message to the targeted player. (This notification code really should check to ensure each messaged player is actually still online to avoid null pointer errors in the console.)

    The next thing we need is the code to alter the particle so it turns towards the newly acquired target. Since there's no guarantee that we have a target for each loop, we first have to check if target is null. Then we will calculate a new direction vector that is 1/2 way between the current particle direction and the target direction. This provides a nice, smooth turn towards the target instead of an instant, unrealistic change:

    Before we can do this, the vecOffset definition must be moved to inside the run() loop, since we will calculate it fresh for each loop.

    Code (Text):

                    // vecOffset is used to determine where the next particle should appear
                    Vector vecOffset = null;

                    // We have a target! Adjust vector to point to this entity
                    if(target != null){
                        // Target the center of the target entity
                        Location targetLoc = target.getLocation().clone().add(0,targetHeight / 2,0);;
                        // Get the current particle trajectory
                        Vector particleDirection = particleLoc.getDirection();
                        // Calc the vector half way between the projectile and the target.
                        Vector inBetween = targetLoc.clone().subtract(particleLoc).toVector().normalize();

                        inBetween.multiply(0.5);
                        // Add the now multiplied "in between" vector to the projectile's direction vector and then normalize it
                        particleDirection.add(inBetween).normalize();

                        vecOffset = particleDirection.clone();

                        // Need to set the new direction, otherwise direction resumes to before tracking direction
                        particleLoc.setDirection(particleDirection);
                    } else {
                        // No target. Continue moving in the previous direction
                        vecOffset = particleLoc.getDirection().clone().multiply(0.5);
                    }
     
    ** Credit for the curving vector "inbetween" calculations used belong to finnbon from this post.

    By itself, this works..kind of. The beam starts to curve towards the target, but only hits it if the beam is very close. Check out the animated gif link to see what I mean:
    https://ibb.co/d0fYKsS

    Here's a shot that was close enough to curve into the target:
    upload_2020-2-19_16-58-54.png

    This one completely missed because the particle adjustment wasn't aggressive enough:
    upload_2020-2-19_16-59-48.png

    finnbon has a suggestion in the post linked above on how to fix this, which also happens to fix another issue if you make the "curve" towards the target too aggressive - the beam will circle the entity and never hit it.

    I played around with some exponential equations until I found one that works perfectly for this homing beam. I also added a speed up to the particle at 5 blocks from the target and again at 3 blocks from the target:

    Code (Text):

    double accuracy = 0.5;

    // If the distance between the particle and the target is 5 or less, tighten the curve towards the target
    //  and speed up the particle slightly
    double distance = particleLoc.distance(targetLoc);
    if (distance < 5) {
        ticksPerParticle = 2;
        ticks = 0;

        // Maths FTW! This creates a nice effect where the closer it gets to the target, the tighter the curve
        // Returns a nice percentage number between 0.06 and .90 that we then multiply by 0.5 and add that to 0.5
        accuracy = accuracy * Math.pow(0.6, distance) + 0.5;
    }
    // If the distance is less than three, speed it up even more
    if (distance < 3) {
        ticksPerParticle = 1;
        ticks = 0;
    }

    // Add the now multiplied "in between" vector to the projectile's direction vector and then normalize it
    inBetween.multiply(accuracy);

     
    There are a couple of new variables in there that I will explain in a moment - ticksPerParticle and ticks. These are used to speed up the particle. For now, just ignore them.


    Now this is MUCH better. The beam finds a target, increases speed and tightens the "curve" towards the target:
    https://ibb.co/ZhrFyk3

    upload_2020-2-19_17-17-25.png
    upload_2020-2-19_17-17-45.png

    We're not done yet. We still need to add the effects to the entity that was hit, and I have one more tweak to optimize the search for entities that ensures we always target the enemy closest to the beam.

    Earlier in this tutorial series I mentioned that running the BukkitRunnable() every tick was on purpose so we could control the speed of the particle. The next tweak for this homing particle beam will put that to use.

    Having the BukkitRunnable() running every tick (20 times per second) makes a homing particle beam fire too fast to be useful, since it won't have enough time to curve towards the target. That means we need to slow it down. The easiest way to control the speed is to add a counter and conditional statement like this:

    Code (Text):
                int ticks = 0; // Tick counter
                int ticksPerParticle = 3; // How many ticks per particle
                public void run(){
                    ticks++;
                    if(ticks == ticksPerParticle){
    // ... the rest of the particle logic & display goes here
     
    The variables are aptly named for clarification. This small change will make it so that a particle is now shown once every 3 ticks instead of every 1 tick, slowing it down to 6.66 times per second instead of 20. Now the homing beam will be able to curve towards targets more effectively, and we will be able to use this to speed up the particle when it's close to a target.

    Here's the code I used to speed up the homing beam when it gets close to a target:

    Code (Text):

                            // If the distance between the particle and the target is 5 or less, tighten the curve towards the target
                            //  and speed up the particle slightly
                            double distance = particleLoc.distance(targetLoc);
                            if (distance < 5) {
                                ticksPerParticle = 2;
                                ticks = 0;

                                // Maths FTW! This creates a nice effect where the closer it gets to the target, the tighter the curve
                                // Returns a nice percentage number between 0.06 and .90 that we then multiply by 0.5 and add that to 0.5
                                accuracy = accuracy * Math.pow(0.6, distance) + 0.5;
                                // Now adjust the distance between particles to prevent circling of targets
                                particleDistance = 0.5 - (0.5 * accuracy);
                            }
                            // If the distance is less than three, speed it up even more
                            if (distance < 3) {
                                ticksPerParticle = 1;
                                ticks = 0;
                            }
     
    So if the distance from the particle to the target is less than 5 blocks, the particle speeds up to 10 particles per second, or once every other tick. If the distance drops to under 3 blocks, the particle goes full speed at 20 particles per second. This adds a nice sense or urgency when playing because you know the particle is about to accelerate and there's not much the target can do to avoid it.

    The second addition is an adjustment to the particle distance. That is the distance between displayed particles in the beam. Once it gets close to a target, the distance decreases significantly to ensure the accuracy calculations are able to actually alter the particle's course and hit the target instead of just swirling around it. (This especially helps with narrow targets, since the default particle distance is 0.5.)

    (continued...)
     
    #4 ZizzyZizzy, Feb 20, 2020
    Last edited: Feb 20, 2020
    • Useful Useful x 2
    • Informative Informative x 1
  5. (...continued)

    One other change I made to the code to make it more playable is to disable the tracking/homing feature until the particle has traveled three blocks from the player:


    Code (Text):
                        // Once the beam has traveled 3 blocks, start homing towards the closest entity
                        if(beamLength >= 6){
     

    All together, these changes now look like this:


    Code (Text):

    public static void particleTutorial(Player player){
        // Player's eye location is the starting location for the particle
        Location startLoc = player.getEyeLocation();

        // We need to clone() this location, because we will add() to it later.
        Location particleLoc = startLoc.clone();

        World world = startLoc.getWorld(); // We need this later to show the particle

        // dir is the Vector direction (offset from 0,0,0) the player is facing in 3D space
        Vector dir = startLoc.getDirection();

        new BukkitRunnable(){
            int maxBeamLength = 30; // Max beam length
            int beamLength = 0; // Current beam length
            Entity target = null; // This is the target for the beam
            double targetHeight = 0.0; // Height of the target entity. Used for tracking to the exact center.

            int ticks = 0; // Tick counter
            int ticksPerParticle = 3; // How many ticks per particle

            // The run() function runs every X number of ticks - see below
            public void run() {
                ticks++;
                if (ticks == ticksPerParticle) {
                    ticks = 0;

                    // Collision detection
                    // Search for any entities near the particle's current location
                    for (Entity entity : world.getNearbyEntities(particleLoc, 5, 5, 5)) {
                        // We only care about living entities. Any others will be ignored.
                        if (entity instanceof LivingEntity) {
                            // Ignore player that initiated the shot
                            if (entity == player) {
                                continue;
                            }

                    /* Define the bounding box of the particle.
                    We will use 0.25 here, since the particle is moving 0.5 blocks each time.
                    That means the particle won't miss very small entities like chickens or bats,
                      as the particle bounding box covers 1/2 of the movement distance.
                     */
                            Vector particleMinVector = new Vector(
                                    particleLoc.getX() - 0.25,
                                    particleLoc.getY() - 0.25,
                                    particleLoc.getZ() - 0.25);
                            Vector particleMaxVector = new Vector(
                                    particleLoc.getX() + 0.25,
                                    particleLoc.getY() + 0.25,
                                    particleLoc.getZ() + 0.25);

                            // Now use a spigot API call to determine if the particle is inside the entity's hitbox
                            if (entity.getBoundingBox().overlaps(particleMinVector, particleMaxVector)) {
                                // We have a hit!
                                // Display a flash at the location of the particle
                                world.spawnParticle(Particle.FLASH, particleLoc, 0);
                                // Play an explosion sound at the particle location
                                world.playSound(particleLoc, Sound.ENTITY_GENERIC_EXPLODE, 2, 1);

                                // Knock-back the entity in the same direction from where the particle is coming.
                                entity.setVelocity(entity.getVelocity().add(particleLoc.getDirection().normalize().multiply(1.5)));

                                // Damage the target, using the shooter as the damager
                                ((Damageable) entity).damage(5, player);
                                // Cancel the particle beam
                                this.cancel();
                                // We must return here, otherwise the code below will display one more particle.
                                return;
                            }
                        }
                    }

                    // vecOffset is used to determine where the next particle should appear
                    Vector vecOffset = null;

                    // Once the beam has traveled 3 blocks, start homing towards the closest entity
                    if(beamLength >= 6) {

                        // Tracking code. First we only activate if there is no target or the previous target has died.
                        if (target == null || target.isDead()) {
                            for (Entity entity : world.getNearbyEntities(particleLoc, 5, 5, 5)) {
                                if (entity instanceof LivingEntity) {
                                    // Ignore player that initiated the shot
                                    if (entity instanceof Player && entity == player) {
                                        continue;
                                    }
                                    // Found a target entity. Stop searching
                                    target = entity; // Save/lock the target
                                    targetHeight = target.getHeight(); // We need this to target the exact center
                                    String targetName = "unknown";
                                    if (target instanceof Player) {
                                        targetName = target.getName();
                                    } else {
                                        targetName = target.getType().toString();
                                    }
                                    player.sendMessage("Target Locked! (" + targetName + ")");
                                    if (target instanceof Player) {
                                        target.sendMessage(player.getName() + "'s magic beam has locked on. Run, Forest, Run!");
                                    }
                                    break;
                                }
                            }
                        }
                    }

                    // We have a target! Adjust vector to point to this entity
                    if (target != null) {
                        // Target the center of the target entity
                        Location targetLoc = target.getLocation().clone().add(0, targetHeight / 2, 0);

                        // Get the current particle trajectory
                        Vector particleDirection = particleLoc.getDirection();
                        // Calc the vector half way between the projectile and the target.
                        Vector inBetween = targetLoc.clone().subtract(particleLoc).toVector().normalize();

                        double accuracy = 0.5;

                        // If the distance between the particle and the target is 5 or less, tighten the curve towards the target
                        //  and speed up the particle slightly
                        double distance = particleLoc.distance(targetLoc);
                        if (distance < 5) {
                            ticksPerParticle = 2;
                            ticks = 0;

                            // Maths FTW! This creates a nice effect where the closer it gets to the target, the tighter the curve
                            // Returns a nice percentage number between 0.06 and .90 that we then multiply by 0.5 and add that to 0.5
                            accuracy = accuracy * Math.pow(0.6, distance) + 0.5;

                            // Now adjust the distance between particles to prevent circling of targets
                            particleDistance = 0.5 - (0.5 * accuracy);
                        }
                        // If the distance is less than three, speed it up even more
                        if (distance < 3) {
                            ticksPerParticle = 1;
                            ticks = 0;
                        }

                        // Add the now multiplied "in between" vector to the projectile's direction vector and then normalize it
                        inBetween.multiply(accuracy);
                        particleDirection.add(inBetween).normalize();
                        vecOffset = particleDirection.clone();
                        // Need to set the new direction, otherwise direction resumes to before tracking direction
                        particleLoc.setDirection(particleDirection);
                    } else {
                        // No target. Continue moving in the previous direction
                        vecOffset = particleLoc.getDirection().clone().multiply(particleDistance);
                    }


                    beamLength++; // This is the current number of particles in the beam.

                    // Kill this task if the beam length is max
                    if (beamLength >= maxBeamLength) {
                        world.spawnParticle(Particle.FLASH, particleLoc, 0);
                        this.cancel();
                        return;
                    }

                    // Now we add the direction vector offset to the particle's current location
                    particleLoc.add(vecOffset);

                    // Display the particle in the new location
                    world.spawnParticle(Particle.FIREWORKS_SPARK, particleLoc, 0);
                }
            }
        }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
        // 0 is the delay in ticks before starting this task
        // 1 is the how often to repeat the run() function, in ticks (20 ticks are in one second)
    }

     

    That works pretty well for homing to ensure the target is always hit without the particles just swirling around them endlessly. Check out some debugging output from the calculations in real-time:

    Code (Text):

    [13:49:58] [Server thread/INFO]: Distance : 4.6294132991587, accuracy: 0.5469829684300878, particle distance: 0.22650851578495612
    [13:49:58] [Server thread/INFO]: Distance : 3.6567003596245296, accuracy: 0.577221032087377, particle distance: 0.21138948395631152
    [13:49:58] [Server thread/INFO]: Distance : 2.6759121778532076, accuracy: 0.6274448333078807, particle distance: 0.18627758334605965

    [13:50:00] [Server thread/INFO]: Distance : 4.435065447934008, accuracy: 0.5518867274610216, particle distance: 0.22405663626948918
    [13:50:00] [Server thread/INFO]: Distance : 3.492564696575196, accuracy: 0.583974783617927, particle distance: 0.2080126081910365
    [13:50:00] [Server thread/INFO]: Distance : 2.533774079779613, accuracy: 0.6370425379523363, particle distance: 0.18147873102383183
    [13:50:00] [Server thread/INFO]: Distance : 1.5689250197829672, accuracy: 0.7243396047878412, particle distance: 0.1378301976060794

    [13:50:03] [Server thread/INFO]: Distance : 4.760904329643133, accuracy: 0.5439308222259269, particle distance: 0.22803458888703654
    [13:50:03] [Server thread/INFO]: Distance : 3.798012928137335, accuracy: 0.5718431862710087, particle distance: 0.21407840686449564
    [13:50:03] [Server thread/INFO]: Distance : 2.823751157830042, accuracy: 0.6181746543604245, particle distance: 0.19091267281978774
    [13:50:03] [Server thread/INFO]: Distance : 1.8442924876437166, accuracy: 0.6949018771886131, particle distance: 0.15254906140569346
    [13:50:03] [Server thread/INFO]: Distance : 0.8680802332441319, accuracy: 0.8209131326777959, particle distance: 0.08954343366110207
     
    Distance is distance to target.
    Accuracy is the exponential equation.
    Particle Distance is the distance between particles in the world.

    You can see that this accuracy formula works near perfectly to ensure the beam curves tighter & increases speed, while at the same time reducing the distance between particles to ensure the target is hit every time. Neat!




    Next problem: the code to find nearby entities is inconsistent. You can fire the beam between two entities and sometimes it will target the one that is a little further away.

    The fix for this is easy. We'll just start checking for nearby entities within a 1 block radius, then 2, then 3, and so on. That guarantees the closest entity is targeted first.

    We'll wrap this around the existing search code

    Code (Text):

                                entitySearch:
                                for (int radius = 1; radius <= 5; radius++) {
                                    for (Entity entity : world.getNearbyEntities(particleLoc, radius, radius, radius)) {
     
    Notice the added label entitySearch. This is to be able to break out of the outer loop easily when an entity is found. The entire updated code for the entity search now looks like this:

    Code (Text):

    // Once the beam has traveled 3 blocks, start homing towards the closest entity
    if(beamLength >= 6) {
        // Tracking code. First we only activate if there is no target or the previous target has died.
        if (target == null || target.isDead()) {
            entitySearch:
            for (int radius = 1; radius <= 5; radius++) {
                for (Entity entity : world.getNearbyEntities(particleLoc, radius, radius, radius)) {
                    if (entity instanceof LivingEntity) {
                        // Ignore player that initiated the shot
                        if (entity instanceof Player && entity == player) {
                            continue;
                        }
                        // Found a target entity. Stop searching
                        target = entity; // Save/lock the target
                        targetHeight = target.getHeight(); // We need this to target the exact center
                        String targetName = "unknown";
                        if (target instanceof Player) {
                            targetName = target.getName();
                        } else {
                            targetName = target.getType().toString();
                        }
                        player.sendMessage("Target Locked! (" + targetName + ")");
                        if (target instanceof Player) {
                            target.sendMessage(player.getName() + "'s magic beam has locked on. Run, Forest, Run!");
                        }
                        break entitySearch;
                    }
                }
            }
        }
    }
     

    If you really wanted to get crazy with the entity tracking/locking, you could check the distance between the particle and each entity nearby, then target the closest one with absolute precision. That is really overkill and adds extra calculations that are not needed.

    The final changes for the homing beacon are to add effects to the entity hit by the particle beam. It's a much slower beam, so let's up the ante and set the entity on fire for a few ticks. Since homing beams are a bit op, let's also give the hit entity a little boost as well. Check the comments in the below code for details on the other small additions to even the fight:

    Code (Text):

                                    // Set the entity on fire for 3 seconds
                                    target.setFireTicks(3 * 20);
                                    if(target instanceof  Player) {
                                        // Give hit players a little speed boost for 3 seconds
                                        // We have to cast the Entity target to a Player class first
                                        ((Player) target).addPotionEffect(new PotionEffect(PotionEffectType.SPEED,60,2));
                                    }
                                    if(target instanceof  Mob) {
                                        /* We have to cast the target Entity to a Mob type so we can use several API
                                           calls not available in the Entity class
                                         */
                                        Mob targetMob = (Mob) target;
                                        // Make the mob target the player that shot them.
                                        targetMob.setTarget(player);
                                        // Make the mob glow to strike terror in the player's heart
                                        targetMob.setGlowing(true);
                                        // Make the mob dead silent
                                        targetMob.setSilent(true);
                                        // Give the mob a speed boost for 3 seconds
                                        targetMob.addPotionEffect(new PotionEffect(PotionEffectType.SPEED, 60, 2));
                                    }
                                    /* For wolf and pig zombies, we can also enable the angry flag to ensure
                                       they and all their friends go after the player that shot one of them.
                                     */
                                    if(target.getType() == EntityType.WOLF) {
                                        Wolf targetToAnger = (Wolf) target;
                                        targetToAnger.setAngry(true);
                                    }
                                    if(target.getType() == EntityType.PIG_ZOMBIE) {
                                        PigZombie targetToAnger = (PigZombie) target;
                                        targetToAnger.setAngry(true);
                                    }
     
    This works exactly as I imagined. The beam isn't too fast so that it's op, and it locks on to a target with a vengeance. Once the mob is hit, he blitzes you, which is pretty intimidating. Check out the link for a game play example. The picture below doesn't do this one justice (especially when it keeps disappearing from this post! Seriously, I've re-added it three times now.)
    https://ibb.co/y0GBbDk

    HomingBeamFinal.png

    Some other changes I personally would make for a complete plugin that used this homing particle beam:
    • Sound effect when it was fired
    • Sound effect to a player when they have been targeted
    • Cool down between uses to prevent spamming
    • Limited number of uses before the End Rod self-destructs
      (hint: use getItemMeta().setDisplayName() to store the number of uses and then update each use.)
    • Lore for the End Rod
    • Make the Rod enchanted so it glows
      (hint: addUnsafeEnchantment(Enchantment.BINDING_CURSE, 1) ;
     
    #5 ZizzyZizzy, Feb 20, 2020
    Last edited: Feb 22, 2020
    • Informative Informative x 1
    • Useful Useful x 1
  6. PART 4 - An Advanced Spinning Circle Particle Beam

    So by now hopefully you have a good grasp of Vector, Location and Directions/Velocity. Let's turn it up a notch (no pun intended) and make something a bit more visually appealing.

    Check this out:
    https://ibb.co/FX2YrYp

    ConeSpiralBeamFinal.png

    That's a pretty neat effect, and not overly difficult to implement using a similar technique to the previous Particle Beam.

    Before you dive in here, do you remember that trigonometry nonsense you learned in school and swore you would never use? You should have paid more attention, because the stuff here is math heavy. (I won't be explaining the math behind how this stuff works, since it's been explained here many times already.) If you need a refresher as it applies to Minecraft, check out these great tutorials by finnbon:
    https://www.spigotmc.org/threads/tu...-about-math-first-steps-in-animations.111238/
    https://linktr.ee/finnbon

    So how do we recreate this effect? Here are the steps to use inside a BukkitRunnable():
    1. Create a certain amount of points in a circle of a specific radius on the x & z axis.
    2. Orient each point so they are properly aligned based on the direction the player is facing.
    3. Shrink the radius slightly each loop.
    4. Rotate all of the points in the circle slightly each loop.
    5. If the radius of the circle is =< 0, cancel the BukkitRunnable()
    Let's take it one step at a time. First we need to generate a circle of a given radius, then pick a certain number of points around the circumference to use for displaying a particle. This is easier than it sounds, so bear with me.

    If you checked out the linked tutorials above, you know that we can easily create a circle of a given radius with sin and cos functions. The only difference here is we only want to display a certain number of points in the circle so the spiraling/rotating effect is obvious. The math for this is pretty simple:

    Code (Text):
                double increment = (2 * Math.PI) / circlePoints;
    increment is going to be the angular distance between each point we will display.
    circlePoints is the number of points we want to display.

    More circlePoints means a smaller angle/distance between points. Too many points will just draw a full circle and it will be impossible to tell that it's rotating in Minecraft using particles.

    Now we need to use the increment to get exactly circlePoints number of points around the circumference of the circle:

    Code (Text):

                    for (int i = 0; i < circlePoints; i++) {
                        double angle = i * increment; // Angle on the circle

                        double x = radius * Math.cos(angle);
                        double z = radius * Math.sin(angle);
     
    There's obviously more to do to complete the effect shown, but let's test just this part out and see how it looks in Minecraft. The animated gif and screenshot above was created with a radius of 2 and 10 circlePoints, so I'll use that here too.

    Code (Text):
        public static void particleTutorial(Player player){
            new BukkitRunnable() {
                // Number of points in each circle
                int circlePoints = 10;
                // radius of the circle
                double radius = 2;
                // Starting location for the first circle will be the player's eye location
                Location playerLoc = player.getEyeLocation();
                // We need world for the spawnParticle function
                World world = playerLoc.getWorld();
                // This is the direction the player is looking, normalized to a length (speed) of 1.
                final Vector dir = player.getLocation().getDirection().normalize();
                // This is the distance between each point around the circumference of the circle.
                double increment = (2 * Math.PI) / circlePoints;
                // Max length of the beam..for now
                int beamLength = 30;
                @Override
                public void run() {
                    beamLength--;
                    if(beamLength < 1){
                        this.cancel();
                        return;
                    }
                    // We need to loop to get all of the points on the circle every loop
                    for (int i = 0; i < circlePoints; i++) {
                        double angle = i * increment;
                        double x = radius * Math.cos(angle);
                        double z = radius * Math.sin(angle);
                        // Convert that to a 3D Vector where the height is always 0
                        Vector vec = new Vector(x, 0, z);
                        // Add that vector to the player's current location
                        playerLoc.add(vec);
                        // Display the particle
                        world.spawnParticle(Particle.FLAME, playerLoc, 0); // Reminder to self - the "data" option for a (particle, location, data) is speed, not count!!
                        // Since add() modifies the original variable, we have to subtract() it so the next calculation starts from the same location as this one.
                        playerLoc.subtract(vec);
                    }
                    /* We multiplied this by 1 already (using normalize()), ensuring the beam will
                       travel one block away from the player each loop.
                     */
                    playerLoc.add(dir);
                }
            }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
        }
     
    Here's what that looks like in-game:
    https://ibb.co/YNh9h5J
    upload_2020-2-20_16-35-37.png
    It worked! Well, no, not quite. That was looking straight up, look what happens if we look in a different direction:

    upload_2020-2-20_16-36-20.png

    It starts to flatten out the more you look towards the horizon, then opens up again if you look more up or down. WTF is going on here!?

    So this is one challenge with working in 3D space. We calculated the points of the circle in the x & z coordinate planes, but the player can be looking in any direction from any position in 3D space. We need some advanced maths to make this circle render correctly regardless of the player's position and direction they are looking.

    Here is the code used to do exactly that. You can find this all over the internet, so it's not exactly a secret. Understanding exactly how it works is a different story.

    Code (Text):

        public static final Vector rotateAroundAxisX(Vector v, double angle) {
            double cos = Math.cos(angle);
            double sin = Math.sin(angle);
            double y = v.getY() * cos - v.getZ() * sin;
            double z = v.getY() * sin + v.getZ() * cos;
            return v.setY(y).setZ(z);
        }

        public static final Vector rotateAroundAxisY(Vector v, double angle) {
            double cos = Math.cos(angle);
            double sin = Math.sin(angle);
            double x = v.getX() * cos + v.getZ() * sin;
            double z = v.getX() * -sin + v.getZ() * cos;
            return v.setX(x).setZ(z);
        }
     

    So that is the magic to making the circle rotated correctly, based on the directional Pitch (for x) and Yaw (for z). One caveat - these functions require the angle in radians, not degrees. We can use Math.toRadians for this, or just do what I did below. First the rotate calls:

    Code (Text):

                        // Now rotate the circle point so it's properly aligned no matter where the player is looking:
                        VectorUtils.rotateAroundAxisX(vec, pitch);
                        VectorUtils.rotateAroundAxisY(vec, yaw);
     
    Notice I put these functions in a new class called VectorUtils. I highly recommend doing the same, because you'll be using these and other functions frequently if you are playing with particle effects.

    The other addition there is the pitch and yaw angles. These need to be provided to the function in radians instead of degrees, along with a couple of other tweaks. We'll calculate them outside of the run() loop, since we always want the circles to display in the direction the player was originally facing when firing the beam:

    Code (Text):

                // We need the pitch in radians for the rotate axis function
                // We also add 90 degrees to compensate for the non-standard use of pitch degrees in Minecraft.
                final double pitch = (playerLoc.getPitch() + 90.0F) * 0.017453292F;

                // The yaw is also converted to radians here, but we need to negate it for the rotate function to work properly
                final double yaw = -playerLoc.getYaw() * 0.017453292F;
     
    To get radians from degrees, just multiply degrees by 0.017453292. Simple, huh? No need for a Math.toRadians() call here.

    With these changes, it looks perfect now in any direction! Math FTW!
    https://ibb.co/s5rr4HQ
    upload_2020-2-20_20-8-49.png

    Here's the working code for this effect so far:
    Code (Text):

    public static void particleTutorial(Player player){
        new BukkitRunnable() {
            // Number of points in each circle
            int circlePoints = 10;
            // radius of the circle
            double radius = 2;
            // Starting location for the first circle will be the player's eye location
            Location playerLoc = player.getEyeLocation();
            // We need world for the spawnParticle function
            World world = playerLoc.getWorld();
            // This is the direction the player is looking, normalized to a length (speed) of 1.
            final Vector dir = player.getLocation().getDirection().normalize();
            // We need the pitch in radians for the rotate axis function
            // We also add 90 degrees to compensate for the non-standard use of pitch degrees in Minecraft.
            final double pitch = (playerLoc.getPitch() + 90.0F) * 0.017453292F;
            // The yaw is also converted to radians here, but we need to negate it for the function to work properly
            final double yaw = -playerLoc.getYaw() * 0.017453292F;
            // This is the distance between each point around the circumference of the circle.
            double increment = (2 * Math.PI) / circlePoints;
            // Max length of the beam..for now
            int beamLength = 30;
            @Override
            public void run() {
                beamLength--;
                if(beamLength < 1){
                    this.cancel();
                    return;
                }
                // We need to loop to get all of the points on the circle every loop
                for (int i = 0; i < circlePoints; i++) {
                    double angle = i * increment;
                    double x = radius * Math.cos(angle);
                    double z = radius * Math.sin(angle);
                    // Convert that to a 3D Vector where the height is always 0
                    Vector vec = new Vector(x, 0, z);
                    // Now rotate the circle point so it's properly aligned no matter where the player is looking:
                    VectorUtils.rotateAroundAxisX(vec, pitch);
                    VectorUtils.rotateAroundAxisY(vec, yaw);
                    // Add that vector to the player's current location
                    playerLoc.add(vec);
                    // Display the particle
                    world.spawnParticle(Particle.FLAME, playerLoc, 0); // Reminder to self - the "data" option for a (particle, location, data) is speed, not count!!
                    // Since add() modifies the original variable, we have to subtract() it so the next calculation starts from the same location as this one.
                    playerLoc.subtract(vec);
                }
                /* We multiplied this by 1 already (using normalize()), ensuring the beam will
                   travel one block away from the player each loop.
                 */
                playerLoc.add(dir);
            }
        }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
    }
     

    Next we'll add a rotating effect by slightly changing the angle of the particles in each loop. Stay tuned...
     

    Attached Files:

    #6 ZizzyZizzy, Feb 21, 2020
    Last edited: Feb 22, 2020
    • Agree Agree x 1
    • Informative Informative x 1
    • Friendly Friendly x 1
    • Useful Useful x 1
  7. So we have a working "circular" beam of particles. It's pretty boring by itself with only 10 points, so lets rotate them and give it a 007 opening screen look.

    The steps to do this are surprisingly easy, but there is a small catch. First we need another variable to keep track of the offset adjustment.

    Code (Text):
                double circlePointOffset = 0; // This is used to rotate the circle as the beam progresses
    Now we need to use that in the angle calculation:

    Code (Text):

                        // Angle on the circle + the offset for rotating each loop
                        double angle = i * increment + circlePointOffset;
     
    Next we need to increment the offset each time a circle is displayed to make it rotate a little each loop. How much should we rotate it each loop? Based on the radius of 2, the size of the flame particle we're using and some testing, a rotation of 1/3 of the increment (distance) between particles in the circle provides a nice, obvious visual spiraling effect. (Changing the circle radius may require changing this value to maintain the same smooth rotation look.)

    Code (Text):

                    /* Rotate the circle points each iteration, like rifling in a gun barrel
                       Since the particles in the circle are separated by "increment" distance, we rotate
                         the circle points 1/3 of increment with each loop. That means on the 3rd rotation,
                         the offset calculation matches the full increment, so we reset the offset to 0
                         instead of continuing to increase the offset.

                         For fun, comment out the if condition here and see what happens to the particles
                           without it. Since circlePointOffset is added to the angle in the calculation
                           above, it has an interesting side effect. Try it!
                     */
                    circlePointOffset += increment / 3;
                    // If the rotation matches the increment, reset to 0 to ensure a smooth rotation.
                    if (circlePointOffset >= increment) {
                        circlePointOffset = 0;
                    }
     
    All that added code will cause the first set of particles in the first loop to display exactly where they would be before we added the rotation code. Let me explain.

    • In the first loop, we are not using the offset at all, since it's set to 0 to start. The particles in the circle display exactly where we calculated them before adding the spiral effect.

    • In the second loop, we add increment / 3 to the calculation for the particle locations in the circle, rotating their position by 1/3 of the distance between the displayed particles. (Counter-clockwise. If we subtract the circlePointOffset instead, the circle will rotate clockwise.)

    • In the third loop, we add another increment / 3 to the calculation for the particle locations in the circle, rotating their position again by 1/3 of the distance between the displayed particles.

    • On the forth loop, circlePointOffset now matches increment, so we reset the offset to 0. That doesn't cancel the rotation effect or make it "jump" instead of being smooth, however. The forth loop displays particles in the exact same positions of the circle that we calculated in the first loop, since the offset is now 0 once again.
    Long story short, we are really only offsetting the placement of the particles in space 2 times before reverting the offset back to the non-adjusted location and starting the offset calculation from 0. Since the circles are displayed moving away from your point of view, it looks like a continuously rotating circle. Neat!

    Here's what it looks like in-game:
    https://ibb.co/qFmTShV
    upload_2020-2-20_19-51-57.png

    The last part of this effect is to reduce the radius with each loop so the circle gets smaller and smaller until the radius is 0, making a spiraling cone effect.

    This is really easy to add. First we need to decide how much to shrink the radius with each loop. I opted for a dynamic calculation that looks really good even with different size circles. This has to go before the run() loop, of course:

    Code (Text):

                // This is the amount we will shrink the circle radius with each loop
                double radiusShrinkage = radius / (double) ((beamLength + 2) / 2);
     
    Next we need to reduce the radius in the run() loop. This can go right after the rotation offset calculation code:

    Code (Text):

                    // Shrink each circle radius until it's just a point at the end of a long swirling cone
                    radius -= radiusShrinkage;
                    if (radius < 0) {
                        this.cancel();
                        return;
                    }
     
    That's all there is to it! The final effect looks just like the first link and image in this post.
    https://ibb.co/FX2YrYp
    upload_2020-2-20_20-2-41.png

    Here's the final full code for a working spiral cone particle beam effect:

    Code (Text):

    public static void particleTutorial(Player player){
        new BukkitRunnable() {
            // Number of points in each circle
            int circlePoints = 10;
            // radius of the circle
            double radius = 2;
            // Starting location for the first circle will be the player's eye location
            Location playerLoc = player.getEyeLocation();
            // We need world for the spawnParticle function
            World world = playerLoc.getWorld();
            // This is the direction the player is looking, normalized to a length (speed) of 1.
            final Vector dir = player.getLocation().getDirection().normalize();
            // We need the pitch in radians for the rotate axis function
            // We also add 90 degrees to compensate for the non-standard use of pitch degrees in Minecraft.
            final double pitch = (playerLoc.getPitch() + 90.0F) * 0.017453292F;
            // The yaw is also converted to radians here, but we need to negate it for the function to work properly
            final double yaw = -playerLoc.getYaw() * 0.017453292F;
            // This is the distance between each point around the circumference of the circle.
            double increment = (2 * Math.PI) / circlePoints;
            // This is used to rotate the circle as the beam progresses
            double circlePointOffset = 0;
            // Max length of the beam..for now
            int beamLength = 30;
            // This is the amount we will shrink the circle radius with each loop
            double radiusShrinkage = radius / (double) ((beamLength + 2) / 2);
            @Override
            public void run() {
                beamLength--;
                if(beamLength < 1){
                    this.cancel();
                    return;
                }
                // We need to loop to get all of the points on the circle every loop
                for (int i = 0; i < circlePoints; i++) {
                    // Angle on the circle + the offset for rotating each loop
                    double angle = i * increment + circlePointOffset;
                    double x = radius * Math.cos(angle);
                    double z = radius * Math.sin(angle);
                    // Convert that to a 3D Vector where the height is always 0
                    Vector vec = new Vector(x, 0, z);
                    // Now rotate the circle point so it's properly aligned no matter where the player is looking:
                    VectorUtils.rotateAroundAxisX(vec, pitch);
                    VectorUtils.rotateAroundAxisY(vec, yaw);
                    // Add that vector to the player's current location
                    playerLoc.add(vec);
                    // Display the particle
                    world.spawnParticle(Particle.FLAME, playerLoc, 0); // Reminder to self - the "data" option for a (particle, location, data) is speed, not count!!
                    // Since add() modifies the original variable, we have to subtract() it so the next calculation starts from the same location as this one.
                    playerLoc.subtract(vec);
                }
                /* Rotate the circle points each iteration, like rifling in a gun barrel
                   Since the particles in the circle are separated by "increment" distance, we rotate
                     the circle points 1/3 of increment with each loop. That means on the 3rd rotation,
                     the offset calculation matches the full increment, so we reset the offset to 0
                     instead of continuing to increase the offset.

                     For fun, comment out the if condition here and see what happens to the particles
                       without it. Since circlePointOffset is added to the angle in the calculation
                       above, it has an interesting side effect. Try it!
                 */
                circlePointOffset += increment / 3;
                // If the rotation matches the increment, reset to 0 to ensure a smooth rotation.
                if (circlePointOffset >= increment) {
                    circlePointOffset = 0;
                }
                // Shrink each circle radius until it's just a point at the end of a long swirling cone
                radius -= radiusShrinkage;
                if (radius < 0) {
                    this.cancel();
                    return;
                }

                /* We multiplied this by 1 already (using normalize()), ensuring the beam will
                   travel one block away from the player each loop.
                 */
                playerLoc.add(dir);
            }
        }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
    }
     

    See the post above this one for the rotation functions.
     
    #7 ZizzyZizzy, Feb 21, 2020
    Last edited: Feb 21, 2020
    • Informative Informative x 1
  8. PART 5 - Combining Effects to Make
    Spinning Orbs Circling a Normal Beam

    For an even better looking beam, let's combine a normal beam with a spinning circle cone beam. This time, we'll change the number of points to change the look quite a bit. Check it out:
    https://ibb.co/PMzXJWj
    https://ibb.co/hBFBr1m

    upload_2020-2-20_20-34-55.png

    With 3 orbs:
    upload_2020-2-20_20-35-19.png

    You can keep increasing the orbs until about 5 or 6 with a radius of 2.5 before it just becomes a spiraling circle of particles again, detracting from the effect.

    So how can we do this? The first, straight particle beam we already know how to do from Part 1 of this tutorial. We kind of know how to make the spiraling particles from part 4. The concept is nearly identical, except for a couple of small changes.

    The first change is how to dynamically display a certain number of particles evenly spaced around the circumference of a circle? A little googling around will lead you to this formula:

    Code (Text):

                        double x =  radius * Math.cos(2 * Math.PI * i / circlePoints);
                        double z =  radius * Math.sin(2 * Math.PI * i / circlePoints);
     
    This works perfectly, as you saw in the screen shots above. We can feed this formula circle points between 1 and 6 to make nice looking rotating orbs for this beam effect. Yes, you read that correctly. You can set circlePoints to 1 and have exactly one orb circling the center particle beam. Neat!

    The other change to the spiraling cone effect is to display a normal, straight particle beam with a firework particle. Since this beam will always head off in the exact same direction the player was looking when firing the beam (variable dir here), this addition is also very easy. We just need to put it inside the run() loop, but outside the loop for displaying the particles around the circle:

    Code (Text):

                    // Always spawn a center particle
                    startLoc.add(dir);
                    world.spawnParticle(Particle.FIREWORKS_SPARK, startLoc, 0);
                    startLoc.subtract(dir);
     
    The final, working for this effect is here:

    Code (Text):

    public static void particleTutorial(Player player){
        new BukkitRunnable() {
            // Number of points to display, evenly spaced around the circle's radius
            int circlePoints = 3;
            // How fast should the particles rotate around the center beam
            int rotationSpeed = 20;
            double radius = 2.5;
            Location startLoc = player.getEyeLocation();
            World world = startLoc.getWorld();
            final Vector dir = player.getLocation().getDirection().normalize().multiply(1);
            final double pitch = (startLoc.getPitch() +90.0F) * 0.017453292F;
            final double yaw = -startLoc.getYaw() * 0.017453292F;
            // Particle offset increment for each loop
            double increment = (2 * Math.PI) / rotationSpeed;
            double circlePointOffset = 0; // This is used to rotate the circle as the beam progresses
            int beamLength = 60;
            double radiusShrinkage = radius / (double) ((beamLength + 2) / 2);
            @Override
            public void run() {
                beamLength--;
                if(beamLength < 1){
                    this.cancel();
                }
                for (int i = 0; i < circlePoints; i++) {
                    double x =  radius * Math.cos(2 * Math.PI * i / circlePoints + circlePointOffset);
                    double z =  radius * Math.sin(2 * Math.PI * i / circlePoints + circlePointOffset);

                    Vector vec = new Vector(x, 0, z);
                    VectorUtils.rotateAroundAxisX(vec, pitch);
                    VectorUtils.rotateAroundAxisY(vec, yaw);

                    startLoc.add(vec);
                    world.spawnParticle(Particle.FLAME, startLoc, 0);
                    startLoc.subtract(vec);
                }
                // Always spawn a center particle in the same direction the player was facing.
                startLoc.add(dir);
                world.spawnParticle(Particle.FIREWORKS_SPARK, startLoc, 0);
                startLoc.subtract(dir);

                // Shrink each circle radius until it's just a point at the end of a long swirling cone
                radius -= radiusShrinkage;
                if (radius < 0) {
                    this.cancel();
                }
               
                // Rotate the circle points each iteration, like rifling in a barrel
                circlePointOffset += increment;
                if (circlePointOffset >= (2 * Math.PI)) {
                    circlePointOffset = 0;
                }
                startLoc.add(dir);
            }
        }.runTaskTimer(TestingPlugin.getPlugin(), 0, 3);
    }
     

    You might have noticed that the circlePointOffset increment reset if condition changed slightly. This is because the calculation for the location of the points was changed. I'll leave that as an exercise for the reader, but it's not too difficult to understand why that works if you do the math for each loop with a given number of circlePoints.
     
  9. PART 6 - Oscillating Circle Particle Beam

    This effect is pretty neat. I saw this in a video and decided I to try it out myself.

    Check it out in action:
    https://ibb.co/KxyS31f

    upload_2020-2-21_15-48-7.png

    This is just a small change from the other circular particle beams in this tutorial. The main change is making the radius oscillate in size. The way I see it, there are two ways to accomplish this effect:
    1. Shrink the radius each run() loop until it reaches 0, then grow it until it reaches the max. Rinse & repeat
    2. Use a sine wave equation to do the same thing elegantly.
    I opted for #2 because it was slightly more difficult and I love a challenge.

    Here are the two changes to the previous fixed-radius circle beam we eventually rotated:

    New variables:
    Code (Text):

                // This will be the maximum number of circles in one oscillation before repeating
                int maxCircles = 12;[/CENTER]
                // This is used to calculate the radius for each loop
                double t = 0;
     
    New calculations:
    Code (Text):

                    // This calculates the radius for the current circle/ring in the pattern
                    double radius = Math.sin(t) * maxRadius;

                    t += Math.PI / maxCircles; // Oscillation effect
                    if (t > Math.PI * 2) {
                        t = 0;
                    }
     
    Everything else you have already seen before from previous tutorials and hopefully by now understand how it all works. You should be able to take this oscillating beam and apply other characteristics to it, like homing, or reducing the number of points in the circle and make the circles all rotate as they shrink and grow.

    Here's the full working code for this particle beam effect:
    Code (Text):

    public static void particleTutorial(Player player){
        new BukkitRunnable() {
            // Number of points on each circle to show a particle
            int circlePoints = 6;
            // Maximum radius before shrinking again.
            double maxRadius = 1.5;
            Location playerLoc = player.getEyeLocation();
            World world = playerLoc.getWorld();
            // Get the player's looking direction and multiply it by 0.5
            // 0.5 is the number of blocks each new ring will be away from the previous ring
            final Vector dir = player.getLocation().getDirection().normalize().multiply(0.5);
            final double pitch = (playerLoc.getPitch() + 90.0F) * 0.017453292F; // Need these in radians, not degrees or the circle flattens out sometimes
            final double yaw = -playerLoc.getYaw() * 0.017453292F; // Need these in radians, not degrees or the circle flattens out sometimes
            double increment = (2 * Math.PI) / circlePoints;
            // This will be the maximum number of circles in one oscillation before repeating
            int maxCircles = 12;
            // This is used to calculate the radius for each loop
            double t = 0;
            double circlePointOffset = 0; // This is used to rotate the circle as the beam progresses
            // Max beam length
            int beamLength = 40;
            @Override
            public void run() {
                beamLength--;
                if(beamLength < 1){
                    this.cancel();
                    return;
                }
                // This calculates the radius for the current circle/ring in the pattern
                double radius = Math.sin(t) * maxRadius;
                for (int i = 0; i < circlePoints; i++) {
                    double angle = i * increment + circlePointOffset; // Angle on the circle
                    double x = radius * Math.cos(angle);
                    double z = radius * Math.sin(angle);
                    Vector vec = new Vector(x, 0, z);
                    VectorUtils.rotateAroundAxisX(vec, pitch);
                    VectorUtils.rotateAroundAxisY(vec, yaw);
                    playerLoc.add(vec);
                    world.spawnParticle(Particle.FIREWORKS_SPARK, playerLoc, 0); // Reminder to self - the "data" option for a (particle, location, data) is speed, not count!!
                    playerLoc.subtract(vec);
                }
                circlePointOffset += increment / 3; // Rotate the circle points each iteration, like rifling in a barrel
                if (circlePointOffset >= increment) {
                    circlePointOffset = 0;
                }
                t += Math.PI / maxCircles; // Oscillation effect
                if (t > Math.PI * 2) {
                    t = 0;
                }
                playerLoc.add(dir);
            }
        }.runTaskTimer(TestingPlugin.getPlugin(), 0, 1);
    }
     

    I've made a few changes to some variables and added the spinning effect so that it now looks like this:
    https://ibb.co/mzFkFQR
    upload_2020-2-21_16-20-22.png
     
    #9 ZizzyZizzy, Feb 21, 2020
    Last edited: Feb 22, 2020
    • Informative Informative x 1
  10. PART 6 - Instant Particle Beam

    Hopefully this should be obvious if you've been following along in this long thread, but if it's not, here you go.

    To make the particle beam instant instead of how it appears in the animated gifs (20 particles or beam parts per second), you can just alter the code so the entire beam is drawn in one tick. That's as easy as adding an outer for loop as the first code in the run() loop. Then you can remove the beamLength check that would cancel the task.

    For the combination beam with the shrinking circle radius, we still want a cancel() event for when the radius shrinks to 0.

    Regardless, make sure you put this in a BukkitRunnable() as a runTaskLater() with a 0 delay, which will prevent holding up the main thread or your TPS will suffer greatly!

    Code (Text):

    public static void particleTutorial(Player player){
        new BukkitRunnable() {
            // Number of points to display, evenly spaced around the circle's radius
            int circlePoints = 3;
            // How fast should the particles rotate around the center beam
            int rotationSpeed = 20;
            double radius = 2.5;
            Location startLoc = player.getEyeLocation();
            World world = startLoc.getWorld();
            final Vector dir = player.getLocation().getDirection().normalize().multiply(1);
            final double pitch = (startLoc.getPitch() +90.0F) * 0.017453292F;
            final double yaw = -startLoc.getYaw() * 0.017453292F;
            // Particle offset increment for each loop
            double increment = (2 * Math.PI) / rotationSpeed;
            double circlePointOffset = 0; // This is used to rotate the circle as the beam progresses
            int beamLength = 60;
            double radiusShrinkage = radius / (double) ((beamLength + 2) / 2);
            @Override
            public void run() {
                // We are going to draw the entire beam instantly instead of waiting for each tick
                for (int count = beamLength; count > 1; count--) {
                    for (int i = 0; i < circlePoints; i++) {
                        double x = radius * Math.cos(2 * Math.PI * i / circlePoints + circlePointOffset);
                        double z = radius * Math.sin(2 * Math.PI * i / circlePoints + circlePointOffset);

                        Vector vec = new Vector(x, 0, z);
                        VectorUtils.rotateAroundAxisX(vec, pitch);
                        VectorUtils.rotateAroundAxisY(vec, yaw);

                        startLoc.add(vec);
                        world.spawnParticle(Particle.FLAME, startLoc, 0);
                        startLoc.subtract(vec);
                    }
                    // Always spawn a center particle in the same direction the player was facing.
                    startLoc.add(dir);
                    world.spawnParticle(Particle.FIREWORKS_SPARK, startLoc, 0);
                    startLoc.subtract(dir);

                    // Shrink each circle radius until it's just a point at the end of a long swirling cone
                    radius -= radiusShrinkage;
                    if (radius < 0) {
                        this.cancel();
                    }

                    // Rotate the circle points each iteration, like rifling in a barrel
                    circlePointOffset += increment;
                    if (circlePointOffset >= (2 * Math.PI)) {
                        circlePointOffset = 0;
                    }
                    startLoc.add(dir);
                }
            }
        }.runTaskLater(TestingPlugin.getPlugin(), 0);
    }
     
     
    • Winner Winner x 1
    • Informative Informative x 1
  11. This is really useful. Thank you so much :D +1
     
  12. Awesome, glad it helped. I hoped you also learned something in the process.

    There's a LOT of information in these forums. Some of it is inaccurate (untested code that doesn't actually work) and some of it is quite outdated. A lot of responses are "check the javadocs", which is sooo not helpful in many cases because the description of the function or variables used are terse and completely vague.

    If you come up with interesting or helpful code, please take a few minutes and write your own resource post to share with the community! Minecraft customization is the reason it's still so popular to this day. My hope is that these type of posts (spoon feeding or not) encourage newbies to java and Minecraft plugin writing to join in the fun.
     
    • Like Like x 2
  13. Thanks so much for posting this! Been having issues with the math side of my plugins and this has massively helped me!
     
  14. Very nice work!