学习Java基础有一段时间了,一直想做个小的桌面程序练下手,最近自己有点时间用Java写了一个2048的桌面程序,和大家分享一下!!!
游戏效果展示:
1、设计思想
AppFrame.java 游戏的启动类,只调用了一个MainFrame的构造方法
MainFrame.java 游戏的界面类,在构造方法中做了所有的初始化操作
MyKeyListener 游戏的控制类,继承自Java中的KeyListener,实现上下左右移动的控制。
程序中的组件除了newGame按钮之外基本都是JLable组件,空块就是上面黄色没有数字的块,空快、2、4、8、16……都是通过修改JLabel的背景图片来实现的。游戏实现整体基本没有太大的难度,重要的逻辑在代码中也有相应的解释说明。
2、在eclipse中创建如下所示的文件和目录结构
由于时间有限,没有截到512以后的图片,但是游戏是编写完成的,有兴趣的可以自己完善下,把512以后的图片进行截图按照上面的命名方式存到numberimage文件夹下即可。已有的图片我也会同时上传上去,方便大家自己练习。
3、三个类的源码及代码的解释
主程序启动类AppFrame.java,只有一个MainFrame的构造方法
package cn.tzfe.app;
import cn.tzfe.view.MainFrame;
public class AppFrame
{
public static void main(String[] args)
{
MainFrame mainFrame = new MainFrame();
mainFrame.setVisible(true);
}
}
程序类MainFrame.java,包括初始化UI、数据、监听等
package cn.tzfe.view;
import java.awt.Cursor;
import java.awt.Desktop;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.net.URI;
import java.util.Random;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import cn.tzfe.listener.MyKeyListener;
public class MainFrame extends JFrame
{
//为了下面图片和组件的方便使用,将资源基本都定义在一起
private static ImageIcon mainFrameIcon;//游戏窗口背景图片
private static ImageIcon logoIcon; //白色背景的2048图片
private static ImageIcon scoreIcon; //score图片
private static ImageIcon bestIcon; //best图片
private static ImageIcon newGameIcon; //newGame按钮图片
private static ImageIcon emptyIcon; //空块(零块)
private static ImageIcon twoIcon; //2块
private static ImageIcon fourIcon; //4块
private JLabel logo_Label; //2048Lable
private JLabel contact_Label; //HOW TO PLAY Label
private JLabel score_Label; //scoreLabel
private static JLabel scoreText_Label; //scoreTextLabel
private JLabel best_Label; //bestLabel
private static JLabel bestText_Label; //bestTextLabel
private JButton newgameBtn; //newGame按钮
private JLabel back_Label; //主窗体背景组件
private static int[][] block_Data = new int[4][4]; //一个二维int数组数据域
private static JLabel[][] block_Lable = new JLabel[4][4]; //一个二维组件块域和上面数组对应,各个数字块的移动与合并就是操作者两个数组共同的结果
//定义一个静态块,将所需要初始化的图片一次性初始化完毕,只初始化0、2、4块的原因是最初只会出现这三种块
static
{
mainFrameIcon = new ImageIcon("mainframebg.png");
logoIcon = new ImageIcon("logo.png");
scoreIcon = new ImageIcon("score.png");
bestIcon = new ImageIcon("best.png");
newGameIcon = new ImageIcon("newgame.png");
emptyIcon = new ImageIcon("numberimage/0.png");
twoIcon = new ImageIcon("numberimage/2.png");
fourIcon = new ImageIcon("numberimage/4.png");
}
//MainFrame的构造方法,在Swing中有这么一个问题在窗体中放置图片时,先放置的图片在最上层,后放置的在下层。
//所以要先初始化游戏块,最后初始化窗体的背景。
public MainFrame()
{
//初始化窗体大小位置等(设置窗体布局方式为自由布局)
initBasic();
//初始化开始游戏安按钮,得分,最好成绩等
initNewGame();
//初始化空块
initEmptyBlocks();
//初始化两个初始值(2或4)到block_Data数组中,并设置block_Label中对应块的图像
initData();
//初始化事件监听
initListener();
//初始化窗体背景
initFrameBackGround();
setVisible(true);
}
private void initListener()
{
//HOW TO PLAY?组件增加监听,设置为模拟超链接的那种方式
contact_Label.addMouseListener(new MouseAdapter()
{
//鼠标进入设置颜色为蓝色并添加下划线,和提示信息
public void mouseEntered(MouseEvent e)
{
contact_Label.setText(""+contact_Label.getText()+"");
contact_Label.setToolTipText("You will open the official website of 2048.");
}
//鼠标移出设置为原来的文本,并将提示信息设置为""
public void mouseExited(MouseEvent e)
{
contact_Label.setText("HOW TO PLAY?");
contact_Label.setToolTipText("");
}
//鼠标单击打开2048的官网
public void mouseClicked(MouseEvent e)
{
try
{
Desktop.getDesktop().browse(new URI("http://2048game.com/"));
} catch (Exception ex)
{
ex.printStackTrace();
}
}
});
//newGame按钮设置鼠标放上去时变成手型样式
newgameBtn.setCursor(new Cursor(Cursor.HAND_CURSOR));
//newGame按钮绑定触发事件,即开始依据新游戏
newgameBtn.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
//清空block_Label,全部设置为空块,并设置block_Data数据都为0
reSetBlocks();
//初始化两个随机数(2或4),并刷新界面
initData();
}
});
//增加键盘事件,并将数据数组传入
newgameBtn.addKeyListener(new MyKeyListener(block_Data));
}
//重置每个块元素
public static void reSetBlocks()
{
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
block_Lable[i][j].setIcon(emptyIcon);
block_Data[i][j] = 0;
}
}
}
//调用两次生成随机块的方法
public static void initData()
{
for(int n=0; n<2; n++)
{
createOneRandomNumber();
}
}
//由于每次移动后,都要生成一个随机块,所以单独定义一个方法来生成随机块,生成2或4的概率是4:1
//将生成的数存放到block_Data中,并设置对应组件的图片
public static void createOneRandomNumber()
{
int i,j;
Random random = new Random();
i = random.nextInt(4);
j = random.nextInt(4);
while(true)
{
if(block_Data[i][j] == 0)
{
break;
}
else
{
i = random.nextInt(4);
j = random.nextInt(4);
}
}
block_Data[i][j] = random.nextDouble() > 0.2 ? 2 : 4;
if(block_Data[i][j] == 2)
{
block_Lable[i][j].setIcon(twoIcon);
}else
{
block_Lable[i][j].setIcon(fourIcon);
}
}
//初始化16个0块(空块),30和166都是自己调试出的左边距和上边距,块元素的宽和高都是90,块之间的
//间隔是10,所以平均出来每一个块元素的位置都是加上95,这个自己在纸上画图很容易理解。
//比较难理解的是,此时初始化块的顺序是按照列初始化的,所以如block_Data[0][3]就表示的是第0列,
//第三行的那个元素,这个点必须要区分清楚,否则在判断移动问题的逻辑就比较混乱了。
private void initEmptyBlocks()
{
//设置16个空块,设置每个块的位置,将块添加到主界面中
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
block_Lable[i][j] = new JLabel(emptyIcon);
block_Lable[i][j].setBounds(30+i*95, 166+j*95, 90, 90);
this.add(block_Lable[i][j]);
}
}
}
//初始化界面上部分组件,并添加到合适的位置,setBounds(x,y,width,height)方法用于设置组件
//在窗体中的绝对位置和组件的大小。位置基本没有什么说的,这个基本都是靠自己调试出来的,不熟悉的话
//调试几个组件就差不多掌握了,大小就基本上都设置为JLabel中图片的大小即可。
private void initNewGame()
{
logo_Label = new JLabel(logoIcon);
logo_Label.setBounds(10, 25, 224, 84);
this.add(logo_Label);
contact_Label = new JLabel("HOW TO PLAY?");
contact_Label.setBounds(10, 110, 224, 20);
this.add(contact_Label);
newgameBtn = new JButton(newGameIcon);
newgameBtn.setBounds(280, 80, 142, 44);
this.add(newgameBtn);
score_Label = new JLabel(scoreIcon);
best_Label = new JLabel(bestIcon);
score_Label.setBounds(280, 25, 60, 24);
best_Label.setBounds(360, 25, 60, 24);
this.add(score_Label);
this.add(best_Label);
scoreText_Label = new JLabel("0");
bestText_Label = new JLabel("0");
scoreText_Label.setBounds(290, 55, 60, 24);
bestText_Label.setBounds(370, 55, 60, 24);
this.add(scoreText_Label);
this.add(bestText_Label);
}
private void initFrameBackGround()
{
back_Label = new JLabel(mainFrameIcon);
back_Label.setBounds(6, 6, 420, 556);
this.add(back_Label);
}
private void initBasic()
{
this.setTitle("2048game"); //设置标题
this.setSize(450, 614); //设置窗体大小
this.setLocation(700, 200); //设置窗体显示位置
this.setLayout(null); //设置窗体布局方式为自由布局方式(自由布局就可以按照像素位置去放置组件)
this.setDefaultCloseOperation(EXIT_ON_CLOSE); //设置窗体x为默认关闭窗口
}
//根据数据数组来刷新游戏界面,使视觉效果和真实数据是一致的,reFreshScore是在每次移动后刷新当前得分
public static void upDateUI(int[][] block_Data)
{
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
block_Lable[i][j].setIcon(new ImageIcon("numberimage/"+block_Data[i][j]+".png"));
}
}
}
public static void reFreshScore()
{
int max = block_Data[0][0];
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
if(block_Data[i][j] > max)
{
max = block_Data[i][j];
}
}
}
scoreText_Label.setText(max+"");
}
}
MyKeyListener实现游戏的逻辑控制
package cn.tzfe.listener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JOptionPane;
import cn.tzfe.view.MainFrame;
//继承Java中的KeyListener接口,实现里面的抽象方法,主要是键被释放时处理游戏逻辑
public class MyKeyListener implements KeyListener
{
//定义一个二维数组用于接收从MainFrame传入的数组
private int[][] block_Data;
public MyKeyListener(int[][] block_Data)
{
this.block_Data = block_Data;
}
public void keyTyped(KeyEvent e)
{
}
public void keyPressed(KeyEvent e)
{
}
//有键被按下时的调用逻辑
public void keyReleased(KeyEvent e)
{
//获取到键盘按键的code值(左上右下分别对应37、38、39、40)
int keyCode = e.getKeyCode();
if(keyCode >=37 && keyCode<=40)
{
//分别从四个方向处理4个按键时的逻辑,以下以向左移动为例来解释
//若按下的是左键,并且当前界面能够向左移动,执行向左移动的方法
if(keyCode == 37 && canMoveLeft())
{
//真正向左移动的方法
MoveLeft();
MainFrame.createOneRandomNumber();
}
else if(keyCode == 39 && canMoveRight())
{
MoveRight();
MainFrame.createOneRandomNumber();
}
else if(keyCode == 38 && canMoveUp())
{
MoveUp();
MainFrame.createOneRandomNumber();
}
else if(keyCode == 40 && canMoveDown())
{
MoveDown();
MainFrame.createOneRandomNumber();
}
//刷新界面和当前得分和判断游戏是否结束
MainFrame.upDateUI(block_Data);
MainFrame.reFreshScore();
isGameOver();
}
}
private void isGameOver()
{
//胜利
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
if(block_Data[i][j] == 256)
{
int result = JOptionPane.showConfirmDialog(null, "恭喜您,游戏胜利,再来一局?", "游戏结果",JOptionPane.YES_NO_OPTION);
if(result == 0)
{
MainFrame.reSetBlocks();
MainFrame.initData();
}
else
{
System.exit(0);
}
return;
}
}
}
//失败
if(block_DataIsFull() && !canMove())
{
int result = JOptionPane.showConfirmDialog(null, "很遗憾您输了,再来一局?", "游戏结果", JOptionPane.YES_NO_OPTION);
if(result == 0)
{
MainFrame.reSetBlocks();
MainFrame.initData();
}
else
{
System.exit(0);
}
return;
}
}
private boolean block_DataIsFull()
{
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
if(block_Data[i][j] == 0)
{
return false;
}
}
}
return true;
}
private boolean canMove()
{
return canMoveLeft() || canMoveUp() || canMoveUp() || canMoveDown();
}
private void MoveDown()
{
for(int i=0; i<4; i++)
{
for(int j=2; j>=0; j--)
{
if(isNeedMoveDown(i, j))
{
int k;
for(k=j+1; k<4;)
{
if(block_Data[i][k] == 0)
{
k++;
continue;
}
if(block_Data[i][k] == block_Data[i][j])
{
block_Data[i][k] = 2 * block_Data[i][j];
block_Data[i][j] = 0;
break;
}
else
{
block_Data[i][k-1] = block_Data[i][j];
block_Data[i][j] = 0;
break;
}
}
if(k==4)
{
block_Data[i][k-1] = block_Data[i][j];
block_Data[i][j] = 0;
}
}
}
}
}
private void MoveUp()
{
for(int i=0; i<4; i++)
{
for(int j=1; j<4; j++)
{
if(isNeedMoveUp(i,j))
{
int k;
for(k=j-1; k>=0;)
{
if(block_Data[i][k] == 0)
{
k--;
continue;
}
if(block_Data[i][k] == block_Data[i][j])
{
block_Data[i][k] = 2 * block_Data[i][j];
block_Data[i][j] = 0;
break;
}
else
{
block_Data[i][k+1] = block_Data[i][j];
block_Data[i][j] = 0;
break;
}
}
if(k<0)
{
block_Data[i][k+1] = block_Data[i][j];
block_Data[i][j] = 0;
}
}
}
}
}
private void MoveRight()
{
for(int j=0; j<4; j++)
{
for(int i=2; i>=0; i--)
{
if(isNeedMoveRight(i,j))
{
int k;
for(k=i+1; k<4;)
{
if(block_Data[k][j] == 0)
{
k++;
continue;
}
if(block_Data[k][j] == block_Data[i][j])
{
block_Data[k][j] = 2 * block_Data[i][j];
block_Data[i][j] = 0;
break;
}
else
{
block_Data[k-1][j] = block_Data[i][j];
block_Data[i][j] = 0;
break;
}
}
if(k == 4)
{
block_Data[k-1][j] = block_Data[i][j];
block_Data[i][j] = 0;
}
}
}
}
}
//真正向左移动的方法。处理思想是遍历除了最左边一列的所有块,判断该块是否需要移动,若需要则移动
//若不需要,则继续遍历下一个元素块
private void MoveLeft()
{
for(int j=0; j<4; j++)
{
for(int i=1; i<4; i++)
{
//先判断当前block_Data[i][j]是否需要移动,需要拿另一个变量k用来查找当前块需要
//移动到的最终位置
if(isNeedMoveLeft(i,j))
{
int k;
for(k=i-1; k>=0;)
{
//向左查找,若为0则继续查找
if(block_Data[k][j] == 0)
{
k--;
continue;
}
//在左边找到相同的合并
if(block_Data[k][j] == block_Data[i][j])
{
block_Data[k][j] = 2 * block_Data[i][j];
block_Data[i][j] = 0;
break;
}
//没有找到相同的移动到相对较左的位置
else
{
block_Data[k+1][j] = block_Data[i][j];
block_Data[i][j] = 0;
break;
}
}
//在左侧方向没有找到相同的,并且已经找到最左边,则移动当前块到最左边的位置
if(k<0)
{
block_Data[k+1][j] = block_Data[i][j];
block_Data[i][j] = 0;
}
}
}
}
}
private boolean isNeedMoveRight(int i, int j)
{
if(block_Data[i][j]!=0&&(block_Data[i+1][j]==0||block_Data[i][j]==block_Data[i+1][j]))
{
return true;
}
return false;
}
private boolean isNeedMoveLeft(int i, int j)
{
if(block_Data[i][j]!=0&&(block_Data[i-1][j]==0||block_Data[i][j]==block_Data[i-1][j]))
{
return true;
}
return false;
}
private boolean isNeedMoveUp(int i, int j)
{
if(block_Data[i][j]!=0&&(block_Data[i][j-1]==0||block_Data[i][j]==block_Data[i][j-1]))
{
return true;
}
return false;
}
private boolean isNeedMoveDown(int i, int j)
{
if(block_Data[i][j]!=0&&(block_Data[i][j+1]==0||block_Data[i][j]==block_Data[i][j+1]))
{
return true;
}
return false;
}
//判断整个界面是否能够向左移动
private boolean canMoveLeft()
{
//判断逻辑:遍历当前界面的块元素,如果当前块不是0 &&(当前块的左边块是0或当前块的左边块和当前块相同,则返回true)
for(int i=1; i<4; i++)
{
for(int j=0; j<4; j++)
{
if(block_Data[i][j]!=0&&(block_Data[i-1][j]==0||block_Data[i][j]==block_Data[i-1][j]))
{
return true;
}
}
}
return false;
}
//判断整个界面是否能够向右移动
private boolean canMoveRight()
{
for(int i=2; i>=0; i--)
{
for(int j=0; j<4; j++)
{
if(block_Data[i][j]!=0&&(block_Data[i+1][j]==0||block_Data[i][j]==block_Data[i+1][j]))
{
return true;
}
}
}
return false;
}
//判断整个界面能否向上移动
public boolean canMoveUp()
{
for(int j=1; j<4; j++)
{
for(int i=0; i<4; i++)
{
if(block_Data[i][j]!=0&&(block_Data[i][j-1]==0||block_Data[i][j]==block_Data[i][j-1]))
{
return true;
}
}
}
return false;
}
//判断整个界面能否向下移动
public boolean canMoveDown()
{
for(int j=2; j>=0; j--)
{
for(int i=0; i<4; i++)
{
if(block_Data[i][j]!=0&&(block_Data[i][j+1]==0||block_Data[i][j]==block_Data[i][j+1]))
{
return true;
}
}
}
return false;
}
}