Main Game Loop

The game loop goes through the following steps:
  1. Test for wall collisions
  2. Test for ball collisions
  3. Find the earliest collision
  4. Move the ball to the first collision. If there are no collision then we move by a certain amount of time
  5. Calculate the bounce for the collision (if any)
  6. Repeat steps 1 to 5. We sleep for awhile to allow the screen to be painted and ensure certain game speed

Representing Motion

Motion is represented by a speed and direction. Therefore we can use a vector to represent the ball€™s motion. In the case of the bouncing balls animation, the length of the ball€™s velocity is the length the ball travels in one unit of time (a millisecond).

Length(A) = sqrt(x2 * y2)
A . B = Ax Bx + Ay By

Wall Collision

Wall collisions occur if in the time frame (t) the ball will pass through the wall.

// Calculate the movement vector for t
Vector movec = velocity.scale(t);

double collision_t = t; // no collision
// time = distance / speed
if (center.x - radius + movec.x <= 0) {
	collision_t = (center.x - radius) / velocity.x;
} else if (center.x + radius + movec.x >= boundWidth) {
	collision_t = (boundWidth - (center.x + radius)) / velocity.x;
} else if (center.y - radius + movec.y <= 0) {
	collision_t = (center.y - radius) / velocity.y;
} else if (center.y + radius + movec.y >= boundHeight) {
	collision_t = (boundHeight - (center.y + radius)) / velocity.y;
}
return Math.abs(collision_t);

Ball Collision

To detect collision between two moving balls we perform following steps:
  1. Calculate the distance between the two balls using Pythagorean Theorem (a^2 + b^2 = c^2)
  2. Reduce the collision from dynamic-dynamic (eg. both balls are moving) to dynamic-static collision detection (eg. only 1 ball is moving).
  3. If the length of the movement vector is less then the distance between the objects, then there is no way they can hit
  4. Find the point where the moving ball is touching the static ball
  5. Calculate and return when the balls touch
Diagram of Circle-Circle Collision
// Calculate the movement vector a for t
Vector movea = this.velocity.scale(t);

// Calculate the movement vector b for t
Vector moveb = b.velocity.scale(t);

// Detect wether the balls are touching
double deltaX = b.center.x - this.center.x;
double deltaXSquared = deltaX * deltaX; // square

double deltaY = b.center.y - this.center.y;
double deltaYSquared = deltaY * deltaY; // square

int sumRadii = this.radius + b.radius;
int sumRadiiSquared = sumRadii * sumRadii; // square

// Calculate distance between objects
double distSquared = deltaXSquared + deltaYSquared;	

// Reduce dynamic-dynamic collision to dynamic-static
Vector movec = movea.subtract(moveb);

// If the length of the movement vector is less
// then the distance between the objects,
// then there is no way they can hit
if (movec.getLength() < Math.sqrt(distSquared) - sumRadii) {
	return t; // no collision
}

// Normalize the movevec
Vector N = movec.normalize();

// Find C, the vector from the center of the moving 
// ball A to the center of ball B
Vector C = this.center.vectorTo(b.center);

// D is the point along movec that is closest to ball b
// D = N . C = ||C|| * cos(angle between N and C)
double D = N.dot(C);

// Find the length of the vector C
double lengthC = C.getLength();

// Math.sqrt(F) is the distance between the center of ball b
// and the closest point on movec	
double F = (lengthC * lengthC) - (D * D);

// If the closest that A will get to B 
// is more than the sum of their radii, there's no 
// way they are going collide
if (F >= sumRadiiSquared) {
	return t; // no collision
}

// We now have F and sumRadii, two sides of a right triangle. 
// Use these to find the third side, sqrt(T)
double T = sumRadiiSquared - F;

// If there is no such right triangle with sides length of 
// sumRadii and sqrt(f), T will probably be less than 0. 
// Better to check now than perform a square root of a 
// negative number. 
if (T < 0) {
	return t; // no collision
}

// Therefore the distance the circle has to travel along 
// the movement vector is D - sqrt(T)
double distance = D - Math.sqrt(T);

// Get the magnitude of the movement vector
double mag = movec.getLength();

// Finally, make sure that the distance A has to move 
// to touch B is not greater than the magnitude of the 
// movement vector. 
if (mag < distance) {
	return t;
}

// Return the time it takes ball A to travel this distance
double collision_t = (distance/mag) * t;

if (collision_t < 0 || collision_t > t) {
	return t;
}

// add collision to head of collision list
collisions.add(0, new Collision(this, b, collision_t));
return collision_t;

Bouncing Balls

Wall collisions are handled by bouncing it back in opposite direction:

if (c.x - radius <= 0 && velocity.x < 0d) {
	velocity = new Vector(-velocity.x, velocity.y);
} else if (c.x + radius >= boundWidth && velocity.x > 0d) {
	velocity = new Vector(-velocity.x, velocity.y);
}
if (c.y - radius <= 0 && velocity.y < 0d) {
	velocity = new Vector(velocity.x, -velocity.y);
} else if (c.y + radius >= boundHeight && velocity.y > 0d) {
	velocity = new Vector(velocity.x, -velocity.y);
}

Ball collisions on other hand are more complex. We are assuming rigid perfect balls.
Using Conservation of Momentum and Conservation of Energy the new vectors of balls A (v1') and B (v2') are:

optimizedP = 2(a1 - a2) / (A.mass + B.mass)
v1' = v1 - optimizedP * A.mass * N;
v2' = v2 + optimizedP * B.mass * N;
N is the normalized vector along which Momentum is exchanged (vector from center of ball A to center of ball B). Note that there is small errors due to loss of precision is using floating point. Balls travelling horizontally or vertically are corrected to continue traveling along that axis.
Vector v1 = a.velocity;
Vector v2 = b.velocity;
double m1 = 1; // mass of ball A
double m2 = 1; // mass of ball B

// First, find the normalized vector n from the center of
// ball A to the center of ball B
// This is the vector along which Momentum is exchanged
Vector N = a.center.vectorTo(b.center).normalize();

// Find the length of the component of each of the movement
// vectors along n.
// a1 = v1 . N
// a2 = v2 . N
double a1 = v1.dot(N);
double a2 = v2.dot(N);

// Using the optimized version,
// optimizedP =  2(a1 - a2)
//              -----------
//                m1 + m2
double optimizedP = (2.0 * (a1 - a2)) / (m1 + m2);

// Calculate v1', the new movement vector of circle1
// v1' = v1 - optimizedP * m2 * N
Vector v1_ = v1.subtract(N.scale(m1 * optimizedP));

// Calculate v1', the new movement vector of circle1
// v2' = v2 + optimizedP * m1 * N
Vector v2_ = v2.add(N.scale(m2 * optimizedP));

// Here we fix some errors that can occur due to loss of precision
// Handle case when both balls are travelling horizontally
if (a.center.y - b.center.y == 0 && v1.y == 0 && v2.y == 0) {
	v1_.setPosition(v1_.x, 0);
	v2_.setPosition(v2_.x, 0);
}
// Handle case when both balls are travelling vertically
if (a.center.x - b.center.x == 0 && v1.x == 0 && v2.x == 0) {
	v1_.setPosition(0, v1_.y);
	v2_.setPosition(0, v2_.y);
}

a.velocity = v1_;
b.velocity = v2_;