Custom Projectiles with Gravity and getFaceHit

Discussion in 'Spigot Plugin Development' started by CrypticCabub, Jun 6, 2018.

  1. Hi There,

    I am currently working on a little personal project of mine that wants to have custom projectiles ranging from "falling" blocks to particle projectiles and I'm trying to figure out how exactly to go about doing this with the Spigot API.

    Initially I was intending to hurl an actual entity as the projectile and then track its flight every tick to see if it crosses within a certain hitbox radius of a target, however I am now tackling 2 major aspects and was wondering if any of you devs out there with more experience using the projectile APIs had any wisdom for me before I do something stupid and end up down a rabbit trail of spaghetti code. The 2 primary considerations right now are:

    Customizing the gravity properties of the projectile -- I want to be able to give a projectile a "gravity" vector that is applied to it every tick/second to adjust dropoff speeds (projectile should also be able to fly quickly or very slowly). My concern would be that actual entities may be affected by their own gravity already as well as interacted with in ways I do not intend. (I could use particle trails but would like to be able to use entities as well)

    Detecting which face of a block the projectile impacted (hardest one right now) -- I was hoping to have projectiles that can impact blocks and provide a BlockFace for which projectile they impact (useful for "burrowing" explosions and the like). Is there any way to actually do this?

    Example of what I'm currently working with:

    Code (Java):
    /**
    * represents a single custom projectile within the game world
    *
    *<p>
    *     when extending this class, it is recommend to set the projectile variable within a private constructor and then
    *     provide a static launchNewProjectile() factory method. For an example see {@link PunchedRock}
    *</p>
    */

    //todo: what about impacting walls? does any tracking need to be done for that
    public abstract class CustomProjectile {

        private Entity projectileEntity;

        private Player source; //the shooter of this projectile

        protected boolean canHitNonPlayers;

        //the radius from the projectile within which if another entity crosses it will be considered hit
        protected double hitBoxX = .5;
        protected double hitBoxY = 1;
        protected double hitBoxZ = .5;

        /**
         * set the physical entity that represents this projectile in the game world.
         * <p>
         *     This value should really only be set at instantiation so as to maintain a contract that 1 CustomProjectile
         *     correlates to 1 and only 1 projectile within the game world
         * </p>
         * @param projectileEntity the physical entity that represents this projectile
         * @return a reference to this object for chaining
         */

        public CustomProjectile setProjectileEntity(Entity projectileEntity) {
            this.projectileEntity = projectileEntity;
            return this;
        }

        /**
         * set whether or not this projectile can impact nonPlayers
         * @param canHitNonPlayers TRUE - the projectile may impact any entity, not just players
         *                         FALSE - the projectile may only impact players
         * @return a reference to this object for chaining
         */

        public CustomProjectile setCanHitNonPlayers(boolean canHitNonPlayers) {
            this.canHitNonPlayers = canHitNonPlayers;
            return this;
        }

        /**
         * set the source of this projectile
         * @param source the player who is considered the shooter of this projectile
         * @return a reference to this object for chaining
         */

        public CustomProjectile setSource(Player source) {
            this.source = source;
            return this;
        }


        /**
         * set the projectile's hitbox along the x-axis
         * @param hitBoxX the radius along the x-axis within which an entity must cross to be considered hit by this projectile
         * @return a reference to this object for chaining
         */

        public CustomProjectile setHitBoxX(double hitBoxX) {
            this.hitBoxX = hitBoxX;
            return this;
        }

        /**
         * set the projectile's hitbox along the y-axis
         * @param hitBoxY the radius along the y-axis within which an entity must cross to be considered hit by this projectile
         * @return a reference to this object for chaining
         */

        public CustomProjectile setHitBoxY(double hitBoxY) {
            this.hitBoxY = hitBoxY;
            return this;
        }

        /**
         * set the projectile's hitbox along the z-axis
         * @param hitBoxZ the radius along the z-axis within which an entity must cross to be considered hit by this projectile
         * @return a reference to this object for chaining
         */

        public CustomProjectile setHitBoxZ(double hitBoxZ) {
            this.hitBoxZ = hitBoxZ;
            return this;
        }

        public Entity getProjectileEntity() {
            return projectileEntity;
        }

        public Player getSource() {
            return source;
        }

        public Entity getActiveEntity() {
            return projectileEntity;
        }

        public boolean canHitNonPlayers() {
            return canHitNonPlayers;
        }

        public double getHitBoxX() {
            return hitBoxX;
        }

        public double getHitBoxY() {
            return hitBoxY;
        }

        public double getHitBoxZ() {
            return hitBoxZ;
        }

        /**
         * called when the projectile impacts an entity
         * @param entity the entity hit
         */

        protected abstract void onHit(Entity entity);

        /**
         * called when the projectile impacts a block
         * @param block the block hit
         */

        protected abstract void onHit(Block block);

    Code (Java):
    //todo: handling for when the projectile hits the ground/a wall?
    public class ProjectileManager extends BukkitRunnable {

        private Map<CustomProjectile, ImpactHandler> registeredProjectiles = new HashMap<>();


        public void registerProjectile(CustomProjectile projectile, ImpactHandler impactHandler) {
            registeredProjectiles.put(projectile, impactHandler);
        }

        public void unregisterProjectile(CustomProjectile projectile) {
            registeredProjectiles.remove(projectile);
        }

        public void run() {
            for(CustomProjectile projectile : registeredProjectiles.keySet()) {
                Entity projectileEntity = projectile.getActiveEntity();

                for(Entity hitEntity : projectileEntity.getNearbyEntities(projectile.getHitBoxX(), projectile.getHitBoxY(), projectile.getHitBoxZ())) {
                    if(projectile.canHitNonPlayers()) {
                        if(registeredProjectiles.get(projectile).handleProjectileImpact(projectile, hitEntity)) {
                            //if the handleProjectileImpact method returns true, remove the projectile from the map of tracked projectiles
                            registeredProjectiles.remove(projectile);
                        }
                    } else {
                        if(hitEntity instanceof Player) {
                            if(registeredProjectiles.get(projectile).handleProjectileImpact(projectile, hitEntity)) {
                                registeredProjectiles.remove(projectile);
                            }
                        }
                    }
                }

            }
        }


    }
    edit: forgot to include projectile manager:
     
  2. If you're up for doing all the hard math I'd recommend *not* using an actual Entity at all. This gives you complete control over gravity, which blocks will stop your projectile, etc.

    It seems like you have the basics down, finding the hit location is a bit more of a challenge.

    How I handle it, basically:

    Each tick, calculate how far the projectile will go. I always assume a projectile travels in a straight line each tick, which makes gravity a little fuzzy but it seems to look just fine.

    1. Use a BlockIterator for broad-level block hit checks.

    If the BlockIterator says it hit a block, then figure out the hit location (see below)

    2. Gather all entities around the projectile within the radius that it will travel this tick

    Check each of those entities' hitboxes (I grab the actual AABB from NMS code, but you could also just approximate the BB based on entity type).

    Perform a line/box intersection test to see if the projectile has hit any of these entities. This test also returns the exact hit location (intersection point). It technically returns two intersections, I take the closest one to the projectile's current location.

    3. If both an entity and block were hit, check to see which is closer to the projectile's current location.

    In the case of a block hit (from step #1) I construct an AABB around the block location and then perform the same line/box test as above to get the hit location.

    The hardest parts of all this (for me) was the math, but I was mostly able to cobble it together from Google searching. If it helps, here's the class I use:

    Check to see if a line intersects a bounding box:
    https://github.com/elBukkit/MagicPl...kers/mine/bukkit/utility/BoundingBox.java#L95

    Get the intersection point:
    https://github.com/elBukkit/MagicPl...ers/mine/bukkit/utility/BoundingBox.java#L133

    The two parameters are the start and end locations of the line segment. In this case, the first vector is the projectile's location, the second vector is where the projectile would end up this tick if it doesn't hit anything (so current location + velocity, basically).

    I hope some of this helps, good luck!
     
  3. Thank You!

    The math is certainly the hard bit. I'm still studying for my degree (about to enter Junior year) and have not taken any of my calculus classes just yet. In my experience at least, it seems we don't need all that much math for software until we really NEED it XD.

    My primary concern about not using an entity would be displaying the projectile's current location. it could be just particles but it would be nice to have physical objects as well (such as a falling block).
     
  4. What I did is build everything assuming you *won't* have an entity, but then add the option to have an entity.

    So my base CustomProjectile class can spawn particle effects in various ways as it flies. Then I have an EntityProjectile class that extends CustomProjectile, which also teleports an entity as the projectiles moves. This supports falling blocks and other entities, though falling blocks can be kind of weird (they always want to land...)

    Then, additionally, I have an ArmorStandProjectile class that extends EntityProjectile, and has some custom features for armor stand support. I'd prefer this over FallingBlock projectiles, just put a block on the armor stand's head :)

    But in any case, none of the logic or (importantly) hit detection relies on an entity being there, the entity is considered secondary for display purposes only.
     
  5. Thanks again @NathanWolf

    I believe I better understand what I need to do now. Do you mind if I re-use some of the code you pasted as I put together an implementation of this?
     
  6. I had this same problem just a while ago, and if you aren't too bothered by exactness, this method is incredibly easy to set up and use.
    Code (Java):

            Location currentPoint;
            Vector direction = currentPoint.getDirection();
            Vec3D startPos = new Vec3D(currentPoint.getX(), currentPoint.getY(), currentPoint.getZ());
            Vec3D endPos = startPos.add(direction.getX(), direction.getY(), direction.getZ());
            MovingObjectPosition hitResult = ((CraftWorld) currentPoint.getWorld()).getHandle().rayTrace(startPos, endPos, false, true, false);
            if (hitResult != null) {
                  Location hitPoint = new Location(currentPoint.getWorld(), hitResult.pos.x, hitResult.pos.y, hitResult.pos.z);
            }
    This will effectively use minecraft's built-in hitbox detection for blocks.
    basically, it'll find an intersecting point between your start position and end position, represented by hitResult (returning null of there was no point of contact).
    This will even work for cases such as stairs, where part of the block is transparent, and trap doors. This way, you don't have to manually calculate the hitboxes of complicated blocks. Although if you have to do something for liquids, this won't check that (You have to change one of those booleans at the end of the MovingObjectPosition constructor, but for the life of me I can't remember which one it is). It is incredibly reliable for default hitboxes of blocks, but if you want to do anything fancy (like a custom hitbox, such as a 3D textured block with a resource pack), you'll have to manually override this.
    I specifically used it to make a bullet, though mine does not account for travel time OR gravity, so you'd have to take that into account yourself. If you would like, here's a link to my bullet class: https://github.com/GreatThane/ThaneGuns/blob/master/src/main/java/org/thane/objects/Bullet.java
    I hope this can simplify some of your math!
    Best part is, it returns doubles so you can detect exactly where it hit in 3D space of a block!
    As a side note, detecting the BlockFace should be simple with the direction Vector. Just hardcode in some conversion of the 3 axis.
     
    #6 GreatThane, Jun 11, 2018
    Last edited: Jun 11, 2018
  7. You're welcome to use any of the code I pasted or from my plugin, it's all MIT-licensed :)
     
  8. I see you've already gotten plenty of help, but just in case, you can take a look at my magic wands plugin here which looks very similar to your setup. I use NMS directly though, so copying is likely not much of help, but feel free to copy if you find anything useful.
     
  9. @GreatThane
    Thanks! I'll look into that, however it appears that your code is using a linear ray hitbox for the projectile. Incredibly useful but I'll need to weigh the costs of not being able to give the projectile its own hitbox (was thinking for example a very large arcing globule as one of the projectiles)
     
  10. Ah, I solved this problem myself by just doing small distances at a time with a for loop and adjusting "direction" to match a section of the curve. However, you are right that it does not check for a large hitbox. For this case, I would mix in a bit of @NathanWolf 's detection methods above in as well.