如何为我的 java 游戏添加流畅的动作?

How to add smooth movement to my java game?

我的目标:在像乒乓球这样的游戏中,关键控件应该能平稳地移动球棒。

期望:希望蝙蝠能顺畅的上下移动。 实际结果:蝙蝠移动起来像滞后或速度变化。

我试过使用带有 lerp 的速度变量,但它们滞后了。 我试过增加和减少 y 值,但它滞后得更厉害。 我搜索过,但找不到解决方案。

我叫长方形球棒和圆形乒乓球。 另外,这是我的 第一天,post 在这里。

Screenshot from the game

这里是 panal 代码:

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;
            }
        }
    }
}

原答案

我修改了桨的移动,只有按住键时桨才会移动。为了测试,我不得不放慢游戏速度。

我在遵循您的代码时遇到了问题。您的 JPanel class 工作量太大,太难理解了。我将从普通 Java getter / setter class 创建一个 Ball class 和一个 Paddle class es 来保持球和桨的区域。

这是完整的可运行代码。

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;
                }
            }
        }
        
    }

}

修改后的答案

我认为这将是一个有趣的项目,所以我重新编写了代码。 GUI 的显示方式如下。

W 和向上箭头键向上移动蓝色桨。 S 和向下箭头键向下移动蓝色桨。必须按住这些键才能继续运动。释放键停止桨运动,

space 条重新开始游戏。

当我创建 Swing GUI 时,我使用 model / view / controller (MVC) 模式。这种模式使我能够将我的关注点分开,并一次专注于应用程序的一小部分。

对于 Swing GUI,MVC 模式定义为:

  1. 视图从模型中读取信息。
  2. 视图不更新模型。
  3. 控制器更新模型并重新绘制/重新验证视图。

一个 Swing GUI 可以有多个控制器 classes。我为这个 Pong 游戏创建了三个控制器 classes。

我创建了 8 个不同的 classes。三个 class 组成应用程序模型,两个 class 定义视图,三个控制器 class 管理应用程序。

型号

我创建了一个 Ball class 来保存创建球和在屏幕上移动球所需的字段。我用 Point 来保持圆心,用 int 来保持球的半径。

Ball class 不是典型的 getter / setter class。 moveBall 方法使用极坐标来计算球的新位置。极坐标转换为直角坐标在绘图JPanel上画球

Paddle class 创建一个球拍。我将桨的形状存储在 Rectangle 中。这是比较典型的Javagetter/setterclass,虽然有代码限制桨在Y轴上能走多远

GameModelclass是应用模型。这个 class 包含一个 Ball 实例和两个 Paddle 实例,以及两个分数 ints 和游戏运行的帧速率。此 class 的实例通过视图和控制器 classes.

传递

查看

我创建了一个 JFrame 和一个绘图 JPanel。绘图JPanel画出了球的当前位置、两个球拍和比分。时期。没有别的。

控制器 classes 负责移动球和球拍并重新绘制 JPanel

我使用 KeyBindings 将键盘键设置为各种操作。有了KeyBindings,我再也不用担心绘图JPanel是否有焦点了。

控制器

TimerListener class 动画 Pong 游戏。

MovePaddleAction class 根据按下的键向上或向下移动桨。必须按住该键才能继续运动。

RestartAction class 重新开始游戏。

代码

这是完整的可运行代码。

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;
        }

    }

}