I'm creating an Arkanoid-like game from scratch. I have no (formal or informal) education of any kind in game-development, so I kinda made up things as I went regarding collision detection based on personal ideas of "how it probably works".
I approximate the ball as a point and ignore its thickness, which is small anyway. I compensate for this visually by very slightly making all objects smaller than they are.
I don't "detect" collision, but anticipate it and change the outcome of the next frame. For example:
The ball is currently in position A, and it "wants" to go to E (red line). But I calculate all intersections with all segments which make up the bricks: B, C, D. The closest one to starting point (A) is B, so I choose B as the "real" intersection. I destroy the brick this segment belongs to.
Now I check how would the ball proceed if it would start at B, with its vector flipped horizontally (because I determine that the segment it hit was horizontal). This is the blue line. Again, from F and G, I pick F. I destroy the brick this segment belongs to.
Lastly, the orange line doesn't intersect, so I determine that the bottom of the orange line is where I should place the ball in the next frame.
This worked fine and I was satisfied with it, until I started nailing the edge case of the ball hitting the T intersection, where it all goes bananas.
What should happen here is that the ball bounces to the left and destroys either of the bricks. But what can happen is that my algorithms decides to choose the middle vertical segment (connecting boxes 1 and 2) as the collision point, either belonging to the 1st or 2nd box.
I thought of ignoring the edges of the segments, but then the ball would pass right through the T-intersection in this scenario, because it would also ignore what it's supposed to hit.
You can see it in action here (around after 00:07 mark): https://www.mp4upload.com/p6966u6kuzad
What are common techniques for handling this case? Even if I introduce the width to the ball, it could still hit the exact T-intersection at the tangent, so the problem would remain.
Requested to post code. Here's the main bit. Please note that I'm not expecting you to (or looking for) debugging the application. I'm more asking about how are these things even handled in general.
export function bounceBall(
ball: Point,
vx: number,
vy: number,
obstacles: Obstacle[],
) {
const projectedNextPoint = ball.clone().translate(vx, vy)
const movement = new Segment(ball, projectedNextPoint)
const touchedObstacles: Obstacle[] = []
let loop = 10
let hasCollision = false
do {
hasCollision = false
if (loop-- < 0) {
throw new Error('oops')
}
// If in the previous loop the ball just grazed the surface
if (movement.isZeroLength()) break
const collisions: Collision[] = []
for (const obstacle of obstacles) {
const collision = getIntersectionOfSegmentAndObstacle_ClosestToStartExcludingStart(movement, obstacle)
if (collision != null) collisions.push(collision)
}
const collision = getClosestCollisionExcludingSelf(movement.start, collisions)
// We don't count a collision if it's the starting point.
// If we did, grazing a surface would turn into an infinite loop.
if (collision == null || Point.AreEqual(collision.point, movement.start)) {
hasCollision = false
} else {
hasCollision = true
touchedObstacles.push(collision.obstacle)
}
if (hasCollision) {
movement.setStart(collision!.point)
if (collision!.segment.isVertical()) {
movement.mirrorVerticallyWrtStart()
vx = -vx
}
if (collision!.segment.isHorizontal()) {
movement.mirrorHorizontallyWrtStart()
vy = -vy
}
}
} while (hasCollision)
return {
touchedObstacles,
vx,
vy,
newBall: movement.end,
}
}