/*
* Aphelion
* Copyright (c) 2014 Joris van der Wel
*
* This file is part of Aphelion
*
* Aphelion is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, version 3 of the License.
*
* Aphelion is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Aphelion. If not, see <http://www.gnu.org/licenses/>.
*
* In addition, the following supplemental terms apply, based on section 7 of
* the GNU Affero General Public License (version 3):
* a) Preservation of all legal notices and author attributions
* b) Prohibition of misrepresentation of the origin of this material, and
* modified versions are required to be marked in reasonable ways as
* different from the original version
*
* Linking this library statically or dynamically with other modules is making a
* combined work based on this library. Thus, the terms and conditions of the
* GNU Affero General Public License cover the whole combination.
*
* As a special exception, the copyright holders of this library give you
* permission to link this library with independent modules to produce an
* executable, regardless of the license terms of these independent modules,
* and to copy and distribute the resulting executable under terms of your
* choice, provided that you also meet, for each linked independent module,
* the terms and conditions of the license of that module. An independent
* module is a module which is not derived from or based on this library.
*/
package aphelion.shared.physics;
import aphelion.shared.gameconfig.GCInteger;
import static aphelion.shared.physics.PhysicsMap.TILE_PIXELS;
import aphelion.shared.physics.entities.MapEntity;
import aphelion.shared.physics.valueobjects.PhysicsPositionVector;
import aphelion.shared.swissarmyknife.EntityGrid;
import aphelion.shared.physics.valueobjects.PhysicsPoint;
import aphelion.shared.physics.valueobjects.PhysicsPointHistoryDetailed;
import aphelion.shared.swissarmyknife.ComparableIntegerDivision;
import aphelion.shared.swissarmyknife.LoopFilter;
import aphelion.shared.swissarmyknife.SwissArmyKnife;
import static aphelion.shared.swissarmyknife.SwissArmyKnife.abs;
import static aphelion.shared.swissarmyknife.SwissArmyKnife.clip;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
/**
* A helper class to perform (cross machine) deterministic collisions.
* @author Joris
*/
public final class Collision
{
private static final Logger log = Logger.getLogger("aphelion.shared.physics");
private PhysicsMap map;
private int radius;
/** The position for the previous tick. */
private final PhysicsPoint prevPos = new PhysicsPoint();
/** The position for the previous tick. */
private final PhysicsPoint newPos = new PhysicsPoint();
private final PhysicsPoint vel = new PhysicsPoint(0, 0);
private PhysicsPointHistoryDetailed posHistoryDetails;
private EntityGrid<MapEntity> collideGrid;
private LoopFilter<MapEntity, Long> collideFilter = LoopFilter.NO_FILTER;
private int bounceFriction;
private int otherAxisFriction;
private int bounces; // how many times did the object bounce of a tile?
private int bounces_left;
private boolean exhaustedBounces;
private final ArrayList<HitData> hitEntities = new ArrayList<>(128);
private final PhysicsPoint hitTile = new PhysicsPoint();
public static final class HitData
{
/** What did we hit?. */
public MapEntity entity;
/** At what location did the hit occur?. */
public final PhysicsPoint location = new PhysicsPoint();
public HitData(MapEntity entity, PhysicsPoint location)
{
this.entity = entity;
this.location.set(location);
}
}
public static enum TILE_SIDE
{
NONE,
TOP,
RIGHT,
BOTTOM,
LEFT;
}
public Collision()
{
reset();
}
public void reset()
{
this.map = null;
this.radius = 0;
this.prevPos.unset();
this.newPos.unset();
this.vel.unset();
this.posHistoryDetails = null;
this.collideGrid = null;
this.collideFilter = LoopFilter.NO_FILTER;
this.bounceFriction = GCInteger.RATIO;
this.otherAxisFriction = GCInteger.RATIO;
this.bounces_left = -1;
}
public void getPreviousPosition(@Nonnull PhysicsPoint pos)
{
pos.set(this.prevPos);
}
public void getNewPosition(@Nonnull PhysicsPoint pos)
{
pos.set(this.newPos);
}
public void getVelocity(@Nonnull PhysicsPoint vel)
{
vel.set(this.vel);
}
public void setMap(@Nullable PhysicsMap map)
{
this.map = map;
}
public void setRadius(int radius)
{
this.radius = radius;
}
public void setPreviousPosition(@Nonnull PhysicsPoint pos)
{
if (!pos.set)
{
throw new IllegalArgumentException("PhysicsPoint should be set");
}
this.prevPos.set(pos);
}
public void setNewPosition(@Nonnull PhysicsPoint pos)
{
if (!pos.set)
{
throw new IllegalArgumentException("PhysicsPoint should be set");
}
this.newPos.set(pos);
}
public void setVelocity(@Nonnull PhysicsPoint vel)
{
if (!vel.set)
{
throw new IllegalArgumentException("PhysicsPoint should be set");
}
this.vel.set(vel);
}
/** If set, the location of multiple collisions within the same tick will be recorded into "posHistoryDetails".
* @param posHistoryDetails
*/
public void setPosHistoryDetails(PhysicsPointHistoryDetailed posHistoryDetails)
{
this.posHistoryDetails = posHistoryDetails;
}
public void setCollideGrid(@Nullable EntityGrid<MapEntity> collideGrid)
{
this.collideGrid = collideGrid;
}
public void setCollideFilter(@Nonnull LoopFilter<MapEntity, Long> collideFilter)
{
this.collideFilter = collideFilter;
}
public void setBounceFriction(int bounceFriction)
{
this.bounceFriction = bounceFriction;
}
public void setOtherAxisFriction(int otherAxisFriction)
{
this.otherAxisFriction = otherAxisFriction;
}
/** Number of bounces on a tile the simulated object has left. Use -1 for infinite.
* if this value is 0 and hits a tile, the object has collided
* @param bounces_left
*/
public void setBouncesLeft(int bounces_left)
{
this.bounces_left = bounces_left;
}
/** Which entities did we hit during the previous tick()?.
* @return
*/
public @Nonnull Iterator<HitData> getHitEntities()
{
return hitEntities.iterator();
}
/** If the entity has exhausted all its bounces in the previous tick(),
* this is the final tile it collided on.
* @param tilePos The return value
*/
public void getHitTile(PhysicsPoint tilePos)
{
tilePos.set(this.hitTile);
}
/** How many times has this entity bounced of a tile in the previous tick()?.
* @return
*/
public int getBounces()
{
return bounces;
}
/** Did tick() stop because the entity made its final bounce?.
* @return
*/
public boolean hasExhaustedBounces()
{
return exhaustedBounces;
}
/** Update the given position using the given velocity and attempt collision with the map.
* Use setPreviousPosition() and setVelocity() to set the position to begin the simulation with.
* The result will end up in getNewPosition() and getVelocity().
* Use setPosHistoryDetails() if you would like to store the positions of any intermediate bounces.
* @param tick For which tick is this collision?
*/
public void tickMap(long tick)
{
this.hitTile.unset();
this.bounces = 0;
this.exhaustedBounces = false;
if (!prevPos.set)
{
throw new IllegalStateException();
}
if (!vel.set)
{
throw new IllegalStateException();
}
if (radius < 0)
{
throw new IllegalStateException();
}
// How much velocity still needs to be added?
// This is used to properly calculate multiple tile bounces within a single tick
// Each iteration of the loop below will take care of one bounce, until there is no more
// velocity left.
// This value is always >= 0
final PhysicsPoint remaining = new PhysicsPoint();
final PhysicsPoint previousPosHistoryDetail = new PhysicsPoint(prevPos);
remaining.set(vel);
remaining.abs();
newPos.set(prevPos);
while (true)
{
final PhysicsPoint pos = new PhysicsPoint(newPos);
newPos.x += vel.x >= 0 ? remaining.x : -remaining.x ;
newPos.y += vel.y >= 0 ? remaining.y : -remaining.y ;
// If map is set, we are doing tile collision
// No need to match tiles if we are stationairy
if (this.map != null && !pos.equals(newPos))
{
final PhysicsPoint intersect = new PhysicsPoint();
boolean tileCollided = tileCollisionRay(pos, newPos, radius, intersect);
// at this point tileCollisionRay has set a value for newPos if there was a hit
// (its value is the earliest collision point in a straight line (adjusted for radius)).
// If not, the newPos is not modified.
// Use this line to try further collision
if (tileCollided)
{
assert intersect.set;
newPos.set(intersect);
++bounces;
if (!hasBouncesLeft())
{
// This entity can no longer bounce
this.hitTile.set(pos);
this.hitTile.divide(TILE_PIXELS);
this.exhaustedBounces = true;
return;
}
}
}
// No final collision, there are more steps to make
final PhysicsPoint prevRemaining = new PhysicsPoint(remaining);
remaining.x -= abs(newPos.x - pos.x);
remaining.y -= abs(newPos.y - pos.y);
remaining.clip(null, prevRemaining);
if (remaining.equals(prevRemaining))
{
break;
}
if (remaining.x < 0 || remaining.y < 0)
{
break;
}
if (remaining.isZero())
{
// done
break;
}
// Remaining has a value > 0, so there are more steps to make
if (posHistoryDetails != null)
{
// do we need to add a detail? (have we not moved in a straight line during this iteration?)
// note that the very first step and the very last step are not a detail!
// the very first step is the end result of the previous tick,
// the very last step is the end result of the current tick.
// if previousPosHistoryDetail == prevPos there is no detail to add yet
if (!previousPosHistoryDetail.equals(this.prevPos))
{
// have we moved in a straight line?
if (findYOnLine(previousPosHistoryDetail, pos, newPos.x) != newPos.y)
{
previousPosHistoryDetail.set(pos);
posHistoryDetails.appendDetail(tick, pos);
}
}
}
}
// At this point, no "final" collision was made
if (map != null)
{
//enforce map limits to be safe
newPos.x = clip(
newPos.x,
map.physicsGetMapLimitMinimum() * TILE_PIXELS + radius,
map.physicsGetMapLimitMaximum() * TILE_PIXELS - radius);
newPos.y = clip(
newPos.y,
map.physicsGetMapLimitMinimum() * TILE_PIXELS + radius,
map.physicsGetMapLimitMaximum() * TILE_PIXELS - radius);
}
newPos.enforceOverflowLimit();
vel.enforceOverflowLimit();
}
/** Attempt collision with other entities using the positions between tick-1 and tick.
* This should usually be called using the results of tickMap().
* Use setPreviousPosition() to set the position of the previous tick (tick-1).
* Use setNewPosition() to set the position of the current tick (tick+0).
* Use setPosHistoryDetails() (optionally) to set the intermediate positions (tick-1 => tick).
* You can retrieve the results using getHitEntities().
* @param tick
*/
public void tickEntityCollision(long tick)
{
this.hitEntities.clear();
if (collideGrid == null)
{
return;
}
if (this.posHistoryDetails != null)
{
this.posHistoryDetails.seekDetail(tick);
}
final PhysicsPoint prev = new PhysicsPoint();
final PhysicsPoint next = new PhysicsPoint();
prev.set(this.prevPos);
while (true)
{
boolean lastIteration = false;
if (this.posHistoryDetails != null && this.posHistoryDetails.hasNextDetail())
{
this.posHistoryDetails.nextDetail(next);
}
else
{
// ran out of details..
// the final line to match is the last detail -> the final position for this tick.
next.set(newPos);
lastIteration = true;
}
// NOTE: This algorithm assumes ticks are quantum.
// Aka there is no tick 8.2, only tick 8 and 9. The whole movevement of the
// colliding entities (including multiple bounces) within a single tick is
// considered for collision.
// This may cause a ship to hit a projectile even though their paths really
// should not have crossed when they both have a high velocity.
// First, find collision candidates quickly using a grid (2d array).
final PhysicsPoint diff = new PhysicsPoint();
diff.set(next);
diff.sub(prev);
diff.abs();
diff.add(radius);
final PhysicsPoint low = new PhysicsPoint(prev);
low.sub(radius);
final PhysicsPoint high = new PhysicsPoint(prev);
high.add(radius);
// Note: the collidGrid is based on non-smoothed positions.
// This does not matter at the moment because collisions are performed
// only with projectiles (which do not have smoothed positions).
// In case we ARE colliding with smoothed positions, a secondary grid is needed.
// Or perhaps simply use a wider area to look for collision candidates
collideGrid.enableQueue();
try
{
Iterator<MapEntity> it = collideGrid.iterator(low, high);
while (it.hasNext())
{
MapEntity collider = it.next();
if (collideFilter.loopFilter(collider, tick))
{
continue;
}
final PhysicsPoint hitLocation = new PhysicsPoint();
if (entityCollision(tick, prev, next, collider, hitLocation))
{
this.hitEntities.add(new HitData(collider, hitLocation));
}
}
}
finally
{
collideGrid.disableQueue();
}
// Prepare for the next position detail step...
prev.set(next);
if (lastIteration)
{
break;
}
}
}
private boolean hasBouncesLeft()
{
// call me after incrementing bounces
return this.bounces_left < 0 || bounces <= this.bounces_left;
}
private boolean tileCollisionRay(PhysicsPoint rayFrom, PhysicsPoint rayTo, int radius, PhysicsPoint intersect)
{
// bresenham line algorithm
// http://tech-algorithm.com/articles/drawing-line-using-bresenham-algorithm/
// http://stackoverflow.com/questions/15295195/can-i-easily-skip-pixels-in-bresenhams-line-algorithm
int x = rayTo.x;
int y = rayTo.y;
int w = rayFrom.x - x;
int h = rayFrom.y - y;
int dx1 = 0, dy1 = 0, dx2 = 0, dy2 = 0;
if (w < 0)
{
dx1 = -1;
}
else if (w > 0)
{
dx1 = 1;
}
if (h < 0)
{
dy1 = -1;
}
else if (h > 0)
{
dy1 = 1;
}
if (w < 0)
{
dx2 = -1;
}
else if (w > 0)
{
dx2 = 1;
}
int biggest = Math.abs(w);
int smallest = Math.abs(h);
if (!(biggest > smallest))
{
int tmp = biggest;
biggest = smallest;
smallest = tmp;
if (h < 0)
{
dy2 = -1;
}
else if (h > 0)
{
dy2 = 1;
}
dx2 = 0;
}
int numerator = biggest / 2;
final PhysicsPoint tilePos = new PhysicsPoint(0, 0);
for (int i = 0; i <= biggest; i += TILE_PIXELS)
{
tilePos.x = x / TILE_PIXELS;
tilePos.y = y / TILE_PIXELS;
if (map.physicsIsSolid(tilePos.x, tilePos.y)
&& tileCollision(rayFrom, rayTo, tilePos.x, tilePos.y, intersect))
{
assert intersect.set;
return true;
}
if (radius > 0)
{
// todo: efficiency?
int radiusTiles = (radius / TILE_PIXELS) + 1;
for (int tileX = tilePos.x - radiusTiles; tileX <= tilePos.x + radiusTiles; ++tileX)
{
for (int tileY = tilePos.y - radiusTiles; tileY <= tilePos.y + radiusTiles; ++tileY)
{
if (map.physicsIsSolid(tileX, tileY)
&& tileCollision(rayFrom, rayTo, tileX, tileY, intersect))
{
return true;
}
}
}
}
if (biggest == 0)
{
break;
}
int k_old = numerator;
numerator = (numerator + smallest * TILE_PIXELS) % biggest;
if (!(numerator < biggest))
{
numerator -= biggest;
}
x += ((k_old + smallest * TILE_PIXELS) / biggest) * dx1;
y += ((k_old + smallest * TILE_PIXELS) / biggest) * dy1;
x += (TILE_PIXELS - ((k_old + smallest * TILE_PIXELS) / biggest)) * dx2;
y += (TILE_PIXELS - ((k_old + smallest * TILE_PIXELS) / biggest)) * dy2;
}
return false;
}
private boolean tileCollision(PhysicsPoint rayFrom, PhysicsPoint rayTo, int tileX, int tileY, PhysicsPoint intersect)
{
final PhysicsPoint tilePixelsTL = new PhysicsPoint();
final PhysicsPoint tilePixelsBR = new PhysicsPoint();
final TileSideDist[] sides = new TileSideDist[4];
final TileSideDist top = new TileSideDist(TILE_SIDE.TOP);
final TileSideDist right = new TileSideDist(TILE_SIDE.RIGHT);
final TileSideDist bottom = new TileSideDist(TILE_SIDE.BOTTOM);
final TileSideDist left = new TileSideDist(TILE_SIDE.LEFT);
tilePixelsTL.set(tileX, tileY);
tilePixelsTL.multiply(TILE_PIXELS);
tilePixelsBR.set(tilePixelsTL);
tilePixelsBR.add(TILE_PIXELS);
boolean matchTopSide = !map.physicsIsSolid(tileX , tileY - 1);
boolean matchRightSide = !map.physicsIsSolid(tileX + 1, tileY );
boolean matchBottomSide = !map.physicsIsSolid(tileX , tileY + 1);
boolean matchLeftSide = !map.physicsIsSolid(tileX - 1, tileY );
/*
______ -----------
(_ __) .-""""-. PEW | |
) (___/ '. PEW | |
( ___ : o o o o o o o x o o o o o o o o
_) (__ \ .' | |
(______) '-....-' | |
-----------
match left line segment first,
then match the right line segment.
Left AND right line segments are matched
because a ship / bullet may be inside a
field of tiles, or because of a tile that
is semi permeable.
*/
sortTileSidesByDist(
sides,
tilePixelsTL, rayFrom,
matchTopSide, matchRightSide, matchBottomSide, matchLeftSide,
top, right, bottom, left);
for (int i = 0; i < 4; ++i)
{
TileSideDist side = sides[i];
if (side == null)
{
break;
}
boolean collide;
if (side.side == TILE_SIDE.LEFT)
{
collide = tileCollisionLineSegment(
rayFrom, rayTo,
tilePixelsTL.x, tilePixelsTL.y,
tilePixelsTL.x, tilePixelsTL.y + TILE_PIXELS,
TILE_SIDE.LEFT,
intersect);
}
else if (side.side == TILE_SIDE.RIGHT)
{
collide = tileCollisionLineSegment(
rayFrom, rayTo,
tilePixelsTL.x + TILE_PIXELS, tilePixelsTL.y,
tilePixelsTL.x + TILE_PIXELS, tilePixelsTL.y + TILE_PIXELS,
TILE_SIDE.RIGHT,
intersect);
}
else if (side.side == TILE_SIDE.TOP)
{
collide = tileCollisionLineSegment(
rayFrom, rayTo,
tilePixelsTL.x, tilePixelsTL.y,
tilePixelsTL.x + TILE_PIXELS, tilePixelsTL.y,
TILE_SIDE.TOP,
intersect);
}
else if (side.side == TILE_SIDE.BOTTOM)
{
collide = tileCollisionLineSegment(
rayFrom, rayTo,
tilePixelsTL.x, tilePixelsTL.y + TILE_PIXELS,
tilePixelsTL.x + TILE_PIXELS, tilePixelsTL.y + TILE_PIXELS,
TILE_SIDE.BOTTOM,
intersect
);
}
else
{
assert false;
continue;
}
if (collide)
{
assert intersect.set;
return true;
}
}
return false;
}
/** Try a single entity on entity collision.
*
* @param tick Current tick
* @param posFrom The previous position of the entity that this Collision belongs to (not argument en)
* @param posTo The next position of the entity that this Collision belongs to (not argument en)
* @param en The entity to collide with
* @param hitLocation The hit location is set here (nearest intersection)
* @return
*/
private boolean entityCollision(long tick, PhysicsPoint posFrom, PhysicsPoint posTo, MapEntity en, PhysicsPoint hitLocation)
{
hitLocation.unset();
if (en.radius.get() != 0)
{
throw new IllegalStateException("Collision between two entities, both with radius is not implemented yet.");
}
final PhysicsPositionVector entityFrom = new PhysicsPositionVector();
final PhysicsPositionVector entityTo = new PhysicsPositionVector();
if (!en.getHistoricPosition(entityFrom, tick - 1, false) ||
!en.getHistoricPosition(entityTo, tick, false))
{
return false;
}
if (lineSegmentIntersectsConvexPolygon(
entityFrom.pos.x, entityFrom.pos.y, entityTo.pos.x, entityTo.pos.y, // The line segment (0 radius)
buildPositionDeltaPolygon(posFrom, posTo, this.radius),
hitLocation, null))
{
if (!hitLocation.set)
{
hitLocation.set(entityFrom.pos);
hitLocation.set = true;
}
return true;
}
return false;
}
private static List<PhysicsPoint> buildPositionDeltaPolygon(PhysicsPoint from, PhysicsPoint to, int radius)
{
final PhysicsPoint v1 = new PhysicsPoint();
final PhysicsPoint v2 = new PhysicsPoint();
final PhysicsPoint v3 = new PhysicsPoint();
final PhysicsPoint v4 = new PhysicsPoint();
final PhysicsPoint v5 = new PhysicsPoint();
final PhysicsPoint v6 = new PhysicsPoint();
final ArrayList<PhysicsPoint> vertices = new ArrayList<>(6);
assert from.set;
assert to.set;
// Create a convex polygon which represents
// the movement the entity rectangle went through.
// it is possible to do this in less lines;
// howeve, in this form you can understand
// wth is going on more easily.
if (to.x >= from.x && to.y >= from.y)
{
// moving to the bottom right
// p2 p3
// -------
// | FROM \
// | o \
// | \
// p1 \ \
// \ \
// \ \
// \ \
// \ TO | p4
// \ o |
// \ |
// p6 -------- p5
v1.set(from);
v1.x -= radius;
v1.y += radius;
v2.set(from);
v2.x -= radius;
v2.y -= radius;
v3.set(from);
v3.x += radius;
v3.y -= radius;
v4.set(to);
v4.x += radius;
v4.y -= radius;
v5.set(to);
v5.x += radius;
v5.y += radius;
v6.set(to);
v6.x -= radius;
v6.y += radius;
}
else if (to.x >= from.x && to.y <= from.y)
{
// moving to the top right
v1.set(from);
v1.x += radius;
v1.y += radius;
v2.set(from);
v2.x -= radius;
v2.y += radius;
v3.set(from);
v3.x -= radius;
v3.y -= radius;
v4.set(to);
v4.x -= radius;
v4.y -= radius;
v5.set(to);
v5.x += radius;
v5.y -= radius;
v6.set(to);
v6.x += radius;
v6.y += radius;
}
else if (to.x <= from.x && to.y >= from.y)
{
// moving to the bottom left
v1.set(from);
v1.x -= radius;
v1.y -= radius;
v2.set(from);
v2.x += radius;
v2.y -= radius;
v3.set(from);
v3.x += radius;
v3.y += radius;
v4.set(to);
v4.x += radius;
v4.y += radius;
v5.set(to);
v5.x -= radius;
v5.y += radius;
v6.set(to);
v6.x -= radius;
v6.y -= radius;
}
else if (to.x <= from.x && to.y <= from.y)
{
// moving to the top left
v1.set(from);
v1.x += radius;
v1.y -= radius;
v2.set(from);
v2.x += radius;
v2.y += radius;
v3.set(from);
v3.x -= radius;
v3.y += radius;
v4.set(to);
v4.x -= radius;
v4.y += radius;
v5.set(to);
v5.x -= radius;
v5.y -= radius;
v6.set(to);
v6.x += radius;
v6.y -= radius;
}
else
{
assert false;
}
vertices.clear();
vertices.add(v1);
vertices.add(v2);
vertices.add(v3);
vertices.add(v4);
vertices.add(v5);
vertices.add(v6);
assert vertices.size() > 3;
return vertices; // clockwise
}
private boolean tileCollisionLineSegment(
PhysicsPoint rayFrom, PhysicsPoint rayTo,
int x1, int y1, int x2, int y2,
TILE_SIDE tileSide,
PhysicsPoint intersection)
{
intersection.unset();
if (this.radius == 0)
{
// fast case for 0 radius
if (!lineSegmentsIntersect(
rayFrom.x, rayFrom.y, rayTo.x, rayTo.y,
x1, y1, x2, y2,
false))
{
return false;
}
}
else
{
if (!lineSegmentIntersectsConvexPolygon(
x1, y1, x2, y2,
buildPositionDeltaPolygon(rayFrom, rayTo, radius),
null, null))
{
return false;
}
}
// yay we hit something!
if (tileSide == TILE_SIDE.LEFT || tileSide == TILE_SIDE.RIGHT)
{
int newX;
if (rayFrom.x <= x1)
{
newX = x1 - radius - 1;
}
else
{
newX = x1 + radius + 1;
}
intersection.set = true;
intersection.y = findYOnLine(rayFrom, rayTo, newX);
intersection.x = newX;
if (hasBouncesLeft())
{
// this is not the result of the last bounce
vel.x = (int) ((long) vel.x * -bounceFriction / GCInteger.RATIO);
vel.y = (int) ((long) vel.y * otherAxisFriction / GCInteger.RATIO);
}
}
else if (tileSide == TILE_SIDE.TOP || tileSide == TILE_SIDE.BOTTOM)
{
int newY;
if (rayFrom.y <= y1)
{
newY = y1 - radius - 1;
}
else
{
newY = y1 + radius + 1;
}
intersection.set = true;
intersection.x = findXOnLine(rayFrom, rayTo, newY);
intersection.y = newY;
if (hasBouncesLeft())
{
// this is not the result of the last bounce
vel.x = (int) ((long) vel.x * otherAxisFriction / GCInteger.RATIO);
vel.y = (int) ((long) vel.y * -bounceFriction / GCInteger.RATIO);
}
}
else
{
assert false;
}
assert intersection.set;
return true;
}
private static class TileSideDist implements Comparable<TileSideDist>
{
final TILE_SIDE side;
long distance;
TileSideDist(TILE_SIDE side)
{
this.side = side;
}
@Override
public int compareTo(TileSideDist o)
{
return Long.compare(distance, o.distance);
}
}
private static void sortTileSidesByDist(
TileSideDist[] ret,
PhysicsPoint tilePixels, PhysicsPoint point,
boolean matchTopSide, boolean matchRightSide, boolean matchBottomSide, boolean matchLeftSide,
TileSideDist top, TileSideDist right, TileSideDist bottom, TileSideDist left
)
{
assert ret.length == 4;
int i = 0;
final PhysicsPoint sideCenter = new PhysicsPoint();
if (matchTopSide)
{
sideCenter.set(tilePixels);
sideCenter.addX(TILE_PIXELS / 2);
ret[i++] = top;
top.distance = sideCenter.distanceSquared(point);
assert top.side == TILE_SIDE.TOP;
}
if (matchRightSide)
{
sideCenter.set(tilePixels);
sideCenter.addX(TILE_PIXELS / 2);
sideCenter.addX(TILE_PIXELS / 2);
sideCenter.addY(TILE_PIXELS / 2);
ret[i++] = right;
right.distance = sideCenter.distanceSquared(point);
assert right.side == TILE_SIDE.RIGHT;
}
if (matchBottomSide)
{
sideCenter.set(tilePixels);
sideCenter.addX(TILE_PIXELS / 2);
sideCenter.addY(TILE_PIXELS / 2);
sideCenter.addY(TILE_PIXELS / 2);
ret[i++] = bottom;
bottom.distance = sideCenter.distanceSquared(point);
assert bottom.side == TILE_SIDE.BOTTOM;
}
if (matchLeftSide)
{
sideCenter.set(tilePixels);
sideCenter.addY(TILE_PIXELS / 2);
ret[i++] = left;
left.distance = sideCenter.distanceSquared(point);
assert left.side == TILE_SIDE.LEFT;
}
Arrays.sort(ret, 0, i);
while (i < ret.length)
{
ret[i++] = null;
}
}
public static boolean lineSegmentsIntersect(
PhysicsPoint start1, PhysicsPoint end1,
PhysicsPoint start2, PhysicsPoint end2,
boolean checkCollinear)
{
return lineSegmentsIntersect(
start1.x, start1.y, end1.x, end1.y,
start2.x, start2.y, end2.x, end2.y,
checkCollinear
);
}
public static boolean lineSegmentsIntersect(
int x1, int y1, int x2, int y2,
int x3, int y3, int x4, int y4,
boolean checkCollinear)
{
// http://www.java-gaming.org/index.php?topic=22590.0
// no overflows aslong as the values are under 2^30
// Return false if either of the lines have zero length
if (x1 == x2 && y1 == y2
|| x3 == x4 && y3 == y4)
{
return false;
}
// Fastest method, based on Franklin Antonio's "Faster Line Segment Intersection"
// topic "in Graphics Gems III" book (http://www.graphicsgems.org/)
long ax = x2 - (long) x1;
long ay = y2 - (long) y1;
long bx = x3 - (long) x4;
long by = y3 - (long) y4;
long cx = x1 - (long) x3;
long cy = y1 - (long) y3;
long alphaNumerator = by * cx - bx * cy;
long commonDenominator = ay * bx - ax * by;
if (commonDenominator > 0)
{
if (alphaNumerator < 0 || alphaNumerator > commonDenominator)
{
return false;
}
}
else if (commonDenominator < 0)
{
if (alphaNumerator > 0 || alphaNumerator < commonDenominator)
{
return false;
}
}
long betaNumerator = ax * cy - ay * cx;
if (commonDenominator > 0)
{
if (betaNumerator < 0 || betaNumerator > commonDenominator)
{
return false;
}
}
else if (commonDenominator < 0)
{
if (betaNumerator > 0 || betaNumerator < commonDenominator)
{
return false;
}
}
if (commonDenominator == 0)
{
if (!checkCollinear)
{
return false;
}
// This code wasn't in Franklin Antonio's method. It was added by Keith Woodward.
// The lines are parallel.
// Check if they're collinear.
// see http://mathworld.wolfram.com/Collinear.html
// If p3 is collinear with p1 and p2 then p4 will also be collinear, since p1-p2 is parallel with p3-p4
if (x1 * (y2 - y3) == 0 &&
x2 * (y3 - y1) == 0 &&
x3 * (y1 - y2) == 0)
{
// The lines are collinear. Now check if they overlap.
if (x1 >= x3 && x1 <= x4 || x1 <= x3 && x1 >= x4
|| x2 >= x3 && x2 <= x4 || x2 <= x3 && x2 >= x4
|| x3 >= x1 && x3 <= x2 || x3 <= x1 && x3 >= x2)
{
if (y1 >= y3 && y1 <= y4 || y1 <= y3 && y1 >= y4
|| y2 >= y3 && y2 <= y4 || y2 <= y3 && y2 >= y4
|| y3 >= y1 && y3 <= y2 || y3 <= y1 && y3 >= y2)
{
return true;
}
}
}
return false;
}
return true;
}
/** Determine if a line segment intersects a convex polygon.
* if startX/Y is equal to endX/Y this method will match a point inside a polygon
* (although a more efficient method exists for this case).
*
* @param startX The start of the line segment
* @param startY The start of the line segment
* @param endX The end of the line segment
* @param endY The end of the line segment
* @param vertices The coordinates of the all the points on the convex vector in clock wise order.
* @param nearIntersection If not null, the intersection point nearest to the start of the line is set here.
* if the intersection is outside the gameplay area, the point will be unset.
* @param farIntersection If not null, the intersection point furthest from the start of the line is set here.
* @return true if the line intersects
*/
public static boolean lineSegmentIntersectsConvexPolygon(
int startX, int startY,
int endX, int endY,
List<PhysicsPoint> vertices,
PhysicsPoint nearIntersection, PhysicsPoint farIntersection)
{
// http://elancev.name/oliver/segment%20-%20polygon%20intersection.htm
// the vertices should be listed in clockwise order!
assert vertices.size() >= 3;
final PhysicsPoint lineStart = new PhysicsPoint();
final PhysicsPoint lineEnd = new PhysicsPoint();
final ComparableIntegerDivision tnear = new ComparableIntegerDivision();
final ComparableIntegerDivision tfar = new ComparableIntegerDivision();
final ComparableIntegerDivision tclip = new ComparableIntegerDivision();
final PhysicsPoint xDir = new PhysicsPoint();
final PhysicsPoint e = new PhysicsPoint();
final PhysicsPoint en = new PhysicsPoint();
final PhysicsPoint d = new PhysicsPoint();
lineStart.set(startX, startY);
lineEnd.set(endX, endY);
int near_index = -1;
int far_index = -1;
// near intersection point is closest to the origin of the linesegment
tnear.div = 0;
tnear.mod = 0;
tfar.div = 1;
tfar.mod = 0;
xDir.set(lineEnd);
xDir.sub(lineStart);
if (nearIntersection != null)
{
nearIntersection.unset();
}
if (farIntersection != null)
{
farIntersection.unset();
}
for (int j = vertices.size() - 1, i = 0;
i < vertices.size();
j = i, i++
)
{
PhysicsPoint e0 = vertices.get(j);
PhysicsPoint e1 = vertices.get(i);
e.set(e1);
e.sub(e0);
en.set(e.y, -e.x);
d.set(e0);
d.sub(lineStart);
long denom = d.dotProduct(en);
long numer = xDir.dotProduct(en);
if (numer == 0) // ray parallel to plane
{
if (denom < 0)
{
return false;
}
}
else
{
tclip.calculate(denom, numer);
if (numer < 0) // near intersection
{
if (tclip.compareTo(tfar) > 0)
{
return false;
}
if (tclip.compareTo(tnear) > 0)
{
tnear.set(tclip);
near_index = i;
}
}
else // far intersection
{
if (tclip.compareTo(tnear) < 0)
{
return false;
}
if (tclip.compareTo(tfar) < 0)
{
tfar.set(tclip);
far_index = i;
}
}
}
}
if (nearIntersection != null)
{
if (near_index >= 0)
{
int j = near_index - 1;
if (j < 0)
{
assert j == -1;
j = vertices.size() - 1;
}
PhysicsPoint e0 = vertices.get(j);
PhysicsPoint e1 = vertices.get(near_index);
findLineLineIntersection(nearIntersection,
lineStart, lineEnd,
e0, e1
);
}
else
{
// point is inside
nearIntersection.set(lineStart);
}
}
if (farIntersection != null)
{
if (far_index >= 0)
{
int j = far_index - 1;
if (j < 0)
{
assert j == -1;
j = vertices.size() - 1;
}
PhysicsPoint e0 = vertices.get(j);
PhysicsPoint e1 = vertices.get(far_index);
findLineLineIntersection(farIntersection,
lineStart, lineEnd,
e0, e1
);
}
else
{
farIntersection.set(lineEnd);
}
}
return true;
}
public static int findXOnLine(PhysicsPoint a, PhysicsPoint b, int y)
{
//y = y1 + (y2 - y1) / (x2 - x1) * (x - x1),
long d = ((long) b.y - a.y);
if (d == 0)
{
return a.x;
}
return (int) (a.x + ((long) b.x - a.x) * ((long) y - a.y) / d);
}
public static int findYOnLine(PhysicsPoint a, PhysicsPoint b, int x)
{
//y = y1 + (y2 - y1) / (x2 - x1) * (x - x1),
long d = ((long) b.x - a.x);
if (d == 0)
{
return a.y;
}
return (int) (a.y + ((long) b.y - a.y) * ((long) x - a.x) / d);
}
/** Find the intersection point between 2 infinite lines.
* @param result
* @param line1Start
* @param line1End
* @param line2Start
* @param line2End
* @return true if an intersection is found; false if the lines
* are parallel or if the intersection point is outside
* the gameplay area.
*
*/
public static boolean findLineLineIntersection(
PhysicsPoint result,
PhysicsPoint line1Start, PhysicsPoint line1End,
PhysicsPoint line2Start, PhysicsPoint line2End
)
{
// http://www.java-gaming.org/index.php?topic=22590.0
result.unset();
long line1Product = line1Start.crossProduct(line1End);
long line2Product = line2Start.crossProduct(line2End);
final PhysicsPoint line1Sub = new PhysicsPoint();
final PhysicsPoint line2Sub = new PhysicsPoint();
line1Sub.set(line1Start);
line1Sub.sub(line1End);
line2Sub.set(line2Start);
line2Sub.sub(line2End);
long linesProduct = line1Sub.crossProduct(line2Sub);
if (linesProduct == 0)
{
// The lines are parallel and there's either no solution or
// multiple solutions if the lines overlap
return false;
}
// a * b / c == a / c * b == b / c * a
// (a * b - c * d) / e == (a * b / e) - (c * d / e) == (a / e * b) - (d / e * c)
long divmulA = divmul(line1Product, linesProduct, line2Sub.x);
long divmulB = divmul(line2Product, linesProduct, line1Sub.x);
if (divmulA == Long.MIN_VALUE || divmulB == Long.MIN_VALUE)
{
return false;
}
long x = SwissArmyKnife.safeSubClipped(divmulA, divmulB);
divmulA = divmul(line1Product, linesProduct, line2Sub.y);
divmulB = divmul(line2Product, linesProduct, line1Sub.y);
if (divmulA == Long.MIN_VALUE || divmulB == Long.MIN_VALUE)
{
return false;
}
long y = SwissArmyKnife.safeSubClipped(divmulA, divmulB);
// is the intersection outside of our possible range of gameplay?
if (x < -EnvironmentConf.MAX_POSITION || x > EnvironmentConf.MAX_POSITION)
{
return false;
}
if (y < -EnvironmentConf.MAX_POSITION || y > EnvironmentConf.MAX_POSITION)
{
return false;
}
result.x = (int) x;
result.y = (int) y;
result.set = true;
return true;
}
// calculate a * b / c
// returns Long.MIN_VALUE when an overflow would have occurred.
// if this occurs the solution is outside the range of integers,
// which is never a solution we would want
private static long divmul(long a, long b, int c)
{
// a / b * c == int(a / b) * c + (a % b) * c / b
// large / large * small
if ( (b == 0) || ( (a == Long.MIN_VALUE || c == Long.MIN_VALUE) && (b == -1) ) )
{
return Long.MIN_VALUE; // error
}
long aDivB = a / b;
long aRemB = a - (b * aDivB);
if (!SwissArmyKnife.isMultiplySafe(aDivB, c) || !SwissArmyKnife.isMultiplySafe(aRemB, c))
{
return Long.MIN_VALUE; // error
}
return aDivB * c + aRemB * c / b;
}
/** Is the specified point inside the given convex polygon?
* @param px
* @param py
* @param vertices Vertices sorted clockwise or counter clockwise; at least 3
* @deprecated currently not in use, convert to integer before use
*/
private static strictfp boolean pointInsideConvexPolygon(int px, int py, PhysicsPoint ... vertices)
{
assert vertices.length >= 3;
double prevDot = 0;
for (int i = 0; i < vertices.length; i++)
{
PhysicsPoint x = vertices[i];
PhysicsPoint y = vertices[(i + 1) % vertices.length];
double dot = ((double)px - x.x)*((double)y.y - x.y) - ((double)y.x - x.x) * ((double)py - x.y);
if (dot >= 0 && prevDot < 0 || dot <= 0 && prevDot > 0)
{
return false;
}
prevDot = dot;
}
return true;
}
/** @deprecated currently not in use, convert to integer before use */
private static boolean pointInsideConvexPolygon(PhysicsPoint point, PhysicsPoint ... vertices)
{
return pointInsideConvexPolygon(point.x, point.y, vertices);
}
}