Java 俄罗斯方块
最后修改于 2023 年 1 月 10 日
在本部分的 Java 2D 游戏教程中,我们将创建一个简单的俄罗斯方块游戏克隆。源代码和图片可以在作者的 Github Java-Space-Invaders 仓库中找到。
俄罗斯方块 是由Tomohiro Nishikado设计的街机视频游戏。它于1978年首次发布。
在俄罗斯方块游戏中,玩家控制一个炮塔。他即将从邪恶的太空入侵者手中拯救地球。
用 Java 开发俄罗斯方块
在我们的 Java 克隆版中,我们有 24 个入侵者。这些外星人会猛烈炮击地面。当玩家发射导弹时,只有在导弹击中外星人或棋盘顶部后才能发射下一枚。玩家用空格键射击。外星人随机发射炸弹。每个外星人仅在前一个炸弹击中地面后才发射炸弹。
package com.zetcode; import java.awt.EventQueue; import javax.swing.JFrame; public class SpaceInvaders extends JFrame { public SpaceInvaders() { initUI(); } private void initUI() { add(new Board()); setTitle("Space Invaders"); setSize(Commons.BOARD_WIDTH, Commons.BOARD_HEIGHT); setDefaultCloseOperation(EXIT_ON_CLOSE); setResizable(false); setLocationRelativeTo(null); } public static void main(String[] args) { EventQueue.invokeLater(() -> { var ex = new SpaceInvaders(); ex.setVisible(true); }); } }
这是主类。它负责设置应用程序。
package com.zetcode; public interface Commons { int BOARD_WIDTH = 358; int BOARD_HEIGHT = 350; int BORDER_RIGHT = 30; int BORDER_LEFT = 5; int GROUND = 290; int BOMB_HEIGHT = 5; int ALIEN_HEIGHT = 12; int ALIEN_WIDTH = 12; int ALIEN_INIT_X = 150; int ALIEN_INIT_Y = 5; int GO_DOWN = 15; int NUMBER_OF_ALIENS_TO_DESTROY = 24; int CHANCE = 5; int DELAY = 17; int PLAYER_WIDTH = 15; int PLAYER_HEIGHT = 10; }
Commons.java
文件包含一些公共常量。它们不言自明。
package com.zetcode.sprite; import javax.swing.ImageIcon; public class Alien extends Sprite { private Bomb bomb; public Alien(int x, int y) { initAlien(x, y); } private void initAlien(int x, int y) { this.x = x; this.y = y; bomb = new Bomb(x, y); var alienImg = "src/images/alien.png"; var ii = new ImageIcon(alienImg); setImage(ii.getImage()); } public void act(int direction) { this.x += direction; } public Bomb getBomb() { return bomb; } public class Bomb extends Sprite { private boolean destroyed; public Bomb(int x, int y) { initBomb(x, y); } private void initBomb(int x, int y) { setDestroyed(true); this.x = x; this.y = y; var bombImg = "src/images/bomb.png"; var ii = new ImageIcon(bombImg); setImage(ii.getImage()); } public void setDestroyed(boolean destroyed) { this.destroyed = destroyed; } public boolean isDestroyed() { return destroyed; } } }
这是 Alien
精灵。每个外星人都有一个内部的 Bomb
类。
public void act(int direction) { this.x += direction; }
act()
方法由 Board
类调用。它用于在水平方向上定位外星人。
public Bomb getBomb() { return bomb; }
当外星人即将投下炸弹时,会调用 getBomb()
方法。
package com.zetcode.sprite; import com.zetcode.Commons; import javax.swing.ImageIcon; import java.awt.event.KeyEvent; public class Player extends Sprite { private int width; public Player() { initPlayer(); } private void initPlayer() { var playerImg = "src/images/player.png"; var ii = new ImageIcon(playerImg); width = ii.getImage().getWidth(null); setImage(ii.getImage()); int START_X = 270; setX(START_X); int START_Y = 280; setY(START_Y); } public void act() { x += dx; if (x <= 2) { x = 2; } if (x >= Commons.BOARD_WIDTH - 2 * width) { x = Commons.BOARD_WIDTH - 2 * width; } } public void keyPressed(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { dx = -2; } if (key == KeyEvent.VK_RIGHT) { dx = 2; } } public void keyReleased(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { dx = 0; } if (key == KeyEvent.VK_RIGHT) { dx = 0; } } }
这是 Player
精灵。我们用光标键控制炮塔。
int START_X = 270; setX(START_X); int START_Y = 280; setY(START_Y);
这些是玩家精灵的初始坐标。
public void keyPressed(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { dx = -2; } ...
如果我们按下左光标键,dx
变量将设置为 -2。下次调用 act()
方法时,玩家将向左移动。
public void keyReleased(KeyEvent e) { int key = e.getKeyCode(); if (key == KeyEvent.VK_LEFT) { dx = 0; } if (key == KeyEvent.VK_RIGHT) { dx = 0; } }
如果我们释放左或右光标键,dx
变量将设置为零。玩家精灵停止移动。
package com.zetcode.sprite; import javax.swing.ImageIcon; public class Shot extends Sprite { public Shot() { } public Shot(int x, int y) { initShot(x, y); } private void initShot(int x, int y) { var shotImg = "src/images/shot.png"; var ii = new ImageIcon(shotImg); setImage(ii.getImage()); int H_SPACE = 6; setX(x + H_SPACE); int V_SPACE = 1; setY(y - V_SPACE); } }
这是 Shot
精灵。射击由 空格键 触发。H_SPACE
和 V_SPACE
常量用于适当的定位导弹。
package com.zetcode.sprite; import java.awt.Image; public class Sprite { private boolean visible; private Image image; private boolean dying; int x; int y; int dx; public Sprite() { visible = true; } public void die() { visible = false; } public boolean isVisible() { return visible; } protected void setVisible(boolean visible) { this.visible = visible; } public void setImage(Image image) { this.image = image; } public Image getImage() { return image; } public void setX(int x) { this.x = x; } public void setY(int y) { this.y = y; } public int getY() { return y; } public int getX() { return x; } public void setDying(boolean dying) { this.dying = dying; } public boolean isDying() { return this.dying; } }
这是基础的 Sprite
类。其他精灵继承自它。它包含一些通用功能。
package com.zetcode; import com.zetcode.sprite.Alien; import com.zetcode.sprite.Player; import com.zetcode.sprite.Shot; import javax.swing.ImageIcon; import javax.swing.JPanel; import javax.swing.Timer; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.Graphics; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Random; public class Board extends JPanel { private Dimension d; private List<Alien> aliens; private Player player; private Shot shot; private int direction = -1; private int deaths = 0; private boolean inGame = true; private String explImg = "src/images/explosion.png"; private String message = "Game Over"; private Timer timer; public Board() { initBoard(); gameInit(); } private void initBoard() { addKeyListener(new TAdapter()); setFocusable(true); d = new Dimension(Commons.BOARD_WIDTH, Commons.BOARD_HEIGHT); setBackground(Color.black); timer = new Timer(Commons.DELAY, new GameCycle()); timer.start(); gameInit(); } 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(); } private void drawAliens(Graphics g) { for (Alien alien : aliens) { if (alien.isVisible()) { g.drawImage(alien.getImage(), alien.getX(), alien.getY(), this); } if (alien.isDying()) { alien.die(); } } } private void drawPlayer(Graphics g) { if (player.isVisible()) { g.drawImage(player.getImage(), player.getX(), player.getY(), this); } if (player.isDying()) { player.die(); inGame = false; } } private void drawShot(Graphics g) { if (shot.isVisible()) { g.drawImage(shot.getImage(), shot.getX(), shot.getY(), this); } } 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); } } } @Override public void paintComponent(Graphics g) { super.paintComponent(g); doDrawing(g); } private void doDrawing(Graphics g) { g.setColor(Color.black); g.fillRect(0, 0, d.width, d.height); g.setColor(Color.green); if (inGame) { g.drawLine(0, Commons.GROUND, Commons.BOARD_WIDTH, Commons.GROUND); drawAliens(g); drawPlayer(g); drawShot(g); drawBombing(g); } else { if (timer.isRunning()) { timer.stop(); } gameOver(g); } Toolkit.getDefaultToolkit().sync(); } private void gameOver(Graphics g) { g.setColor(Color.black); g.fillRect(0, 0, Commons.BOARD_WIDTH, Commons.BOARD_HEIGHT); g.setColor(new Color(0, 32, 48)); g.fillRect(50, Commons.BOARD_WIDTH / 2 - 30, Commons.BOARD_WIDTH - 100, 50); g.setColor(Color.white); g.drawRect(50, Commons.BOARD_WIDTH / 2 - 30, Commons.BOARD_WIDTH - 100, 50); var small = new Font("Helvetica", Font.BOLD, 14); var fontMetrics = this.getFontMetrics(small); g.setColor(Color.white); g.setFont(small); g.drawString(message, (Commons.BOARD_WIDTH - fontMetrics.stringWidth(message)) / 2, Commons.BOARD_WIDTH / 2); } private void update() { if (deaths == Commons.NUMBER_OF_ALIENS_TO_DESTROY) { inGame = false; timer.stop(); message = "Game won!"; } // player player.act(); // shot if (shot.isVisible()) { int shotX = shot.getX(); int shotY = shot.getY(); for (Alien alien : aliens) { int alienX = alien.getX(); int alienY = alien.getY(); 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(); } } } int y = shot.getY(); y -= 4; if (y < 0) { shot.die(); } else { shot.setY(y); } } // aliens for (Alien alien : aliens) { int x = alien.getX(); 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); } } if (x <= Commons.BORDER_LEFT && direction != 1) { direction = 1; Iterator<Alien> i2 = aliens.iterator(); while (i2.hasNext()) { Alien a = i2.next(); a.setY(a.getY() + Commons.GO_DOWN); } } } 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); } } // bombs var generator = new Random(); for (Alien alien : aliens) { 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()); } int bombX = bomb.getX(); int bombY = bomb.getY(); int playerX = player.getX(); int playerY = player.getY(); if (player.isVisible() && !bomb.isDestroyed()) { if (bombX >= (playerX) && bombX <= (playerX + Commons.PLAYER_WIDTH) &;& bombY >= (playerY) && bombY <= (playerY + Commons.PLAYER_HEIGHT)) { var ii = new ImageIcon(explImg); player.setImage(ii.getImage()); player.setDying(true); bomb.setDestroyed(true); } } if (!bomb.isDestroyed()) { bomb.setY(bomb.getY() + 1); if (bomb.getY() >= Commons.GROUND - Commons.BOMB_HEIGHT) { bomb.setDestroyed(true); } } } } private void doGameCycle() { update(); repaint(); } private class GameCycle implements ActionListener { @Override public void actionPerformed(ActionEvent e) { doGameCycle(); } } private class TAdapter extends KeyAdapter { @Override public void keyReleased(KeyEvent e) { player.keyReleased(e); } @Override public void keyPressed(KeyEvent e) { player.keyPressed(e); int x = player.getX(); int y = player.getY(); int key = e.getKeyCode(); if (key == KeyEvent.VK_SPACE) { if (inGame) { if (!shot.isVisible()) { shot = new Shot(x, y); } } } } } }
游戏的主要逻辑位于 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 个外星人。外星人图像大小为 12x12 像素。我们在外星人之间留有 6 像素的间距。我们还创建了玩家和射击对象。
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(); } }
如果玩家触发的射击与外星人发生碰撞,则外星飞船被摧毁。更准确地说,会设置死亡标志。我们用它来显示爆炸。deaths
变量增加,射击精灵被销毁。
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); } }
如果外星人到达棋盘的右边缘,它们会向下移动并改变方向向左。
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()); }
这段代码决定外星人是否会投下炸弹。外星人不得被摧毁;也就是说,它必须是可见的。炸弹的 destroyed
标志必须被设置。换句话说,这是外星人第一次投弹,或者之前投下的炸弹已经击中地面。如果满足这两个条件,则投弹就交给机会。
if (!bomb.isDestroyed()) { bomb.setY(bomb.getY() + 1); if (bomb.getY() >= Commons.GROUND - Commons.BOMB_HEIGHT) { bomb.setDestroyed(true); } }
如果炸弹未被摧毁,它将向下移动 1 像素。如果它击中地面,则设置 destroyed
标志。外星人现在已准备好投下另一枚炸弹。
public void keyReleased(KeyEvent e) { player.keyReleased(e); }
该特定 KeyEvent
的实际处理被委托给玩家精灵。

在本部分的 Java 游戏教程中,我们创建了俄罗斯方块。