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

在 Java 2D 游戏教程的这一部分中,我们将使用 Java 创建一个简单的太空侵略者游戏克隆。 源代码和图像可在作者的 Github Java-Space-Invaders 存储库中找到。

太空侵略者是由 Nishikado Tomohiro Nishikado 设计的街机游戏。 它于 1978 年首次发布。

在“太空侵略者”游戏中,玩家控制一门大炮。 他即将拯救地球免遭邪恶太空入侵者的入侵。

用 Java 开发太空侵略者

在我们的 Java 克隆中,我们有 24 个入侵者。 这些外星人重重炮击地面。 当玩家射击导弹时,只有当导弹击中外星人或棋盘顶部时,他才能射击另一枚导弹。 玩家使用空格键射击。 外星人随机发射炸弹。 每个外星人只有在前一个击中底部后才会发射炸弹。

com/zetcode/SpaceInvaders.java

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

这是主要的类。 它设置了应用。

com/zetcode/Commons.java

  1. package com.zetcode;
  2. public interface Commons {
  3. int BOARD_WIDTH = 358;
  4. int BOARD_HEIGHT = 350;
  5. int BORDER_RIGHT = 30;
  6. int BORDER_LEFT = 5;
  7. int GROUND = 290;
  8. int BOMB_HEIGHT = 5;
  9. int ALIEN_HEIGHT = 12;
  10. int ALIEN_WIDTH = 12;
  11. int ALIEN_INIT_X = 150;
  12. int ALIEN_INIT_Y = 5;
  13. int GO_DOWN = 15;
  14. int NUMBER_OF_ALIENS_TO_DESTROY = 24;
  15. int CHANCE = 5;
  16. int DELAY = 17;
  17. int PLAYER_WIDTH = 15;
  18. int PLAYER_HEIGHT = 10;
  19. }

Commons.java文件具有一些公共常数。 他们是不言自明的。

com/zetcode/sprite/Alien.java

  1. package com.zetcode.sprite;
  2. import javax.swing.ImageIcon;
  3. public class Alien extends Sprite {
  4. private Bomb bomb;
  5. public Alien(int x, int y) {
  6. initAlien(x, y);
  7. }
  8. private void initAlien(int x, int y) {
  9. this.x = x;
  10. this.y = y;
  11. bomb = new Bomb(x, y);
  12. var alienImg = "simg/alien.png";
  13. var ii = new ImageIcon(alienImg);
  14. setImage(ii.getImage());
  15. }
  16. public void act(int direction) {
  17. this.x += direction;
  18. }
  19. public Bomb getBomb() {
  20. return bomb;
  21. }
  22. public class Bomb extends Sprite {
  23. private boolean destroyed;
  24. public Bomb(int x, int y) {
  25. initBomb(x, y);
  26. }
  27. private void initBomb(int x, int y) {
  28. setDestroyed(true);
  29. this.x = x;
  30. this.y = y;
  31. var bombImg = "simg/bomb.png";
  32. var ii = new ImageIcon(bombImg);
  33. setImage(ii.getImage());
  34. }
  35. public void setDestroyed(boolean destroyed) {
  36. this.destroyed = destroyed;
  37. }
  38. public boolean isDestroyed() {
  39. return destroyed;
  40. }
  41. }
  42. }

这是Alien子画面。 每个外星人都有一个内部的Bomb类。

  1. public void act(int direction) {
  2. this.x += direction;
  3. }

Board类中调用act()方法。 用于在水平方向上定位外星人。

  1. public Bomb getBomb() {
  2. return bomb;
  3. }

当外星人将要投下炸弹时,将调用getBomb()方法。

com/zetcode/sprite/Player.java

  1. package com.zetcode.sprite;
  2. import com.zetcode.Commons;
  3. import javax.swing.ImageIcon;
  4. import java.awt.event.KeyEvent;
  5. public class Player extends Sprite {
  6. private int width;
  7. public Player() {
  8. initPlayer();
  9. }
  10. private void initPlayer() {
  11. var playerImg = "simg/player.png";
  12. var ii = new ImageIcon(playerImg);
  13. width = ii.getImage().getWidth(null);
  14. setImage(ii.getImage());
  15. int START_X = 270;
  16. setX(START_X);
  17. int START_Y = 280;
  18. setY(START_Y);
  19. }
  20. public void act() {
  21. x += dx;
  22. if (x <= 2) {
  23. x = 2;
  24. }
  25. if (x >= Commons.BOARD_WIDTH - 2 * width) {
  26. x = Commons.BOARD_WIDTH - 2 * width;
  27. }
  28. }
  29. public void keyPressed(KeyEvent e) {
  30. int key = e.getKeyCode();
  31. if (key == KeyEvent.VK_LEFT) {
  32. dx = -2;
  33. }
  34. if (key == KeyEvent.VK_RIGHT) {
  35. dx = 2;
  36. }
  37. }
  38. public void keyReleased(KeyEvent e) {
  39. int key = e.getKeyCode();
  40. if (key == KeyEvent.VK_LEFT) {
  41. dx = 0;
  42. }
  43. if (key == KeyEvent.VK_RIGHT) {
  44. dx = 0;
  45. }
  46. }
  47. }

这是Player子画面。 我们用光标键控制大炮。

  1. int START_X = 270;
  2. setX(START_X);
  3. int START_Y = 280;
  4. setY(START_Y);

这些是播放器精灵的初始坐标。

  1. public void keyPressed(KeyEvent e) {
  2. int key = e.getKeyCode();
  3. if (key == KeyEvent.VK_LEFT) {
  4. dx = -2;
  5. }
  6. ...

如果按左光标键,则dx变量将设置为 -2。 下次调用act()方法时,播放器向左移动。

  1. public void keyReleased(KeyEvent e) {
  2. int key = e.getKeyCode();
  3. if (key == KeyEvent.VK_LEFT) {
  4. dx = 0;
  5. }
  6. if (key == KeyEvent.VK_RIGHT) {
  7. dx = 0;
  8. }
  9. }

如果释放左或右光标,则dx变量将设置为零。 播放器精灵停止移动。

com/zetcode/sprite/Shot.java

  1. package com.zetcode.sprite;
  2. import javax.swing.ImageIcon;
  3. public class Shot extends Sprite {
  4. public Shot() {
  5. }
  6. public Shot(int x, int y) {
  7. initShot(x, y);
  8. }
  9. private void initShot(int x, int y) {
  10. var shotImg = "simg/shot.png";
  11. var ii = new ImageIcon(shotImg);
  12. setImage(ii.getImage());
  13. int H_SPACE = 6;
  14. setX(x + H_SPACE);
  15. int V_SPACE = 1;
  16. setY(y - V_SPACE);
  17. }
  18. }

这是Shot子画面。 用 Space 键触发射击。 H_SPACEV_SPACE常数用于适当地定位导弹。

com/zetcode/sprite/Sprite.java

  1. package com.zetcode.sprite;
  2. import java.awt.Image;
  3. public class Sprite {
  4. private boolean visible;
  5. private Image image;
  6. private boolean dying;
  7. int x;
  8. int y;
  9. int dx;
  10. public Sprite() {
  11. visible = true;
  12. }
  13. public void die() {
  14. visible = false;
  15. }
  16. public boolean isVisible() {
  17. return visible;
  18. }
  19. protected void setVisible(boolean visible) {
  20. this.visible = visible;
  21. }
  22. public void setImage(Image image) {
  23. this.image = image;
  24. }
  25. public Image getImage() {
  26. return image;
  27. }
  28. public void setX(int x) {
  29. this.x = x;
  30. }
  31. public void setY(int y) {
  32. this.y = y;
  33. }
  34. public int getY() {
  35. return y;
  36. }
  37. public int getX() {
  38. return x;
  39. }
  40. public void setDying(boolean dying) {
  41. this.dying = dying;
  42. }
  43. public boolean isDying() {
  44. return this.dying;
  45. }
  46. }

这是基本的Sprite类。 其他精灵也继承自它。 它具有一些常用功能。

com/zetcode/Board.java

  1. package com.zetcode;
  2. import com.zetcode.sprite.Alien;
  3. import com.zetcode.sprite.Player;
  4. import com.zetcode.sprite.Shot;
  5. import javax.swing.ImageIcon;
  6. import javax.swing.JPanel;
  7. import javax.swing.Timer;
  8. import java.awt.Color;
  9. import java.awt.Dimension;
  10. import java.awt.Font;
  11. import java.awt.Graphics;
  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. import java.util.ArrayList;
  18. import java.util.Iterator;
  19. import java.util.List;
  20. import java.util.Random;
  21. public class Board extends JPanel {
  22. private Dimension d;
  23. private List<Alien> aliens;
  24. private Player player;
  25. private Shot shot;
  26. private int direction = -1;
  27. private int deaths = 0;
  28. private boolean inGame = true;
  29. private String explImg = "simg/explosion.png";
  30. private String message = "Game Over";
  31. private Timer timer;
  32. public Board() {
  33. initBoard();
  34. gameInit();
  35. }
  36. private void initBoard() {
  37. addKeyListener(new TAdapter());
  38. setFocusable(true);
  39. d = new Dimension(Commons.BOARD_WIDTH, Commons.BOARD_HEIGHT);
  40. setBackground(Color.black);
  41. timer = new Timer(Commons.DELAY, new GameCycle());
  42. timer.start();
  43. gameInit();
  44. }
  45. private void gameInit() {
  46. aliens = new ArrayList<>();
  47. for (int i = 0; i < 4; i++) {
  48. for (int j = 0; j < 6; j++) {
  49. var alien = new Alien(Commons.ALIEN_INIT_X + 18 * j,
  50. Commons.ALIEN_INIT_Y + 18 * i);
  51. aliens.add(alien);
  52. }
  53. }
  54. player = new Player();
  55. shot = new Shot();
  56. }
  57. private void drawAliens(Graphics g) {
  58. for (Alien alien : aliens) {
  59. if (alien.isVisible()) {
  60. g.drawImage(alien.getImage(), alien.getX(), alien.getY(), this);
  61. }
  62. if (alien.isDying()) {
  63. alien.die();
  64. }
  65. }
  66. }
  67. private void drawPlayer(Graphics g) {
  68. if (player.isVisible()) {
  69. g.drawImage(player.getImage(), player.getX(), player.getY(), this);
  70. }
  71. if (player.isDying()) {
  72. player.die();
  73. inGame = false;
  74. }
  75. }
  76. private void drawShot(Graphics g) {
  77. if (shot.isVisible()) {
  78. g.drawImage(shot.getImage(), shot.getX(), shot.getY(), this);
  79. }
  80. }
  81. private void drawBombing(Graphics g) {
  82. for (Alien a : aliens) {
  83. Alien.Bomb b = a.getBomb();
  84. if (!b.isDestroyed()) {
  85. g.drawImage(b.getImage(), b.getX(), b.getY(), this);
  86. }
  87. }
  88. }
  89. @Override
  90. public void paintComponent(Graphics g) {
  91. super.paintComponent(g);
  92. doDrawing(g);
  93. }
  94. private void doDrawing(Graphics g) {
  95. g.setColor(Color.black);
  96. g.fillRect(0, 0, d.width, d.height);
  97. g.setColor(Color.green);
  98. if (inGame) {
  99. g.drawLine(0, Commons.GROUND,
  100. Commons.BOARD_WIDTH, Commons.GROUND);
  101. drawAliens(g);
  102. drawPlayer(g);
  103. drawShot(g);
  104. drawBombing(g);
  105. } else {
  106. if (timer.isRunning()) {
  107. timer.stop();
  108. }
  109. gameOver(g);
  110. }
  111. Toolkit.getDefaultToolkit().sync();
  112. }
  113. private void gameOver(Graphics g) {
  114. g.setColor(Color.black);
  115. g.fillRect(0, 0, Commons.BOARD_WIDTH, Commons.BOARD_HEIGHT);
  116. g.setColor(new Color(0, 32, 48));
  117. g.fillRect(50, Commons.BOARD_WIDTH / 2 - 30, Commons.BOARD_WIDTH - 100, 50);
  118. g.setColor(Color.white);
  119. g.drawRect(50, Commons.BOARD_WIDTH / 2 - 30, Commons.BOARD_WIDTH - 100, 50);
  120. var small = new Font("Helvetica", Font.BOLD, 14);
  121. var fontMetrics = this.getFontMetrics(small);
  122. g.setColor(Color.white);
  123. g.setFont(small);
  124. g.drawString(message, (Commons.BOARD_WIDTH - fontMetrics.stringWidth(message)) / 2,
  125. Commons.BOARD_WIDTH / 2);
  126. }
  127. private void update() {
  128. if (deaths == Commons.NUMBER_OF_ALIENS_TO_DESTROY) {
  129. inGame = false;
  130. timer.stop();
  131. message = "Game won!";
  132. }
  133. // player
  134. player.act();
  135. // shot
  136. if (shot.isVisible()) {
  137. int shotX = shot.getX();
  138. int shotY = shot.getY();
  139. for (Alien alien : aliens) {
  140. int alienX = alien.getX();
  141. int alienY = alien.getY();
  142. if (alien.isVisible() && shot.isVisible()) {
  143. if (shotX >= (alienX)
  144. && shotX <= (alienX + Commons.ALIEN_WIDTH)
  145. && shotY >= (alienY)
  146. && shotY <= (alienY + Commons.ALIEN_HEIGHT)) {
  147. var ii = new ImageIcon(explImg);
  148. alien.setImage(ii.getImage());
  149. alien.setDying(true);
  150. deaths++;
  151. shot.die();
  152. }
  153. }
  154. }
  155. int y = shot.getY();
  156. y -= 4;
  157. if (y < 0) {
  158. shot.die();
  159. } else {
  160. shot.setY(y);
  161. }
  162. }
  163. // aliens
  164. for (Alien alien : aliens) {
  165. int x = alien.getX();
  166. if (x >= Commons.BOARD_WIDTH - Commons.BORDER_RIGHT && direction != -1) {
  167. direction = -1;
  168. Iterator<Alien> i1 = aliens.iterator();
  169. while (i1.hasNext()) {
  170. Alien a2 = i1.next();
  171. a2.setY(a2.getY() + Commons.GO_DOWN);
  172. }
  173. }
  174. if (x <= Commons.BORDER_LEFT && direction != 1) {
  175. direction = 1;
  176. Iterator<Alien> i2 = aliens.iterator();
  177. while (i2.hasNext()) {
  178. Alien a = i2.next();
  179. a.setY(a.getY() + Commons.GO_DOWN);
  180. }
  181. }
  182. }
  183. Iterator<Alien> it = aliens.iterator();
  184. while (it.hasNext()) {
  185. Alien alien = it.next();
  186. if (alien.isVisible()) {
  187. int y = alien.getY();
  188. if (y > Commons.GROUND - Commons.ALIEN_HEIGHT) {
  189. inGame = false;
  190. message = "Invasion!";
  191. }
  192. alien.act(direction);
  193. }
  194. }
  195. // bombs
  196. var generator = new Random();
  197. for (Alien alien : aliens) {
  198. int shot = generator.nextInt(15);
  199. Alien.Bomb bomb = alien.getBomb();
  200. if (shot == Commons.CHANCE && alien.isVisible() && bomb.isDestroyed()) {
  201. bomb.setDestroyed(false);
  202. bomb.setX(alien.getX());
  203. bomb.setY(alien.getY());
  204. }
  205. int bombX = bomb.getX();
  206. int bombY = bomb.getY();
  207. int playerX = player.getX();
  208. int playerY = player.getY();
  209. if (player.isVisible() && !bomb.isDestroyed()) {
  210. if (bombX >= (playerX)
  211. && bombX <= (playerX + Commons.PLAYER_WIDTH)
  212. && bombY >= (playerY)
  213. && bombY <= (playerY + Commons.PLAYER_HEIGHT)) {
  214. var ii = new ImageIcon(explImg);
  215. player.setImage(ii.getImage());
  216. player.setDying(true);
  217. bomb.setDestroyed(true);
  218. }
  219. }
  220. if (!bomb.isDestroyed()) {
  221. bomb.setY(bomb.getY() + 1);
  222. if (bomb.getY() >= Commons.GROUND - Commons.BOMB_HEIGHT) {
  223. bomb.setDestroyed(true);
  224. }
  225. }
  226. }
  227. }
  228. private void doGameCycle() {
  229. update();
  230. repaint();
  231. }
  232. private class GameCycle implements ActionListener {
  233. @Override
  234. public void actionPerformed(ActionEvent e) {
  235. doGameCycle();
  236. }
  237. }
  238. private class TAdapter extends KeyAdapter {
  239. @Override
  240. public void keyReleased(KeyEvent e) {
  241. player.keyReleased(e);
  242. }
  243. @Override
  244. public void keyPressed(KeyEvent e) {
  245. player.keyPressed(e);
  246. int x = player.getX();
  247. int y = player.getY();
  248. int key = e.getKeyCode();
  249. if (key == KeyEvent.VK_SPACE) {
  250. if (inGame) {
  251. if (!shot.isVisible()) {
  252. shot = new Shot(x, y);
  253. }
  254. }
  255. }
  256. }
  257. }
  258. }

游戏的主要逻辑位于Board类中。

private void gameInit() {

    aliens = new ArrayList<>();

    for (int i = 0; i < 4; i++) {
        for (int j = 0; j < 6; j++) {

            var alien = new Alien(Commons.ALIEN_INIT_X + 18 * j,
                    Commons.ALIEN_INIT_Y + 18 * i);
            aliens.add(alien);
        }
    }

    player = new Player();
    shot = new Shot();
}

gameInit()方法中,我们创建了 24 个外星人。 外星人图像大小为12x12px。 我们在外星人中间放了 6px 的空间。 我们还创建了播放器和射击对象。

private void drawBombing(Graphics g) {

    for (Alien a : aliens) {

        Alien.Bomb b = a.getBomb();

        if (!b.isDestroyed()) {

            g.drawImage(b.getImage(), b.getX(), b.getY(), this);
        }
    }
}

drawBombing()方法绘制由外星人发射的炸弹。

if (inGame) {

    g.drawLine(0, Commons.GROUND,
            Commons.BOARD_WIDTH, Commons.GROUND);

    drawAliens(g);
    drawPlayer(g);
    drawShot(g);
    drawBombing(g);

} ...

doDrawing()方法中,我们绘制地面,外星人,玩家,射击和炸弹。

private void update() {

    if (deaths == Commons.NUMBER_OF_ALIENS_TO_DESTROY) {

        inGame = false;
        timer.stop();
        message = "Game won!";
    }
...

update()方法内,我们检查损坏的行数。 如果我们消灭所有外星人,我们将赢得比赛。

if (alien.isVisible() && shot.isVisible()) {
    if (shotX >= (alienX)
            && shotX <= (alienX + Commons.ALIEN_WIDTH)
            && shotY >= (alienY)
            && shotY <= (alienY + Commons.ALIEN_HEIGHT)) {

        var ii = new ImageIcon(explImg);
        alien.setImage(ii.getImage());
        alien.setDying(true);
        deaths++;
        shot.die();
    }
}

如果玩家触发的射击与外星人发生碰撞,则该外星人的船只将被摧毁。 更确切地说,将设置垂死标记。 我们用它来显示爆炸。 死亡变量增加,射击精灵被破坏。

if (x >= Commons.BOARD_WIDTH - Commons.BORDER_RIGHT && direction != -1) {

    direction = -1;

    Iterator<Alien> i1 = aliens.iterator();

    while (i1.hasNext()) {

        Alien a2 = i1.next();
        a2.setY(a2.getY() + Commons.GO_DOWN);
    }
}

如果外星人到达Board的右端,他们将向下移动并将其方向更改为左侧。

Iterator<Alien> it = aliens.iterator();

    while (it.hasNext()) {

        Alien alien = it.next();

        if (alien.isVisible()) {

            int y = alien.getY();

            if (y > Commons.GROUND - Commons.ALIEN_HEIGHT) {
                inGame = false;
                message = "Invasion!";
            }

            alien.act(direction);
        }
    }

此代码可移动外星人。 如果他们到达最低点,入侵就开始了。

int shot = generator.nextInt(15);
Alien.Bomb bomb = alien.getBomb();

if (shot == Commons.CHANCE && alien.isVisible() && bomb.isDestroyed()) {

    bomb.setDestroyed(false);
    bomb.setX(alien.getX());
    bomb.setY(alien.getY());
}

这是确定外星人是否会投下炸弹的代码。 不得破坏外星人; 即他必须是可见的。 必须设置炸弹的销毁标志。 换句话说,这是外星人的第一枚炸弹掉落,或者是先前投下的炸弹已经落地。 如果满足这两个条件,轰炸就留给了机会。

if (!bomb.isDestroyed()) {

    bomb.setY(bomb.getY() + 1);

    if (bomb.getY() >= Commons.GROUND - Commons.BOMB_HEIGHT) {

        bomb.setDestroyed(true);
    }
}

如果炸弹没有被销毁,它将离地面 1px。 如果到达底部,则设置销毁标志。 外星人现在准备投下另一枚炸弹。

public void keyReleased(KeyEvent e) {

    player.keyReleased(e);
}

此特定KeyEvent的实际处理被委派给播放器精灵。

Java 太空侵略者 - 图1

图:太空侵略者

在 Java 游戏教程的这一部分中,我们创建了太空侵略者。