/* Copyright 2010 Christian Matt
*
* This file is part of PonkOut.
*
* PonkOut is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* PonkOut 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 General Public License
* along with PonkOut. If not, see <http://www.gnu.org/licenses/>.
*/
package ponkOut.logic;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.lwjgl.util.vector.Vector2f;
import ponkOut.logic.Block.CornerPlace;
import ponkOut.logic.Block.FacePlace;
/**
* methods for collision detection
*/
public class CollisionDetector {
/** positive epsilon to deal with inaccuracies */
private final float EPS = 1e-8f;
/** contains all blocks currently in the game */
private LinkedList<Block> blocks;
// maintain four lists of block faces, i.e. combine blocks which share a
// common face
// to speed up collision detection and to prevent balls from being reflected
// at a corner between two adjacent blocks
private LinkedList<BlockFace> leftBlockFaces;
private LinkedList<BlockFace> rightBlockFaces;
private LinkedList<BlockFace> topBlockFaces;
private LinkedList<BlockFace> bottomBlockFaces;
/**
* contains the four lists of block faces in the order of the values of
* FacePlace
*/
private ArrayList<LinkedList<BlockFace>> blockFaces;
public CollisionDetector() {
blocks = new LinkedList<Block>();
leftBlockFaces = new LinkedList<BlockFace>();
rightBlockFaces = new LinkedList<BlockFace>();
topBlockFaces = new LinkedList<BlockFace>();
bottomBlockFaces = new LinkedList<BlockFace>();
blockFaces = new ArrayList<LinkedList<BlockFace>>(Collections.nCopies(4, (LinkedList<BlockFace>) null));
blockFaces.set(FacePlace.LEFT.ordinal(), leftBlockFaces);
blockFaces.set(FacePlace.RIGHT.ordinal(), rightBlockFaces);
blockFaces.set(FacePlace.TOP.ordinal(), topBlockFaces);
blockFaces.set(FacePlace.BOTTOM.ordinal(), bottomBlockFaces);
}
public void addBlock(Block block) {
ArrayList<LinkedList<BlockFace>> horizontalNeighbours = new ArrayList<LinkedList<BlockFace>>();
ArrayList<LinkedList<BlockFace>> verticalNeighbours = new ArrayList<LinkedList<BlockFace>>();
horizontalNeighbours.add(new LinkedList<BlockFace>());
horizontalNeighbours.add(new LinkedList<BlockFace>());
verticalNeighbours.add(new LinkedList<BlockFace>());
verticalNeighbours.add(new LinkedList<BlockFace>());
Vector2f[] blockLeft = { block.getCornerPos(CornerPlace.TOP_LEFT), block.getCornerPos(CornerPlace.BOTTOM_LEFT) };
Vector2f[] blockRight = { block.getCornerPos(CornerPlace.TOP_RIGHT),
block.getCornerPos(CornerPlace.BOTTOM_RIGHT) };
Vector2f[] blockTop = { block.getCornerPos(CornerPlace.TOP_LEFT), block.getCornerPos(CornerPlace.TOP_RIGHT) };
Vector2f[] blockBottom = { block.getCornerPos(CornerPlace.BOTTOM_LEFT),
block.getCornerPos(CornerPlace.BOTTOM_RIGHT) };
// update the vertical block faces
ArrayList<LinkedList<BlockFace>> verticalFaces = new ArrayList<LinkedList<BlockFace>>();
verticalFaces.add(leftBlockFaces);
verticalFaces.add(rightBlockFaces);
BlockFace[] topExtension = { null, null };
BlockFace[] bottomExtension = { null, null };
for (int i = 0; i < 2; ++i) {
for (BlockFace face : verticalFaces.get(i)) {
Vector2f faceTop = face.getFirstCorner();
Vector2f faceBottom = face.getSecondCorner();
// find right/left neighbour
if (Math.abs(faceTop.x - blockTop[(i + 1) % 2].x) < EPS && faceBottom.y < blockTop[i].y
&& faceTop.y > blockBottom[i].y)
horizontalNeighbours.get((i + 1) % 2).add(face);
// find top/bottom extension
if (Math.abs(faceTop.x - blockTop[i].x) < EPS) {
if (Math.abs(faceBottom.y - blockTop[i].y) < EPS)
topExtension[i] = face;
else if (Math.abs(faceTop.y - blockBottom[i].y) < EPS)
bottomExtension[i] = face;
}
}
}
for (int i = 0; i < 2; ++i) {
BlockFace face = null;
// create a new face if no extension was found
// add it to existing face if there is exactly one extension
// merge to faces if two extensions were found
if (topExtension[i] == null && bottomExtension[i] == null) {
// only if block is not smaller than neighbour
// if it has more than one neighbours it cannot be completely
// hidden by them
BlockFace neighbour = null;
if (horizontalNeighbours.get(i).size() == 1)
neighbour = horizontalNeighbours.get(i).getFirst();
if (neighbour == null || neighbour.getFirstCorner().y < blockTop[i].y
|| neighbour.getSecondCorner().y > blockBottom[i].y) {
face = new BlockFace(block, i == 0 ? FacePlace.LEFT : FacePlace.RIGHT);
verticalFaces.get(i).add(face);
}
} else if (topExtension[i] != null && bottomExtension[i] == null) {
face = topExtension[i];
face.addBack(block);
} else if (topExtension[i] == null && bottomExtension[i] != null) {
face = bottomExtension[i];
face.addFront(block);
} else {
face = topExtension[i];
face.addBack(block);
face.addBack(bottomExtension[i]);
verticalFaces.get(i).remove(bottomExtension[i]);
}
for (BlockFace neighbour : horizontalNeighbours.get(i)) {
float neighbourTop = neighbour.getFirstCorner().y;
float neighbourBottom = neighbour.getSecondCorner().y;
// adjust corners if exactly one overlaps neighbour
// both cases cannot happen for more than one neighbour
// it is not necessary to split face if there is a neighbour in
// the middle
if (face != null) {
if (neighbourTop < face.getFirstCorner().y && neighbourBottom <= face.getSecondCorner().y)
face.setSecondCorner(neighbour.getFirstCorner());
else if (neighbourBottom > face.getSecondCorner().y && neighbourTop >= face.getFirstCorner().y)
face.setFirstCorner(neighbour.getSecondCorner());
}
// delete neighbour if it is smaller
// adjust corners if exactly one overlaps
if (neighbourTop <= blockTop[i].y && neighbourBottom >= blockBottom[i].y)
removeFace(neighbour);
else if (neighbourTop > blockTop[i].y && neighbourBottom >= blockBottom[i].y) {
// follow the block's face upwards
Block lastBlock = block;
boolean found;
do {
found = false;
for (Block nextBlock : blocks) {
Vector2f dv = new Vector2f();
if (i == 0) // left face
Vector2f.sub(lastBlock.getCornerPos(CornerPlace.TOP_LEFT),
nextBlock.getCornerPos(CornerPlace.BOTTOM_LEFT), dv);
else
// right face
Vector2f.sub(lastBlock.getCornerPos(CornerPlace.TOP_RIGHT),
nextBlock.getCornerPos(CornerPlace.BOTTOM_RIGHT), dv);
if (Math.abs(dv.x) < EPS && Math.abs(dv.y) < EPS) {
lastBlock = nextBlock;
found = true;
break;
}
}
} while (found);
neighbour.setSecondCorner(lastBlock.getCornerPos((i == 0) ? CornerPlace.TOP_LEFT
: CornerPlace.TOP_RIGHT));
} else if (neighbourBottom < blockBottom[i].y && neighbourTop <= blockTop[i].y) {
// follow the block's face downwards
Block lastBlock = block;
boolean found;
do {
found = false;
for (Block nextBlock : blocks) {
Vector2f dv = new Vector2f();
if (i == 0) // left face
Vector2f.sub(lastBlock.getCornerPos(CornerPlace.BOTTOM_LEFT),
nextBlock.getCornerPos(CornerPlace.TOP_LEFT), dv);
else
// right face
Vector2f.sub(lastBlock.getCornerPos(CornerPlace.BOTTOM_RIGHT),
nextBlock.getCornerPos(CornerPlace.TOP_RIGHT), dv);
if (Math.abs(dv.x) < EPS && Math.abs(dv.y) < EPS) {
lastBlock = nextBlock;
found = true;
break;
}
}
} while (found);
neighbour.setFirstCorner(lastBlock.getCornerPos((i == 0) ? CornerPlace.BOTTOM_LEFT
: CornerPlace.BOTTOM_RIGHT));
}
}
}
// update the horizontal block faces
ArrayList<LinkedList<BlockFace>> horizontalFaces = new ArrayList<LinkedList<BlockFace>>();
horizontalFaces.add(topBlockFaces);
horizontalFaces.add(bottomBlockFaces);
BlockFace[] leftExtension = { null, null };
BlockFace[] rightExtension = { null, null };
for (int i = 0; i < 2; ++i) {
for (BlockFace face : horizontalFaces.get(i)) {
Vector2f faceLeft = face.getFirstCorner();
Vector2f faceRight = face.getSecondCorner();
// find bottom/top neighbour
if (Math.abs(faceLeft.y - blockLeft[(i + 1) % 2].y) < EPS && faceRight.x > blockLeft[i].x
&& faceLeft.x < blockRight[i].x)
verticalNeighbours.get((i + 1) % 2).add(face);
// find left/right extension
if (Math.abs(faceLeft.y - blockLeft[i].y) < EPS) {
if (Math.abs(faceRight.x - blockLeft[i].x) < EPS)
leftExtension[i] = face;
else if (Math.abs(faceLeft.x - blockRight[i].x) < EPS)
rightExtension[i] = face;
}
}
}
for (int i = 0; i < 2; ++i) {
BlockFace face = null;
// create a new face if no extension was found
// add it to existing face if there is exactly one extension
// merge to faces if two extensions were found
if (leftExtension[i] == null && rightExtension[i] == null) {
// only if block is not smaller than neighbour
// if it has more than one neighbours it cannot be completely
// hidden by them
BlockFace neighbour = null;
if (verticalNeighbours.get(i).size() == 1)
neighbour = verticalNeighbours.get(i).getFirst();
if (neighbour == null || neighbour.getFirstCorner().x > blockLeft[i].x
|| neighbour.getSecondCorner().x < blockRight[i].x) {
face = new BlockFace(block, i == 0 ? FacePlace.TOP : FacePlace.BOTTOM);
horizontalFaces.get(i).add(face);
}
} else if (leftExtension[i] != null && rightExtension[i] == null) {
face = leftExtension[i];
face.addBack(block);
} else if (leftExtension[i] == null && rightExtension[i] != null) {
face = rightExtension[i];
face.addFront(block);
} else {
face = leftExtension[i];
face.addBack(block);
face.addBack(rightExtension[i]);
horizontalFaces.get(i).remove(rightExtension[i]);
}
for (BlockFace neighbour : verticalNeighbours.get(i)) {
float neighbourLeft = neighbour.getFirstCorner().x;
float neighbourRight = neighbour.getSecondCorner().x;
// adjust corners if exactly one overlaps neighbour
// both cases cannot happen for more than one neighbour
// it is not necessary to split face if there is a neighbour in
// the middle
if (face != null) {
if (neighbourLeft > face.getFirstCorner().x && neighbourRight >= face.getSecondCorner().x)
face.setSecondCorner(neighbour.getFirstCorner());
else if (neighbourRight < face.getSecondCorner().x && neighbourLeft <= face.getFirstCorner().x)
face.setFirstCorner(neighbour.getSecondCorner());
}
// delete neighbour if it is smaller
// adjust corners if exactly one overlaps
if (neighbourLeft >= blockLeft[i].x && neighbourRight <= blockRight[i].x)
removeFace(neighbour);
else if (neighbourLeft < blockLeft[i].x && neighbourRight <= blockRight[i].x) {
// follow the block's face leftwards
Block lastBlock = block;
boolean found;
do {
found = false;
for (Block nextBlock : blocks) {
Vector2f dv = new Vector2f();
if (i == 0) // top face
Vector2f.sub(lastBlock.getCornerPos(CornerPlace.TOP_LEFT),
nextBlock.getCornerPos(CornerPlace.TOP_RIGHT), dv);
else
// bottom face
Vector2f.sub(lastBlock.getCornerPos(CornerPlace.BOTTOM_LEFT),
nextBlock.getCornerPos(CornerPlace.BOTTOM_RIGHT), dv);
if (Math.abs(dv.x) < EPS && Math.abs(dv.y) < EPS) {
lastBlock = nextBlock;
found = true;
break;
}
}
} while (found);
neighbour.setSecondCorner(lastBlock.getCornerPos((i == 0) ? CornerPlace.TOP_LEFT
: CornerPlace.BOTTOM_LEFT));
} else if (neighbourRight > blockRight[i].x && neighbourLeft >= blockLeft[i].x) {
// follow the block's face rightwards
Block lastBlock = block;
boolean found;
do {
found = false;
for (Block nextBlock : blocks) {
Vector2f dv = new Vector2f();
if (i == 0) // top face
Vector2f.sub(lastBlock.getCornerPos(CornerPlace.TOP_RIGHT),
nextBlock.getCornerPos(CornerPlace.TOP_LEFT), dv);
else
// bottom face
Vector2f.sub(lastBlock.getCornerPos(CornerPlace.BOTTOM_RIGHT),
nextBlock.getCornerPos(CornerPlace.BOTTOM_LEFT), dv);
if (Math.abs(dv.x) < EPS && Math.abs(dv.y) < EPS) {
lastBlock = nextBlock;
found = true;
break;
}
}
} while (found);
neighbour.setFirstCorner(lastBlock.getCornerPos((i == 0) ? CornerPlace.TOP_RIGHT
: CornerPlace.BOTTOM_RIGHT));
}
}
}
blocks.add(block);
}
/**
* Removes this face from the list and removes corresponding face-reference
* from each block
*
* @param face
* face to remove
*/
private void removeFace(BlockFace face) {
for (Block block : face.getBlocks())
block.removeFace(face.getFacePlace());
blockFaces.get(face.getFacePlace().ordinal()).remove(face);
}
public void removeBlock(Block block) {
// remove block from faces
for (FacePlace facePlace : Block.FacePlace.values()) {
BlockFace face = block.getFace(facePlace);
if (face != null)
face.remove(block, blockFaces.get(facePlace.ordinal()));
}
// find all adjacent blocks
LinkedList<Block> leftNeighbours = new LinkedList<Block>();
LinkedList<Block> rightNeighbours = new LinkedList<Block>();
LinkedList<Block> topNeighbours = new LinkedList<Block>();
LinkedList<Block> bottomNeighbours = new LinkedList<Block>();
Vector2f topLeft = block.getCornerPos(CornerPlace.TOP_LEFT);
Vector2f bottomRight = block.getCornerPos(CornerPlace.BOTTOM_RIGHT);
for (Block block2 : blocks) {
Vector2f b2TopLeft = block2.getCornerPos(CornerPlace.TOP_LEFT);
Vector2f b2BottomRight = block2.getCornerPos(CornerPlace.BOTTOM_RIGHT);
if (Math.abs(topLeft.x - b2BottomRight.x) < EPS && b2BottomRight.y < topLeft.y
&& b2TopLeft.y > bottomRight.y)
leftNeighbours.add(block2);
else if (Math.abs(bottomRight.x - b2TopLeft.x) < EPS && b2BottomRight.y < topLeft.y
&& b2TopLeft.y > bottomRight.y)
rightNeighbours.add(block2);
else if (Math.abs(topLeft.y - b2BottomRight.y) < EPS && b2BottomRight.x > topLeft.x
&& b2TopLeft.x < bottomRight.x)
topNeighbours.add(block2);
else if (Math.abs(bottomRight.y - b2TopLeft.y) < EPS && b2BottomRight.x > topLeft.x
&& b2TopLeft.x < bottomRight.x)
bottomNeighbours.add(block2);
}
// sort lists for easy creation of new faces
Collections.sort(leftNeighbours, Block.verticalComparator);
Collections.sort(rightNeighbours, Block.verticalComparator);
Collections.sort(topNeighbours, Block.horizontalComperator);
Collections.sort(bottomNeighbours, Block.horizontalComperator);
// create new faces for the adjacent blocks if they do not already exist
// create shortcuts here: left neighbour will get a right face etc.
ArrayList<LinkedList<Block>> neighbours = new ArrayList<LinkedList<Block>>(Collections.nCopies(4,
(LinkedList<Block>) null));
neighbours.set(FacePlace.LEFT.ordinal(), rightNeighbours);
neighbours.set(FacePlace.RIGHT.ordinal(), leftNeighbours);
neighbours.set(FacePlace.TOP.ordinal(), bottomNeighbours);
neighbours.set(FacePlace.BOTTOM.ordinal(), topNeighbours);
for (FacePlace facePlace : FacePlace.values()) {
BlockFace face = null;
for (Block neighbour : neighbours.get(facePlace.ordinal())) {
if (neighbour.getFace(facePlace) == null) {
if (face == null) // first hit
face = new BlockFace(neighbour, facePlace);
else {
// check if block extends old face
Vector2f dv = new Vector2f();
switch (facePlace) {
case LEFT:
Vector2f.sub(face.getSecondCorner(), neighbour.getCornerPos(CornerPlace.TOP_LEFT), dv);
break;
case RIGHT:
Vector2f.sub(face.getSecondCorner(), neighbour.getCornerPos(CornerPlace.TOP_RIGHT), dv);
break;
case TOP:
Vector2f.sub(face.getSecondCorner(), neighbour.getCornerPos(CornerPlace.TOP_LEFT), dv);
break;
case BOTTOM:
Vector2f.sub(face.getSecondCorner(), neighbour.getCornerPos(CornerPlace.BOTTOM_LEFT), dv);
break;
}
// add block to face if it extends it
if (Math.abs(dv.x) < EPS && Math.abs(dv.y) < EPS)
face.addBack(neighbour);
else {
// otherwise create new face
// first adjust old face and add it to list
limitFaceByBlock(face, block);
addFace(face);
face = new BlockFace(neighbour, facePlace);
}
}
}
}
if (face != null) {
limitFaceByBlock(face, block);
addFace(face);
}
}
blocks.remove(block);
}
/**
* ensures the face does not overlap block
*
* @param face
* the face to adjust
* @param block
* a neighbour of the face (i.e. e.g. if face is a left face then
* the right face of the block touches the face)
*/
private void limitFaceByBlock(BlockFace face, Block block) {
switch (face.getFacePlace()) {
case LEFT:
if (face.getFirstCorner().y > block.getCornerPos(CornerPlace.TOP_RIGHT).y)
face.setFirstCorner(block.getCornerPos(CornerPlace.TOP_RIGHT));
if (face.getSecondCorner().y < block.getCornerPos(CornerPlace.BOTTOM_RIGHT).y)
face.setSecondCorner(block.getCornerPos(CornerPlace.BOTTOM_RIGHT));
break;
case RIGHT:
if (face.getFirstCorner().y > block.getCornerPos(CornerPlace.TOP_LEFT).y)
face.setFirstCorner(block.getCornerPos(CornerPlace.TOP_LEFT));
if (face.getSecondCorner().y < block.getCornerPos(CornerPlace.BOTTOM_LEFT).y)
face.setSecondCorner(block.getCornerPos(CornerPlace.BOTTOM_LEFT));
break;
case TOP:
if (face.getFirstCorner().x < block.getCornerPos(CornerPlace.BOTTOM_LEFT).x)
face.setFirstCorner(block.getCornerPos(CornerPlace.BOTTOM_LEFT));
if (face.getSecondCorner().x > block.getCornerPos(CornerPlace.BOTTOM_RIGHT).x)
face.setSecondCorner(block.getCornerPos(CornerPlace.BOTTOM_RIGHT));
break;
case BOTTOM:
if (face.getFirstCorner().x < block.getCornerPos(CornerPlace.TOP_LEFT).x)
face.setFirstCorner(block.getCornerPos(CornerPlace.TOP_LEFT));
if (face.getSecondCorner().x > block.getCornerPos(CornerPlace.TOP_RIGHT).x)
face.setSecondCorner(block.getCornerPos(CornerPlace.TOP_RIGHT));
break;
}
}
/**
* tries to find extensions to this face and adds it to them or to the list
* of block faces if no extension is found
*
* @param face
* a face to add
*/
private void addFace(BlockFace face) {
BlockFace[] extension = { null, null };
Vector2f firstCorner = face.getFirstCorner();
Vector2f secondCorner = face.getSecondCorner();
switch (face.getFacePlace()) {
case LEFT:
case RIGHT:
for (BlockFace face2 : blockFaces.get(face.getFacePlace().ordinal())) {
if (Math.abs(face2.getFirstCorner().x - firstCorner.x) < EPS) {
if (Math.abs(face2.getSecondCorner().y - firstCorner.y) < EPS)
extension[0] = face2;
else if (Math.abs(face2.getFirstCorner().y - secondCorner.y) < EPS)
extension[1] = face2;
}
}
break;
case TOP:
case BOTTOM:
for (BlockFace face2 : blockFaces.get(face.getFacePlace().ordinal())) {
if (Math.abs(face2.getFirstCorner().y - firstCorner.y) < EPS) {
if (Math.abs(face2.getSecondCorner().x - firstCorner.x) < EPS)
extension[0] = face2;
else if (Math.abs(face2.getFirstCorner().x - secondCorner.x) < EPS)
extension[1] = face2;
}
}
break;
}
if (extension[0] == null && extension[1] == null)
blockFaces.get(face.getFacePlace().ordinal()).add(face);
else if (extension[0] != null && extension[1] == null)
extension[0].addBack(face);
else if (extension[0] == null && extension[1] != null)
extension[1].addFront(face);
else {
extension[0].addBack(face);
extension[0].addBack(extension[1]);
blockFaces.get(face.getFacePlace().ordinal()).remove(extension[1]);
}
}
/**
* @param balls
* list of all balls in the game
* @param paddles
* list of all paddles in the game
* @param blocks
* list of all blocks in the game
* @return first occurring collision or null if no collision will occur
*/
public Collision getFirstCollision(List<Ball> balls, List<Paddle> paddles, List<Block> blocks) {
Collision first = null;
// test every Ball with every other Entity for collisions
for (int i = 0; i < balls.size(); ++i) {
Ball ball = balls.get(i);
// test Ball - Ball
for (int j = i + 1; j < balls.size(); ++j) {
Ball ball2 = balls.get(j);
float time = getCollisionTimeBall(ball.getPosition(), ball.getVelocity(), ball.getRadius(),
ball2.getPosition(), ball2.getVelocity(), ball2.getRadius());
if (first == null || time < first.time) {
first = new BallCollision(time, ball, ball2);
}
}
// test Ball - Paddle
for (Paddle paddle : paddles) {
float cTime = getCollisionTimePaddleCylinder(ball, paddle);
float h1Time = getCollisionTimePaddleSphere(ball, paddle, Paddle.HemispherePlace.TOP_LEFT);
float h2Time = getCollisionTimePaddleSphere(ball, paddle, Paddle.HemispherePlace.BOTTOM_RIGHT);
// find minimum of those 3 values
if (cTime < h1Time && cTime < h2Time) {
if (first == null || cTime < first.time) {
first = new PaddleCylinderCollision(cTime, ball, paddle);
}
} else if (h1Time < h2Time) {
if (first == null || h1Time < first.time) {
first = new PaddleHemisphereCollision(h1Time, ball, paddle, Paddle.HemispherePlace.TOP_LEFT);
}
} else if (first == null || h2Time < first.time) {
first = new PaddleHemisphereCollision(h2Time, ball, paddle, Paddle.HemispherePlace.BOTTOM_RIGHT);
}
}
// test Ball - Block
if (ball.getVelocity().length() > EPS) {
Collision faceCollision = getCollisionBlockFace(ball);
Collision cornerCollision = getCollisionBlockCorner(ball);
if (faceCollision != null && (cornerCollision == null || faceCollision.time <= cornerCollision.time)) {
if (first == null || faceCollision.time < first.time)
first = faceCollision;
} else if (cornerCollision != null) {
if (first == null || cornerCollision.time < first.time)
first = cornerCollision;
}
}
}
return first;
}
/**
* calculates smallest t solving ||p2-p1 + t*(v2-v1)|| = r1+r2
*
* @param p1
* position of first ball
* @param v1
* velocity of first ball
* @param r1
* radius of first ball
* @param p2
* position of second ball
* @param v2
* velocity of second ball
* @param r2
* radius of second ball
* @return first time in the future the balls will collide or
* Float.POSITIVE_INFINITY if they won't
*/
private float getCollisionTimeBall(Vector2f p1, Vector2f v1, float r1, Vector2f p2, Vector2f v2, float r2) {
// change to system where p1 = 0 and v1 = 0
Vector2f p = new Vector2f();
Vector2f v = new Vector2f();
Vector2f.sub(p2, p1, p);
Vector2f.sub(v2, v1, v);
// first check if potential collision is in the future
if (Vector2f.angle(p, v) < Math.PI / 2.0f)
return Float.POSITIVE_INFINITY;
// now do the real calculation
float rsqr = (r1 + r2) * (r1 + r2);
float dv = (v.x * v.x + v.y * v.y);
if (dv < EPS)
return Float.POSITIVE_INFINITY;
float r = 2 * p.x * v.x * p.y * v.y + v.x * v.x * rsqr - v.x * v.x * p.y * p.y - v.y * v.y * p.x * p.x + v.y
* v.y * rsqr;
if (r < EPS)
return Float.POSITIVE_INFINITY;
return (float) ((-p.x * v.x - p.y * v.y - Math.sqrt(r)) / dv);
}
/**
* @return first time in the future the ball will collide with the cylinder
* of the paddle or Float.POSITIVE_INFINITY if it won't
*/
private float getCollisionTimePaddleCylinder(Ball b, Paddle p) {
Vector2f ballPos = b.getPosition();
Vector2f ballVel = b.getVelocity();
Vector2f paddlePos = p.getPosition();
float pHalfLength = p.getLength() / 2.0f;
float time = Float.POSITIVE_INFINITY;
switch (p.getPlace()) {
case LEFT:
if (ballPos.x < paddlePos.x || ballVel.x > -EPS) {
time = Float.POSITIVE_INFINITY;
} else {
time = (paddlePos.x - ballPos.x + (b.getRadius() + p.getRadius())) / ballVel.x;
float newY = ballPos.y + time * ballVel.y;
if (newY < paddlePos.y - pHalfLength || newY > paddlePos.y + pHalfLength) {
time = Float.POSITIVE_INFINITY;
}
}
break;
case RIGHT:
if (ballPos.x > paddlePos.x || ballVel.x < EPS) {
time = Float.POSITIVE_INFINITY;
} else {
time = (paddlePos.x - ballPos.x - (b.getRadius() + p.getRadius())) / ballVel.x;
float newY = ballPos.y + time * ballVel.y;
if (newY < paddlePos.y - pHalfLength || newY > paddlePos.y + pHalfLength) {
time = Float.POSITIVE_INFINITY;
}
}
break;
case TOP:
if (ballPos.y > paddlePos.y || ballVel.y < EPS) {
time = Float.POSITIVE_INFINITY;
} else {
time = (paddlePos.y - b.getPosition().y - (b.getRadius() + p.getRadius())) / ballVel.y;
float newX = ballPos.x + time * ballVel.x;
if (newX < paddlePos.x - pHalfLength || newX > paddlePos.x + pHalfLength) {
time = Float.POSITIVE_INFINITY;
}
}
break;
case BOTTOM:
if (ballPos.y < paddlePos.y || ballVel.y > -EPS) {
time = Float.POSITIVE_INFINITY;
} else {
time = (paddlePos.y - b.getPosition().y + (b.getRadius() + p.getRadius())) / ballVel.y;
float newX = ballPos.x + time * ballVel.x;
if (newX < paddlePos.x - pHalfLength || newX > paddlePos.x + pHalfLength) {
time = Float.POSITIVE_INFINITY;
}
}
break;
}
return time;
}
/**
* Calculates collision with ball and the hemisphere completed to whole
* spheres.
*
* This is no problem, because if a collision with the nonexistent part of
* the hemisphere is detected, there will be a collision with the cylinder
* beforehand.
*
* @return first time in the future the ball will collide with one of the
* hemispheres of the paddle or Float.POSITIVE_INFINITY if they
* won't
*/
private float getCollisionTimePaddleSphere(Ball b, Paddle p, Paddle.HemispherePlace hemisphere) {
return getCollisionTimeBall(b.getPosition(), b.getVelocity(), b.getRadius(),
p.getHemispherePosition(hemisphere), p.getVelocity(), p.getRadius());
}
/**
* @return first collision between ball and a block face in the future or
* null if there will not be such a collision
*/
private BlockFaceCollision getCollisionBlockFace(Ball ball) {
float firstTime = Float.POSITIVE_INFINITY;
BlockFace firstFace = null;
Vector2f ballPos = ball.getPosition();
Vector2f ballVel = ball.getVelocity();
// left faces
if (ballVel.x > EPS) {
for (BlockFace face : leftBlockFaces) {
Vector2f tlCornerPos = face.getFirstCorner();
Vector2f blCornerPos = face.getSecondCorner();
if (ballPos.x < tlCornerPos.x) {
float time = (tlCornerPos.x - ballPos.x - ball.getRadius()) / ballVel.x;
if (time < firstTime) {
float newY = ballPos.y + time * ballVel.y;
if (newY >= blCornerPos.y && newY <= tlCornerPos.y) {
firstTime = time;
firstFace = face;
}
}
}
}
}
// right faces
if (ballVel.x < -EPS) {
for (BlockFace face : rightBlockFaces) {
Vector2f trCornerPos = face.getFirstCorner();
Vector2f brCornerPos = face.getSecondCorner();
if (ballPos.x > trCornerPos.x) {
float time = (trCornerPos.x - ballPos.x + ball.getRadius()) / ballVel.x;
if (time < firstTime) {
float newY = ballPos.y + time * ballVel.y;
if (newY >= brCornerPos.y && newY <= trCornerPos.y) {
firstTime = time;
firstFace = face;
}
}
}
}
}
// top faces
if (ballVel.y < -EPS) {
for (BlockFace face : topBlockFaces) {
Vector2f tlCornerPos = face.getFirstCorner();
Vector2f trCornerPos = face.getSecondCorner();
if (ballPos.y > tlCornerPos.y) {
float time = (tlCornerPos.y - ballPos.y + ball.getRadius()) / ballVel.y;
if (time < firstTime) {
float newX = ballPos.x + time * ballVel.x;
if (newX >= tlCornerPos.x && newX <= trCornerPos.x) {
firstTime = time;
firstFace = face;
}
}
}
}
}
// bottom faces
if (ballVel.y > EPS) {
for (BlockFace face : bottomBlockFaces) {
Vector2f blCornerPos = face.getFirstCorner();
Vector2f brCornerPos = face.getSecondCorner();
if (ballPos.y < blCornerPos.y) {
float time = (blCornerPos.y - ballPos.y - ball.getRadius()) / ballVel.y;
if (time < firstTime) {
float newX = ballPos.x + time * ballVel.x;
if (newX >= blCornerPos.x && newX <= brCornerPos.x) {
firstTime = time;
firstFace = face;
}
}
}
}
}
if (firstTime < Float.POSITIVE_INFINITY) {
Vector2f pos = new Vector2f(ballPos.x + firstTime * ballVel.x, ballPos.y + firstTime * ballVel.y);
Block block = firstFace.getBlockAt(pos);
return new BlockFaceCollision(firstTime, ball, block, firstFace.getFacePlace());
} else
return null;
}
/**
* @return first collision between ball and a block corner in the future or
* null if there will not be such a collision
*/
private BlockCornerCollision getCollisionBlockCorner(Ball ball) {
float firstTime = Float.POSITIVE_INFINITY;
BlockFace firstFace = null;
Vector2f ballPos = ball.getPosition();
Vector2f ballVel = ball.getVelocity();
float ballRadius = ball.getRadius();
Vector2f blockVel = new Vector2f(0.0f, 0.0f);
// test top corner of left face, bottom corner of right face, right
// corner of top face and left corner of bottom face
for (BlockFace face : leftBlockFaces) {
float time = getCollisionTimeBall(ballPos, ballVel, ballRadius, face.getFirstCorner(), blockVel, 0.0f);
if (time < firstTime) {
firstTime = time;
firstFace = face;
}
}
for (BlockFace face : rightBlockFaces) {
float time = getCollisionTimeBall(ballPos, ballVel, ballRadius, face.getSecondCorner(), blockVel, 0.0f);
if (time < firstTime) {
firstTime = time;
firstFace = face;
}
}
for (BlockFace face : topBlockFaces) {
float time = getCollisionTimeBall(ballPos, ballVel, ballRadius, face.getSecondCorner(), blockVel, 0.0f);
if (time < firstTime) {
firstTime = time;
firstFace = face;
}
}
for (BlockFace face : bottomBlockFaces) {
float time = getCollisionTimeBall(ballPos, ballVel, ballRadius, face.getFirstCorner(), blockVel, 0.0f);
if (time < firstTime) {
firstTime = time;
firstFace = face;
}
}
BlockCornerCollision firstCollision = null;
if (firstTime < Float.POSITIVE_INFINITY) {
Vector2f pos = new Vector2f(ballPos.x + firstTime * ballVel.x, ballPos.y + firstTime * ballVel.y);
Block block = firstFace.getBlockAt(pos);
switch (firstFace.getFacePlace()) {
case LEFT:
firstCollision = new BlockCornerCollision(firstTime, ball, block, firstFace.getFirstCorner());
break;
case RIGHT:
firstCollision = new BlockCornerCollision(firstTime, ball, block, firstFace.getSecondCorner());
break;
case TOP:
firstCollision = new BlockCornerCollision(firstTime, ball, block, firstFace.getSecondCorner());
break;
case BOTTOM:
firstCollision = new BlockCornerCollision(firstTime, ball, block, firstFace.getFirstCorner());
break;
}
}
return firstCollision;
}
}