基于Java的俄罗斯方块游戏的设计与实现

时间:2022-07-22
本文章向大家介绍基于Java的俄罗斯方块游戏的设计与实现,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

俄罗斯方块项目,基本功能包括:游戏主界面显示模块、方块及数据显示模块、方块移动控制模块、游戏界面颜色控制模块、游戏进度、等级控制模块等。本项目结构如下: (1)游戏主界面显示模块: 显示游戏和帮助两个菜单; 游戏使用功能键盘,得分 等级;

(2)画布、方块显示模块: 可以根据自己的需求来自己动手更改背景图片,在方块下落过程中,根据颜色的变化识别下落的方块。 (3)背景画布模块设计: 游戏用继承自JPanel的GameCanvas类控制背景画布的显示,用rows代表画布的行数,cols代表画布的列数,行数和列数决定着画布拥有方格的数目;

(4)方块移动、旋转模块 通过更改横坐标或纵坐标,然后重新绘制方块实现方块移动、旋转。

Java是一种纯面向对象(Object-Oriented)的程序语言,它的诸多优点在此就不作详细论述了。从面向对象的观念出发,本程序主要可分为以下几个模块:

    ●游戏主界面显示模块

    ●方块及数据显示模块

    ●方块移动控制模块

    ●游戏界面颜色控制模块

    ●游戏进度、等级控制模块

分析对象的技巧在于它的功能的扩展性及维护效率。试想,如果因为外部环境或者用户需求的变化需要对程序进行功能扩展或者维护,就要对代码作大幅度的更动甚至重写,这样就失去了面向对象的优势,所以在本程序分析时将独立性高的模块作为一个对象,以提高程序的可扩展性和可维护性。以下该游戏中类的设计:

    MyFrame类:继承自JFrame类,作为游戏的主类,负责对游戏的主体全局进行控制,连接与指挥各个类的枢纽。

    PreView类:继承自JPanel类,作为操作面板类,负责安放主游戏窗口,积分板,级别控制按钮等窗口用以控制游戏进程。

    GameCanvas类:继承自JPanel类,ChangeBlockColor线程类动态改变画布类的方格颜色,画布类通过检查方格颜色来体现ChangeBlockColor方块的移动情况与消去状况。

    Box类:方格类,组成方块的基本元素,主要表达方式为颜色。

    Block类:作为操控方块的类,控制方块的移动,下落以及变形。

5.1 游戏主界面显示模块

一个优秀的软件系统不仅体现在核心的功能的多样性和强大上,如果使用者所面对的是枯燥的、陈旧的界面的话,那么这个的软件系统是不成功的,所以好的、精美的界面设计是极为重要的环节。为玩家设计制作布局合理、视觉效果良好的界面的重要性就不言而喻了。

游戏主界面采用Swing组件开发,并且向其注册监听器,以实现各种控制功能,综合游戏窗体的设计,其上至少需要注册三个监听器,分别是动作监听器(ActionListener)、键盘监听器(KeyListener)、选项监听器(ItemListener)。

根据初步设计,可以确定客户端上所要用到的Swing组件对象有JFrame对象、JPanel对象,JLabel对象、JButton对象、JMenuBar对象、JMenu对象、JMenuItem对象、JTextField对象、JTextArea对象、JDialog对象等,至少十个Swing组件对象。下图5-1为游戏主界面截图。

图5-1游戏主界面截图

本游戏主界面设计的初始颜色搭配基于对比鲜明的原则,默认背景色为深绿色,左上角设置俄罗斯方块图标,得分初始值为0,等级初始值为1,最高分记录初始值为0。游戏主窗体尺寸设置为(520,580),方块移动范围窗格由一个20行、12列的二维数组控制,且左上角图标设置为方块图案,起标识作用。

5.2 画布、方块显示模块

本游戏中将画布设计为自定义图片,可以根据自己的需求来自己动手更改背景图片,在方块下落过程中,根据颜色的变化识别下落的方块。

5.2.1 背景画布模块设计

该游戏的主背景画布是一个20行、12列的二维数组,方块显示是由相应颜色变化来标识,主窗体用颜色填充后可形成呈现出来背景样式和方块。本游戏用继承自JPanel的GameCanvas类控制背景画布的显示,用rows代表画布的行数,cols代表画布的列数,行数和列数决定着画布拥有方格的数目。背景画布主要实现代码如下:

首先,用一个画布类的构造函数来表示整个主界面的行数、列数以及主界

中的相对位置:

  1    /**
  2 
  3         * 画布类的构造函数
  4 
  5         * @param rows int, 画布的行数
  6 
  7         * @param cols int, 画布的列数
  8 
  9         * 行数和列数决定着画布拥有方格的数目
 10 
 11         */
 12 
 13        public GameCanvas(int rows, int cols) {
 14 
 15               this.rows = rows;
 16 
 17               this.cols = cols;
 18 
 19               this.setOpaque(false);
 20 
 21               boxes = new Box[rows][cols];
 22 
 23               for (int i = 0; i < boxes.length; i++) {
 24 
 25                      for (int j = 0; j < boxes[i].length; j++) {
 26 
 27                             boxes[i][j] = new Box(false);
 28 
 29                      }
 30 
 31               }
 32 
 33               setBounds(0, 0, 300, 500);//设置相对位置坐标
 34 
 35               setBorder(new EtchedBorder(
 36 
 37                       EtchedBorder.RAISED, Color.white, new Color(148, 145, 140)));
 38 
 39        }
 40 
 41        /**
 42 
 43         * 取得画布中方格的行数
 44 
 45         * @return int, 方格的行数
 46 
 47         */
 48 
 49        public int getRows() {
 50 
 51               return rows;
 52 
 53        }
 54 
 55  
 56 
 57        /**
 58 
 59         * 取得画布中方格的列数
 60 
 61         * @return int, 方格的列数
 62 
 63         */
 64 
 65        public int getCols() {
 66 
 67               return cols;
 68 
 69        }
 70 
 71     其次,在设置一个画布类的构造函数来表示整个主界面的前景色,背景色并获取其前景色和背景色:
 72 
 73        /**
 74 
 75         * 画布类的构造函数
 76 
 77         * @param rows 与public GameCanvas(int rows, int cols)同
 78 
 79         * @param cols 与public GameCanvas(int rows, int cols)同
 80 
 81         * @param backColor Color, 背景色
 82 
 83         * @param frontColor Color, 前景色
 84 
 85         */
 86 
 87        public GameCanvas(int rows, int cols,
 88 
 89                          Color backColor, Color frontColor) {
 90 
 91               this(rows, cols);
 92 
 93               this.backColor = backColor;
 94 
 95               this.frontColor = frontColor;
 96 
 97        }
 98 
 99  
100 
101        /**
102 
103         * 设置游戏背景色彩
104 
105        * @param backColor Color, 背景色彩
106 
107         */
108 
109        public void setBackgroundColor(Color backColor) {
110 
111               this.backColor = backColor;
112 
113        }
114 
115  
116 
117        /**
118 
119         * 取得游戏背景色彩
120 
121        * @return Color, 背景色彩
122 
123         */
124 
125        public Color getBackgroundColor() {
126 
127               return backColor;
128 
129        }

5.2.2 预览方块模块设计

方块和数据信息是游戏中最基本的功能模块。Box这个类方格类,是组成块的基本元素,用自己的颜色来表示块的外观 ,MyTask继承TimerTask类用来定时下落,用计数方式来实现速度的改变,MyListener类继承KeyAdapter类用来实现按键监听,控制方块的上下左右。定义一个4x4方阵,共16个小格。用“0”和“1”来表示每个方格是绘制新颜色还是保留底色。

每得到一个新方块,都是随机从七种形态的方块中选取一种。游戏定义了一个变量,代表新方块的模型。比如定义int型数组STYLE代表28中方块类型,7行4列,每个元素代表其中一种方块。即0<=blockkindnum<=6,0=<blockstatusnum<=3

那么,当方块落下需要得到新方块时,只需随机得到一对blockkindnum,blockstatusnum值,然后再根据这个STYLE的值构画相应的方块。剩下的问题就是应该怎么随机到一对STYLE行列值。

Java语言包中的Math类提供了一个生成随机数的方法random(),调用这个方法会产生一个在0-1之间的双精度浮点数。所以每次要得到新方块时,只需调用一次这个方法,得到一个0-1的双精度浮点数,然后用该数乘以7,之后强转成整型,即可得到1—7的整数,用来控制行。用该数乘以4,之后强转成整型,即可得到1—4的整数,用来控制列。

由此可以组合出多种图形定义然后用代码实现下列功能:

1>每次执行首先为随机数产生不同的初值。

int col = (int) (Math.random() * (gc.getCols() - 3));//随即位置生成列

int style = Constant.STYLES[(int) (Math.random() * Block.get_addl())][(int) (Math.random() * 4)];

图5-2随机产生方块流程图

2>随机选取一个图形,图5-2随机产生方块图具体描述用生成的随机数控

产生的图形。

3>当前图形在其4*4网格中的位置信息。

绘制4行4列的方块预显方格,随机生成预显示的方块样式。本游戏用二维数组存储方块的28种样式。

值得注意的是:在传统的俄罗斯方块游戏的基础上,本游戏系统为了体现出创新的思维,本着为了学习的原则,在传统游戏的基础上增加了中级三种其他的方块样式和高级三种其他的方块样式。一共有52种方块样式,具体的存储方式主要实现代码如下:

 1    /**
 2 
 3         * 分别对应对13种模型的52种状态
 4 
 5         */
 6 
 7        public final static int[][] STYLES = {// 共28种状态
 8 
 9               {0xf000, 0x8888, 0xf000, 0x8888}, // 长条型的四种状态
10 
11               {0x4e00, 0x4640, 0xe400, 0x4c40}, // 'T'型的四种状态
12 
13               {0x4620, 0x6c00, 0x4620, 0x6c00}, // 反'Z'型的四种状态
14 
15               {0x2640, 0xc600, 0x2640, 0xc600}, // 'Z'型的四种状态
16 
17               {0x6220, 0x1700, 0x2230, 0x7400}, // '7'型的四种状态
18 
19               {0x6440, 0xe200, 0x44c0, 0x8e00}, // 反'7'型的四种状态
20 
21               {0x6600, 0x6600, 0x6600, 0x6600}, // 方块的四种状态
22 
23               {0x8c88,0xf200,0x44c4,0x4f00},//增加的中级样式方块3个
24 
25               {0xea00,0xc4c0,0xae00,0xc8c0},
26 
27               {0x8c00,0xc800,0xc400,0x4c00},
28 
29               {0xac00,0xcc40,0x6e00,0x8cc0},//增加的高级样式方块3个
30 
31               {0x4e40,0x4e40,0x4e40,0x4e40},
32 
33               {0x8480,0xa400,0x4840,0x4a00},
34 
35        };

传统俄罗斯方块游戏的7种方块样式,相信很多人都知道,在这里就不一一截图展示常见的方块样式。以下是在传统游戏的模式下增加的三种中级难度和三种高级难度的方块模型:

●增加的三种中级难度方块模型(经过90度、180度、270度、360度旋得到四种转状态)

图5-10增加的三种中级难度方块模型

●增加的三种高级难度方块模型(经过90度、180度、270度、360度旋得到四种转状态)

图5-11增加的三种高级难度方块模型

5.2.3 方块移动、旋转模块设计

方块的翻转与移动比较容易实现,方块移动只需要改变方块的横坐标或纵坐标,然后重新绘制方块即可。方块翻转也只需要改变背景数组的值,重新绘制方块即可。

本游戏方块下落时,进行动态绘制,实现Cloneable接口, 以指示 Object.clone() 方法可以合法地对该类实例进行按字段复制。方块的操作类BlockOperation继承Thread类,重写run()方法,以实现方块的动态正确下落。当然,在线程中要判定方块是处于moving状态还是pausing状态。

 1 publicvoid run()
 2 
 3        {
 4 
 5               //moving判定方块是否在动态下落
 6 
 7               while (moving)
 8 
 9               {
10 
11                      try
12 
13                      {
14 
15                             //betweenleveltime指示相邻等级之间相差时间
16 
17                             sleep(betweenleveltime
18 
19                                     * (ControlMainGame.maxlevel - level + flatgene));
20 
21                      } catch (InterruptedException ie)
22 
23                      {
24 
25                             ie.printStackTrace();
26 
27                      }
28 
29                      //pausing判定游戏是否处于暂停状态
30 
31                      if (!pausing)
32 
33                             moving = (moveTo(y + 1, x) && moving);
34 
35                      //moving是在等待的100毫秒间,moving没被改变
36 
37              }}

当然,在游戏中还要判定方块移动的边界问题, 比如,一个方块在它左边正好差一个格子的空间才能够翻转,但是它的右边恰好有一个格子的空间,这种情况,如果方块不能够翻转,就不方便用户操作,如果能够翻转,就会发生越界,将已经存在的方块挤占掉。要想实现翻转又不发生越界,那么,就应该在方块翻转后把它往右边移动一个格子,然后再绘制方块,这样,方块就不会挤占掉其它已经固定住的方块了,以下解决越界问题。

1>方块翻转判定

在两种情况可能发生越界,一种是方块落下去固定住以后,第二种是周围的空间不允许它进行翻转。

第一种情况只需要参考方块落下去后不能够再移动的判定即可。

对于第二种情况,在每次方块翻转前,必须首先计算出方块周围的空间,如果空间允许则翻转。否则,不能翻转。

因为七种方块是不规则的,每种方块要求的翻转空间都是不一样的,甚至是在它的不同翻转状态下,所要求的翻转空间也是不一样的,首先想到的自然就是为每一种方块,方块的每一种状态都写一个判定条件,但是这样做未免过于麻烦。

根据观察,不难发现,七种形态的方块,长条形的方块如果以横条的形态下落,则只要能够下落,就能翻转,如果以竖条的形态下落,那么它翻转后所处的位置必须要有4x1个格子的空间才能够翻转。对于田字形的方块,只有能够继续下坠,就一定能够翻转,所以田字型的方块只要没有落下,就一直能够翻转。而其它五种形态的方块,又有一个共同点,就是它们都有两种翻转状态横向占三个格子的空间,竖直方向占两个空间,另外两种翻转状态横向占两个格子的空间,竖直方向占三个格子空间,如果他们是以横向占三个格子的状态下落,那么只要能下落,就一定能够翻转,如果是以横向两个格子的状态下落,那么在翻转后,周围必须要有3x2个格子的空间。

所以,方块翻转的判定,要分三种情况,第一种情况是方块落下去后不能翻转;第二种情况是对竖直状态出现的长条形的方块进行翻转判定;第三种情况是对除长条形和田字形之外的其它五种以横向占两个格子的状态出现的方块进行翻转判定。

何种情况下方块能够翻转的问题解决了,接下来,我们就应该解决方块翻转后所处的位置的问题了,因为只有事先知道方块翻转后所处的位置,才能够对那个位置的空间范围进行判定,判定它是否能够容纳方块。

可以确定的是,无论方块怎么翻转,都处在方块数组中,也就是说方块必定是在游戏地图中某一4x4个格子的空间范围内。

方块数组在游戏主界面中的坐标是确定的,不确定的是方块翻转后到底处在方块数组的哪个位置,为了解决这个问题,我们可以限定方块在方块数组中的存储原则是靠左、靠上,这样,无论翻转怎么翻转,方块数组中第一行和第一列都是有方块的,这样也就确定了方块在方块数组中的位置,也就可以得知方块翻转后在游戏地图中的位置了。

假定方块数组的横纵坐标是x和y,那么,这个位置就是,长条形的方块翻转后所处的那一行是游戏地图的第y行,所占的列是第x到x+3列,长条形和田字形以外的五种方块翻转后的所占的行数是游戏地图的第y和第y+1行,所占的列是第x到x+2列。

所以,如果以上空间有空格子,方块就能够翻转。

2>翻转越界纠正

只要方块翻转后所处的空间足够,方块就能够翻转,但是,如果方块翻转后所处的空间不足够,而在它的另一边却有足够的空间呢?

方块在边界处时,翻转后不仅可能翻出地图外,还可能发生数组越界,当然,只需要将地图数组定义得大一些,就能够避免数组越界错误,对于方块越界,如果在它的另一边有足够空间,那么,就应该把方块往另一个方向移动适当的单位,纠正方块越界错误。如图5-12方块翻转流程图所示,方块翻转需要经三次判定:是否已经下落到底部、翻转后是否有足够空间、翻转后是否越界。

图5-12 方块翻转处理流程图

玩家操作键盘实现方块的移动、旋转,代码引进ControlKeyListener类继承KeyAdapter类进行键盘监听功能的实现。KeyAdapter类继承自Object类,实现KeyListener接口,用来接收键盘事件的抽象适配器类。此类中的方法为空。此类存在的目的是方便创建侦听器对象。扩展此类即可创建 KeyEvent 侦听器并重写所需事件的方法,即是 ControlKeyListener类。使用ControlKeyListener可创建侦听器对象,然后使用组件的 addKeyListener 方法向该组件注册此侦听器对象。当按下、释放或键入某个键时,将调用该侦听器对象中的相应方法,并将 KeyEvent 传递给相应的方法。实现代码如下:
 1  privateclass ControlKeyListener extends KeyAdapter
 2 
 3        {
 4 
 5               publicvoid keyPressed(KeyEvent ke)
 6 
 7               {
 8 
 9                      if (!game.isPlaying())
10 
11                             return;
12 
13  
14 
15                      BlockOperation blockope = game.getCurBlock();
16 
17                      switch (ke.getKeyCode())
18 
19                      {
20 
21                             case KeyEvent.VK_DOWN:
22 
23                                    blockope.moveDown();
24 
25                                    break;
26 
27                             case KeyEvent.VK_LEFT:
28 
29                                    blockope.moveLeft();
30 
31                                    break;
32 
33                             case KeyEvent.VK_RIGHT:
34 
35                                    blockope.moveRight();
36 
37                                    break;
38 
39                             case KeyEvent.VK_UP:
40 
41                                    blockope.turnNext();
42 
43                                    break;
44 
45                             default:
46 
47                                    break;}}}

5.3 控制面版模块

5.3.1 菜单栏模块设计

菜单栏中有“游戏”、“帮助”四个菜单选项。“游戏”选项又分“开局”、“初级”、“中级”、“高级”、“自定义”、“方块颜色”、“退出”等七个选项。“帮助”选项中有“关于”选项,用于显示游戏版本等信息。

1>“开局”的按钮功能为实现游戏画布的重新绘制,类似reset的功能。该按钮的监听实现代码如下:

/

 1 **
 2 
 3         * 重置画布
 4 
 5         */
 6 
 7        public void reset() {
 8 
 9               for (int i = 0; i < boxes.length; i++) {
10 
11                      for (int j = 0; j < boxes[i].length; j++)
12 
13                             boxes[i][j].setColor(false);
14 
15               }
16 
17  
18 
19               repaint();
20 
21        }

2>“初级”、“中级”、“高级”按钮用来手动调节游戏的等级,从而改变游戏的等级难度。 “退出”按钮控制游戏随时退出,终止游戏。

3>“帮助”按钮中点击“关于”按钮显示与游戏软件本身相关信息,具体信息如图所示:

图4—19 “关于”选项截图

5.3.2 控制面板按钮设计

本游戏控制面板中包含得分统计、等级统计等字段。

其中的TextField控件均由游戏本身统计给出,玩家不能私自编辑。本游戏的游戏规则为每消一行得10分,每增加100分上升一个等级,初始得分为0,初始等级为1。

以下给出得分、等级更新等功能实现的主要代码:

  1  /**
  2 
  3                * 判断是否满行,满行则调用消行方法。
  4 
  5                */
  6 
  7               private void isFullLine() {
  8 
  9                      // TODO Auto-generated method stub
 10 
 11  
 12 
 13                      for (int i = 0; i < 20; i++) {
 14 
 15                             int row = 0;
 16 
 17                             boolean flag = true;
 18 
 19                             for (int j = 0; j < 12; j++) {
 20 
 21                                    if (!gc.getBox(i, j).isColorBox()) {
 22 
 23                                           flag = false;
 24 
 25                                           break;
 26 
 27                                    }
 28 
 29                             }
 30 
 31  
 32 
 33                             if (flag == true) {
 34 
 35                                    row = i;
 36 
 37                                    gc.delete(row);//删除行
 38 
 39                                    if(isMusic==true)
 40 
 41                                    {mp.playEraseSound();}
 42 
 43                                    addScor();//增加分数
 44 
 45                                    if(scor%100==0)//设置为100分增加一个等级
 46 
 47                                    upspeed=true;//将速度增加标志位至为true
 48 
 49                                    if(upspeed==true)
 50 
 51                                           upLevel();
 52 
 53                             }
 54 
 55                      }
 56 
 57  
 58 
 59               }
 60 
 61  
 62 
 63               /**
 64 
 65                * 得分的计算方法
 66 
 67                */
 68 
 69               private void addScor() {
 70 
 71                      scor=scor+10;
 72 
 73                      jt9.setText("得分:"+MyFrame.scor);
 74 
 75               }
 76 
 77       
 78 
 79        }
 80 
 81        private void reset() {
 82 
 83               scor=0;
 84 
 85               rank=0;
 86 
 87               jt10.setText("等级:"+rank);
 88 
 89               jt9.setText("得分:"+scor);
 90 
 91               upspeed=false;
 92 
 93               playing=true;
 94 
 95               runstop=false;
 96 
 97               gc.setGameOver(false);
 98 
 99               gc.repaint();
100 
101               gc.reset();
102 
103        }

控制面板中按钮的功能在4.3.1中已给出代码,在此不再赘述。