Search code examples
javamathawtponglerp

How to add smooth movement to my java game?


my goal: in something like the pong, key controls should move bats smoothly.

expectation: want the bats to move smoothly up or down with keys. actual results: bats move like lagging or change in speed.

I tried using velocity vars with maybe lerp, but they lag. I tried Increasing and reducing y value, but it lags worse. I searched, but can't find a solution.

I am calling rectangles bats and circle pong. also, this is my first day and post here.

Screenshot from the game

here is the panal code:

import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.util.Random;

import javax.swing.JPanel;
import javax.swing.Timer;

public class gamePanel extends JPanel implements ActionListener {

    /**
     * 
     */
    private static final long serialVersionUID = 6350159535293799269L;
    static final int SCREEN_WIDTH = 840;
    static final int SCREEN_HEIGHT = 610;
    static final int PONG_SPAWN_FRAME = 50;
    static final int padding = 30;

    
    
    static final int pongSize = 30;
    static final int batHeight = SCREEN_HEIGHT / 4;
    static final int batWidth = SCREEN_WIDTH / 32;
    static final int batSpeed = 4;
    static final int pongSpeed = 3;
    static final int scoreTextSize = 45;
    
    boolean running = false;
    
    int redX = padding;
    int redY = SCREEN_HEIGHT / 2;
    int redVelocity = 0;

    int blueX = SCREEN_WIDTH - padding;
    int blueY = SCREEN_HEIGHT / 2;
    int blueVelocity = 0;

    int redPoints = 0;
    int bluePoints = 0;

    int pongX = SCREEN_WIDTH / 4 - pongSize / 2;
    int pongY = 0;

    int pongVelocityX = pongSpeed;
    int pongVelocityY = pongSpeed;

    final int DELAY = 6;
    Timer timer;

    Random random;

    gamePanel() {
        random = new Random();
        pongY = random.nextInt((int) (SCREEN_HEIGHT - PONG_SPAWN_FRAME) + PONG_SPAWN_FRAME);
        if (pongVelocityY == 0) {
            if (pongY > SCREEN_HEIGHT / 2 + pongSize) {
                pongVelocityY = -pongSpeed;
            } else {
                pongVelocityY = pongSpeed;
            }
        }
        this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
        this.setBackground(Color.DARK_GRAY);
        this.setFocusable(true);
        this.addKeyListener(new myKeyAdapter());
        startGame();
    }

    void startGame() {
        running = true;
        timer = new Timer(DELAY, this);
        timer.start();

    }

    void restart() {
        redX = padding;
        redY = SCREEN_HEIGHT / 2;
        redVelocity = 0;

        blueX = SCREEN_WIDTH - padding;
        blueY = SCREEN_HEIGHT / 2;
        blueVelocity = 0;

        pongX = SCREEN_WIDTH / 4 - pongSize / 2;
        pongY = 0;

        pongVelocityX = pongSpeed;
        pongVelocityY = pongSpeed;

        spawnPong();

    }

    void spawnPong() {
        pongY = random.nextInt((int) (SCREEN_HEIGHT - PONG_SPAWN_FRAME * 2)) + PONG_SPAWN_FRAME;
        if (pongVelocityY == 0) {
            if (pongY > SCREEN_HEIGHT / 2 + pongSize) {
                pongVelocityY = -pongSpeed;
            } else {
                pongVelocityY = pongSpeed;
            }
        }
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        draw(g);
    }

    void draw(Graphics g) {
        g.setColor(Color.LIGHT_GRAY);
        g.fillOval(pongX, pongY, pongSize, pongSize);
        g.setColor(Color.red);
        g.fillRect(redX - batWidth / 2, redY - batHeight / 2, batWidth, batHeight);
        g.setColor(Color.blue);
        g.fillRect(blueX - batWidth / 2, blueY - batHeight / 2, batWidth, batHeight);
        g.setColor(Color.white);
        g.setFont(new Font("Roman New Times", Font.PLAIN, scoreTextSize));
        g.drawString(String.valueOf(redPoints), SCREEN_WIDTH / 4, scoreTextSize);
        g.drawString(String.valueOf(bluePoints), SCREEN_WIDTH / 4 * 3, scoreTextSize);
    }

    void pongMove() {
        pongX += pongVelocityX;
        pongY += pongVelocityY;
    }

    void batMove() {
        if (redY > batHeight / 2 && redVelocity < 0) {
            redY += redVelocity * batSpeed;
        }
        if (redY < SCREEN_HEIGHT - batHeight / 2 && redVelocity > 0) {
            redY += redVelocity * batSpeed;
        }

        blueY = pongY;
        /*
         * if (blueY>batHeight/2 && blueVelocity < 0) { blueY += blueVelocity *
         * pongSpeed; } if (blueY<SCREEN_HEIGHT-batHeight/2 && blueVelocity > 0) { blueY
         * += blueVelocity * pongSpeed; }
         */
    }

    void batLerp() {
        redVelocity *= 0.5f;
        blueVelocity *= 0.5f; 
    }

    void checkCollision() {
        if ((pongX > blueX - pongSize - batWidth / 2 && pongX < blueX - pongSize + batWidth / 2)
                && (pongY > blueY - batHeight / 2 && pongY < blueY + batHeight / 2)) {
            pongVelocityX *= -1;
        }
        if (pongX < redX + batWidth / 2 && (pongY > redY - batHeight / 2 && pongY < redY + batHeight / 2)) {
            pongVelocityX *= -1;
        }
        if (pongY < 0) {
            pongVelocityY *= -1;
        }
        if (pongY > SCREEN_HEIGHT - pongSize) {
            pongVelocityY *= -1;
        }
        if (pongX < -pongSize) {
            bluePoints += 1;
            restart();
        }
        if (pongX > SCREEN_WIDTH) {
            redPoints += 1;
            restart();
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (running) {
            checkCollision();
            batMove();
            batLerp();
            pongMove();
        }
        repaint();

    }

    public class myKeyAdapter extends KeyAdapter {
        public void keyPressed(KeyEvent e) {
            switch (e.getKeyCode()) {
            case KeyEvent.VK_W:
                redVelocity = -1;
                break;
            case KeyEvent.VK_S:
                redVelocity = 1;
                break;
            case KeyEvent.VK_UP:
                blueVelocity = -1;
                break;
            case KeyEvent.VK_DOWN:
                blueVelocity = 1;
                break;
            }
        }
    }
}

Solution

  • Original Answer

    I modified the paddle moving so that the paddle only moves when you hold the key down. I had to slow the game down a good bit for testing.

    I had trouble following your code. Your JPanel class is doing too much work and is too hard to understand. I'd create a Ball class and a Paddle class from plain Java getter / setter classes to hold the fields for the ball and a paddle.

    Here's the complete runnable code.

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Font;
    import java.awt.Graphics;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.KeyAdapter;
    import java.awt.event.KeyEvent;
    import java.util.Random;
    
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    
    public class ClassicPongGUI implements Runnable {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new ClassicPongGUI());
        }
    
        @Override
        public void run() {
            JFrame frame = new JFrame("Pong");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            frame.add(new GamePanel());
    
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setVisible(true);
        }
    
        public class GamePanel extends JPanel implements ActionListener {
    
            private static final long serialVersionUID = 6350159535293799269L;
    
            static final int SCREEN_WIDTH = 840;
            static final int SCREEN_HEIGHT = 610;
            static final int PONG_SPAWN_FRAME = 50;
            static final int padding = 30;
    
            static final int pongSize = 30;
            static final int batHeight = SCREEN_HEIGHT / 4;
            static final int batWidth = SCREEN_WIDTH / 32;
            static final int batSpeed = 20;
            static final int pongSpeed = 1;
            static final int scoreTextSize = 45;
    
            boolean running = false;
    
            int redX = padding;
            int redY = SCREEN_HEIGHT / 2;
            int redVelocity = 0;
    
            int blueX = SCREEN_WIDTH - padding;
            int blueY = SCREEN_HEIGHT / 2;
            int blueVelocity = 0;
    
            int redPoints = 0;
            int bluePoints = 0;
    
            int pongX = SCREEN_WIDTH / 4 - pongSize / 2;
            int pongY = 0;
    
            int pongVelocityX = pongSpeed;
            int pongVelocityY = pongSpeed;
    
            final int DELAY = 6;
            Timer timer;
    
            Random random;
    
            public GamePanel() {
                random = new Random();
                pongY = random.nextInt((int) (SCREEN_HEIGHT - PONG_SPAWN_FRAME) + PONG_SPAWN_FRAME);
                if (pongVelocityY == 0) {
                    if (pongY > SCREEN_HEIGHT / 2 + pongSize) {
                        pongVelocityY = -pongSpeed;
                    } else {
                        pongVelocityY = pongSpeed;
                    }
                }
                this.setPreferredSize(new Dimension(SCREEN_WIDTH, SCREEN_HEIGHT));
                this.setBackground(Color.DARK_GRAY);
                this.setFocusable(true);
                this.addKeyListener(new myKeyAdapter());
                startGame();
            }
    
            void startGame() {
                running = true;
                timer = new Timer(DELAY, this);
                timer.start();
    
                redX = padding;
                redY = SCREEN_HEIGHT / 2;
                redVelocity = 0;
            }
    
            void restart() {
                blueX = SCREEN_WIDTH - padding;
                blueY = SCREEN_HEIGHT / 2;
                blueVelocity = 0;
    
                pongX = SCREEN_WIDTH / 4 - pongSize / 2;
                pongY = 0;
    
                pongVelocityX = pongSpeed;
                pongVelocityY = pongSpeed;
    
                spawnPong();
    
            }
    
            void spawnPong() {
                pongY = random.nextInt((int) (SCREEN_HEIGHT - PONG_SPAWN_FRAME * 2)) + PONG_SPAWN_FRAME;
                if (pongVelocityY == 0) {
                    if (pongY > SCREEN_HEIGHT / 2 + pongSize) {
                        pongVelocityY = -pongSpeed;
                    } else {
                        pongVelocityY = pongSpeed;
                    }
                }
            }
    
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                draw(g);
            }
    
            void draw(Graphics g) {
                g.setColor(Color.LIGHT_GRAY);
                g.fillOval(pongX, pongY, pongSize, pongSize);
                g.setColor(Color.red);
                g.fillRect(redX - batWidth / 2, redY - batHeight / 2, batWidth, batHeight);
                g.setColor(Color.blue);
                g.fillRect(blueX - batWidth / 2, blueY - batHeight / 2, batWidth, batHeight);
                g.setColor(Color.white);
                g.setFont(new Font("Times New Roman", Font.PLAIN, scoreTextSize));
                g.drawString(String.valueOf(redPoints), SCREEN_WIDTH / 4, scoreTextSize);
                g.drawString(String.valueOf(bluePoints), SCREEN_WIDTH / 4 * 3, scoreTextSize);
            }
    
            void pongMove() {
                pongX += pongVelocityX;
                pongY += pongVelocityY;
            }
    
            void batMove() {
                if (redY > batHeight / 2 && redVelocity < 0) {
                    redY += redVelocity * batSpeed;
                }
                if (redY < SCREEN_HEIGHT - batHeight / 2 && redVelocity > 0) {
                    redY += redVelocity * batSpeed;
                }
    
                blueY = pongY;
                /*
                 * if (blueY>batHeight/2 && blueVelocity < 0) { blueY += blueVelocity *
                 * pongSpeed; } if (blueY<SCREEN_HEIGHT-batHeight/2 && blueVelocity > 0) { blueY
                 * += blueVelocity * pongSpeed; }
                 */
            }
    
            void batLerp() {
                redVelocity = 0;
                blueVelocity = 0;
            }
    
            void checkCollision() {
                if ((pongX > blueX - pongSize - batWidth / 2 
                        && pongX < blueX - pongSize + batWidth / 2)
                        && (pongY > blueY - batHeight / 2 
                        && pongY < blueY + batHeight / 2)) {
                    pongVelocityX *= -1;
                }
                if (pongX < redX + batWidth / 2 && (pongY > redY - batHeight / 2 
                        && pongY < redY + batHeight / 2)) {
                    pongVelocityX *= -1;
                }
                if (pongY < 0) {
                    pongVelocityY *= -1;
                }
                if (pongY > SCREEN_HEIGHT - pongSize) {
                    pongVelocityY *= -1;
                }
                if (pongX < -pongSize) {
                    bluePoints += 1;
                    restart();
                }
                if (pongX > SCREEN_WIDTH) {
                    redPoints += 1;
                    restart();
                }
            }
    
            @Override
            public void actionPerformed(ActionEvent e) {
                if (running) {
                    checkCollision();
                    batMove();
                    batLerp();
                    pongMove();
                }
                repaint();
    
            }
    
            public class myKeyAdapter extends KeyAdapter {
                @Override
                public void keyPressed(KeyEvent e) {
                    switch (e.getKeyCode()) {
                    case KeyEvent.VK_W:
                        redVelocity = -1;
                        break;
                    case KeyEvent.VK_S:
                        redVelocity = 1;
                        break;
                    case KeyEvent.VK_UP:
                        redVelocity = -1;
                        break;
                    case KeyEvent.VK_DOWN:
                        redVelocity = 1;
                        break;
                    }
                }
            }
            
        }
    
    }
    

    Revised Answer

    I thought this would be an interesting project, so I reworked the code. Here's how the GUI appears.

    Pong GUI

    The W and up arrow keys move the blue paddle up. The S and down arrow keys move the blue paddle down. These keys must be held down to continue the motion. Releasing the key stops the paddle motion,

    The space bar restarts the game.

    When I create a Swing GUI, I use the model / view / controller (MVC) pattern. This pattern allows me to separate my concerns and focus on one small part of the application at a time.

    For a Swing GUI, the MVC pattern is defined as:

    1. The view reads information from the model.
    2. The view does not update the model.
    3. The controller updates the model and repaints / revalidates the view.

    A Swing GUI can have many controller classes. I created three controller classes for this Pong game.

    I created 8 different classes. Three classes make up the application model, two classes define the view, and three controller classes manage the application.

    Model

    I created a Ball class to hold the fields needed to create the ball and move the ball around the screen. I use a Point to hold the center point and an int to hold the radius of the ball.

    The Ball class is not your typical getter / setter class. The moveBall method uses polar coordinates to calculate the new position of the ball. The polar coordinates are converted to cartesian coordinates to draw the ball on the drawing JPanel.

    The Paddle class creates a paddle. I store the shape of the paddle in a Rectangle. This is a more typical Java getter / setter class, although there's code to limit how far the paddle can travel on the Y-axis.

    The GameModel class is the application model. This class holds a Ball instance and two Paddle instances, as well as two score ints and the frame rate that the game runs at. An instance of this class is passed through the view and controller classes.

    View

    I create a JFrame and a drawing JPanel. The drawing JPanel draws the current position of the ball, two paddles, and the scores. Period. Nothing else.

    The controller classes are responsible for moving the ball and paddles and redrawing the JPanel.

    I use KeyBindings to set the keyboard keys to various actions. With KeyBindings, I don't have to worry about whether or not the drawing JPanel has focus.

    Controller

    The TimerListener class animates the Pong game.

    The MovePaddleAction class moves the paddle up or down, depending on which key is pressed. The key must be held down to continue the motion.

    The RestartAction class restarts the game.

    Code

    Here's the complete runnable code.

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Font;
    import java.awt.Graphics;
    import java.awt.Point;
    import java.awt.Rectangle;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.event.KeyEvent;
    import java.awt.geom.Point2D;
    import java.util.Random;
    
    import javax.swing.AbstractAction;
    import javax.swing.ActionMap;
    import javax.swing.InputMap;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.KeyStroke;
    import javax.swing.SwingUtilities;
    import javax.swing.Timer;
    
    public class ClassicPongGUI implements Runnable {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new ClassicPongGUI());
        }
        
        private final Dimension panelSize;
        
        private GameModel gameModel;
        
        private GamePanel gamePanel;
        
        private Timer timer;
        
        public ClassicPongGUI() {
            this.panelSize = new Dimension(840, 610);
            this.gameModel = new GameModel(panelSize);
        }
    
        @Override
        public void run() {
            JFrame frame = new JFrame("Pong");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    
            this.gamePanel = new GamePanel(this, gameModel);
            frame.add(gamePanel, BorderLayout.CENTER);
    
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setVisible(true);
            
            setKeyBindings();
            startTimer();
        }
        
        private void setKeyBindings() {
            String moveUp = "moveUp";
            String moveDown = "moveDown";
            String restart = "restart";
            
            InputMap inputMap = gamePanel.getInputMap(
                    JPanel.WHEN_IN_FOCUSED_WINDOW);
            ActionMap actionMap = gamePanel.getActionMap();
            
            inputMap.put(KeyStroke.getKeyStroke(
                    KeyEvent.VK_W, 0), moveUp);
            inputMap.put(KeyStroke.getKeyStroke(
                    KeyEvent.VK_UP, 0), moveUp);
            inputMap.put(KeyStroke.getKeyStroke(
                    KeyEvent.VK_S, 0), moveDown);
            inputMap.put(KeyStroke.getKeyStroke(
                    KeyEvent.VK_DOWN, 0), moveDown);
            inputMap.put(KeyStroke.getKeyStroke(
                    KeyEvent.VK_SPACE, 0), restart);
            
            int increment = 15;
            actionMap.put(moveUp, new MovePaddleAction(
                    this, gameModel, -increment));
            actionMap.put(moveDown, new MovePaddleAction(
                    this, gameModel, increment));
            actionMap.put(restart, new RestartAction(
                    this, gameModel));
        }
        
        public void startTimer() {
            int delay = 1000 / gameModel.getFrameRate();
            timer = new Timer(delay, new TimerListener(this, gameModel));
            timer.setInitialDelay(5000);
            timer.start();
        }
        
        public void stopTimer() {
            if (timer != null) {
                timer.stop();
            }
        }
        
        public Dimension getPanelSize() {
            return panelSize;
        }
    
        public void repaint() {
            gamePanel.repaint();
        }
    
        public class GamePanel extends JPanel {
    
            private static final long serialVersionUID = 1L;
            
            private final GameModel model;
            
            public GamePanel(ClassicPongGUI frame, GameModel model) {
                this.model = model;
                
                this.setBackground(Color.DARK_GRAY);
                this.setPreferredSize(frame.getPanelSize());
            }
            
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
                
                drawBall(g);
                drawPaddles(g);
                drawScores(g);
            }
    
            private void drawBall(Graphics g) {
                Ball ball = model.getBall();
                g.setColor(ball.getColor());
                int radius = ball.getRadius();
                int diameter = radius + radius;
                Point point = ball.getCenterPoint();
                g.fillOval(point.x - radius, point.y - radius, 
                        diameter, diameter);
            }
    
            private void drawPaddles(Graphics g) {
                Paddle[] paddles = model.getPaddles();
                for (Paddle paddle : paddles) {
                    g.setColor(paddle.getColor());
                    Rectangle r = paddle.getPaddle();
                    g.fillRect(r.x, r.y, r.width, r.height);
                }
            }
    
            private void drawScores(Graphics g) {
                int[] scores = model.getScore();
                int scoreTextSize = 45;
                g.setColor(Color.WHITE);
                g.setFont(new Font("Times New Roman", 
                        Font.PLAIN, scoreTextSize));
                g.drawString(String.valueOf(scores[0]), 
                        panelSize.width / 4, scoreTextSize);
                g.drawString(String.valueOf(scores[1]), 
                        panelSize.width * 3 / 4, scoreTextSize);
            }
            
        }
        
        public class TimerListener implements ActionListener {
            
            private final ClassicPongGUI frame;
            
            private final GameModel model;
    
            public TimerListener(ClassicPongGUI frame, GameModel model) {
                this.frame = frame;
                this.model = model;
            }
    
            @Override
            public void actionPerformed(ActionEvent event) {
                int frameRate = model.getFrameRate();
                model.getBall().moveBall(frameRate);
                
                Point point = model.getBall().getCenterPoint();
                Paddle[] paddles = model.getPaddles();
                for (int i = 0; i < paddles.length; i++) {
                    Rectangle p = paddles[i].getPaddle();
                    int y = point.y - p.height / 2;
                    if (i == 0) {
                        paddles[i].setPaddle(y);
                    }
                    model.contactsPaddle();
                }
                
                if (point.x < 0) {
                    model.incrementScore(1, 1);
                    model.resetBall();
                }
                
                if (point.x > frame.getPanelSize().width) {
                    model.incrementScore(0, 1);
                    model.resetBall();
                }
                
                frame.repaint();
            }
            
        }
        
        public class MovePaddleAction extends AbstractAction {
    
            private static final long serialVersionUID = 1L;
            
            private final int increment;
    
            private final ClassicPongGUI frame;
            
            private final GameModel model;
    
            public MovePaddleAction(ClassicPongGUI frame, GameModel model, 
                    int increment) {
                this.frame = frame;
                this.model = model;
                this.increment = increment;
            }
    
            @Override
            public void actionPerformed(ActionEvent event) {
                model.getPaddles()[1].movePaddle(increment);
                frame.repaint();
            }
            
        }
        
        public class RestartAction extends AbstractAction {
    
            private static final long serialVersionUID = 1L;
    
            private final ClassicPongGUI frame;
            
            private final GameModel model;
    
            public RestartAction(ClassicPongGUI frame, GameModel model) {
                this.frame = frame;
                this.model = model;
            }
    
            @Override
            public void actionPerformed(ActionEvent event) {
                frame.stopTimer();
                model.restartGame();
                frame.startTimer();
                frame.repaint();
            }
            
        }
            
        public class GameModel {
    
            private int[] score;
            
            /** frames per second **/
            private final int frameRate;
    
            private Ball ball;
    
            private final Dimension panelSize;
    
            private Paddle[] paddles;
    
            public GameModel(Dimension panelSize) {
                this.panelSize = panelSize;
                this.score = new int[2];
                this.paddles = new Paddle[2];
                this.frameRate = 50;
    
                int ballRadius = 15;
                Point centerPoint = new Point(panelSize.width / 2, 
                        panelSize.height / 2);
                this.ball = new Ball(centerPoint, ballRadius, 
                        panelSize, Color.LIGHT_GRAY);
    
                int batWidth = panelSize.width / 32;
                int batHeight = panelSize.height / 4;
                int padding = 30;
                int x = padding;
                int y = calculateCenterY(batHeight);
                Rectangle r = new Rectangle(x, y, batWidth, batHeight);
                this.paddles[0] = new Paddle(r, panelSize.height, Color.RED);
    
                x = panelSize.width - padding - batWidth;
                r = new Rectangle(x, y, batWidth, batHeight);
                this.paddles[1] = new Paddle(r, panelSize.height, Color.BLUE);
    
                resetScore();
            }
            
            public void restartGame() {
                int batHeight = panelSize.height / 4;
                resetBall();
                paddles[0].setPaddle(calculateCenterY(batHeight));
                paddles[1].setPaddle(calculateCenterY(batHeight));
                resetScore();
            }
            
            public void resetBall() {
                ball.setCenterPoint(new Point(panelSize.width / 2, 
                        panelSize.height / 2));
            }
    
            private int calculateCenterY(int batHeight) {
                return (panelSize.height - batHeight) / 2;
            }
            
            public void contactsPaddle() {
                int radius = ball.getRadius();
                int diameter = radius + radius;
                Point point = ball.getCenterPoint();
                Rectangle b = new Rectangle(point.x - radius, 
                        point.y - radius, diameter, diameter);
                for (Paddle paddle : paddles) {
                    if (paddle.getPaddle().intersects(b)) {
                        ball.reverseXDirection(frameRate);
                    }
                }
                
            }
    
            public void resetScore() {
                score[0] = 0;
                score[1] = 0;
            }
    
            public void incrementScore(int index, int increment) {
                score[index] += increment;
            }
    
            public int[] getScore() {
                return score;
            }
    
            public Ball getBall() {
                return ball;
            }
    
            public Paddle[] getPaddles() {
                return paddles;
            }
    
            public int getFrameRate() {
                return frameRate;
            }
    
        }
    
        public class Paddle {
    
            private final int upperBound;
    
            private final Color color;
    
            private Rectangle paddle;
    
            public Paddle(Rectangle paddle, int upperBound, Color color) {
                this.paddle = paddle;
                this.upperBound = upperBound;
                this.color = color;
            }
    
            public void movePaddle(int increment) {
                paddle.y += increment;
                limitMovement();
            }
            
            public void setPaddle(int y) {
                paddle.y = y;
                limitMovement();
            }
    
            private void limitMovement() {
                paddle.y = Math.max(paddle.y, 0);
                int height = paddle.y + paddle.height;
                height = Math.min(height, upperBound);
                paddle.y = height - paddle.height;
            }
    
            public Rectangle getPaddle() {
                return paddle;
            }
    
            public Color getColor() {
                return color;
            }
    
        }
    
        public class Ball {
            
            /** Pixels per second **/
            private final double velocity;
    
            private final int radius;
            private int angle;
    
            private final Color color;
    
            private final Dimension panelSize;
    
            private Point centerPoint;
            
            private Point2D.Double doubleCenterPoint;
            
            private Random random;
    
            public Ball(Point centerPoint, int radius, Dimension panelSize, 
                    Color color) {
                this.centerPoint = centerPoint;
                setDoubleCenterPoint(centerPoint);
                this.radius = radius;
                this.panelSize = panelSize;
                this.color = color;
                this.velocity = 300.0;
                this.random = new Random();
                setRandomAngle();
            }
            
            public void reverseXDirection(int frameRate) {
                angle = bounceXAngle(angle);
                angle += (angle < 0) ? 360 : 0;
                moveBall(frameRate);
            }
            
            public void moveBall(int frameRate) {
                double theta = Math.toRadians(angle);
                double distance = velocity / frameRate;
                doubleCenterPoint.x = Math.cos(theta) * distance + 
                        doubleCenterPoint.x;
                doubleCenterPoint.y = Math.sin(theta) * distance + 
                        doubleCenterPoint.y;
                
                if (doubleCenterPoint.y < radius) {
                    double adjustment = radius - doubleCenterPoint.y;
                    doubleCenterPoint.y += adjustment;
                    angle = bounceYAngle(angle);
                }
                
                if ((doubleCenterPoint.y + radius) > panelSize.height) {
                    double adjustment = panelSize.height - 
                            doubleCenterPoint.y - radius;
                    doubleCenterPoint.y += adjustment;
                    angle = bounceYAngle(angle);
                }
                
                int x = (int) Math.round(doubleCenterPoint.x);
                int y = (int) Math.round(doubleCenterPoint.y);
                this.centerPoint = new Point(x, y);
            }
            
            private int bounceXAngle(int angle) {
                return 180 - angle;
            }
    
            
            private int bounceYAngle(int angle) {
                return 360 - angle;
            }
            
            public Point getCenterPoint() {
                return centerPoint;
            }
    
            public void setCenterPoint(Point centerPoint) {
                this.centerPoint = centerPoint;
                setRandomAngle();
                setDoubleCenterPoint(centerPoint);
            }
    
            private void setDoubleCenterPoint(Point centerPoint) {
                this.doubleCenterPoint = 
                        new Point2D.Double(centerPoint.x, centerPoint.y);
            }
            
            private void setRandomAngle() {
                int[] angles = {30, 150, 210, 330 };
                this.angle = angles[random.nextInt(angles.length)];
            }
    
            public int getRadius() {
                return radius;
            }
    
            public Color getColor() {
                return color;
            }
    
        }
    
    }