16

Alright, so I am using Bukkit API (Minecraft), which should not be too much of a problem in this as it is used minimally. So a Location contains world, x, y, z, yaw and pitch. This may come in handy, but I doubt it.

My problem is that I go to shoot using the Shot class (below), and there appears to be a +-5 difference in either one when approximately 3 blocks away, and the HitBox is instantiated about 5 blocks away (this could be the issue (move/rotate methods)). I have tried working this out on paper, and have used half a notepad doing so, but I am not yet able to figure out a solution. What I need is someone who understands trigonometry and java well, so they can help out.

Other information that may be of use:

  • +z is 0 degrees yaw, -x is 90 degrees, -z is 180, and +x is 270.
  • The variables seem to be incorrect sporadically, and under certain circumstances they work correctly and constitute a hit.
  • The Location (from) parameter in the Shot constructor is of the player's location in the world, therefore from is not (0, 0, 0).
  • ShotData should not affect any values, as wind speed and wind direction in my case are 0 (if there is a problem with the math involving this though, feel free to let me know, haha)
  • Pitch appears to be fine, although +y is -90, and -y is 90 (weird right?)

So my question is... Where is the problem, and how do I fix it? I am sorry that it is such a general question and that it is a very common one, but this is one of those times when it is truly necessary. I tried to remove all unnecessary code, but you can remove more if needed. Also, if you want to see anything else that may be referenced in here, I can get that for you.

Shot.java:

private final Location from;
private ShotData data;

public Shot(Location from, ShotData data) {
    this.from = from;
    this.data = data;
}

// TODO - Checking for obstacles
public List<Hit> shoot(List<HitBox> hitBoxes) {
    List<Hit> hits = new ArrayList<Hit>();
    for (HitBox hitBox : hitBoxes) {
        hitBox.update();
        float fromYaw = from.getYaw() % 360;
        float fromPitch = from.getPitch() % 360;
        // making sure the center location is within range
        if (hitBox.getCenter().distanceSquared(from) > Math.pow(data.getDistanceToTravel(), 2)) {
            continue;
        }
        /* TODO Only allow hits on parts of the rectangle that are within range,
         * not just the whole thing if the center is within range. */
        // accounting for wind speed/direction
        float windCompassDirection = data.getWindCompassDirection(from.getWorld());
        float windSpeed = data.getWindSpeedMPH(from.getWorld());
        fromYaw += (windCompassDirection > fromYaw ? 1 : windCompassDirection < fromYaw ? -1 : 0) * windSpeed;
        fromYaw %= 360;
        int[] orderClockwise = new int[] {0, 1, 4, 3};
        Location thisSideCorner = hitBox.getCorner(0);
        Location oppositeSideCorner = hitBox.getCorner(0);
        for (int i = 0; i < orderClockwise.length; i++) {
            int num = orderClockwise[i];
            Location corner = hitBox.getCorner(num);
            Location clockwise = hitBox.getCorner(orderClockwise[(i + 1) % 3]);
            if ((Math.atan2(from.getZ() - corner.getZ(), from.getX() - corner.getX()) * 180 / Math.PI) > 0 && corner.distanceSquared(from) < clockwise.distanceSquared(from)) {
                thisSideCorner = corner;
                int exitCornerClockwiseAmount = (Math.atan2(from.getZ() - clockwise.getZ(), from.getX() - clockwise.getX()) * 180 / Math.PI) < 0 ? 2 : 3;
                oppositeSideCorner = hitBox.getCorner((i + exitCornerClockwiseAmount) % 3);
            }
        }
        Location entrance = getProjectileLocation(thisSideCorner, data, hitBox, fromYaw, fromPitch);
        double distance = entrance.distance(from);
        double deltaX = data.getDeltaX(distance, fromYaw);
        double deltaY = data.getDeltaY(distance, fromPitch);
        double deltaZ = data.getDeltaZ(distance, fromYaw);
        entrance.add(deltaX, deltaY, deltaZ);
        Location exit = getProjectileLocation(oppositeSideCorner, data, hitBox, deltaX, deltaY, deltaZ, fromYaw, fromPitch);
        // hit detection and reaction
        boolean hitX = entrance.getX() <= hitBox.getHighestX() && entrance.getX() >= hitBox.getLowestX();
        boolean hitY = entrance.getY() <= hitBox.getHighestY() && entrance.getY() >= hitBox.getLowestY();
        boolean hitZ = entrance.getZ() <= hitBox.getHighestZ() && entrance.getZ() >= hitBox.getLowestZ();
        if (hitX && hitY && hitZ) {
            hits.add(new Hit(from, entrance, exit, hitBox, data));
        }
    }
    return hits;
}

private Location getProjectileLocation(Location thisSideCorner, ShotData data, HitBox hitBox, float fromYaw, float fromPitch) {
    return getProjectileLocation(thisSideCorner, data, hitBox, 0, 0, 0, fromYaw, fromPitch);
}

private Location getProjectileLocation(Location thisSideCorner, ShotData data, HitBox hitBox, double addX, double addY, double addZ, float fromYaw, float fromPitch) {
    double deltaFromToSideCornerX = thisSideCorner.getX() - from.getX();
    double deltaFromToSideCornerY = thisSideCorner.getY() - from.getY();
    double deltaFromToSideCornerZ = thisSideCorner.getZ() - from.getZ();
    double xzDistFromSideCorner = Math.sqrt(Math.pow(deltaFromToSideCornerX, 2) + Math.pow(deltaFromToSideCornerZ, 2));
    double yawToSideCorner = Math.atan2(deltaFromToSideCornerX, deltaFromToSideCornerZ) * 180 / Math.PI;// flipped x and z from normal
    double theta1 = yawToSideCorner - fromYaw;
    double theta2 = yawToSideCorner - theta1;
    double outerAngle = 180 - yawToSideCorner - 90;// previously theta1
    double outerAngleInShotCone = outerAngle + 90 + hitBox.getYawRotation();
    double lastAngleInShotCone = 180 - theta1 - outerAngleInShotCone;
    double xzDistanceFromHit = (xzDistFromSideCorner * Math.sin(Math.toRadians(outerAngleInShotCone))) / Math.sin(Math.toRadians(lastAngleInShotCone));
    double deltaX = xzDistanceFromHit * Math.sin(Math.toRadians(theta2));// leaves out sin 90 because its just equal to 1...
    double deltaZ = xzDistanceFromHit * Math.sin(Math.toRadians(90 - theta2));// leaves out sin 90 because its just equal to 1...
    double xyzDistFromSideCorner = Math.sqrt(Math.pow(xzDistFromSideCorner, 2) + Math.pow(deltaFromToSideCornerY, 2));
    double theta3 = Math.atan2(Math.abs(deltaFromToSideCornerY), xzDistFromSideCorner) * 180 / Math.PI;
    double theta4 = Math.abs(fromPitch) - theta3;
    double theta5 = 90 + theta3;
    double theta6 = 180 - theta4 - theta5;
    double hitDistance = (xyzDistFromSideCorner * Math.sin(Math.toRadians(theta5))) / Math.sin(Math.toRadians(theta6));
    double deltaY = hitDistance * Math.sin(Math.toRadians(Math.abs(fromPitch)));// leaves out sin 90 because its just equal to 1...
    if (deltaFromToSideCornerX < 0 && deltaX > 0) {
        deltaX *= -1;
    }
    if (fromPitch > 0 && deltaY > 0) {// pitch in minecraft is backwards, normally it would be fromPitch < 0
        deltaY *= -1;
    }
    if (deltaFromToSideCornerZ < 0 && deltaZ > 0) {
        deltaZ *= -1;
    }
    Location hit = from.clone().add(deltaX + addX, deltaY + addY, deltaZ + addZ);
    hit.setYaw(fromYaw);
    hit.setPitch(fromPitch);
    return hit;
}

HitBox.java:

private float yawRotation;
private double x, y, z;
private double[][] additions;
private Location center;
private Location[] corners = new Location[8];
private List<DataZone> dataZones = new ArrayList<DataZone>();
private UUID uuid = UUID.randomUUID();

//@formatter:off
/*
 * O = origin
 * X = x-axis
 * Y = y-axis
 * Z = z-axis
 * C = center
 * 
 *    ---------------------
 *   /                   /|
 *  /                   / |
 * Y--------------------  |
 * |                90 |  |     0 yaw
 * |   ^               |  |    /
 * |   |               |  |
 * |   |               |  |  /
 * | HEIGHT    C       |  |
 * |   |               |  |/
 * |   |               |  Z
 * |   v               | /
 * |   <---WIDTH--->   |/<---LENGTH
 * O-------------------X - - - - - - - - - -270 yaw
 */

/**
 * An invisible box in the world that can be hit with a shot.
 * Additionally, {@link DataZone} instances can be added to this, 
 * allowing for different damage and thickness on an area of the box.
 * 
 * @param center The center of the hit box
 * @param length The length (z axis) of the hit box
 * @param width The width (x axis) of the hit box
 * @param height The height (y axis) of the hit box
 * @param yawRotation The rotation around the center of the origin (or any other point)
 */
public HitBox(Location center, double length, double width, double height, float yawRotation) {  
    corners[0] = center.clone().add(-1 * width / 2, -1 * height / 2, -1 * length / 2);
    this.center = center;
    this.x = width;
    this.y = height;
    this.z = length;
    rotate(yawRotation);
}
//@formatter:on
public Location[] getCorners() {
    return corners;
}

public Location getCorner(int corner) {
    return corners[corner];
}

public Location getOrigin() {
    return corners[0];
}

public void update() {};

public boolean isZoneOpen(DataZone zone) {
    for (DataZone placed : dataZones) {
        boolean Xs = overlap_1D(placed.xFrom, placed.xTo, zone.xFrom, zone.xTo);
        boolean Ys = overlap_1D(placed.yFrom, placed.yTo, zone.yFrom, zone.yTo);
        boolean Zs = overlap_1D(placed.zFrom, placed.zTo, zone.zFrom, zone.zTo);
        if (Xs && Ys && Zs) {
            return true;
        }
    }
    return false;
}

public void rotate(float degrees) {
    Location origin = corners[0];
    this.yawRotation = (yawRotation + degrees) % 360;
    additions = new double[][] { {0, 0, 0}, {x, 0, 0}, {0, y, 0}, {0, 0, z}, {x, 0, z}, {x, y, 0}, {x, y, z}, {0, y, z}};
    for (int i = 0; i < 8; i++) {
        double[] addition = additions[i];
        double xPrime = center.getX() + (center.getX() - (origin.getX() + addition[0])) * Math.cos(Math.toRadians(yawRotation)) - (center.getZ() - (origin.getZ() + addition[2])) * Math.sin(Math.toRadians(yawRotation));
        double zPrime = center.getZ() + (center.getX() - (origin.getX() + addition[0])) * Math.sin(Math.toRadians(yawRotation)) + (center.getZ() - (origin.getZ() + addition[2])) * Math.cos(Math.toRadians(yawRotation));
        corners[i] = new Location(center.getWorld(), xPrime, origin.getY() + addition[1], zPrime, yawRotation, 0);
    }
}

public void move(Location center) {
    double deltaX = center.getX() - this.center.getX();
    double deltaY = center.getY() - this.center.getY();
    double deltaZ = center.getZ() - this.center.getZ();
    for (int i = 0; i < 8; i++) {
        corners[i].add(deltaX, deltaY, deltaZ);
    }
    this.center = center;
}

protected void setY(double y) {
    int[] toChange = new int[] {2, 5, 6, 7};
    for (int i : toChange) {
        corners[i].setY(corners[0].getY() + y);
    }
    this.y = y;
}

public double getHighestX() {
    double highestX = Double.MIN_VALUE;
    for (Location location : corners) {
        if (location.getX() > highestX) {
            highestX = location.getX();
        }
    }
    return highestX;
}

public double getHighestY() {
    return corners[0].getY() + y;
}

public double getHighestZ() {
    double highestZ = Double.MIN_VALUE;
    for (Location location : corners) {
        if (location.getZ() > highestZ) {
            highestZ = location.getZ();
        }
    }
    return highestZ;
}

public double getLowestX() {
    double lowestX = Double.MAX_VALUE;
    for (Location location : corners) {
        if (location.getX() < lowestX) {
            lowestX = location.getX();
        }
    }
    return lowestX;
}

public double getLowestY() {
    return corners[0].getY();
}

public double getLowestZ() {
    double lowestZ = Double.MAX_VALUE;
    for (Location location : corners) {
        if (location.getZ() < lowestZ) {
            lowestZ = location.getZ();
        }
    }
    return lowestZ;
}

public float getYawRotation() {
    return yawRotation;
}
JNorr44
  • 276
  • 1
  • 2
  • 11
  • 1
    Have you traced through the code in a debugger and examined the variable values? You could probably find the problem in a few minutes. – Jim Garrison Oct 20 '13 at 03:38
  • I have extensively printed out all values in the Shot class, only to find that some of them sometimes make no sense. The problems with this include the inaccessibility of the Eclipse debugger (due to MC), and the fact that the incorrect variables seem to be sporadic. Sometimes the Shot works, however, many times it does not. – JNorr44 Oct 20 '13 at 03:42
  • 2
    Probably one of the nicest questions I've seen, good job. You could try to observe any patterns in the environment when your shot works and then try to find out it doesn't work in others. – Josh M Oct 20 '13 at 04:07
  • As far as I can tell it has no relation to environment. My thought is that it is related to rotation or movement of the HitBox, both of which are called on update() (in my subclass of HitBox). It could also simply be a mathematical error in the getProjectileLocation() method. – JNorr44 Oct 20 '13 at 04:12
  • Somehow I think all this looks too complicated for the job you are trying to do. Isn't there a much simpler way? – Daniel S. Oct 24 '13 at 09:34
  • Is there a brief way how the concept behind your code can be described? I didn't get it yet from the code. – Daniel S. Oct 24 '13 at 09:46
  • Location has the method getDirection(), which I would expect to play a central role in your code, but I can't find it there. This might be helpful. – Daniel S. Oct 24 '13 at 09:48
  • also, Math.abs(fromPitch) looks suspicious. Taking the ABS of an angle +can+ change the angle (in the sense that it does not point into the right direction anymore), but the angle can also stay unharmed. This might be a problem. – Daniel S. Oct 24 '13 at 09:51
  • general comment about the code: E.g. the big block of code at the beginning of getProjectileLocation() can surely be broken up into smaller logical parts. If you put these parts into submethods, you automatically "label" code blocks and thus it gets way clearer what is being done, and makes thinking about it easier. – Daniel S. Oct 24 '13 at 09:52
  • As far as I can tell there isn't a simpler way, but if there is let me know. Also, I will try an post a picture that describes the code well. getDirection() will not be useful, as I need the location without running a massive loop searching for where it hits. Math.abs(fromPitch) works for MC, simply because the pitch is backwards in MC. The problem is with yaw. My plan was to split up the getProjectileLocation() method once I get it working properly. Again, I will try and post that explanation picture later. – JNorr44 Oct 24 '13 at 20:15

2 Answers2

2

Perhaps consider drawing a line following the same vector that your bullet travels along, this will provide a visual indicator for what is happening, pass in the same calculations and etc.

As other have mentioned also include lots of debug printouts. Hopefully once you have a visual cue you can see when/where the problem calculations are occuring.

Also you should aim to use a standard data type for calculations, a float or a double, NOT both as this can cause some weird rounding and calculation problems.

Matthew Pigram
  • 1,400
  • 3
  • 25
  • 65
  • I can't use for/while loops, as those are too slow for my purposes, but I keep attempting printouts. The float and double mix is not really up to me, I am working off of MC, which uses a float for yaw. – JNorr44 Oct 25 '13 at 00:25
  • so change all of your calculations to use floats, that will provide the best precision anyways. Unit testing your functions might even be a good idea at this point so you can break it down into pieces and check that data is coming out as expected, additionally comment out some more complex calculations (wind and etc), focus on the basics first. – Matthew Pigram Oct 25 '13 at 00:36
  • 2
    most accurate type is the float xD – Ashley Mar 26 '14 at 12:44
1

I know this is extremely late (almost 5 years in fact), but I developed a solution a few weeks after this question. After revisiting StackOverflow I decided to provide my solution for any who may find it useful.

The issue with the massive wall of code found in the question is that many values are being computed and every computation loses precision, resulting in some degree of variation (like I said, +/-5 blocks).

The solution source may be found here: https://github.com/JamesNorris/MCShot

To compute intersect (found in util/Plane3D.java):

public Vector getIntersect(LineSegment3D segment) {
    Vector u = segment.getEnd().subtract(segment.getStart());
    Vector w = segment.getStart().subtract(point);
    double D = normal.dot(u);
    double N = -normal.dot(w);
    if (Math.abs(D) < .00000001) {
        /* if N == 0, segment lies in the plane */
        return null;
    }
    double sI = N / D;
    if (sI < 0 || sI > 1) {
        return null;
    }
    return segment.getStart().add(u.multiply(sI));
}

As you can see, this requires way fewer computations and as a result provides much greater precision. This function gets the intersection of a 3D plane. A 3D plane is constructed using the values:

point - some point found within the plane
normal - the normal vector of the plane

I hope someone finds this solution helpful, or at least can use it as a lesson in computational precision!

JNorr44
  • 276
  • 1
  • 2
  • 11