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

在 Java 2D 游戏教程的这一部分中,我们将讨论碰撞检测。

许多游戏都需要处理碰撞,尤其是街机游戏。 简而言之,我们需要检测两个对象何时在屏幕上碰撞。

在下一个代码示例中,我们将扩展上一个示例。 我们添加了一个新的外星人精灵。 我们将检测两种类型的碰撞:当导弹击中外星人的飞船和我们的航天器与外星人相撞时。

射击外星人

在示例中,我们有一个航天器和外星人。 我们可以使用光标键在板上移动航天器。 用空格键发射消灭外星人的导弹。

Sprite.java

  1. package com.zetcode;
  2. import java.awt.Image;
  3. import java.awt.Rectangle;
  4. import javax.swing.ImageIcon;
  5. public class Sprite {
  6. protected int x;
  7. protected int y;
  8. protected int width;
  9. protected int height;
  10. protected boolean visible;
  11. protected Image image;
  12. public Sprite(int x, int y) {
  13. this.x = x;
  14. this.y = y;
  15. visible = true;
  16. }
  17. protected void getImageDimensions() {
  18. width = image.getWidth(null);
  19. height = image.getHeight(null);
  20. }
  21. protected void loadImage(String imageName) {
  22. ImageIcon ii = new ImageIcon(imageName);
  23. image = ii.getImage();
  24. }
  25. public Image getImage() {
  26. return image;
  27. }
  28. public int getX() {
  29. return x;
  30. }
  31. public int getY() {
  32. return y;
  33. }
  34. public boolean isVisible() {
  35. return visible;
  36. }
  37. public void setVisible(Boolean visible) {
  38. this.visible = visible;
  39. }
  40. public Rectangle getBounds() {
  41. return new Rectangle(x, y, width, height);
  42. }
  43. }

可以由所有子画面(工艺品,外星人和导弹)共享的代码放在Sprite类中。

  1. public Rectangle getBounds() {
  2. return new Rectangle(x, y, width, height);
  3. }

getBounds()方法返回精灵图像的边界矩形。 在碰撞检测中我们需要边界。

SpaceShip.java

  1. package com.zetcode;
  2. import java.awt.event.KeyEvent;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. public class SpaceShip extends Sprite {
  6. private int dx;
  7. private int dy;
  8. private List<Missile> missiles;
  9. public SpaceShip(int x, int y) {
  10. super(x, y);
  11. initCraft();
  12. }
  13. private void initCraft() {
  14. missiles = new ArrayList<>();
  15. loadImage("src/resources/spaceship.png");
  16. getImageDimensions();
  17. }
  18. public void move() {
  19. x += dx;
  20. y += dy;
  21. if (x < 1) {
  22. x = 1;
  23. }
  24. if (y < 1) {
  25. y = 1;
  26. }
  27. }
  28. public List<Missile> getMissiles() {
  29. return missiles;
  30. }
  31. public void keyPressed(KeyEvent e) {
  32. int key = e.getKeyCode();
  33. if (key == KeyEvent.VK_SPACE) {
  34. fire();
  35. }
  36. if (key == KeyEvent.VK_LEFT) {
  37. dx = -1;
  38. }
  39. if (key == KeyEvent.VK_RIGHT) {
  40. dx = 1;
  41. }
  42. if (key == KeyEvent.VK_UP) {
  43. dy = -1;
  44. }
  45. if (key == KeyEvent.VK_DOWN) {
  46. dy = 1;
  47. }
  48. }
  49. public void fire() {
  50. missiles.add(new Missile(x + width, y + height / 2));
  51. }
  52. public void keyReleased(KeyEvent e) {
  53. int key = e.getKeyCode();
  54. if (key == KeyEvent.VK_LEFT) {
  55. dx = 0;
  56. }
  57. if (key == KeyEvent.VK_RIGHT) {
  58. dx = 0;
  59. }
  60. if (key == KeyEvent.VK_UP) {
  61. dy = 0;
  62. }
  63. if (key == KeyEvent.VK_DOWN) {
  64. dy = 0;
  65. }
  66. }
  67. }

此类代表航天器。

  1. private List<Missile> missiles;

航天器发射的所有导弹都存储在missiles列表中。

  1. public void fire() {
  2. missiles.add(new Missile(x + width, y + height / 2));
  3. }

当我们发射导弹时,新的Missile对象将添加到missiles列表中。 它会保留在列表中,直到与外星人碰撞或离开窗口为止。

Board.java

  1. package com.zetcode;
  2. import java.awt.Color;
  3. import java.awt.Dimension;
  4. import java.awt.Font;
  5. import java.awt.FontMetrics;
  6. import java.awt.Graphics;
  7. import java.awt.Rectangle;
  8. import java.awt.Toolkit;
  9. import java.awt.event.ActionEvent;
  10. import java.awt.event.ActionListener;
  11. import java.awt.event.KeyAdapter;
  12. import java.awt.event.KeyEvent;
  13. import java.util.ArrayList;
  14. import java.util.List;
  15. import javax.swing.JPanel;
  16. import javax.swing.Timer;
  17. public class Board extends JPanel implements ActionListener {
  18. private Timer timer;
  19. private SpaceShip spaceship;
  20. private List<Alien> aliens;
  21. private boolean ingame;
  22. private final int ICRAFT_X = 40;
  23. private final int ICRAFT_Y = 60;
  24. private final int B_WIDTH = 400;
  25. private final int B_HEIGHT = 300;
  26. private final int DELAY = 15;
  27. private final int[][] pos = {
  28. {2380, 29}, {2500, 59}, {1380, 89},
  29. {780, 109}, {580, 139}, {680, 239},
  30. {790, 259}, {760, 50}, {790, 150},
  31. {980, 209}, {560, 45}, {510, 70},
  32. {930, 159}, {590, 80}, {530, 60},
  33. {940, 59}, {990, 30}, {920, 200},
  34. {900, 259}, {660, 50}, {540, 90},
  35. {810, 220}, {860, 20}, {740, 180},
  36. {820, 128}, {490, 170}, {700, 30}
  37. };
  38. public Board() {
  39. initBoard();
  40. }
  41. private void initBoard() {
  42. addKeyListener(new TAdapter());
  43. setFocusable(true);
  44. setBackground(Color.BLACK);
  45. ingame = true;
  46. setPreferredSize(new Dimension(B_WIDTH, B_HEIGHT));
  47. spaceship = new SpaceShip(ICRAFT_X, ICRAFT_Y);
  48. initAliens();
  49. timer = new Timer(DELAY, this);
  50. timer.start();
  51. }
  52. public void initAliens() {
  53. aliens = new ArrayList<>();
  54. for (int[] p : pos) {
  55. aliens.add(new Alien(p[0], p[1]));
  56. }
  57. }
  58. @Override
  59. public void paintComponent(Graphics g) {
  60. super.paintComponent(g);
  61. if (ingame) {
  62. drawObjects(g);
  63. } else {
  64. drawGameOver(g);
  65. }
  66. Toolkit.getDefaultToolkit().sync();
  67. }
  68. private void drawObjects(Graphics g) {
  69. if (spaceship.isVisible()) {
  70. g.drawImage(spaceship.getImage(), spaceship.getX(), spaceship.getY(),
  71. this);
  72. }
  73. List<Missile> ms = spaceship.getMissiles();
  74. for (Missile missile : ms) {
  75. if (missile.isVisible()) {
  76. g.drawImage(missile.getImage(), missile.getX(),
  77. missile.getY(), this);
  78. }
  79. }
  80. for (Alien alien : aliens) {
  81. if (alien.isVisible()) {
  82. g.drawImage(alien.getImage(), alien.getX(), alien.getY(), this);
  83. }
  84. }
  85. g.setColor(Color.WHITE);
  86. g.drawString("Aliens left: " + aliens.size(), 5, 15);
  87. }
  88. private void drawGameOver(Graphics g) {
  89. String msg = "Game Over";
  90. Font small = new Font("Helvetica", Font.BOLD, 14);
  91. FontMetrics fm = getFontMetrics(small);
  92. g.setColor(Color.white);
  93. g.setFont(small);
  94. g.drawString(msg, (B_WIDTH - fm.stringWidth(msg)) / 2,
  95. B_HEIGHT / 2);
  96. }
  97. @Override
  98. public void actionPerformed(ActionEvent e) {
  99. inGame();
  100. updateShip();
  101. updateMissiles();
  102. updateAliens();
  103. checkCollisions();
  104. repaint();
  105. }
  106. private void inGame() {
  107. if (!ingame) {
  108. timer.stop();
  109. }
  110. }
  111. private void updateShip() {
  112. if (spaceship.isVisible()) {
  113. spaceship.move();
  114. }
  115. }
  116. private void updateMissiles() {
  117. List<Missile> ms = spaceship.getMissiles();
  118. for (int i = 0; i < ms.size(); i++) {
  119. Missile m = ms.get(i);
  120. if (m.isVisible()) {
  121. m.move();
  122. } else {
  123. ms.remove(i);
  124. }
  125. }
  126. }
  127. private void updateAliens() {
  128. if (aliens.isEmpty()) {
  129. ingame = false;
  130. return;
  131. }
  132. for (int i = 0; i < aliens.size(); i++) {
  133. Alien a = aliens.get(i);
  134. if (a.isVisible()) {
  135. a.move();
  136. } else {
  137. aliens.remove(i);
  138. }
  139. }
  140. }
  141. public void checkCollisions() {
  142. Rectangle r3 = spaceship.getBounds();
  143. for (Alien alien : aliens) {
  144. Rectangle r2 = alien.getBounds();
  145. if (r3.intersects(r2)) {
  146. spaceship.setVisible(false);
  147. alien.setVisible(false);
  148. ingame = false;
  149. }
  150. }
  151. List<Missile> ms = spaceship.getMissiles();
  152. for (Missile m : ms) {
  153. Rectangle r1 = m.getBounds();
  154. for (Alien alien : aliens) {
  155. Rectangle r2 = alien.getBounds();
  156. if (r1.intersects(r2)) {
  157. m.setVisible(false);
  158. alien.setVisible(false);
  159. }
  160. }
  161. }
  162. }
  163. private class TAdapter extends KeyAdapter {
  164. @Override
  165. public void keyReleased(KeyEvent e) {
  166. spaceship.keyReleased(e);
  167. }
  168. @Override
  169. public void keyPressed(KeyEvent e) {
  170. spaceship.keyPressed(e);
  171. }
  172. }
  173. }

这是Board类。

  1. private final int[][] pos = {
  2. {2380, 29}, {2500, 59}, {1380, 89},
  3. {780, 109}, {580, 139}, {680, 239},
  4. {790, 259}, {760, 50}, {790, 150},
  5. {980, 209}, {560, 45}, {510, 70},
  6. {930, 159}, {590, 80}, {530, 60},
  7. {940, 59}, {990, 30}, {920, 200},
  8. {900, 259}, {660, 50}, {540, 90},
  9. {810, 220}, {860, 20}, {740, 180},
  10. {820, 128}, {490, 170}, {700, 30}
  11. };

这些是外星飞船的初始位置。

  1. public void initAliens() {
  2. aliens = new ArrayList<>();
  3. for (int[] p : pos) {
  4. aliens.add(new Alien(p[0], p[1]));
  5. }
  6. }

initAliens()方法创建一个外星对象列表。 外星人从pos数组中占据初始位置。

  1. @Override
  2. public void paintComponent(Graphics g) {
  3. super.paintComponent(g);
  4. if (ingame) {
  5. drawObjects(g);
  6. } else {
  7. drawGameOver(g);
  8. }
  9. Toolkit.getDefaultToolkit().sync();
  10. }

paintComponent()方法内,我们绘制游戏精灵或通过消息编写游戏。 这取决于ingame变量。

  1. private void drawObjects(Graphics g) {
  2. if (spaceship.isVisible()) {
  3. g.drawImage(spaceship.getImage(), spaceship.getX(), spaceship.getY(),
  4. this);
  5. }
  6. ...
  7. }

drawObjects()方法在窗口上绘制游戏精灵。 首先,我们绘制工艺精灵。

  1. for (Alien alien : aliens) {
  2. if (alien.isVisible()) {
  3. g.drawImage(alien.getImage(), alien.getX(), alien.getY(), this);
  4. }
  5. }

在这个循环中,我们吸引了所有外星人。 仅在以前未销毁它们的情况下才绘制它们。 通过isVisible()方法检查。

  1. g.setColor(Color.WHITE);
  2. g.drawString("Aliens left: " + aliens.size(), 5, 15);

在窗口的左上角,我们绘制了剩余的外星人数量。

  1. private void drawGameOver(Graphics g) {
  2. String msg = "Game Over";
  3. Font small = new Font("Helvetica", Font.BOLD, 14);
  4. FontMetrics fm = getFontMetrics(small);
  5. g.setColor(Color.white);
  6. g.setFont(small);
  7. g.drawString(msg, (B_WIDTH - fm.stringWidth(msg)) / 2,
  8. B_HEIGHT / 2);
  9. }

drawGameOver()在窗口中间的消息上方绘制游戏。 该消息显示在游戏结束时,当我们摧毁所有外星飞船或与其中一艘发生碰撞时。

  1. @Override
  2. public void actionPerformed(ActionEvent e) {
  3. inGame();
  4. updateShip();
  5. updateMissiles();
  6. updateAliens();
  7. checkCollisions();
  8. repaint();
  9. }

每个动作事件代表一个游戏周期。 游戏逻辑被纳入特定方法中。 例如,updateMissiles()会移动所有可用的导弹。

  1. private void updateAliens() {
  2. if (aliens.isEmpty()) {
  3. ingame = false;
  4. return;
  5. }
  6. for (int i = 0; i < aliens.size(); i++) {
  7. Alien a = aliens.get(i);
  8. if (a.isVisible()) {
  9. a.move();
  10. } else {
  11. aliens.remove(i);
  12. }
  13. }
  14. }

updateAliens()方法内部,我们首先检查aliens列表中是否还有任何异物。 如果列表为空,则游戏结束。 如果它不为空,则浏览列表并移动其所有项目。 被摧毁的外星人将从名单中删除。

  1. public void checkCollisions() {
  2. Rectangle r3 = spaceship.getBounds();
  3. for (Alien alien : aliens) {
  4. Rectangle r2 = alien.getBounds();
  5. if (r3.intersects(r2)) {
  6. spaceship.setVisible(false);
  7. alien.setVisible(false);
  8. ingame = false;
  9. }
  10. }
  11. ...
  12. }

checkCollisions()方法检查可能的冲突。 首先,我们检查工艺对象是否与任何外来对象发生碰撞。 我们使用getBounds()方法获得对象的矩形。 intersects()方法检查两个矩形是否相交。

  1. List<Missile> ms = spaceship.getMissiles();
  2. for (Missile m : ms) {
  3. Rectangle r1 = m.getBounds();
  4. for (Alien alien : aliens) {
  5. Rectangle r2 = alien.getBounds();
  6. if (r1.intersects(r2)) {
  7. m.setVisible(false);
  8. alien.setVisible(false);
  9. }
  10. }
  11. }

该代码检查导弹与外星人之间的碰撞。

Alien.java

  1. package com.zetcode;
  2. public class Alien extends Sprite {
  3. private final int INITIAL_X = 400;
  4. public Alien(int x, int y) {
  5. super(x, y);
  6. initAlien();
  7. }
  8. private void initAlien() {
  9. loadImage("src/resources/alien.png");
  10. getImageDimensions();
  11. }
  12. public void move() {
  13. if (x < 0) {
  14. x = INITIAL_X;
  15. }
  16. x -= 1;
  17. }
  18. }

这是Alien类。

  1. public void move() {
  2. if (x < 0) {
  3. x = INITIAL_X;
  4. }
  5. x -= 1;
  6. }

外星人消失在左侧后,他们会返回到右侧的屏幕。

Missile.java

  1. package com.zetcode;
  2. public class Missile extends Sprite {
  3. private final int BOARD_WIDTH = 390;
  4. private final int MISSILE_SPEED = 2;
  5. public Missile(int x, int y) {
  6. super(x, y);
  7. initMissile();
  8. }
  9. private void initMissile() {
  10. loadImage("src/resources/missile.png");
  11. getImageDimensions();
  12. }
  13. public void move() {
  14. x += MISSILE_SPEED;
  15. if (x > BOARD_WIDTH)
  16. visible = false;
  17. }
  18. }

这是Missile类。

  1. public void move() {
  2. x += MISSILE_SPEED;
  3. if (x > BOARD_WIDTH)
  4. visible = false;
  5. }

导弹只能向一个方向移动。 它们到达右窗口边框后消失。

CollisionEx.java

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

最后,这是主要类。

碰撞检测 - 图1

图:射击外星人

本章是关于碰撞检测的。