项目需求:
用户可通过目录,选定要进行拼图的照片,照片经过处理后,被分割为3*3的小块;
将其中的小块放置到3*3的框中,其中的最右下角留白;
按上下左右方向键,移动方框中的照片小块,直到拼接出原始的图像,游戏结束;
已有资料:
http://blog.sina.com.cn/s/blog_5e3ab00c0100igqh.html
http://blog.sina.com.cn/s/blog_5e3ab00c0100ipz6.html
参考上述的源代码来完成自己的设计。
任务表:
完成对话框定位目录的功能(优先级低);
对定位到的图片进行标准化,分割处理(优先级高);
将分割后的图片贴在图文框中(优先级高);
添加对键盘的监听,重绘图像,形成移动的效果;每移动一次判断是否完成拼图(优先级高);
如何判断当前初始的拼图是否可拼?(优先级高)
任务2:
需要将图片进行分割,参考的源代码目录如下:
按照代码的名字,我们可以到Split中去看看是否有我们需要的内容,发现该类中的divid方法,正好完成对图片的分割工作,该部分代码可以拿来用。
public BufferedImage[][] divid(int type)
{
try
{
if(filename == null)
return null;
BufferedImage image = ImageIO.read(new File(path));
int len = level[type];
int cal = image.getWidth() / len;
int row = image.getHeight() / len;
BufferedImage [][] subimage = new BufferedImage[row][cal];
for (int i = 0; i < row; i++)
for (int j = 0; j < cal; j++)
subimage[i][j] = image.getSubimage(j*len, i*len, len, len);
return subimage;
}
catch (Exception e)
{
return null;
}
}
}
任务3:将分割后的图片贴在图文框中(优先级高);
此处是要对分割后的image进行处理,暂时我们跟着参考源代码看看,他是怎么做的:
public void menuNewClick()
{
Split sp = Split.get();
BufferedImage [][] image;
if(!sp.set(getFilename()) || (image = sp.divid(getType())) == null)
{
JOptionPane.showMessageDialog(null, "File"+getFilename()+" not exists!\nPlease select again~");
return;
}
startGame();
this.setSize(fWidth, fHeight);
this.setVisible(true);
intlen = Split.level[getType()];
introw = image.length;
intcal = image[0].length;
gOver = new GameOver(this);
JButton [][] button = new JButton[row][cal];
Matrix matrix = new Matrix(button, panel[0], len, gOver);
matrix.init(image);
从上述的代码可见,对于image分割方法的调用在image = sp.divid(getType())),而对于image的进一步处理就落在matrix部分的init方法中。
public void init(BufferedImage [][] image)
{
…
icon = new ImageIcon(image[d/cal][d%cal]);
button[i/cal][i%cal].setIcon(icon);
}
}
从上述的代码可见,其主要的思路:将分割得到的image转化成为icon对象,然后再将icon对象设置给button;而在matrix的初始化函数中:
public Matrix(JButton [][] b, JPanel p, intlen, GameOver g){
…
for(int i = 0; i < row; i++)
for (int j = 0; j < cal; j++)
{
button[i][j] = new JButton();
button[i][j].setBounds(j*len, i*len, len, len);
button[i][j].addActionListener(new ButtonClick(button, pint, matrix, i,j, gOver));
panel.add(button[i][j]);
}
}
}
上述的代码给我们的设计指出大方向:将整个的panel划分为3*3的区域,其中的依次无缝隙放置9个按钮;然后再将我们的图片分割为3*3的小块,再将每个小块转化为icon类型贴在每个按钮的表面,如此完成游戏的静态设置。
任务4:添加对键盘的监听,重绘图像,形成移动的效果;每移动一次判断是否完成拼图(优先级高);
本任务总体可分解得子任务:
如何产生移动的效果?
如何判断拼图是否完成?
按照任务3中的总体设计,实现移动的效果:要么移动按钮在panel中的位置,要么重置按钮上的icon;上述只是粗略的技术路线,还需要具体到:
每条路线的具体实现难度;
该技术路线与后续任务协同上的难易程度;
启发:技术路线的除了考虑自身的难易程度,还需要考虑与其他技术的协同难度;
先不想动手?那就在脑子里跑火车,看看上述两条技术路线的技术难度:
如果采用重置icon的方式,那就是每次沿着按键方向,将前者的icon置为空白的即可;而判断是否已经处于拼图完成状态,只需要检查每个按钮上的icon是否按顺序排列;
如果采用移动按钮的方式,只不过是带着icon一起移动而已,因为我们要的只是icon的移动,所以两者方式的对比,更合理的是第一种方式。
任务5.2:如何判断拼图当前是否已经完成?
此处需要记录各个icon所对应的imag变化的情况,也即记录当前的icon重置后,需要记录其位置的变化。此处的思路可以有:1.根据icon身上附加的属性,根据icon身上的信息判断当前的布局是否满足拼图完成的条件;2.建立icon位置相对应的数据结构,比如3*3的二维数组,给每个icon设定一个初始的编号:
如果第8个位置对应的icon,移动到空格的位置,那么对应的二维数组变为:
拥有这样的对应icon位置的二维数组,那么判断当前的拼图是否出于完成状态就简单很多了,只要查看下二维数组中的数据是否按序排列即可。
任务5:判断初始化的拼图是否可拼?
我们百度下,看看网上是否有相关资料:
http://www.cnblogs.com/idche/archive/2012/04/25/2469516.html
http://blog.csdn.net/tailzhou/article/details/3002442
数学终于派上用场,使用到数学是大学一年级的线性代数。此处具体的细节与数学证明略过。主要的结论:二维数组中数的逆序对,如果逆序对为偶数,说明可拼图;否则,就不可以。那剩下的就只是用两层循环来判断下逆序对的数量。
按照上述的设计,拼图的技术框架构建完成。
拼图游戏-实现方案
以下罗列技术方案中的主要任务:
完成对话框定位目录的功能(优先级低);
对定位到的图片进行标准化,分割处理(优先级高);
将分割后的图片贴在图文框中(优先级高);
添加对键盘的监听,重绘图像,形成移动的效果;每移动一次判断是否完成拼图(优先级高);
如何判断当前初始的拼图是否可拼?(优先级高)
实现1:
任务2中对图形的处理和分割可以独立到一个类中,如参考源代码中的处理方式,独立为handlePic类;
主要方法:完成对该图像的分割,并且返回分割后的对象;
实现2:
判断当前的拼图是否已经完成,使用二维数组,该二维数组还需要提供必要的方法,所以将其设计为独立的类Matrix类;
主要方法1:提供二维数组中的数据交换;
主要方法2:检查当前的二维数组,查看是否已经处于拼图完成状态;
主要方法3:提供初始(可拼接)的二维数组,根据该二维数组来完成分割后image到按钮的对应;
实现3:
界面的创建:创建一个主界面类,其中添加Panel以及3*3个的button对象,再调用handlePic类和Matrix类完成icon的初始化;将类的名字暂时命名为puzzle;
实现4:
对于按键或者鼠标的响应:出于简单,我们只实现关于鼠标点击按钮的事务处理。问题来了:并不是点击所有的按钮都有反应,有反应的按钮主要是其周边存在空白的那个按钮。所以需要判断当前被点击的按钮与空白按钮的距离;只有当两者相邻的时候才能进行icon重置的操作。
此处就涉及到两个对象,其中一个负责按钮的事务处理;另一个表示当前空白的那个按钮;
事务处理的按钮暂停命名为:buttonClick。
而空白按钮也作为一个对象hole,其包含的主要方法:
方法1:设置当前空白按钮所在的坐标;
方法2:判断当前hole的位置是否与被点击的按钮相邻;
按照上述方案,拼图游戏基本可以实现。