Search code examples
javaswingawt

Multiple Collisions as A Result of The Ball Passing Through The Object in Java Swing


I'm writing a basic Pong game that a ball bounces back from two flying pads which controlled by player. And my problem start where ball is colliding with "right pad". I have two pads, collision with left pad works pretty well but ball's collision with right pad is not working as I wanted.

The ball goes through the right pad (I don't know why, it's not supposed to do it) and collides with it multiple times, which causes unpleasant visuals and sounds in the game. It doesn't happen for left pad. I added a video to better understanding:

Video of the output (game) and the bug

My code is a bit long, so to make it easier for you I will mention what I suspect.

  • checkCollision(): in this method I only calculated the collisions of the ball with the pads. The method works correctly for the left pad but not for the right pad for some reason.
public void checkCollision() {

        for (ColoredShape tile : tiles) {

            if (ball.getShape().getBounds().intersects(tile.getShape().getBounds())) {

                playSound("sounds\\collisionsound1.wav");

                // Calculating the angle of incidence
                double tileCenterX = tile.getShape().getBounds2D().getCenterX();
                double tileCenterY = tile.getShape().getBounds2D().getCenterY();
                double ballCenterX = ball.getShape().getCenterX();
                double ballCenterY = ball.getShape().getCenterY();

                double angle = Math.atan2(ballCenterY - tileCenterY, ballCenterX - tileCenterX);

                if (Math.abs(angle) < Math.PI / 2) {
                    ball.setVx(-ball.getVx());
                } else {
                    ball.setVy(-ball.getVy());
                }

            }

        }
    }
  • moveBall() method:
public void moveBalls() {
            
        if (!gameOver && ballShouldMove) {    
            ball.setX(ball.getX() + ball.getVx());
            ball.setY(ball.getY() + ball.getVy());
        }
    
        if (ball.getX() > getWidth() - ball.getShape().width || ball.getX() < 0) {
            
            playSound("sounds\\collisionsound3.wav");
            
            if (ball.getX() > getWidth() - ball.getShape().width) {
                
                ball.setVx(-Math.abs(ball.getVx()));
                leftScore += 1;

            } else if (ball.getX() < 0) {
                
                ball.setVx(Math.abs(ball.getVx()));
                rightScore += 1;

            }

            resetBallLocation();
            ballShouldMove = false;

        }

        if (ball.getY() > getHeight() - ball.getShape().height || ball.getY() < 0) {
            
            playSound("sounds\\collisionsound2.wav");
            ball.setVy(-ball.getVy());

        }

    }
  • moveTiles() method, moves the paddles when player presses right keys:
public void moveTiles() {

        if (moveTile1Up) {
            Rectangle bounds = (Rectangle) tiles.get(0).getShape().getBounds2D();
            int newY = (int) (bounds.getY() - 5);
            
            if (newY >= 0) { 
                tiles.get(0).setShape(new Rectangle((int) bounds.getX(), newY, tileWidth, tileHeight));
            }

        } else if (moveTile1Down) {
            
            Rectangle bounds = (Rectangle) tiles.get(0).getShape().getBounds2D();
            int newY = (int) (bounds.getY() + 5);
            
            if (newY + tileHeight <= getHeight()) { // Check if the new position is within the frame
                tiles.get(0).setShape(new Rectangle((int) bounds.getX(), newY, tileWidth, tileHeight));
            }

        }
    
        if (moveTile2Up) {

            Rectangle bounds = (Rectangle) tiles.get(1).getShape().getBounds2D();
            int newY = (int) (bounds.getY() - 5);
            
            if (newY >= 0) { // Check if the new position is within the frame
                tiles.get(1).setShape(new Rectangle((int) bounds.getX(), newY, tileWidth, tileHeight));
            }

        } else if (moveTile2Down) {

            Rectangle bounds = (Rectangle) tiles.get(1).getShape().getBounds2D();
            int newY = (int) (bounds.getY() + 5);
            
            if (newY + tileHeight <= getHeight()) { // Check if the new position is within the frame
                tiles.get(1).setShape(new Rectangle((int) bounds.getX(), newY, tileWidth, tileHeight));
            }

        }
    }
  • I declare the pad objects here, I don't think there is a problem here but just in case.
Shape tile1 = new Rectangle(tileWidth / 2, Interface.frameHeight / 3, tileWidth, tileHeight);
Shape tile2 = new Rectangle(Interface.frameWidth - tileWidth*2, Interface.frameHeight / 3, tileWidth, tileHeight);
ColoredShape tileLeft = new ColoredShape(tile1, Color.white);
ColoredShape tileRight = new ColoredShape(tile2, Color.white);
tiles.add(0, tileLeft);
tiles.add(1, tileRight);

--Source Code--

Interface.java

import javax.swing.*;
import java.awt.*;
import java.awt.geom.Ellipse2D;

public class Interface extends JFrame {

    protected static int frameHeight = 637;
    protected static int frameWidth = 1214;
    protected int tileWidth = 20;
    protected int tileHeight = 200;

    public Interface() {

        setSize(frameWidth, frameHeight);
        getContentPane().setBackground(Color.BLACK);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new BorderLayout());
        setTitle("Ping Pong");
        setResizable(false);
        add(new Core(tileWidth, tileHeight));
        setVisible(true);

    }

    public static void main(String[] args) {
        new Interface();
    }

}

class ColoredShape {
    
    private Shape shape;
    private Color color;

    public ColoredShape(Shape shape, Color color) {
        
        this.shape = shape;
        this.color = color;

    }

    public Shape getShape() {
        return shape;
    }

    public void setShape(Shape shape) {
        this.shape = shape;
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        this.color = color;
    }

}

class Ball {
    
    private Ellipse2D.Double shape;
    private Color color;
    private int vx;
    private int vy;

    public Ball(int x, int y, int diameter, Color color, int vx, int vy) {
        
        this.shape = new Ellipse2D.Double(x, y, diameter, diameter);
        this.color = color;
        this.vx = vx;
        this.vy = vy;

    }

    public Ellipse2D.Double getShape() {
        return shape;
    }

    public Color getColor() {
        return color;
    }

    public int getVx() {
        return vx;
    }

    public void setVx(int vx) {
        this.vx = vx;
    }

    public int getVy() {
        return vy;
    }

    public void setVy(int vy) {
        this.vy = vy;
    }

    public int getX() {
        return (int) shape.x;
    }

    public void setX(int x) {
        shape.x = x;
    }

    public int getY() {
        return (int) shape.y;
    }

    public void setY(int y) {
        shape.y = y;
    }

}

Core.java

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.lang.Math;
import javax.sound.sampled.*;
import java.io.*;

class Core extends JPanel {

    private String winner = "";
    protected int x;
    protected int y;
    protected int diameter = 10;
    private int vx = 10, vy = 3;
    private int rightScore = 0, leftScore = 0;
    private int tileWidth, tileHeight;
    private boolean moveTile1Up, moveTile1Down, moveTile2Up, moveTile2Down;
    private boolean ballShouldMove = false;
    private boolean gameOver = false;
    private Timer speedIncreaseTimer;
    private ArrayList<ColoredShape> tiles;
    protected Ball ball;


    public Core(int tileWidth, int tileHeight) {
        
        setOpaque(false);
        
        this.tileWidth = tileWidth;
        this.tileHeight = tileHeight;
        this.tiles = new ArrayList<>();

        x = (int) Interface.frameWidth / 2;
        y = (int) Interface.frameHeight / 2;
        vx = (Math.random() < 0.5) ? vx : -1*vx;
        vy = (Math.random() < 0.5) ? vy : -1*vy;

        Shape tile1 = new Rectangle(tileWidth / 2, Interface.frameHeight / 3, tileWidth, tileHeight);
        Shape tile2 = new Rectangle(Interface.frameWidth - tileWidth*2, Interface.frameHeight / 3, tileWidth, tileHeight);
        ColoredShape tileLeft = new ColoredShape(tile1, Color.white);
        ColoredShape tileRight = new ColoredShape(tile2, Color.white);
        tiles.add(0, tileLeft);
        tiles.add(1, tileRight);

        ball = new Ball(x, y, diameter, Color.white, vx, vy);

        // Add KeyListener to detect key presses
        setFocusable(true);
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                int keyCode = e.getKeyCode();
                if (keyCode == KeyEvent.VK_W) {
                    moveTile1Up = true;
                } else if (keyCode == KeyEvent.VK_S) {
                    moveTile1Down = true;
                } else if (keyCode == KeyEvent.VK_UP) {
                    moveTile2Up = true;
                } else if (keyCode == KeyEvent.VK_DOWN) {
                    moveTile2Down = true;
                }

                if (keyCode == KeyEvent.VK_SPACE) {
                    
                    if (gameOver) {
                        restartGame();
                    } else {
                        ballShouldMove = true;
                    }

                }

                if (keyCode == KeyEvent.VK_ESCAPE) {

                    restartGame();

                }
            }

            @Override
            public void keyReleased(KeyEvent e) {
                int keyCode = e.getKeyCode();
                if (keyCode == KeyEvent.VK_W) {
                    moveTile1Up = false;
                } else if (keyCode == KeyEvent.VK_S) {
                    moveTile1Down = false;
                } else if (keyCode == KeyEvent.VK_UP) {
                    moveTile2Up = false;
                } else if (keyCode == KeyEvent.VK_DOWN) {
                    moveTile2Down = false;
                }
            }
        });

        ImageIcon originalIcon = new ImageIcon("images\\restartButton.jpg");

        int scaledWidth = 50;
        int scaledHeight = -1; // Automatically calculate the height to preserve aspect ratio
        Image scaledImage = originalIcon.getImage().getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH);

        ImageIcon scaledIcon = new ImageIcon(scaledImage);

        JLabel restartLabel = new JLabel(scaledIcon);

        add(restartLabel);

        restartLabel.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                restartGame();
            }
        });
        
        Timer timer = new Timer(10, new ActionListener() {
            
            @Override
            public void actionPerformed(ActionEvent e) {
                
                moveBalls();
                checkCollision();
                moveTiles();
                gameOver();
                repaint();
                
            }

        });

        timer.start();

        speedIncreaseTimer = new Timer(30000, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                
                if (ballShouldMove) {

                    ball.setVx((int) (ball.getVx() * 1.25));
                    ball.setVy((int) (ball.getVy() * 1.25));

                }

            }
        });
        
        speedIncreaseTimer.start();

    }

    public void moveBalls() {
            
        if (!gameOver && ballShouldMove) {    
            ball.setX(ball.getX() + ball.getVx());
            ball.setY(ball.getY() + ball.getVy());
        }
    
        if (ball.getX() > getWidth() - ball.getShape().width || ball.getX() < 0) {
            
            playSound("sounds\\collisionsound3.wav");
            
            if (ball.getX() > getWidth() - ball.getShape().width) {
                
                ball.setVx(-Math.abs(ball.getVx()));
                leftScore += 1;

            } else if (ball.getX() < 0) {
                
                ball.setVx(Math.abs(ball.getVx()));
                rightScore += 1;

            }

            resetBallLocation();
            ballShouldMove = false;

        }

        if (ball.getY() > getHeight() - ball.getShape().height || ball.getY() < 0) {
            
            playSound("sounds\\collisionsound2.wav");
            ball.setVy(-ball.getVy());

        }

    }

    public void gameOver() {

        if (rightScore >= 10 || leftScore >= 10) {
            
            playSound("sounds\\gameoversound1.wav");
            gameOver = true;
            if (rightScore >= 10) {
                winner = "Right Player";
            } else {
                winner = "Left Player";
            }

            rightScore = 0;
            leftScore = 0;

        }

    }

    private void restartGame() {

        gameOver = false;
        winner = "";
        resetBallLocation();
        ballShouldMove = false;
        rightScore = 0;
        leftScore = 0;

    }
    
    public void resetBallLocation() {
        
        ball.setX((int) (Interface.frameWidth / 2));
        ball.setY((int) (Interface.frameHeight / 2));

        ball.setVx(10);
        ball.setVy(3);

        vy = (Math.random() < 0.5) ? vy : -1*vy;
    }
    

    public void checkCollision() {
            
        for (ColoredShape tile : tiles) {
                
            if (ball.getShape().getBounds().intersects(tile.getShape().getBounds())) {

                playSound("sounds\\collisionsound1.wav");
                
                // Calculating the angle of incidence
                double tileCenterX = tile.getShape().getBounds2D().getCenterX();
                double tileCenterY = tile.getShape().getBounds2D().getCenterY();
                double ballCenterX = ball.getShape().getCenterX();
                double ballCenterY = ball.getShape().getCenterY();

                double angle = Math.atan2(ballCenterY - tileCenterY, ballCenterX - tileCenterX);

                if (Math.abs(angle) < Math.PI / 2) {
                    ball.setVx(-ball.getVx());
                } else {
                    ball.setVy(-ball.getVy());
                }
                    
            }

        }
    }

    public void moveTiles() {

        if (moveTile1Up) {
            Rectangle bounds = (Rectangle) tiles.get(0).getShape().getBounds2D();
            int newY = (int) (bounds.getY() - 5);
            
            if (newY >= 0) { 
                tiles.get(0).setShape(new Rectangle((int) bounds.getX(), newY, tileWidth, tileHeight));
            }

        } else if (moveTile1Down) {
            
            Rectangle bounds = (Rectangle) tiles.get(0).getShape().getBounds2D();
            int newY = (int) (bounds.getY() + 5);
            
            if (newY + tileHeight <= getHeight()) { // Check if the new position is within the frame
                tiles.get(0).setShape(new Rectangle((int) bounds.getX(), newY, tileWidth, tileHeight));
            }

        }
    
        if (moveTile2Up) {

            Rectangle bounds = (Rectangle) tiles.get(1).getShape().getBounds2D();
            int newY = (int) (bounds.getY() - 5);
            
            if (newY >= 0) { // Check if the new position is within the frame
                tiles.get(1).setShape(new Rectangle((int) bounds.getX(), newY, tileWidth, tileHeight));
            }

        } else if (moveTile2Down) {

            Rectangle bounds = (Rectangle) tiles.get(1).getShape().getBounds2D();
            int newY = (int) (bounds.getY() + 5);
            
            if (newY + tileHeight <= getHeight()) { // Check if the new position is within the frame
                tiles.get(1).setShape(new Rectangle((int) bounds.getX(), newY, tileWidth, tileHeight));
            }

        }
    }
    

    public void playSound(String filePath) {

        try {

            File sound = new File(filePath);
            AudioInputStream audioInput = AudioSystem.getAudioInputStream(sound);
            Clip clip = AudioSystem.getClip();
            clip.open(audioInput);
            clip.start();

        } catch (UnsupportedAudioFileException | IOException | LineUnavailableException e) {

            e.printStackTrace();

        }

    }

    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g;

        for (ColoredShape tile : tiles) {
            g2d.setColor(tile.getColor());
            g2d.fill(tile.getShape());
        }

        g2d.setColor(ball.getColor());
        g2d.fill(ball.getShape());

        String strLeftScore = String.valueOf(leftScore);
        String strRightScore = String.valueOf(rightScore);

        Font scoreFont = new Font("Arial", Font.BOLD, 36);
        g2d.setFont(scoreFont);

        FontMetrics fmLeft = g.getFontMetrics(scoreFont);
        FontMetrics fmRight = g.getFontMetrics(scoreFont);
        int xLeft = (getWidth() - fmLeft.stringWidth(strLeftScore)) / 2 - 80;
        int xRight = (getWidth() - fmRight.stringWidth(strRightScore)) / 2 + 80;
        int yLeft = 40;
        int yRight = 40;
            
        g2d.drawString(strLeftScore, xLeft, yLeft);
        g2d.drawString(strRightScore, xRight, yRight);

        if (gameOver) {
            
            String gameOverMessage = "Game Over, " + winner + " won.";
            
            Font font = new Font("Arial", Font.BOLD, 36);
            g2d.setFont(font);
            
            FontMetrics fm = g.getFontMetrics(font);
            int x = (getWidth() - fm.stringWidth(gameOverMessage)) / 2;
            int y = getHeight() / 2;
            
            g2d.drawString(gameOverMessage, x, y);
            
        }
        
    }

    public int getRightScore() {
        return rightScore;
    }

    public int getLeftScore() {
        return leftScore;
    }

}

Solution

  • Your collision detection algorithm is insufficient. If the ball's speed increases to a point where it moves a significant distance between frames, or if the pad object is oriented perpendicular to the direction of motion and the ball approaches at a shallow angle, it's possible for ball to pass through objects without being detected (at least at the right time) by the collision detection mechanism. This is called tunneling.

    • I divided the movement of the ball into numSteps substeps based on the maximum velocity component. This ensures that the ball's movement is sufficiently sampled to detect collisions correctly.
    • I used (int) casting in setVy and setVx, because you have defined vx and vy as an integer.

    New checkCollision() method:

    public void checkCollision() {
                
            for (ColoredShape tile : tiles) {
                    
                double ballX = ball.getX();
                double ballY = ball.getY();
                double ballRadius = ball.getRadius();
                double ballVx = ball.getVx();
                double ballVy = ball.getVy();
    
                Shape tileShape = tile.getShape();
                Rectangle2D tileBounds = tileShape.getBounds2D();
                double tileLeft = tileBounds.getMinX();
                double tileRight = tileBounds.getMaxX();
                double tileTop = tileBounds.getMinY();
                double tileBottom = tileBounds.getMaxY();
    
                // Calculate the number of steps based on the maximum velocity component
                double maxSpeed = Math.max(Math.abs(ballVx), Math.abs(ballVy));
                int numSteps = (int) Math.ceil(maxSpeed / ballRadius);
    
                // Perform collision detection for each step
                for (int i = 0; i < numSteps; i++) {
                    double stepX = ballX + ballVx * (i + 1) / numSteps;
                    double stepY = ballY + ballVy * (i + 1) / numSteps;
    
                    if (stepX + ballRadius >= tileLeft && stepX - ballRadius <= tileRight &&
                    stepY + ballRadius >= tileTop && stepY - ballRadius <= tileBottom) {
    
                        playSound("sounds\\collisionsound1.wav");
    
                        // Determine the side of the collision
                        double overlapX = Math.max(0, Math.min(stepX + ballRadius, tileRight) - Math.max(stepX - ballRadius, tileLeft));
                        double overlapY = Math.max(0, Math.min(stepY + ballRadius, tileBottom) - Math.max(stepY - ballRadius, tileTop));
    
                        boolean collisionFromLeftOrRight = overlapX < overlapY;
    
                        if (collisionFromLeftOrRight) {
                            // Adjust x velocity
                            ball.setVx((int) -ballVx);
                        } else {
                            // Adjust y velocity
                            ball.setVy((int) -ballVy);
                        }
                        break; // Exit the loop after handling the collision
                    }
                }
    
            }
        }