数据结构课程设计写的2048小游戏,答辩完了就开源了,因为这次的技术文档任性地写成了傻瓜式教程了,就干脆也放出来了,供参考,源代码打包在最后面会附上。
一、 实现方案本游戏采用Java语言编写,使用Eclipse编译器, jdk1.7.0_51编译环境。 二、 具体代码及程序框图分析整个游戏有三个类,分别为游戏的主类Game.class、事件处理类MyListener.class、声音处理类PlaySound.class,下面对Game.class和MyListener.class进行说明。 public static void main(String[] args) {
Game UI = new Game();
UI.IntUI();
}
IntUI()方法用于JFrame控件及界面框架的搭建,代码解析如下: this.setTitle('2048小游戏');
this.setLocation(450, 100);
this.setSize(400, 500);
this.setLayout(null);
接下来分别是【新游戏】、【帮助】、和【退一步】的按钮,以【新游戏】按钮为例,创建一个新游戏的图片按钮,图片相对路径为res/start.png,为了达到更美观的显示效果,把聚焦,边线等特征设置为false,把相对窗体的坐标设置为(5, 10),大小设置为宽120像素高30像素,具体代码如下: ImageIcon imgicon = new ImageIcon('res/start.png');
JButton bt = new JButton(imgicon);
bt.setFocusable(false);
bt.setBorderPainted(false);
bt.setFocusPainted(false);
bt.setContentAreaFilled(false);
bt.setBounds(-15, 10, 120, 30);
this.add(bt);
而分数显示控件与按钮控件类似,不再赘述。 this.setDefaultCloseOperation(3);
this.setResizable(false);
this.setVisible(true);
对于按钮的监听,是在MyListener.class中处理的,在IntUI中只是新建一个对象来引用该类,该类具体后面会有说明,这里引用的代码如下: MyListener cl = new MyListener(this, Numbers, lb, bt, about,back);
bt.addActionListener(cl);
about.addActionListener(cl);
back.addActionListener(cl);
this.addKeyListener(cl);
IntUI方法至此结束。 super.paint(g);
g.setColor(new Color(0xBBADA0));
g.fillRoundRect(15, 110, 370, 370, 15, 15);
接下来,通过双重循环,绘制4*4的小方框,关于小方框的绘制的相对位置这里有个关键的算法,因为每一个小方框都要距离边框及相邻边框的大小相等才能达到相对美观的效果,以水平方向为例,分析如下: g.setColor(new Color(0xCDC1B4));
for (int i = 0; i < 4; i ) {
for (int j = 0; j < 4; j ) {
g.fillRoundRect(25 i * 90, 120 j * 90, 80, 80, 15, 15);
}
}
由于2048游戏里面可能显示的数值有2、4、8、16等等不同的数值,同样是为了美观考虑,我们给显示不同数值的方框绘制不同的颜色,同样,由于一位数字2与两位数字16甚至多位数字128或1024等来说,如果显示的位置与大小相同,那么2等一位数字的显示是完美的,但是2048这些数字的显示就会超出小方框,影响观感所以还要对数字的相对位置和大小做一定的调整,这里的具体代码如下: for (int i = 0; i < 4; i ) {
for (int j = 0; j < 4; j ) {
if (Numbers[j][i] != 0) {
int FontSize = 30;
int MoveX = 0, MoveY = 0;
switch (Numbers[j][i]) {
case 2:
g.setColor(new Color(0xeee4da));
FontSize = 30;
MoveX = 0;
MoveY = 0;
break;
case 4:
g.setColor(new Color(0xede0c8));
FontSize = 30;
MoveX = 0;
MoveY = 0;
break;
case 8:
g.setColor(new Color(0xf2b179));
FontSize = 30;
MoveX = 0;
MoveY = 0;
break;
case 16:
g.setColor(new Color(0xf59563));
FontSize = 29;
MoveX = -5;
MoveY = 0;
break;
case 32:
g.setColor(new Color(0xf67c5f));
FontSize = 29;
MoveX = -5;
MoveY = 0;
break;
case 64:
g.setColor(new Color(0xf65e3b));
FontSize = 29;
MoveX = -5;
MoveY = 0;
break;
case 128:
g.setColor(new Color(0xedcf72));
FontSize = 28;
MoveX = -10;
MoveY = 0;
break;
case 256:
g.setColor(new Color(0xedcc61));
FontSize = 28;
MoveX = -10;
MoveY = 0;
break;
case 512:
g.setColor(new Color(0xedc850));
FontSize = 28;
MoveX = -10;
MoveY = 0;
break;
case 1024:
g.setColor(new Color(0xedc53f));
FontSize = 27;
MoveX = -15;
MoveY = 0;
break;
case 2048:
g.setColor(new Color(0xedc22e));
FontSize = 27;
MoveX = -15;
MoveY = 0;
break;
default:
g.setColor(new Color(0x000000));
break;
}
g.fillRoundRect(25 i * 90, 120 j * 90, 80, 80, 10, 10);
g.setColor(new Color(0x000000));
g.setFont(new Font('Arial', Font.PLAIN, FontSize));
g.drawString(Numbers[j][i] '', 25 i * 90 30 MoveX,
120 j * 90 50 MoveY);
}
}
}
至此,Game.class的分析结束,下面是对按钮监听及时间处理的MyListener.class的分析。 private Game UI;
private int Numbers[][];
private Random rand = new Random();
private int BackUp[][]= new int[4][4];
private int BackUp2[][]= new int[4][4];
public JLabel lb;
int score = 0;
int tempscore,tempscore2;
public JButton bt,about,back;
private boolean isWin=false,relive=false,hasBack=false,isSound=true;
然后是MyListener的构造方法,把来自Game类中传进来的参数接收,代码如下: public MyListener(Game UI, int Numbers[][], JLabel lb,JButton bt,JButton about,JButton back,JCheckBox isSoundBox) {
this.UI = UI;
this.Numbers = Numbers;
this.lb = lb;
this.bt=bt;
this.about=about;
this.back=back;
this.isSoundBox=isSoundBox;
}
接下来是按钮的监听,通过e.getSource()取得按钮相应的值,进行相应的处理,下面分别解析下【新游戏】按钮和【退一步】按钮的代码: if(e.getSource() ==bt ){
isWin=false;
for (int i = 0; i < 4; i )
for (int j = 0; j < 4; j )
Numbers[i][j] = 0;
score = 0;
lb.setText('分数:' score);
int r1 = rand.nextInt(4);
int r2 = rand.nextInt(4);
int c1 = rand.nextInt(4);
int c2 = rand.nextInt(4);
while (r1 == r2 && c1 == c2) {
r2 = rand.nextInt(4);
c2 = rand.nextInt(4);
}
int value1 = rand.nextInt(2) * 2 2;
int value2 = rand.nextInt(2) * 2 2;
Numbers[r1][c1] = value1;
Numbers[r2][c2] = value2;
UI.paint(UI.getGraphics());
}
【退一步】按钮分了两种情况考虑,为了出现退一步继续按【退一步】按钮出现异常情况,加了一个是否已经进行过一次回退操作了的标志hasBack,进入回退按钮的操作后,先判断是否是起死回生类型的回退,如果不是起死回生类型的回退则把非起死回生分数备份的变量tempscore赋值回记录分数的变量score,然后循环调用java.util.Arrays.copyOf()方法复制数组,把备份的数组复制回去Numbers数组;如果是起死回生类型的回退,重新复制等操作和前面是一样的,不同点在于起死回生类型的回退在操作后,把起死回生回退的标志relive重新置为false。在完成这些操作后,重新绘图,代码如下: else if(e.getSource()==back&&hasBack==false){
hasBack=true;
if(relive==false){
score=tempscore;
lb.setText('分数:' score);
for(int i=0;i<BackUp.length;i ){
Numbers[i]=Arrays.copyOf(BackUp[i], BackUp[i].length);
}
}
else{
score=tempscore2;
lb.setText('分数:' score);
for(int i=0;i<BackUp2.length;i ){
Numbers[i]=Arrays.copyOf(BackUp2[i], BackUp2[i].length);
}
relive=false;
}
UI.paint(UI.getGraphics());
}
下面是按键监听的解析,按键监听通过相应的键值识别按键,然后运用switch开关语句控制不同按键的事件。在处理所有时间之前,先定义三个整型变量,用于计数,然后判断BackUp数组是否为空,因为第一次运行时用于普通数组备份的BackUp数组是没有任何东西的,直接拿来备份会报异常。在备份好数组后,就是对应的键值的监听,以按下方向键左为例(其它三个方向键处理方式基本相同,只是行列及方向的区别),向左移动时,采用三个双重循环,三个循环均为外循环的变量h控制行数,内循环变量l控制列数。在第一个双重循环中,内循环先遍历每一列,判断该列的前一列是否为0,如果前一列为0,则把该列赋值给一个临时变量,把临时变量的值赋值给前一列,然后把该列置0;外循环是遍历行数,每完成一行的操作转至下一行继续重复操作。该操作的代码如下: for (int h = 0; h < 4; h )
for (int l = 0; l < 4; l )
if (Numbers[h][l] != 0) {
int temp = Numbers[h][l];
int pre = l - 1;
while (pre >=0 && Numbers[h][pre] == 0) {
Numbers[h][pre] = temp;
Numbers[h][pre 1] = 0;
pre--;
Counter ;
}
}
在完成一次靠左移动后,接下来的双重循环使用来合并小方格中数字相同的元素的,把他们相加并放到靠左的方格中,同样外循环是用来控制行数,内循环控制列数,在内循环中加了一个判断条件,当列数加1小于4时(因为只需循环到第三列即可完成该操作),同时该列与该列 1的那一列的元素值相等且它们都不等于0时,即可进行合并操作,合并后,把该列的值置为两元素相加的结果,并把该列 1列置为0,然后把统计是否移动了的计数器进行一个自增操作。完成该操作的代码如下: for(int h=0;h<4;h )
for(int l=0;l<4;l )
if(l 1<4&&(Numbers[h][l]==Numbers[h][l 1])&&(Numbers[h][l]!=0||Numbers[h][l 1]!=0)){
new PlaySound('merge.wav').start();
Numbers[h][l]=Numbers[h][l] Numbers[h][l 1];
Numbers[h][l 1]=0;
Counter ;
score =Numbers[h][l];
if(Numbers[h][l]==2048){
isWin=true;
}
}
在完成该操作后,为了避免合并后,出现方块不在最左边的情况,还需要一次整体遍历左移,这次整体遍历左移和第一次的双重循环一样,所以这里不再分析。 for (int i = 0; i < 3; i ) {
for (int j = 0; j < 3; j ) {
if (Numbers[i][j] == Numbers[i][j 1]&& Numbers[i][j] != 0) {
NumNearCounter ;
}
if (Numbers[i][j] == Numbers[i 1][j]&& Numbers[i][j] != 0) {
NumNearCounter ;
}
if (Numbers[3][j] == Numbers[3][j 1]&& Numbers[3][j] != 0) {
NumNearCounter ;
}
if (Numbers[i][3] == Numbers[i 1][3]&& Numbers[i][3] != 0) {
NumNearCounter ;
}
}
}
在完成上述操作后,在判断非0元素的个数,应为当非零元素个数为0时,即代表所有的小方块中已经有元素存在了,16个方块全部都满了,这里的代码如下: for (int i = 0; i < 4; i ) {
for (int j = 0; j < 4; j ) {
if (Numbers[i][j] != 0) {
NumCounter ;
}
}
}
在完成统计空格个数后,如果按下按键后发生了移动,就完成分数的更新,因为要移动一次后在随机的空的格子(这里指元素值为0的格子)里面产生新的基于2或者4的随机数,代码如下: if (Counter > 0) {
lb.setText('分数:' score);
int r1 = rand.nextInt(4);
int c1 = rand.nextInt(4);
while (Numbers[r1][c1] != 0) {
r1 = rand.nextInt(4);
c1 = rand.nextInt(4);
}
int value1 = rand.nextInt(2) * 2 2;
Numbers[r1][c1] = value1;
}
接下来判断这次移动后,有没有出现isWin == true的情况,如果出现了,就弹出游戏胜利的标志,代码如下: if (isWin == true){
UI.paint(UI.getGraphics());
JOptionPane.showMessageDialog(UI, '恭喜你赢了!\n您的最终得分为:' score);
}
然后,判断是否是16个格子全满了同时相邻的方格不能进一步合并了,如果是则把可以进行起死回生操作的标志relive置为true,同时弹出游戏结束的提示语,代码如下: if (NumCounter == 16 && NumNearCounter == 0) {
relive = true;
JOptionPane.showMessageDialog(UI, '没地方可以合并咯!!'
'\n很遗憾,您输了~>_<~' '\n悄悄告诉你,游戏有起死回生功能哦,不信你“退一步”试试?'
'\n说不定能扭转乾坤捏 (^_~)');
}
最后,重新绘制图形。 三、 参考资料Java API1.6.0中文版 |
|