Ilingis 发表于 2022-10-5 18:22

基于MATLAB的拼图游戏设计(图文详解,附完整代码)

内容摘要:MATLAB强大的运算和图形展示功能,使图像处理变得更加的简单和直观。本博文基于MATLAB编程语言,详细介绍了如何利用MATLAB及其图像处理函数进行经典拼图游戏设计,并通过具体方法步骤及相应代码逐步实现游戏的完美运行。(有详细步骤及代码解释,适合新手以及进阶的朋友参考)
关键词:拼图游戏;MATLAB;数字图像处理


1. 前言

Matlab是MathWork公司推出的一套高性能的数值计算和可视化软件。它是一个高度集成的系统,集科学计算、图像处理、声音处理于一体,具有极高的编程效率。数字图像处理是一种通过计算机采用一定的算法对图形图像进行处理的技术。数字图像处理技术已经在各个领域上都有了比较广泛的应用。MATLAB强大的运算和图形展示功能,使图像处理变得更加的简单和直观。本文介绍了如何利用MATLAB进行经典拼图游戏设计。拼图游戏是一款非常经典的小游戏,因为它比较简单有趣,可变性很高且耐玩,有利于开发智力,帮助提高动手解决问题的能力。
拼图游戏的设计对一个MATLAB语言设计者进行语言提高和进阶都是一个很好的锻炼机会,说起来这个程序是笔者当年大学时完全自己构思摸索的第一个长的完整程序,作为一名刚接触编程语言的菜鸟,在痛苦与激动中整整花了一个星期才完成。如今我已是一名研究生,转眼再看过去写的那些程序,可能诸多不足但那些编程的锻炼确实让我感获良多,面对如今众多的研究课题时,入门编程让我可以从容地用编程语言去释放自己的思想,不必在有好想法时因为编程问题而浇灭灵感的火花。临近年关,辞旧迎新之际,外面的世界熙来攘往,早已沉浸着春节的热闹与喜悦中,我仍要挑个安静的时间总结反思过去一年各方面的得与失。一个想法划过脑海想把前面做过的程序做个整理和改进,分享出来希望能够给刚接触编程的朋友们一点帮助和启发,对于我也是一个总结与提高,期待我们今后一起能够继续学习和进步。
2. MATLAB的图像处理

MATLAB的基本数据单位是矩阵,它的指令表达式与数学,工程中常用的形式十分相似,故用MATLAB来解算问题要比用C、FORTRAN等语言完相同的事情简捷得多。MATLAB支持五种图像类型,即索引图像、灰度图像、二值图像、RGB图像和多帧图像阵列。MATLAB中,一幅图像包含一个数据矩阵,RGB图像分别用红,绿,蓝三个亮度值为一组,代表每个像素的颜色。图像数组为M*N*3,M,N表示图像像素的行列数。MATLAB图像处理工具箱是由一系列支持图像处理操作的函数组成,可以进行诸如几何操作、线性滤波和滤波器设计、图像变换、图像分析与图像增强、二值图像操作以及形态学处理等图像处理操作。
这里拼图游戏中的图像处理主要是对图像数组的操作,涉及矩阵分割、重组等步骤。本文将在后面几节结合代码进行详细介绍。
3. 设计步骤

到此为止,前面的内容稍作了解即可,接下来的主要设计思路无需细看但了解大概的流程却是有必要的,毕竟编写程序前还是得清楚到底要做什么的,简洁清晰的设计方案能起到提纲挈领的作用。
拼图游戏的主要思想是先读入准备好的图片,将存储图像的数据矩阵平均分为9个小的矩阵块,并用一个矩阵元素为1,2,3,4,5,6,7,8,0的3*3的数组矩阵标记每一个小矩阵块。仿照人工随机打乱拼图的方法,将标记矩阵打乱,根据标记矩阵拼接相应的拼图块并显示整个图像,通过设置图形窗口鼠标点击事件的回调函数的方式获得鼠标点击位置的坐标值,判断点击位置并移动相应的拼图块,当拼图顺序完全正确时提示游戏完成。图3.1和图3.2是拼图游戏的程序流程图。



图3.1 总体程序流程图(左图)



图3.2 回调函数程序流程图(右图)

上面流程图的方式可能有点学术,但确实不失为一种好的叙述整个程序设计思想的表示方法。解释一下吧,为了使整个程序整洁清晰,图1中“打乱拼图矩阵”以及”按照标记矩阵显示拼图“步骤都写成了自定义函数,分别为disrupt( )、dramap( ),其功能及具体细节会在后面章节展开。接着需要得到点击鼠标的位置坐标,这里利用figure的WindowButtonDownFcn属性自行定义一个回调函数。此时每次当你在图上按下鼠标的时候,就会转而执行回调函数,整个程序处于监听鼠标事件与调用一次回调函数的循环之中,回调函数的流程如图2中所示。在回调函数中,计算鼠标点击坐标后,根据坐标移动拼图标记矩阵中的元素,这一步由自定义的函数movejig( )完成。然后根据标记矩阵刷新拼图图像,即调用dramap( )函数。最后判断拼图的标记矩阵中元素是否排列正确,如果顺序完全正确则提示游戏完成并结束程序,而不正确时则结束本次的回调函数继续等待鼠标点击。
3.1 开始程序设计

首先需要准备一张彩色图片,图片的尺寸最好为正方形(否则不易于后面的处理,而且可能不好看),这里我选择的图片是一张经过裁剪的长宽为300*300的图片,如下文件'jigsawImage.jpeg'



jigsawImage.jpeg

在matlab中新建一个m文件,这里我命名的文件名为'jigsaw.m',为了方便两个文件放在同一文件夹下,如下图所示。



图3.1.1 文件存放情况

准备好这些我们就可以正式开始编写程序了,所有代码编写在jigsaw.m文件中,下面将逐个介绍其中的代码及设计细节。
3.2 分割拼图

这里我们设计的是一个九宫格的拼图游戏,因此在打乱拼图前需要先将图片平均切割为相等的九份。首先我们来看看MATLAB中图片的储存与表示,执行命令”image=imread('jigsawImage.jpeg');“读入jigsawImage图片(若图片与m文件不在同一位置需输入绝对路径,如image=imread('C:\Users\Administrator\Desktop\jigsaw puzzle\jigsawImage.jpeg');)在MATLAB中的存储情况如图3.2.1所示。



图3.2.1 原图存放情况

可以看出MATLAB中图片的存储是以矩阵的形式进行,image是个300*300*3的矩阵,300、300分别是矩阵的横纵尺寸。平均分割image矩阵,则第一块小拼图的数据矩阵可以为x1=image(1:100,1:100,:);这行代码的意思是选取image矩阵中横坐标从1到100,纵坐标也从1到100的那部分矩阵数据。可通过下面的代码验证,帮助简单理解
image=imread('jigsawImage.jpeg'); %读入图片
figure %产生一个图形窗口
imshow(image) %显示原图
axis on %显示坐标轴
figure
x1=image(1:100,1:100,:);%分割一个拼图块
imshow(x1);
axis on
在命令窗口执行以上代码,可以看到如下运行结果



图3.2.2 分割演示结果图

将整幅图像平均分割成9块,为了方便每一块用一个数字表示,从左到右,从上到下开始将9个小拼图块分别标记为1,2,3,4,5,6,7,8,0;其位置及坐标系如图3.2.3所示



图3.2.3 拼图标记

从上图中可以看到每块拼图在原图中的坐标位置,其实也是矩阵的索引范围,因此同样可以通过上面的方式引用标号为2,3,4,5,6,7,8,0的拼图块,代码如下
x2=image(1:100,101:200,:);%拼图块2矩阵数据
x3=image(1:100,201:300,:);%拼图块3矩阵数据
x4=image(101:200,1:100,:);%拼图块4矩阵数据
x5=image(101:200,101:200,:);%拼图块5矩阵数据
x6=image(101:200,201:300,:);%拼图块6矩阵数据
x7=image(201:300,1:100,:);%拼图块7矩阵数据
x8=image(201:300,101:200,:);%拼图块8矩阵数据
x0=image(201:300,201:300,:);%拼图块0矩阵数据
按照上面的思路,现在自定义一个从原图中分割拼图并按照相应拼图块的标记截取小拼图的函数choose( ),输入参数为image,index,其中image为原图的数据矩阵,index为要选择的拼图块的标记;输出为x,x是标记为index的小拼图块的数据矩阵。代码如下
function x = choose(image,index)
%% 根据索引选择对应位置上的拼图块
if index > 0 %标记为1,2,3,4,5,6,7,8的拼图块
    % 计算出行数row以及列数column
    row=fix((index-1)/3);
    column=mod(index-1,3);
    % 分割出对应拼图块数据
    x=image(1+row*100:100*(row+1),1+column*100:100*(column+1),:);
else
    x=uint8(255*ones(100,100,3));%拼图块0矩阵数据
end
首先第3到第6行中当index为1,2,3,...,8时先计算出行数row以及列数column,行数可通过求除以3后的商确定,fix( )是MATLAB中的取整函数,而列数可通过求余数确定,mod( )是求余函数,返回index-1除以3的余数。为了便于按照上面的方法分割矩阵,这里的row、column相当于一个倍数,如 x=image(1+row*100:100*(row+1),1+column*100:100*(column+1),:);则将行列数乘分割间隔100(因为选择的图片大小是300*300的,分成3*3的拼图块所以索引间隔为100,若为其他尺寸的图片应为长宽分别除以3后的结果)即可根据行列数row,column引用相应位置上的小拼图块的数据矩阵了。例如index=4时,经过第5,6行代码计算出的row=1,column=0,代入第8行代码中,则实际x=image(1+1*100:100*2,1+0*100:100*1,:);这与前面代码中写出的”x4=image(101:200,1:100,:);%拼图块4矩阵数据“作用一致。
当index=0时则执行第9行else下的代码部分,你可能会奇怪,为什么拼图块0不是从imge矩阵中分割而是放到else中了?这是因为0这个位置比较特殊,留作空白处,拼图游戏进行时当空白处附近某个拼图块被点击时,该拼图块就会移动到这个空白位置。这里ones( )函数产生一个100*100*3的全1矩阵,乘255则像素灰度值最亮,显示为全白,最后uint8( )统一数据类型。因此当index=0时,执行else中的代码,输出x为一块全白的拼图块。
3.3 按标记序号矩阵显示拼图

前面一节介绍了可以通过标记序号分割原图获得小拼图块数据矩阵的方法,一幅拼图可由9块小拼图块拼接而成,因此用一个9个元素的标记矩阵,每个位置上的元素值作为一个标记序号表示相应位置的拼图块,就可以通过这个标记矩阵表示整个的拼图了。
这样原图就可以用矩阵 \begin{array}{rrr}1 & 2 & 3 \\ 4 & 5 & 6 \\ 7 & 8 & 0\end{array} 表示,而任意打乱的拼图也可以通过改变矩阵相应位置上的元素值表示了。现在我们编写一个根据标记矩阵显示对应拼图的函数drawmap( ),输入参数为A,A为一幅拼图的标记矩阵,A大小为3*3。代码如下
function drawmap(A)
%% 将运算数字与对应拼图对应显示图片
origin=imread('jigsawImage.jpeg');
image=origin;

% 对要显示的拼图进行赋值
for row=1:3
    for col=1:3
    image(1+(row-1)*100:100*row,1+(col-1)*100:100*col,:)=choose(origin,A(row,col));
    end
end

imshow(image)%显示拼图
第3行代码读入图片并将原图数据矩阵存储在origin中;第4行中image是要显示的拼图数据矩阵,先预定义与origin相同。接下来对image进行逐个赋值,image每一块数据的赋值通过调用前面编写的choose( )函数完成;第9行中”choose(image,A(row,col))“返回标记矩阵A中第row行第col列的元素表示的那一块拼图块的数据矩阵,而前面的一半image(1+(row-1)*100:100*row,1+(col-1)*100:100*col,:)与3.2节中选取每个拼图块的作用类似,这里用于对行数row从1到3,列数col从1到3的拼图块矩阵上的元素逐个赋值。经过9次循环9处拼图块的数据赋值完成,最终显示图像image。可以通过下面的代码简单测试一下drawmap( )函数,设置一个标记矩阵Tag_A=,即 Tag\_A=\begin{array}{lll}8 & 4 & 5 \\ 6 & 0 & 3 \\ 1 & 7 & 2\end{array} ,按照该矩阵显示这幅拼图。
function jigsaw()
%% 主函数
Tag_A=;
drawmap(Tag_A);%按照标记矩阵显示拼图


function x = choose(image,index)
%% 根据索引选择对应位置上的拼图块
if index>0 %标记为1,2,3,4,5,6,7,8的拼图块
    % 计算出行数row以及列数column
    row=fix((index-1)/3);
    column=mod(index-1,3);
    % 分割出对应拼图块数据
    x=image(1+row*100:100*(row+1),1+column*100:100*(column+1),:);
else
    x=uint8(255*ones(100,100,3));%拼图块0矩阵数据
end


function drawmap(A)
%% 将运算数字与对应拼图对应显示图片
origin=imread('jigsawImage.jpeg');
image=origin;
% 对要显示的拼图进行赋值
for row=1:3
    for col=1:3
    image(1+(row-1)*100:100*row,1+(col-1)*100:100*col,:)=choose(origin,A(row,col));
    end
end
imshow(image)%显示拼图
运行结果如下图所示,对比每个拼图块的标记可以看出函数能实现根据矩阵的值显示拼图了,距离成功又近了一步。



图3.3.1 测试结果

3.4 移动拼图

这部分我们要解决一个逻辑问题,那就是怎么移动拼图。例如,在图3.4.1的拼图中,如果鼠标在空白块的左侧一个拼图块内点击一次,此时左侧的那个拼图块应该往空白处移动,但要怎样才能实现这一过程?



图3.4.1 移动拼图过程(左图)



图3.4.2 拼图行列数(右图)

为了实现这一过程,这里同样定义一个函数,实现在当前的拼图中根据鼠标所在的行列数移动拼图块,命名为movejig( ) 。输入参数:tag(当前拼图的标记矩阵)、row(鼠标点击位置的行数)、column(鼠标点击位置的列数);输出参数:tag(移动后得到的标记矩阵)。行列数的规定按照图3.4.2的坐标系确定,图3.4.3简单示出了movejig( )函数的大致运行方式。



图3.4.3 函数功能示意图

这里默认已经知道了鼠标坐标并经过计算得出了鼠标点击处的行列数row,col,至于如何计算将在后面介绍。 movejig( )函数的具体代码如下
function tag=movejig(tag,row,col)
%% 4个if分4种情况对不同位置处的点坐标与矩阵行列式统一
    num = tag(row,col);%鼠标位置与号码牌一致
    if (row > 1)&&(tag(row-1,col)==0)%点击位置在第二或第三行,空白块在点击位置的上一行
      tag(row-1,col) = num;%交换两个位置上的值
      tag(row,col) = 0;
    end
    if (row < 3)&&(tag(row+1,col)==0)%点击位置在第一或第二行,空白块在点击位置的下一行
      tag(row+1,col) = num;
      tag(row,col) = 0;
    end
    if (col > 1)&&(tag(row,col-1)==0)%点击位置在第二或第三列,空白块在点击位置的左边一列
      tag(row,col-1) = num;
      tag(row,col) = 0;
    end
    if (col < 3)&&(tag(row,col+1)==0)%点击位置在第二或第三列,空白块在点击位置的右边一列
      tag(row,col+1) = num;
      tag(row,col) = 0;
    end
第3行根据点击处的行列号可取出点击位置处拼图块的标记,存为num。第4行至第19行分四种情况考虑可能出现的点击情况,第4行if中的条件row>1说明点击位置在第2行或第3行,并且要求tag(row-1,col)==0即点击处的上一行位置上的标记是0(也就是表示空白拼图块),这两个条件同时满足就是表示鼠标点击的拼图块上面一个拼图块是空白块。条件满足后第5,6行就是将点击处的拼图块和上面的空白块的标记值互换,表示点击拼图块上移。同理,后面三种情况空白处分别出现在点击下方、左边、右边同样交换两个拼图块实现移动。其他情况如点击了某个拼图块而这个拼图块相邻位置上没有空白则不满足条件是不会进行任何操作的。
其实,上面的四个if条件在拼图时是不会有同时满足的情况的,上述代码中的形式需要判断四次条件虽然没错但显得有点多余,在编程时写成嵌套的if...elseif...end形式则更加合理,这里不这么写是为了防止可能有初学的朋友容易混淆而出错,熟悉的朋友可以自行改写。
movejig( )函数实现的是标记矩阵中相应元素的移动,结合前面编写的显示函数drawmap( )将得到的移动后的标记作为输入就可以显示移动后的拼图了。
3.5 打乱拼图

游戏开始时需要将一幅完整的图片打乱,那又要怎么实现呢?读到这里您可能就知道了,整个拼图游戏的实现主要是通过标记矩阵进行操作的,即改变标记矩阵,然后按标记矩阵将拼图显示出来。对此前面已多有铺垫,通过打乱标记矩阵的元素的方法打乱拼图自然也就水到渠成。
一个简单的想法是,生成一个由0-8的数字随机排列构成的3*3的矩阵作为标记矩阵如下面的矩阵Tag1
\begin{array}{rrr} 2 & 5 & 0 \\ \operatorname{Tag} 1=6 & 1 & 8 \\ 7 & 4 & 3 \end{array}\\然而这样存在的bug是随机产生的矩阵其实大多通过移动拼图是不能完成正确排序的,这在数学上有相关理论研究可得出结论,不研究数学的笔者就不多说了。这里我的想法是模仿人手动打乱拼图的方式,不断随机移动拼图直至拼图顺序完全打乱。前面介绍了移动拼图的函数,这里随机产生点击的行列数然后调用移动拼图函数movejig( ),重复一定次数则可完成拼图打乱。定义一个打乱拼图函数Disrupt( ),返回一个仿手动打乱之后得到的标记矩阵,MATLAB代码如下
function y = Disrupt()
%% 随机打乱原拼图排列顺序
y =;
for i = 1:360
    row=randi();%产生一个范围在1到3的整数
    col=randi();
    y=movejig(y,row,col);%按随机产生的动作打乱拼图
end
代码第3行,默认y是一个顺序正确的3*3的矩阵,在第5行for循环中设置移动的次数为360次,每次都产生一个行数row和列数col,其值为1,2,3中随机的一个数,然后第8行调用movejig( )移动拼图。这就仿佛一个人不断在拼图上随机地点击很多次,由于点击拼图块,相应位置上的拼图块就会不断移动。这样做虽然每次盲目产生的row和col不一定都有效,但次数多了无疑会起到打乱拼图的作用。
接下来就可以测试一下了,新建一个m文件,命名为jigsaw.m,在文件中输入如下代码
function jigsaw()
%% 主函数
Tag_A= Disrupt()%将图像的排列顺序打乱
drawmap(Tag_A);%按照标记矩阵显示拼图


function tag=movejig(tag,row,col)
%% 4个if分4种情况对不同位置处的点坐标与矩阵行列式统一
    num = tag(row,col);%鼠标位置与号码牌一致
    if (row > 1)&&(tag(row-1,col)==0)%点击位置在第二或第三行,空白块在点击位置的上一行
      tag(row-1,col) = num;%交换两个位置上的值
      tag(row,col) = 0;
    end
    if (row < 3)&&(tag(row+1,col)==0)%点击位置在第一或第二行,空白块在点击位置的下一行
      tag(row+1,col) = num;
      tag(row,col) = 0;
    end
    if (col > 1)&&(tag(row,col-1)==0)%点击位置在第二或第三列,空白块在点击位置的左边一列
      tag(row,col-1) = num;
      tag(row,col) = 0;
    end
    if (col < 3)&&(tag(row,col+1)==0)%点击位置在第二或第三列,空白块在点击位置的右边一列
      tag(row,col+1) = num;
      tag(row,col) = 0;
    end
   

function y = Disrupt()
%% 随机打乱原拼图排列顺序
y =;

for i = 1:360
    row=randi();%产生一个范围在1到3的整数
    col=randi();
    y=movejig(y,row,col);%按随机产生的动作打乱拼图
end



function x = choose(image,index)
%% 根据索引选择对应位置上的拼图块
if index>0 %标记为1,2,3,4,5,6,7,8的拼图块
    % 计算出行数row以及列数column
    row=fix((index-1)/3);
    column=mod(index-1,3);
    % 分割出对应拼图块数据
    x=image(1+row*100:100*(row+1),1+column*100:100*(column+1),:);
else
    x=uint8(255*ones(100,100,3));%拼图块0矩阵数据
end


function drawmap(A)
%% 将运算数字与对应拼图对应显示图片
origin=imread('jigsawImage.jpeg');
image=origin;
% 对要显示的拼图进行赋值
for row=1:3
    for col=1:3
    image(1+(row-1)*100:100*row,1+(col-1)*100:100*col,:)=choose(origin,A(row,col));
    end
end
imshow(image)%显示拼图
运行结果如图3.5.1所示



图3.5.1 打乱拼图测试

3.6 拼图主函数

主函数是完成拼图任务的核心,主函数的设计思路是首先将标记矩阵打乱,并按照标记矩阵中的排列显示拼图块,然后需要获得鼠标点击处的位置坐标以移动拼图,每次移动后判断拼图顺序是否已经正确,顺序正确后结束游戏。前面已经完成了标记矩阵的打乱和显示以及根据鼠标位置移动拼图的函数,那么现在的问题就剩下鼠标位置的获取了。
MATLAB中获取鼠标坐标值有两种途径。第一种是利用ginput( )函数,该函数提供一个十字光标帮助更精确选择所需要的位置并返回坐标值,函数调用形式如=ginput(1),x,y分别为横纵坐标。这确实为一个简单实用的方法,开始时我就是采用的这种方式,在主函数中利用while循环重复调用ginput( )函数获取每次点击处的坐标值,结果证明是可行的,效果如下图3.6.1所示。



图3.6.1 利用ginput( )的方式实现效果

这样的类似方法网上提到很多,让我感觉不太好的一点是这个十字光标精确是精确,但是在这个拼图中显得有些多余,怎么删去这个光标寻找半天,未果。还有就是用while循环编写的代码每次关掉这个图形窗口都会甩出一个大大的类似下图那样的错误,这就让有点小小处女座的我不能忍了,转而编写了另外一个版本。


第二种方法是利用figure的WindowButtonDownFcn属性定义一个坐标获取的回调函数。当在图上按下鼠标的时候,就会自动执行回调函数来获取坐标值。主函数命名为jigsaw( ),与文件名一致,其代码如下
function jigsaw()
%% 主函数
Tag_A= Disrupt();%将标记矩阵的排列顺序打乱
drawmap(Tag_A);%按照标记矩阵显示拼图

global Tag;%Tag是标记矩阵,定义成全局变量,方便传递参数
Tag=Tag_A;
set(gcf,'windowButtonDownFcn',@ButtonDownFcn);%点击鼠标时调用ButtonDownFcn函数
代码第3,4行调用前面的函数打乱和显示,不必多说;第6,7行意在将标记矩阵定义成全局变量并赋值为Tag_A,其目的在于在后面的回调函数中需要用到标记矩阵,这样方便传递参数,这里可以不必深究;第8行就是设置windowButtonDownFcn属性的回调函数,gcf表示当前图形窗口句柄,ButtonDownFcn是回调函数名,@ButtonDownFcn表示其函数句柄,整条代码就是设置当在当前图形窗口中点击鼠标时就会转而执行ButtonDownFcn函数。
3.7 回调函数

根据上一节的设置每次点击鼠标时就会执行一次回调函数,因此可以在回调函数中编写程序获取当前鼠标位置并据此移动一次拼图,然后判断拼图是否完成。定义回调函数ButtonDownFcn( ),输入参数src、event为系统约定变量,函数代码如下
function ButtonDownFcn(src,event)
%% 回调函数,鼠标点击事件发生时调用
pt=get(gca,'CurrentPoint');%获取当前鼠标点击位置坐标
xpos=pt(1,1);%鼠标点击处的横坐标实际值
ypos=pt(1,2);%鼠标点击处的纵坐标实际值
   
col = ceil(xpos/100);%将横坐标值转换为列数
row = ceil(ypos/100);%将纵坐标值转换为行数

global Tag; %全局变量声明

if(col<=3&&col>0)&&(row<=3&&row>0)%鼠标点击位置在有效范围内   
    Tag=movejig(Tag,row,col);%按点击位置移动拼图
   
    drawmap(Tag)%显示拼图
   
    order = ;%顺序矩阵
    zt = abs(Tag-order);%比较两个矩阵
    if sum(zt(:))==0 %顺序已经完全吻合
      image=imread('jigsawImage.jpeg');
      imshow(image) %游戏完成,补全拼图
      msgbox('You did a good job ,恭喜完成!!!') %提示完成信息
      pause(0.5);%延迟半秒
      close all %游戏结束,关闭所有图像窗口
    end
   
else
    return
   
end
代码第3行利用get( )获取鼠标位置坐标,gca表示获取当前坐标系句柄即在当前坐标系中获取坐标值,CurrentPoint是当前点属性值,返回的pt为一行两列的数组,其元素分别为横纵坐标值。第4,5行是分别取出横纵坐标值,第7,8行代码求出行列数,因为图片的尺寸为300*300,分成3行3列,所以将鼠标坐标值除以100即可得出所在的行列数,如果选取的图片为其他尺寸应除以其他相应的数字。
第10行声明全局变量Tag,在3.6节中定义过了,这里再度声明表示与前面定义的一致,Tag共享其数值,前面的标记矩阵这里就能使用了。
第12行中,判断点击鼠标的位置是不是在拼图中,因为在图形窗口中点击时若点击位置不在图片上时,返回的坐标值会出现异常值,避免的方法是只有行列数在1到3内的点击才进行处理,不满足条件时跳至第27行返回,结束本次回调函数的运行。
第13,15行按照点击的行列数移动拼图的标记矩阵,然后按照标记矩阵显示拼图。第17行定义一个顺序矩阵用于标记矩阵的比较,第18行将移动后的标记矩阵与顺序矩阵相减取绝对值,abs( )为取绝对值函数,可以知道如果两个矩阵完全一致则相减之后的结果每个位置上的元素都为0,反之不然。
第19行中,if sum(zt(:))==0即如果zt中所有元素的和等于0,满足这个条件时表示游戏完成了,此时将拼图空白的那块补全,第20,21行读取原图然后显示,这一过程连续起来的瞬间就表现为拼图空白处被补全了。
第22行弹出一个提示窗口,展示的信息为“You did a good job ,恭喜完成!!!”。第23行暂停0.5秒,第24行在暂停半秒之后关闭所有图形窗口,游戏结束。
4. 完整代码

所有工作完成,完整的程序m文件以及图片文件已经上传大家可以点击链接下载基于MATLAB的拼图游戏,直接打开jigsaw.m文件即可运行程序。下面是完整的MATLAB代码,大家可以自行新建jigsaw.m文件,复制以下代码至文件中同样可以运行拼图程序。注意下载jigsawImage.jpeg图片文件,与jigsaw.m文件放在同一文件夹下。注意,如更换图片需要适当修改程序。
%% 制作人:思绪无限
% 2018年2月14日
function jigsaw()
%% 主函数
Tag_A= Disrupt();%将标记矩阵的排列顺序打乱
drawmap(Tag_A);%按照标记矩阵显示拼图

global Tag;%Tag是标记矩阵,定义成全局变量,方便传递参数
Tag=Tag_A;
set(gcf,'windowButtonDownFcn',@ButtonDownFcn);%点击鼠标时调用ButtonDownFcn函数



function ButtonDownFcn(src,event)
%% 回调函数,鼠标点击事件发生时调用
pt=get(gca,'CurrentPoint');%获取当前鼠标点击位置坐标
xpos=pt(1,1);%鼠标点击处的横坐标实际值
ypos=pt(1,2);%鼠标点击处的纵坐标实际值
   
col = ceil(xpos/100);%将横坐标值转换为列数
row = ceil(ypos/100);%将纵坐标值转换为行数

global Tag; %全局变量声明

if(col <= 3 && col >0)&&(row <= 3&&row > 0)%鼠标点击位置在有效范围内   
    Tag=movejig(Tag,row,col);%按点击位置移动拼图
   
    drawmap(Tag)%显示拼图
   
    order = ;%顺序矩阵
    zt = abs(Tag-order);%比较两个矩阵
    if sum(zt(:))==0 %顺序已经完全吻合
      image=imread('jigsawImage.jpeg');
      imshow(image) %游戏完成,补全拼图
      msgbox('You did a good job ,恭喜完成!!!') %提示完成信息
      pause(0.5);%延迟半秒
      close all %游戏结束,关闭所有图像窗口
    end
   
else
    return
   
end




function tag=movejig(tag,row,col)
%% 4个if分4种情况对不同位置处的点坐标与矩阵行列式统一
    num = tag(row,col);%鼠标位置与号码牌一致
    if (row > 1)&&(tag(row-1,col)==0)%点击位置在第二或第三行,空白块在点击位置的上一行
      tag(row-1,col) = num;%交换两个位置上的值
      tag(row,col) = 0;
    end
    if (row < 3)&&(tag(row+1,col)==0)%点击位置在第一或第二行,空白块在点击位置的下一行
      tag(row+1,col) = num;
      tag(row,col) = 0;
    end
    if (col > 1)&&(tag(row,col-1)==0)%点击位置在第二或第三列,空白块在点击位置的左边一列
      tag(row,col-1) = num;
      tag(row,col) = 0;
    end
    if (col < 3)&&(tag(row,col+1)==0)%点击位置在第二或第三列,空白块在点击位置的右边一列
      tag(row,col+1) = num;
      tag(row,col) = 0;
    end
   


function y = Disrupt()
%% 随机打乱原拼图排列顺序
y =;

for i = 1:360
    row=randi();%产生一个范围在1到3的整数
    col=randi();
    y=movejig(y,row,col);%按随机产生的动作打乱拼图
end



function x = choose(image,index)
%% 根据索引选择对应位置上的拼图块
if index > 0 %标记为1,2,3,4,5,6,7,8的拼图块
    % 计算出行数row以及列数column
    row=fix((index-1)/3);
    column=mod(index-1,3);
    % 分割出对应拼图块数据
    x=image(1+row*100:100*(row+1),1+column*100:100*(column+1),:);
else
    x=uint8(255*ones(100,100,3));%拼图块0矩阵数据
end

function drawmap(A)
%% 将运算数字与对应拼图对应显示图片
origin=imread('jigsawImage.jpeg');
image=origin;

% 对要显示的拼图进行赋值
for row=1:3
    for col=1:3
    image(1+(row-1)*100:100*row,1+(col-1)*100:100*col,:)=choose(origin,A(row,col));
    end
end

imshow(image)%显示拼图
5. 反思与总结

MATLAB大多用于数据处理以及工程计算,几乎很少有用MATLAB编写游戏的,这也符合实际情况因为编写游戏毕竟不是MATLAB的专长,其实MATLAB发展至今已经成为一个足够完善的编程语言许多java,c中的功能在MATLAB中同样也能实现。例如MATLAB具有高级图形处理功能,可以通过图形对象的属性完成许多复杂工作,前面鼠标位置的获取用的就是这一功能。
整个拼图游戏的设计其实可以看成一个数学建模过程,代表拼图块的标记矩阵就是我们建立的模型,通过对这个模型矩阵元素排序问题的求解、分析最终完成这个拼图任务。更多数学建模的知识大家可以自行上网搜索。
纵观整个拼图游戏的编写,在基本功能上可以改进与提高的地方如下
一、增加自行挑选设置拼图所用图片的功能。
二、增加拼图的难度,设计4*4或5*5甚至更复杂的拼图。
三、利用MATLAB的GUI功能,为游戏设计一个好看的用户图形界面。
四、设计一个一键完成拼图的算法,让程序以最优步数自行移动拼图完成拼图。实现上可采用原始的方法或者机器学习的算法。
五、拼图块的形状与移动方式上也可不必局限这一种,稍作创新兴许一个新型的游戏就会被创造出来。
关于上述的改进之处,有机会将在后面的博文中介绍。
6. 结束语

这就是利用MATLAB进行拼图游戏编写的全部内容了,虽然拼图游戏本身不足为奇,但利用MATLAB编写的完整程序不多,而且或多或少会有一点小bug,本博文介绍的方法在多次修改之下,其程序严谨没有错误。由于编者能力有限,代码即使经过了多次校对,也难免会有疏漏之处。希望您能热心指出其中的错误,以便下次修改时能以一个更完美更严谨的样子,呈现在大家面前。同时如果有更好的实现方法也请您不吝赐教。
【公众号获取】
本人微信公众号已创建,搜索并关注公众号“AI技术研究与分享”,后台回复“JP20180210”即可获取全部资源文件。
页: [1]
查看完整版本: 基于MATLAB的拼图游戏设计(图文详解,附完整代码)