原文: https://zetcode.com/tutorials/javagamestutorial/breakout/

在 Java 2D 游戏教程的这一部分中,我们创建一个简单的 Breakout 游戏克隆。 源代码和图像可以在作者的 Github Java-Breakout-Game 存储库中找到。

Breakout 是一款最初由 Atari Inc. 开发的街机游戏。该游戏创建于 1976 年。

在此游戏中,玩家移动屏幕上的桨叶并弹起一个或多个球。 目的是销毁窗口顶部的砖块。

开发

在我们的游戏中,我们只有一个桨,一个球和 30 块砖。 我在 Inkscape 中为球,桨和砖块创建了图像。 我们使用计时器来创建游戏周期。 我们不使用角度,只是改变方向。 顶部,底部,左侧和右侧。 pybreakout 游戏启发了我。 它是由 Nathan Dawson 在 PyGame 库中开发的。

游戏包含七个文件:Commons.javaSprite.javaBall.javaPaddle.javaBrick.javaBoard.javaBreakout.java

com/zetcode/Commons.java

  1. package com.zetcode;
  2. public interface Commons {
  3. int WIDTH = 300;
  4. int HEIGHT = 400;
  5. int BOTTOM_EDGE = 390;
  6. int N_OF_BRICKS = 30;
  7. int INIT_PADDLE_X = 200;
  8. int INIT_PADDLE_Y = 360;
  9. int INIT_BALL_X = 230;
  10. int INIT_BALL_Y = 355;
  11. int PERIOD = 10;
  12. }

Commons.java文件具有一些公共常数。 WIDTHHEIGHT常数存储电路板的大小。 当球通过BOTTOM_EDGE时,比赛结束。 N_OF_BRICKS是游戏中的积木数量。 INIT_PADDLE_XINIT_PADDLE_Y是桨状对象的初始坐标。 INIT_BALL_XINIT_BALL_Y是球对象的初始坐标。 DELAY是执行任务之前的初始延迟(以毫秒为单位),PERIOD是形成游戏周期的连续任务执行之间的时间(以毫秒为单位)。

com/zetcode/Sprite.java

  1. package com.zetcode;
  2. import java.awt.Image;
  3. import java.awt.Rectangle;
  4. public class Sprite {
  5. int x;
  6. int y;
  7. int imageWidth;
  8. int imageHeight;
  9. Image image;
  10. protected void setX(int x) {
  11. this.x = x;
  12. }
  13. int getX() {
  14. return x;
  15. }
  16. protected void setY(int y) {
  17. this.y = y;
  18. }
  19. int getY() {
  20. return y;
  21. }
  22. int getImageWidth() {
  23. return imageWidth;
  24. }
  25. int getImageHeight() {
  26. return imageHeight;
  27. }
  28. Image getImage() {
  29. return image;
  30. }
  31. Rectangle getRect() {
  32. return new Rectangle(x, y,
  33. image.getWidth(null), image.getHeight(null));
  34. }
  35. void getImageDimensions() {
  36. imageWidth = image.getWidth(null);
  37. imageHeight = image.getHeight(null);
  38. }
  39. }

Sprite类是Board中所有对象的基类。 我们将BallBrickPaddle对象中的所有方法和变量都放在此处,例如getImage()getX()方法。

com/zetcode/Brick.java

  1. package com.zetcode;
  2. import javax.swing.ImageIcon;
  3. public class Brick extends Sprite {
  4. private boolean destroyed;
  5. public Brick(int x, int y) {
  6. initBrick(x, y);
  7. }
  8. private void initBrick(int x, int y) {
  9. this.x = x;
  10. this.y = y;
  11. destroyed = false;
  12. loadImage();
  13. getImageDimensions();
  14. }
  15. private void loadImage() {
  16. var ii = new ImageIcon("src/resources/brick.png");
  17. image = ii.getImage();
  18. }
  19. boolean isDestroyed() {
  20. return destroyed;
  21. }
  22. void setDestroyed(boolean val) {
  23. destroyed = val;
  24. }
  25. }

这是Brick类。

  1. private boolean destroyed;

destroyed变量中,我们保留砖的状态。

com/zetcode/Ball.java

  1. package com.zetcode;
  2. import javax.swing.ImageIcon;
  3. public class Ball extends Sprite {
  4. private int xdir;
  5. private int ydir;
  6. public Ball() {
  7. initBall();
  8. }
  9. private void initBall() {
  10. xdir = 1;
  11. ydir = -1;
  12. loadImage();
  13. getImageDimensions();
  14. resetState();
  15. }
  16. private void loadImage() {
  17. var ii = new ImageIcon("src/resources/ball.png");
  18. image = ii.getImage();
  19. }
  20. void move() {
  21. x += xdir;
  22. y += ydir;
  23. if (x == 0) {
  24. setXDir(1);
  25. }
  26. if (x == Commons.WIDTH - imageWidth) {
  27. System.out.println(imageWidth);
  28. setXDir(-1);
  29. }
  30. if (y == 0) {
  31. setYDir(1);
  32. }
  33. }
  34. private void resetState() {
  35. x = Commons.INIT_BALL_X;
  36. y = Commons.INIT_BALL_Y;
  37. }
  38. void setXDir(int x) {
  39. xdir = x;
  40. }
  41. void setYDir(int y) {
  42. ydir = y;
  43. }
  44. int getYDir() {
  45. return ydir;
  46. }
  47. }

这是Ball类。

  1. void move() {
  2. x += xdir;
  3. y += ydir;
  4. if (x == 0) {
  5. setXDir(1);
  6. }
  7. if (x == Commons.WIDTH - imageWidth) {
  8. System.out.println(imageWidth);
  9. setXDir(-1);
  10. }
  11. if (y == 0) {
  12. setYDir(1);
  13. }
  14. }

move()方法将球移到Board上。 如果球撞到边界,方向将相应更改。

  1. void setXDir(int x) {
  2. xdir = x;
  3. }
  4. void setYDir(int y) {
  5. ydir = y;
  6. }

当球击中桨或砖时,将调用这两种方法。

com/zetcode/Paddle.java

  1. package com.zetcode;
  2. import java.awt.event.KeyEvent;
  3. import javax.swing.ImageIcon;
  4. public class Paddle extends Sprite {
  5. private int dx;
  6. public Paddle() {
  7. initPaddle();
  8. }
  9. private void initPaddle() {
  10. loadImage();
  11. getImageDimensions();
  12. resetState();
  13. }
  14. private void loadImage() {
  15. var ii = new ImageIcon("src/resources/paddle.png");
  16. image = ii.getImage();
  17. }
  18. void move() {
  19. x += dx;
  20. if (x <= 0) {
  21. x = 0;
  22. }
  23. if (x >= Commons.WIDTH - imageWidth) {
  24. x = Commons.WIDTH - imageWidth;
  25. }
  26. }
  27. void keyPressed(KeyEvent e) {
  28. int key = e.getKeyCode();
  29. if (key == KeyEvent.VK_LEFT) {
  30. dx = -1;
  31. }
  32. if (key == KeyEvent.VK_RIGHT) {
  33. dx = 1;
  34. }
  35. }
  36. void keyReleased(KeyEvent e) {
  37. int key = e.getKeyCode();
  38. if (key == KeyEvent.VK_LEFT) {
  39. dx = 0;
  40. }
  41. if (key == KeyEvent.VK_RIGHT) {
  42. dx = 0;
  43. }
  44. }
  45. private void resetState() {
  46. x = Commons.INIT_PADDLE_X;
  47. y = Commons.INIT_PADDLE_Y;
  48. }
  49. }

这是Paddle类。 它封装了 Breakout 游戏中的桨对象。 操纵杆通过左右箭头键控制。 通过按箭头键,我们设置方向变量。 通过释放箭头键,将dx变量设置为零。 这样,桨停止移动。

  1. void move() {
  2. x += dx;
  3. if (x <= 0) {
  4. x = 0;
  5. }
  6. if (x >= Commons.WIDTH - imageWidth) {
  7. x = Commons.WIDTH - imageWidth;
  8. }
  9. }

桨叶仅在水平方向上移动,因此我们仅更新 x 坐标。 如果条件确保桨不通过窗口边缘。

com/zetcode/Board.java

  1. package com.zetcode;
  2. import javax.swing.JPanel;
  3. import javax.swing.Timer;
  4. import java.awt.Color;
  5. import java.awt.Dimension;
  6. import java.awt.Font;
  7. import java.awt.FontMetrics;
  8. import java.awt.Graphics;
  9. import java.awt.Graphics2D;
  10. import java.awt.Point;
  11. import java.awt.RenderingHints;
  12. import java.awt.Toolkit;
  13. import java.awt.event.ActionEvent;
  14. import java.awt.event.ActionListener;
  15. import java.awt.event.KeyAdapter;
  16. import java.awt.event.KeyEvent;
  17. public class Board extends JPanel {
  18. private Timer timer;
  19. private String message = "Game Over";
  20. private Ball ball;
  21. private Paddle paddle;
  22. private Brick[] bricks;
  23. private boolean inGame = true;
  24. public Board() {
  25. initBoard();
  26. }
  27. private void initBoard() {
  28. addKeyListener(new TAdapter());
  29. setFocusable(true);
  30. setPreferredSize(new Dimension(Commons.WIDTH, Commons.HEIGHT));
  31. gameInit();
  32. }
  33. private void gameInit() {
  34. bricks = new Brick[Commons.N_OF_BRICKS];
  35. ball = new Ball();
  36. paddle = new Paddle();
  37. int k = 0;
  38. for (int i = 0; i < 5; i++) {
  39. for (int j = 0; j < 6; j++) {
  40. bricks[k] = new Brick(j * 40 + 30, i * 10 + 50);
  41. k++;
  42. }
  43. }
  44. timer = new Timer(Commons.PERIOD, new GameCycle());
  45. timer.start();
  46. }
  47. @Override
  48. public void paintComponent(Graphics g) {
  49. super.paintComponent(g);
  50. var g2d = (Graphics2D) g;
  51. g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
  52. RenderingHints.VALUE_ANTIALIAS_ON);
  53. g2d.setRenderingHint(RenderingHints.KEY_RENDERING,
  54. RenderingHints.VALUE_RENDER_QUALITY);
  55. if (inGame) {
  56. drawObjects(g2d);
  57. } else {
  58. gameFinished(g2d);
  59. }
  60. Toolkit.getDefaultToolkit().sync();
  61. }
  62. private void drawObjects(Graphics2D g2d) {
  63. g2d.drawImage(ball.getImage(), ball.getX(), ball.getY(),
  64. ball.getImageWidth(), ball.getImageHeight(), this);
  65. g2d.drawImage(paddle.getImage(), paddle.getX(), paddle.getY(),
  66. paddle.getImageWidth(), paddle.getImageHeight(), this);
  67. for (int i = 0; i < Commons.N_OF_BRICKS; i++) {
  68. if (!bricks[i].isDestroyed()) {
  69. g2d.drawImage(bricks[i].getImage(), bricks[i].getX(),
  70. bricks[i].getY(), bricks[i].getImageWidth(),
  71. bricks[i].getImageHeight(), this);
  72. }
  73. }
  74. }
  75. private void gameFinished(Graphics2D g2d) {
  76. var font = new Font("Verdana", Font.BOLD, 18);
  77. FontMetrics fontMetrics = this.getFontMetrics(font);
  78. g2d.setColor(Color.BLACK);
  79. g2d.setFont(font);
  80. g2d.drawString(message,
  81. (Commons.WIDTH - fontMetrics.stringWidth(message)) / 2,
  82. Commons.WIDTH / 2);
  83. }
  84. private class TAdapter extends KeyAdapter {
  85. @Override
  86. public void keyReleased(KeyEvent e) {
  87. paddle.keyReleased(e);
  88. }
  89. @Override
  90. public void keyPressed(KeyEvent e) {
  91. paddle.keyPressed(e);
  92. }
  93. }
  94. private class GameCycle implements ActionListener {
  95. @Override
  96. public void actionPerformed(ActionEvent e) {
  97. doGameCycle();
  98. }
  99. }
  100. private void doGameCycle() {
  101. ball.move();
  102. paddle.move();
  103. checkCollision();
  104. repaint();
  105. }
  106. private void stopGame() {
  107. inGame = false;
  108. timer.stop();
  109. }
  110. private void checkCollision() {
  111. if (ball.getRect().getMaxY() > Commons.BOTTOM_EDGE) {
  112. stopGame();
  113. }
  114. for (int i = 0, j = 0; i < Commons.N_OF_BRICKS; i++) {
  115. if (bricks[i].isDestroyed()) {
  116. j++;
  117. }
  118. if (j == Commons.N_OF_BRICKS) {
  119. message = "Victory";
  120. stopGame();
  121. }
  122. }
  123. if ((ball.getRect()).intersects(paddle.getRect())) {
  124. int paddleLPos = (int) paddle.getRect().getMinX();
  125. int ballLPos = (int) ball.getRect().getMinX();
  126. int first = paddleLPos + 8;
  127. int second = paddleLPos + 16;
  128. int third = paddleLPos + 24;
  129. int fourth = paddleLPos + 32;
  130. if (ballLPos < first) {
  131. ball.setXDir(-1);
  132. ball.setYDir(-1);
  133. }
  134. if (ballLPos >= first && ballLPos < second) {
  135. ball.setXDir(-1);
  136. ball.setYDir(-1 * ball.getYDir());
  137. }
  138. if (ballLPos >= second && ballLPos < third) {
  139. ball.setXDir(0);
  140. ball.setYDir(-1);
  141. }
  142. if (ballLPos >= third && ballLPos < fourth) {
  143. ball.setXDir(1);
  144. ball.setYDir(-1 * ball.getYDir());
  145. }
  146. if (ballLPos > fourth) {
  147. ball.setXDir(1);
  148. ball.setYDir(-1);
  149. }
  150. }
  151. for (int i = 0; i < Commons.N_OF_BRICKS; i++) {
  152. if ((ball.getRect()).intersects(bricks[i].getRect())) {
  153. int ballLeft = (int) ball.getRect().getMinX();
  154. int ballHeight = (int) ball.getRect().getHeight();
  155. int ballWidth = (int) ball.getRect().getWidth();
  156. int ballTop = (int) ball.getRect().getMinY();
  157. var pointRight = new Point(ballLeft + ballWidth + 1, ballTop);
  158. var pointLeft = new Point(ballLeft - 1, ballTop);
  159. var pointTop = new Point(ballLeft, ballTop - 1);
  160. var pointBottom = new Point(ballLeft, ballTop + ballHeight + 1);
  161. if (!bricks[i].isDestroyed()) {
  162. if (bricks[i].getRect().contains(pointRight)) {
  163. ball.setXDir(-1);
  164. } else if (bricks[i].getRect().contains(pointLeft)) {
  165. ball.setXDir(1);
  166. }
  167. if (bricks[i].getRect().contains(pointTop)) {
  168. ball.setYDir(1);
  169. } else if (bricks[i].getRect().contains(pointBottom)) {
  170. ball.setYDir(-1);
  171. }
  172. bricks[i].setDestroyed(true);
  173. }
  174. }
  175. }
  176. }
  177. }

这是Board类。 这里我们把游戏逻辑。

  1. private void gameInit() {
  2. bricks = new Brick[Commons.N_OF_BRICKS];
  3. ball = new Ball();
  4. paddle = new Paddle();
  5. int k = 0;
  6. for (int i = 0; i < 5; i++) {
  7. for (int j = 0; j < 6; j++) {
  8. bricks[k] = new Brick(j * 40 + 30, i * 10 + 50);
  9. k++;
  10. }
  11. }
  12. timer = new Timer(Commons.PERIOD, new GameCycle());
  13. timer.start();
  14. }

gameInit()方法中,我们创建一个球,一个球拍和三十块积木。 然后,我们创建并启动一个计时器。

  1. if (inGame) {
  2. drawObjects(g2d);
  3. } else {
  4. gameFinished(g2d);
  5. }

根据inGame变量,我们可以使用drawObjects()方法绘制所有对象,也可以使用gameFinished()方法完成游戏。

  1. private void drawObjects(Graphics2D g2d) {
  2. g2d.drawImage(ball.getImage(), ball.getX(), ball.getY(),
  3. ball.getImageWidth(), ball.getImageHeight(), this);
  4. g2d.drawImage(paddle.getImage(), paddle.getX(), paddle.getY(),
  5. paddle.getImageWidth(), paddle.getImageHeight(), this);
  6. for (int i = 0; i < Commons.N_OF_BRICKS; i++) {
  7. if (!bricks[i].isDestroyed()) {
  8. g2d.drawImage(bricks[i].getImage(), bricks[i].getX(),
  9. bricks[i].getY(), bricks[i].getImageWidth(),
  10. bricks[i].getImageHeight(), this);
  11. }
  12. }
  13. }

drawObjects()方法绘制游戏的所有对象。 使用drawImage()方法绘制精灵。

  1. private void gameFinished(Graphics2D g2d) {
  2. var font = new Font("Verdana", Font.BOLD, 18);
  3. FontMetrics fontMetrics = this.getFontMetrics(font);
  4. g2d.setColor(Color.BLACK);
  5. g2d.setFont(font);
  6. g2d.drawString(message,
  7. (Commons.WIDTH - fontMetrics.stringWidth(message)) / 2,
  8. Commons.WIDTH / 2);
  9. }

gameFinished()方法将"Game Over""Victory"绘制到窗口的中间。

  1. private class GameCycle implements ActionListener {
  2. @Override
  3. public void actionPerformed(ActionEvent e) {
  4. doGameCycle();
  5. }
  6. }

计时器会定期调用actionPerformed()方法,该方法又调用doGameCycle()方法,从而创建游戏周期。

  1. private void doGameCycle() {
  2. ball.move();
  3. paddle.move();
  4. checkCollision();
  5. repaint();
  6. }

doGameCycle()移动球和球拍。 我们检查是否可能发生碰撞并重新粉刷屏幕。

  1. private void checkCollision() {
  2. if (ball.getRect().getMaxY() > Commons.BOTTOM_EDGE) {
  3. stopGame();
  4. }
  5. ...

如果球触底,我们将停止比赛。

  1. for (int i = 0, j = 0; i < Commons.N_OF_BRICKS; i++) {
  2. if (bricks[i].isDestroyed()) {
  3. j++;
  4. }
  5. if (j == Commons.N_OF_BRICKS) {
  6. message = "Victory";
  7. stopGame();
  8. }
  9. }

我们检查了多少砖被破坏了。 如果我们摧毁了所有N_OF_BRICKS bricks,我们将赢得这场比赛。

  1. if (ballLPos < first) {
  2. ball.setXDir(-1);
  3. ball.setYDir(-1);
  4. }

如果球碰到了桨的第一部分,我们会将球的方向更改为西北。

  1. if (bricks[i].getRect().contains(pointTop)) {
  2. ball.setYDir(1);
  3. }...

如果球撞击砖的底部,我们将改变球的 y 方向; 它下降了。

com/zetcode/Breakout.java

  1. package com.zetcode;
  2. import javax.swing.JFrame;
  3. import java.awt.EventQueue;
  4. public class Breakout extends JFrame {
  5. public Breakout() {
  6. initUI();
  7. }
  8. private void initUI() {
  9. add(new Board());
  10. setTitle("Breakout");
  11. setDefaultCloseOperation(EXIT_ON_CLOSE);
  12. setLocationRelativeTo(null);
  13. setResizable(false);
  14. pack();
  15. }
  16. public static void main(String[] args) {
  17. EventQueue.invokeLater(() -> {
  18. var game = new Breakout();
  19. game.setVisible(true);
  20. });
  21. }
  22. }

这是具有主要输入方法的Breakout类。

Breakout 游戏 - 图1

图:打砖块游戏

这是打砖块游戏。