Breakout 游戏
最后修改于 2023 年 1 月 10 日
在 Java 2D 游戏教程的这一部分,我们创建一个简单的 Breakout 游戏克隆。源代码和图像可以在作者的 Github Java-Breakout-Game 存储库中找到。
Breakout 是雅达利公司(Atari Inc.)最初开发的一款街机游戏。该游戏于 1976 年创作。
开发
在我们的游戏中,有一个挡板,一个球和 30 个砖块。我用 Inkscape 创建了球、挡板和砖块的图像。我们使用计时器来创建游戏周期。我们不使用角度,而是简单地改变方向:上、下、左、右。我受到 pybreakout 游戏的启发,它是由 Nathan Dawson 在 PyGame 库中开发的。
在这款游戏中,玩家在屏幕上移动挡板,并反弹一个或多个球。目标是摧毁窗口顶部的砖块。
游戏包含七个文件:Commons.java
, Sprite.java
, Ball.java
, Paddle.java
, Brick.java
, Board.java
, 和 Breakout.java
。
package com.zetcode; public interface Commons { int WIDTH = 300; int HEIGHT = 400; int BOTTOM_EDGE = 390; int N_OF_BRICKS = 30; int INIT_PADDLE_X = 200; int INIT_PADDLE_Y = 360; int INIT_BALL_X = 230; int INIT_BALL_Y = 355; int PERIOD = 10; }
Commons.java
文件包含一些通用常量。WIDTH
和 HEIGHT
常量存储了棋盘的尺寸。当球越过 BOTTOM_EDGE
时,游戏结束。N_OF_BRICKS
是游戏中砖块的数量。INIT_PADDLE_X
和 INIT_PADDLE_Y
是挡板对象的初始坐标。INIT_BALL_X
和 INIT_BALL_Y
是球对象的初始坐标。DELAY
是任务执行前的初始延迟(毫秒),PERIOD
是构成游戏周期的连续任务执行之间的时间间隔(毫秒)。
package com.zetcode; import java.awt.Image; import java.awt.Rectangle; public class Sprite { int x; int y; int imageWidth; int imageHeight; Image image; protected void setX(int x) { this.x = x; } int getX() { return x; } protected void setY(int y) { this.y = y; } int getY() { return y; } int getImageWidth() { return imageWidth; } int getImageHeight() { return imageHeight; } Image getImage() { return image; } Rectangle getRect() { return new Rectangle(x, y, image.getWidth(null), image.getHeight(null)); } void getImageDimensions() { imageWidth = image.getWidth(null); imageHeight = image.getHeight(null); } }
Sprite
类是 Board
中所有对象的基类。我们将所有在 Ball
, Brick
, 和 Paddle
对象中的方法和变量放在这里,例如 getImage()
或 getX()
方法。
package com.zetcode; import javax.swing.ImageIcon; public class Brick extends Sprite { private boolean destroyed; public Brick(int x, int y) { initBrick(x, y); } private void initBrick(int x, int y) { this.x = x; this.y = y; destroyed = false; loadImage(); getImageDimensions(); } private void loadImage() { var ii = new ImageIcon("src/resources/brick.png"); image = ii.getImage(); } boolean isDestroyed() { return destroyed; } void setDestroyed(boolean val) { destroyed = val; } }
这是 Brick
类。
private boolean destroyed;
在 destroyed
变量中,我们保留了砖块的状态。
package com.zetcode; import javax.swing.ImageIcon; public class Ball extends Sprite { private int xdir; private int ydir; public Ball() { initBall(); } private void initBall() { xdir = 1; ydir = -1; loadImage(); getImageDimensions(); resetState(); } private void loadImage() { var ii = new ImageIcon("src/resources/ball.png"); image = ii.getImage(); } void move() { x += xdir; y += ydir; if (x == 0) { setXDir(1); } if (x == Commons.WIDTH - imageWidth) { System.out.println(imageWidth); setXDir(-1); } if (y == 0) { setYDir(1); } } private void resetState() { x = Commons.INIT_BALL_X; y = Commons.INIT_BALL_Y; } void setXDir(int x) { xdir = x; } void setYDir(int y) { ydir = y; } int getYDir() { return ydir; } }
这是 Ball
类。
void move() { x += xdir; y += ydir; if (x == 0) { setXDir(1); } if (x == Commons.WIDTH - imageWidth) { System.out.println(imageWidth); setXDir(-1); } if (y == 0) { setYDir(1); } }
move()
方法在 Board
上移动球。如果球碰到边界,方向会相应改变。
void setXDir(int x) { xdir = x; } void setYDir(int y) { ydir = y; }
这两个方法在球碰到挡板或砖块时被调用。
package com.zetcode; import java.awt.event.KeyEvent; import javax.swing.ImageIcon; public class Paddle extends Sprite { private int dx; public Paddle() { initPaddle(); } private void initPaddle() { loadImage(); getImageDimensions(); resetState(); } private void loadImage() { var ii = new ImageIcon("src/resources/paddle.png"); image = ii.getImage(); } void move() { x += dx; if (x <= 0) { x = 0; } if (x >= Commons.WIDTH - imageWidth) { x = Commons.WIDTH - imageWidth; } } void keyPressed(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { dx = -1; } if (key == KeyEvent.VK_RIGHT) { dx = 1; } } void keyReleased(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { dx = 0; } if (key == KeyEvent.VK_RIGHT) { dx = 0; } } private void resetState() { x = Commons.INIT_PADDLE_X; y = Commons.INIT_PADDLE_Y; } }
这是 Paddle
类。它封装了 Breakout 游戏中的挡板对象。挡板通过左右箭头键进行控制。按下箭头键时,我们设置方向变量。释放箭头键时,我们将 dx
变量设置为零。这样挡板就会停止移动。
void move() { x += dx; if (x <= 0) { x = 0; } if (x >= Commons.WIDTH - imageWidth) { x = Commons.WIDTH - imageWidth; } }
挡板只在水平方向移动,所以我们只更新 x 坐标。if 条件确保挡板不会穿过窗口边缘。
package com.zetcode; import javax.swing.JPanel; import javax.swing.Timer; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Point; import java.awt.RenderingHints; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; public class Board extends JPanel { private Timer timer; private String message = "Game Over"; private Ball ball; private Paddle paddle; private Brick[] bricks; private boolean inGame = true; public Board() { initBoard(); } private void initBoard() { addKeyListener(new TAdapter()); setFocusable(true); setPreferredSize(new Dimension(Commons.WIDTH, Commons.HEIGHT)); gameInit(); } private void gameInit() { bricks = new Brick[Commons.N_OF_BRICKS]; ball = new Ball(); paddle = new Paddle(); int k = 0; for (int i = 0; i < 5; i++) { for (int j = 0; j < 6; j++) { bricks[k] = new Brick(j * 40 + 30, i * 10 + 50); k++; } } timer = new Timer(Commons.PERIOD, new GameCycle()); timer.start(); } @Override public void paintComponent(Graphics g) { super.paintComponent(g); var g2d = (Graphics2D) g; g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); if (inGame) { drawObjects(g2d); } else { gameFinished(g2d); } Toolkit.getDefaultToolkit().sync(); } private void drawObjects(Graphics2D g2d) { g2d.drawImage(ball.getImage(), ball.getX(), ball.getY(), ball.getImageWidth(), ball.getImageHeight(), this); g2d.drawImage(paddle.getImage(), paddle.getX(), paddle.getY(), paddle.getImageWidth(), paddle.getImageHeight(), this); for (int i = 0; i < Commons.N_OF_BRICKS; i++) { if (!bricks[i].isDestroyed()) { g2d.drawImage(bricks[i].getImage(), bricks[i].getX(), bricks[i].getY(), bricks[i].getImageWidth(), bricks[i].getImageHeight(), this); } } } private void gameFinished(Graphics2D g2d) { var font = new Font("Verdana", Font.BOLD, 18); FontMetrics fontMetrics = this.getFontMetrics(font); g2d.setColor(Color.BLACK); g2d.setFont(font); g2d.drawString(message, (Commons.WIDTH - fontMetrics.stringWidth(message)) / 2, Commons.WIDTH / 2); } private class TAdapter extends KeyAdapter { @Override public void keyReleased(KeyEvent e) { paddle.keyReleased(e); } @Override public void keyPressed(KeyEvent e) { paddle.keyPressed(e); } } private class GameCycle implements ActionListener { @Override public void actionPerformed(ActionEvent e) { doGameCycle(); } } private void doGameCycle() { ball.move(); paddle.move(); checkCollision(); repaint(); } private void stopGame() { inGame = false; timer.stop(); } private void checkCollision() { if (ball.getRect().getMaxY() > Commons.BOTTOM_EDGE) { stopGame(); } for (int i = 0, j = 0; i < Commons.N_OF_BRICKS; i++) { if (bricks[i].isDestroyed()) { j++; } if (j == Commons.N_OF_BRICKS) { message = "Victory"; stopGame(); } } if ((ball.getRect()).intersects(paddle.getRect())) { int paddleLPos = (int) paddle.getRect().getMinX(); int ballLPos = (int) ball.getRect().getMinX(); int first = paddleLPos + 8; int second = paddleLPos + 16; int third = paddleLPos + 24; int fourth = paddleLPos + 32; if (ballLPos < first) { ball.setXDir(-1); ball.setYDir(-1); } if (ballLPos >= first && ballLPos < second) { ball.setXDir(-1); ball.setYDir(-1 * ball.getYDir()); } if (ballLPos >= second && ballLPos < third) { ball.setXDir(0); ball.setYDir(-1); } if (ballLPos >= third && ballLPos < fourth) { ball.setXDir(1); ball.setYDir(-1 * ball.getYDir()); } if (ballLPos > fourth) { ball.setXDir(1); ball.setYDir(-1); } } for (int i = 0; i < Commons.N_OF_BRICKS; i++) { if ((ball.getRect()).intersects(bricks[i].getRect())) { int ballLeft = (int) ball.getRect().getMinX(); int ballHeight = (int) ball.getRect().getHeight(); int ballWidth = (int) ball.getRect().getWidth(); int ballTop = (int) ball.getRect().getMinY(); var pointRight = new Point(ballLeft + ballWidth + 1, ballTop); var pointLeft = new Point(ballLeft - 1, ballTop); var pointTop = new Point(ballLeft, ballTop - 1); var pointBottom = new Point(ballLeft, ballTop + ballHeight + 1); if (!bricks[i].isDestroyed()) { if (bricks[i].getRect().contains(pointRight)) { ball.setXDir(-1); } else if (bricks[i].getRect().contains(pointLeft)) { ball.setXDir(1); } if (bricks[i].getRect().contains(pointTop)) { ball.setYDir(1); } else if (bricks[i].getRect().contains(pointBottom)) { ball.setYDir(-1); } bricks[i].setDestroyed(true); } } } } }
这是 Board
类。我们在其中放置了游戏逻辑。
private void gameInit() { bricks = new Brick[Commons.N_OF_BRICKS]; ball = new Ball(); paddle = new Paddle(); int k = 0; for (int i = 0; i < 5; i++) { for (int j = 0; j < 6; j++) { bricks[k] = new Brick(j * 40 + 30, i * 10 + 50); k++; } } timer = new Timer(Commons.PERIOD, new GameCycle()); timer.start(); }
在 gameInit()
方法中,我们创建了一个球、一个挡板和三十个砖块。然后我们创建并启动一个计时器。
if (inGame) { drawObjects(g2d); } else { gameFinished(g2d); }
根据 inGame
变量,我们在 drawObjects()
方法中绘制所有对象,或者在 gameFinished()
方法中结束游戏。
private void drawObjects(Graphics2D g2d) { g2d.drawImage(ball.getImage(), ball.getX(), ball.getY(), ball.getImageWidth(), ball.getImageHeight(), this); g2d.drawImage(paddle.getImage(), paddle.getX(), paddle.getY(), paddle.getImageWidth(), paddle.getImageHeight(), this); for (int i = 0; i < Commons.N_OF_BRICKS; i++) { if (!bricks[i].isDestroyed()) { g2d.drawImage(bricks[i].getImage(), bricks[i].getX(), bricks[i].getY(), bricks[i].getImageWidth(), bricks[i].getImageHeight(), this); } } }
drawObjects()
方法绘制游戏的所有对象。精灵图像使用 drawImage()
方法绘制。
private void gameFinished(Graphics2D g2d) { var font = new Font("Verdana", Font.BOLD, 18); FontMetrics fontMetrics = this.getFontMetrics(font); g2d.setColor(Color.BLACK); g2d.setFont(font); g2d.drawString(message, (Commons.WIDTH - fontMetrics.stringWidth(message)) / 2, Commons.WIDTH / 2); }
gameFinished()
方法在窗口中间绘制“游戏结束”或“胜利”。
private class GameCycle implements ActionListener { @Override public void actionPerformed(ActionEvent e) { doGameCycle(); } }
计时器会周期性地调用 actionPerformed()
方法,该方法又调用 doGameCycle()
方法,从而创建一个游戏周期。
private void doGameCycle() { ball.move(); paddle.move(); checkCollision(); repaint(); }
doGameCycle()
方法移动球和挡板。我们检查可能的碰撞并重新绘制屏幕。
private void checkCollision() { if (ball.getRect().getMaxY() > Commons.BOTTOM_EDGE) { stopGame(); } ...
如果球碰到底部,我们就停止游戏。
for (int i = 0, j = 0; i < Commons.N_OF_BRICKS; i++) { if (bricks[i].isDestroyed()) { j++; } if (j == Commons.N_OF_BRICKS) { message = "Victory"; stopGame(); } }
我们检查被摧毁的砖块数量。如果我们摧毁了所有 N_OF_BRICKS
块砖,我们就赢得了游戏。
if (ballLPos < first) { ball.setXDir(-1); ball.setYDir(-1); }
如果球碰到挡板的第一部分,我们将球的方向改变为西北。
if (bricks[i].getRect().contains(pointTop)) { ball.setYDir(1); }...
如果球击中砖块的底部,我们改变球的 y 方向;它向下移动。
package com.zetcode; import javax.swing.JFrame; import java.awt.EventQueue; public class Breakout extends JFrame { public Breakout() { initUI(); } private void initUI() { add(new Board()); setTitle("Breakout"); setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); setResizable(false); pack(); } public static void main(String[] args) { EventQueue.invokeLater(() -> { var game = new Breakout(); game.setVisible(true); }); } }
这是 Breakout
类,它包含主入口方法。

这就是 Breakout 游戏。