俄罗斯方块是一款风靡全球的电视游戏机和掌上游戏机的游戏,它曾经造成的轰动与造成的经济价值可以说是游戏史上的一件大事。这款游戏最初是由苏联的游戏制作人Alex Pajitnov制作的,它看似简单但却变化无穷,游戏过程中仅需要玩家将不断下落的各种形状的方块移动、翻转,如果某一行被方块充满了,那就将这行消除。当窗口中无法再容纳下落的方块时,游戏结束。
可见俄罗斯方块的需求如下:
①由移动的方块和不能动的固定方块组成
②如果一行排满则消除该行
③能够随机产生多种方块
④玩家可以看到游戏分数和下一方块的形状
⑤当前方块可以逆时针旋转
本次开发的俄罗斯方块游戏运行效果如下:
完整工程源码链接:https://pan.baidu.com/s/13f3GO_y7IUtIk-beu9NVbQ 提取码: yf5k
Ⅰ俄罗斯方块形状设计
游戏中下落的方块有各种不同的形状,要在游戏中绘画不同形状的方块,就需要使用合理的数据表示方式。目前常见的俄罗斯方块一共有七种基本的形状及它们旋转后的变形体,具体的形状如下图所示。
I形方块
倒T形方块
倒Z形方块
Z形方块
J形方块
田字形方块
L形方块
为了实现对应方块的形状信息存储,这里可以采用数组来存储方块信息,比如说L形方块,可以存储为
int [] block = new int[]{
1, 0, 0, 0,
1, 0, 0, 0,
1, 1, 0, 0,
0, 0, 0, 0
}
而一种方块的所有变形体的信息可以用二维数组来存储,依旧拿L形方块举例,它的所有形态可以存储为
int[][] blocks = new int[][]{
{ 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0 },
{ 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0 } }
由于二维数组只能存储一种方块的所有变形体信息,故要存储七种方块的所有变体信息则要使用三维数组来进行存储。
Ⅱ 游戏面板屏幕
游戏的面板由20行10列的网格组成,为了存储游戏画面已经固定的方块,采用二维数组map,当相应的数组元素值大于0时表示有方块,数组元素值等于0时表示空白,数组元素值为-1时,表示围墙。而当前方块的信息并不存储在map里,只是当线程对画面进行重画时,将该方块绘制到屏幕上。另外还要使用x,y变量记录当前方块左上角所在行列数,用来判断是否和别的方块发生碰撞。
Ⅲ 游戏运行流程
整个游戏的画面由定时器或线程来进行重画操作,用户利用键盘输入来改变游戏状态。每隔一定时间就重画当前下落方块和map存储的固定方块。从而看到动态的游戏效果。
Ⅳ 游戏核心逻辑
下落判断部分:由于x,y变量记录的是当前方块的左上角一格的左边一格的所在的列数和行数,所以如果要判断当前方块下一步下落是否会落在其他方块上,只需要将y+1和x以及当前方块的数组信息作为依据,然后挨个与对应的地图数组中的部分元素信息进行核对,判断是否重叠,如果不会重叠则可以下落,否则不能下落并将当前方块信息添加到数组map中。
旋转判断部分:如果要判断当前方块是否可以旋转,则将当前方块的行列数和方块信息以及旋转状态作为依据,尝试旋转一次,并与map数组的信息进行核对,如果不会重叠,则可以旋转。
满行消去部分:一行一行地对地图信息进行扫描,如果一行的每个元素值均大于0,说明满行,消去该行,该行上面的方块依次下落一行。
自动下落部分:使用定时器定期执行当前方块的下落操作。
游戏面板类GamePanel:
public class GamePanel extends JPanel implements KeyListener {
private int blockType;// 方块类型
private int turnState;// 方块状态
private int score = 0;// 得分
private int nextBlockType = -1, nextTurnState = -1;// 下一个方块的类型及状态
private int x, y;// 当前方块位置
private Timer timer;// 定时器
int[][] map = new int[12][21];// 游戏地图,存储方块(1~7)和围墙(-1)及空块(0)的信息
// 方块的形状,有倒Z,Z,L,J,I,田,T七种方块
// 存储七种形状及其旋转变形
private final int shapes[][][] = new int[][][] {
// 长条形I形方块
{ { 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0 },
{ 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0 } },
// 倒Z字形方块
{ { 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 2, 0, 0, 0, 2, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0 },
{ 0, 2, 2, 0, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 2, 0, 0, 0, 2, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0 } },
// Z形方块
{ { 3, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 3, 0, 0, 3, 3, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0 },
{ 3, 3, 0, 0, 0, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 3, 0, 0, 3, 3, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0 } },
// J形方块
{ { 0, 4, 0, 0, 0, 4, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0 },
{ 4, 0, 0, 0, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 4, 4, 0, 0, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0 },
{ 4, 4, 4, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
// 田字形
{ { 5, 5, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 5, 5, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 5, 5, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 5, 5, 0, 0, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
// L字形
{ { 6, 0, 0, 0, 6, 0, 0, 0, 6, 6, 0, 0, 0, 0, 0, 0 },
{ 6, 6, 6, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 6, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0 },
{ 0, 0, 6, 0, 6, 6, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0 } },
// 倒T字形
{ { 0, 7, 0, 0, 7, 7, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 7, 0, 0, 7, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0 },
{ 7, 7, 7, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 },
{ 0, 7, 0, 0, 0, 7, 7, 0, 0, 7, 0, 0, 0, 0, 0, 0 } } };
省略......
}
上面代码是7种方块的信息存储以及一些必要的属性。方块信息从1~7是为了每种方块都展示出不同的颜色。方块的旋转状态是0~3之间。x,y表示方块左上角的所在行列数。
// 生成新方块
public void newBlock() {
// 如果当前没有下一方块 (游戏刚开局,生成当前方块和下一方块)
if (nextBlockType == -1 && nextTurnState == -1) {
blockType = (int) (Math.random() * 1000) % 7;
turnState = (int) (Math.random() * 1000) % 4;
nextBlockType = (int) (Math.random() * 1000) % 7;
nextTurnState = (int) (Math.random() * 1000) % 4;
} else {// 已有下一方块
blockType = nextBlockType;
turnState = nextTurnState;
nextBlockType = (int) (Math.random() * 1000) % 7;
nextTurnState = (int) (Math.random() * 1000) % 4;
}
x = 4;
y = 0;// 屏幕上方中央
if (gameover(x, y) == 1) {// 游戏结束
newMap();
drawWall();
score = 0;
JOptionPane.showMessageDialog(null, "GAME OVER!");
}
}
newBlock方法用于随机生成新方块,假如旋转状态为1时,则是逆时针旋转1次的方块。
// 画墙方法
public void drawWall() {
int i, j;
for (i = 0; i < 12; i++) {// 底部第20行画墙
map[i][20] = -1;
}
for (j = 0; j < 21; j++) {// 在第0列和第11列画墙
map[0][j] = -1;
map[11][j] = -1;
}
}
drawWall方法用于地图中围墙的绘画。
// 初始化地图
public void newMap() {
int i, j;
for (i = 0; i < 12; i++) {
for (j = 0; j < 21; j++) {
map[i][j] = 0;
}
}
}
newMap方法初始化地图,将游戏区域清空。
public GamePanel() {
newBlock();
newMap();
drawWall();
timer = new Timer(500, new TimerListener());
timer.start();
}
class TimerListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent arg0) {
if (blow(x, y + 1, blockType, turnState) == 1) {// 可以下落
y += 1;// 当前方块下移
} else {// 不可以下落
add(x, y, blockType, turnState);// 固定当前方块
delLine();// 消去满行
newBlock();// 产生新的方块
}
repaint();// 屏幕重画
}
}
构造方法产生一个新到的下落方块,同时启动定时器。定时器每隔半秒触发一次、定时器触发事件中完成屏幕的重画,同时判断当前方块是否可以下落。如果不可以下落则固定当前方块,并消去满行,同时产生新的当前方块。
// 新游戏
public void newGame() {
newBlock();
newMap();
drawWall();
}
// 暂停游戏
public void pauseGame() {
timer.stop();
}
// 继续游戏
public void continueGame() {
timer.start();
}
菜单事件的相关代码,通过定时器启动、停止来达到游戏的暂停和继续。
public void turn() {
int tempTurnState = turnState;
turnState = (turnState + 1) % 4;
if (blow(x, y, blockType, turnState) == 0) {// 如果不能旋转
turnState = tempTurnState;// 恢复
}
repaint();
}
turn()是旋转当前方块的方法。将turnState加1后,通过blow(x,y,blockType,turnState)来判断当前方块是否可以旋转,如果不能旋转则将turnState恢复为原值。
public void left() {
if (blow(x - 1, y, blockType, turnState) == 1) {// 如果可以左移
x -= 1;
}
repaint();
}
left方法用于左移当前方块。
public void right() {
if (blow(x + 1, y, blockType, turnState) == 1) {// 如果可以右移
x += 1;
}
repaint();
}
right方法用于右移当前方块。
public void down() {
if (blow(x, y + 1, blockType, turnState) == 1) {// 如果可以下落
y += 1;
} else {
add(x, y, blockType, turnState);
newBlock();
delLine();
}
repaint();
}
down方法用于当前方块的下落。通过blow(x,y+1,blockType,turnState)方法判断是否可以下落,如果可以下落,当前方块下落一行;如果不能下落,则固定当前方块,并消去满行,同时产生新的方块。
// 判断移动后当前方块的位置是否合法
// 如果方块围墙或者固定的方块重叠,return 0
public int blow(int x, int y, int blockType, int turnState) {
for (int a = 0; a < 4; a++) {
for (int b = 0; b < 4; b++) {
if (((shapes[blockType][turnState][a * 4 + b] >= 1) && (map[x
+ b + 1][y + a] >= 1))
|| ((shapes[blockType][turnState][a * 4 + b] >= 1) && (map[x
+ b + 1][y + a] == -1))) {
return 0;
}
}
}
return 1;
}
blow方法用于判断当前方块是否与围墙和固定方法重叠,如果是则return 0,如果否则return 1。
之前说过,x,y是当前方块左上角左边一格的列数和行数,所以map[x+b+1][y+a]中的[x+b+1]对应的是列,而[y+a]对应的是行。
public void delLine() {
int c = 0;
for (int b = 0; b < 21; b++) {
for (int a = 0; a < 12; a++) {
if (map[a][b] >= 1) {
c += 1;
if (c == 10) {// 满行消去,得10分
score += 10;
for (int d = b; d > 0; d--) {
for (int e = 0; e < 12; e++) {// 方块下落
map[e][d] = map[e][d - 1];
}
}
}
}
}
c = 0;
}
}
delLine方法用于消去满行方块。
public int gameover(int x, int y) {
if (blow(x, y, blockType, turnState) == 0) {// 如果碰到围墙或固定方块,游戏结束
return 1;
}
return 0;
}
gameover方法用于判断游戏是否结束。
// 将当前方块信息添加到地图数组信息中
public void add(int x, int y, int blockType, int turnState) {
int j = 0;
for (int a = 0; a < 4; a++) {
for (int b = 0; b < 4; b++) {
if (shapes[blockType][turnState][j] >= 1) {
map[x + b + 1][y + a] = shapes[blockType][turnState][j];
}
j++;
}
}
}
add方法将当前方块添加到游戏地图数组信息中。
public void paint(Graphics g) {
super.paint((g));// 调用父类的paint()方法,实现清屏
int i, j;
// 画当前方块
for (j = 0; j < 16; j++) {
if (shapes[blockType][turnState][j] >= 1) {
switch (shapes[blockType][turnState][j]) {
case 1:
g.setColor(Color.RED);
break;
case 2:
g.setColor(Color.ORANGE);
break;
case 3:
g.setColor(Color.YELLOW);
break;
case 4:
g.setColor(Color.GREEN);
break;
case 5:
g.setColor(Color.BLUE);
break;
case 6:
g.setColor(Color.PINK);
break;
case 7:
g.setColor(Color.MAGENTA);
break;
}
g.fillRect((j % 4 + x + 1) * 15, (j / 4 + y) * 15, 15, 15);
g.setColor(Color.WHITE);
g.drawRect((j % 4 + x + 1) * 15, (j / 4 + y) * 15, 15, 15);
}
}
// 画已经固定的方块和围墙
for (j = 0; j < 21; j++) {
for (i = 0; i < 12; i++) {
if (map[i][j] >= 1) {
switch (map[i][j]) {
case 1:
g.setColor(Color.RED);
break;
case 2:
g.setColor(Color.ORANGE);
break;
case 3:
g.setColor(Color.YELLOW);
break;
case 4:
g.setColor(Color.GREEN);
break;
case 5:
g.setColor(Color.BLUE);
break;
case 6:
g.setColor(Color.PINK);
break;
case 7:
g.setColor(Color.MAGENTA);
break;
}
g.fillRect(i * 15, j * 15, 15, 15);
g.setColor(Color.WHITE);
g.drawRect(i * 15, j * 15, 15, 15);
} else if (map[i][j] == -1) {
g.setColor(Color.gray);
g.fillRect(i * 15, j * 15, 15, 15);
g.setColor(Color.WHITE);
g.drawRect(i * 15, j * 15, 15, 15);
}
}
}
g.setColor(Color.BLACK);
g.drawString("当前分数:" + score, 225, 15);
g.drawString("下一个方块是:", 225, 50);
// 窗口右侧绘制下一方块
for (j = 0; j < 16; j++) {
if (shapes[nextBlockType][nextTurnState][j] >= 1) {
switch (shapes[nextBlockType][nextTurnState][j]) {
case 1:
g.setColor(Color.RED);
break;
case 2:
g.setColor(Color.ORANGE);
break;
case 3:
g.setColor(Color.YELLOW);
break;
case 4:
g.setColor(Color.GREEN);
break;
case 5:
g.setColor(Color.BLUE);
break;
case 6:
g.setColor(Color.PINK);
break;
case 7:
g.setColor(Color.MAGENTA);
break;
}
g.fillRect(225 + (j % 4) * 15, 100 + (j / 4) * 15, 15, 15);
g.setColor(Color.WHITE);
g.drawRect(225 + (j % 4) * 15, 100 + (j / 4) * 15, 15, 15);
}
}
}
paint方法用于屏幕的重画。
// 键盘监听
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_DOWN:
down();
break;
case KeyEvent.VK_UP:
turn();
break;
case KeyEvent.VK_RIGHT:
right();
break;
case KeyEvent.VK_LEFT:
left();
break;
}
}
@Override
public void keyReleased(KeyEvent arg0) {
}
@Override
public void keyTyped(KeyEvent arg0) {
}
通过键盘监听方法实现用户按键的事件响应。
package 俄罗斯方块;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;
public class Client extends JFrame implements ActionListener {
static JMenu game = new JMenu("游戏");
JMenuItem newGame = game.add("新游戏");
JMenuItem pauseGame = game.add("暂停");
JMenuItem continueGame = game.add("继续");
JMenuItem exitGame = game.add("退出");
static JMenu help = new JMenu("帮助");
JMenuItem about = help.add("关于");
GamePanel panel = new GamePanel();
public Client() {
addKeyListener(panel);// 为游戏面板添加监听
this.getContentPane().add(panel);
newGame.addActionListener(this);
pauseGame.addActionListener(this);
continueGame.addActionListener(this);
exitGame.addActionListener(this);
about.addActionListener(this);
JMenuBar menu = new JMenuBar();
setJMenuBar(menu);
menu.add(game);
menu.add(help);
setLocationRelativeTo(null);// 让窗口显示在屏幕正中间
setTitle("俄罗斯方块");
setSize(340, 395);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);// 当用户点击窗体右上角x时自动退出程序
setResizable(false);
setVisible(true);
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == newGame) {
panel.newGame();
} else if (e.getSource() == pauseGame) {
panel.pauseGame();
} else if (e.getSource() == continueGame) {
panel.continueGame();
} else if (e.getSource() == exitGame) {
System.exit(0);
} else if (e.getSource() == about) {
displayToast("左右键移动,向上键旋转");
}
}
private void displayToast(String string) {
JOptionPane.showMessageDialog(null, string, "提示",
JOptionPane.ERROR_MESSAGE);
}
public static void main(String[] args) {
new Client();
}
}
最后是游戏窗口类Client的全部代码,至此俄罗斯方块游戏制作完成。