1. 前言
这篇博文紧接着前面一篇继续介绍MATLAB GUI拼图游戏的详细过程,这一篇终于到了介绍最后的拼图逻辑功能实现的部分了。本篇博文中拼图逻辑的代码与早前的基础版本相似,在其基础上做了一些改进使其能适应新加入的功能。因为GUI界面版内容较多,这里限于篇幅分成了上、中、下连续三篇依次讲解,可点击跳转查看拼图游戏完整代码。希望本文能给对MATLAB GUI界面设计或小游戏感兴趣的朋友有所启发。
2. 开始并完成拼图
简单来说,我们这一节实现的功能是:点击“开始拼图”按钮后,在拼图区点击拼图块能够移动拼图并最终能够完成游戏,得到完整的拼好的拼图。效果如下:
这里自上而下逐个函数讲解,首先和上篇的方法一样,我们打开之前设计好的包含GUI界面的fig文件,选中开始拼图的按钮,右击选择“查看回调”,点击“Callback”即可跳转至该函数的定义编辑文件中,在该函数中添加如下代码:
java % --- 按钮pushbutton_run被点击时执行. function pushbutton_run_Callback(hObject, eventdata, handles) % 获取选中的图片绝对路径 file_name=get(handles.edit_path,'String'); if exist(file_name,'file')==0 % 不存在则读取默认图片 pic_data=imread('jigsawImage.jpeg'); else pic_data=imread(file_name); end n=get(handles.popupmenu_rank,'value'); % 获取下拉选择框的值 rank_Tag=n+2;% 计算选择的拼图阶数 % 确定每块拼图长宽 len=min([size(pic_data,1),size(pic_data,2)]); len_col=round(len/rank_Tag); len_row=round(len/rank_Tag); % 转换图片为正方形 pic_data=imresize(pic_data,[rank_Tag*len_col rank_Tag*len_row]); jigsaw(handles,rank_Tag, pic_data);% 调用拼图游戏
以上代码中,第3-18行其目的是为了获得当前初始的图片数据pic_data、拼图的阶数rank_Tag及所有控件的句柄handles,第19行调用拼图函数jigsaw,这个函数之前没有创建,这里新建一个m文件命名为jigsaw,在里面添加实现拼图逻辑的代码。
3. 拼图主函数介绍
3.1 分割拼图并显示
在编写拼图游戏主函数前,先写几个主函数中要用到的子函数,这部分先在jigsaw.m文件中添加两个用于分割拼图并显示的函数。这节代码与前面的基础版本基本类似,与前面的思路相同,将整幅图像按阶数分成对应份数并用数字表示。不过这里因选择的图片尺寸不一,因此需要先计算每块拼图的宽高,可由图片的整体尺寸除以拼图阶数求得;同时因为不是默认的单一坐标系,在显示拼图的时候为避免混乱需要指定显示拼图的坐标轴。将图片分割并显示的函数的代码如下:
java function x = choose(image,index,pic_data,rank_Tag) %% 根据索引选择对应位置上的拼图块 len_row=size(pic_data,1)/rank_Tag; % 每块拼图的宽度 len_col=size(pic_data,2)/rank_Tag; % 每块拼图的高度 if index>0 %标记为1,2,3,4,5,6,7,8的拼图块 % 计算出行数row以及列数column row=fix((index-1)/rank_Tag); column=mod(index-1,rank_Tag); % 分割出对应拼图块数据 x=image(1+row*len_row:len_row*(row+1),1+column*len_col:len_col*(column+1),:); else x=uint8(255*ones(len_col,len_row,3));% 拼图块0矩阵数据 end function drawmap(A,handle,pic_data,rank_Tag) % A:当前标签矩阵 % handle:图形句柄 % pic_data:图片数据 % rankTag:拼图阶数 %% 将运算数字与对应拼图对应显示图片 origin=pic_data; len_row=size(pic_data,1)/rank_Tag; % 每块拼图的宽度 len_col=size(pic_data,2)/rank_Tag; % 每块拼图的高度 % 对要显示的拼图进行赋值 for row=1:rank_Tag for col=1:rank_Tag pic_data(1+(row-1)*len_row:len_row*row,1+(col-1)*len_col:len_col*col,:)=choose(origin,A(row,col),pic_data,rank_Tag); end end axes(handle) % 选定坐标轴 image(pic_data) % 显示拼图 set(handle,'Visible','off');% 隐藏坐标轴
在以上代码中,choose函数的输入参数除了image, index还加入了pic_data, rank_Tag,分别是用于拼图的图像数据以及当前拼图阶数。在对图片进行分割时应考虑到拼图的阶数,因此在代码第10行按照拼图块标记选择原图中相应数据进行赋值。drawmap函数中加入了handle表示要显示拼图的坐标轴句柄,以便在第31行指定坐标系。
3.2 移动拼图
为了实现拼图的移动,需要知道鼠标点击的位置,这里鼠标位置所在的拼图块的行列号由row, col传入。判断是否移动拼图的标准是点击位置是否在空白拼图块(标号为0)的4邻域内(行号或列号相差1),因此我们只需比较row, col加或减1的标号矩阵的值是否为0,如为0表示位置相邻则交换两个位置上的值。
值得注意的是为保证程序不会出错,需要先判断row, col的值是否在可点击范围内,同时为了实时显示游戏过程中的移动步数,有必要声明一个变量用于计数。移动拼图块的代码如下:
java function tag=movejig(tag,row,col,handle_step) global steps; % 计步 %% 4个if分4种情况对不同位置处的点坐标与矩阵行列式统一 last_tag=tag; num = tag(row,col);%鼠标位置与号码牌一致 [max_row,max_col]=size(tag); % 检测点击位置是否处于0号临域 if (row-1)<=max_row && (row-1)>0 % 点击在范围内 if tag(row-1,col)==0 % 空白块在点击位置的上一行 tag(row-1,col) = num; % 交换两个位置上的值 tag(row,col) = 0; end end if (row+1)<=max_row && (row+1)>0 if tag(row+1,col)==0 % 空白块在点击位置的下一行 tag(row+1,col) = num; tag(row,col) = 0; end end if (col-1)<=max_col && (col-1)>0 if tag(row,col-1)==0 % 空白块在点击位置的左边一列 tag(row,col-1) = num; tag(row,col) = 0; end end if (col+1)<=max_col && (col+1)>0 if tag(row,col+1)==0 % 空白块在点击位置的右边一列 tag(row,col+1) = num; tag(row,col) = 0; end end zt = abs(tag-last_tag); % 比较两个矩阵 if handle_step~=0 && sum(zt(:))~=0 % 矩阵已改变表示,拼图发生移动 if exist('steps','var') steps=steps+1; % 步数加1 set(handle_step,'String',num2str(steps))% 实时显示步数 end end
代码通过改变标记矩阵tag后根据tag的值显示拼图对应拼图块达到逻辑和显示上的拼图移动,第33-39行判断拼图是否发生移动并据此记录移动拼图块的步数,并显示在显示步数的文本标签上。
3.3 打乱拼图
打乱拼图采用的是模拟手动打乱拼图的方式,不断随机产生点击位置坐标,利用上面编写的移动拼图函数,不断随机打乱拼图。随机动作的次数应随拼图的阶数而增加,重复一定次数可将拼图完全打乱,返回一个打乱之后的标记矩阵。打乱拼图的代码如下:
java function y = Disrupt(rank_Tag) %% 按人工打乱方式,随机打乱原拼图排列顺序 % y初始为顺序矩阵,用于打乱拼图 y=[1:1:rank_Tag^2-1,0]; y=reshape(y,rank_Tag,rank_Tag); y=y'; for i = 1:300*rank_Tag % 打乱次数应随阶数增加而增加 row=randi([1,rank_Tag]);% 产生一个范围在1到rank的整数 col=randi([1,rank_Tag]); y=movejig(y,row,col,0);% 按随机产生的动作打乱拼图 end
3.4 拼图主函数
主函数的设计思路是首先将标记矩阵打乱,并按照标记矩阵中的排列显示拼图块,然后需要获得鼠标点击处的位置坐标以移动拼图,每次移动后判断拼图顺序是否已经正确,顺序正确后结束游戏。获取鼠标点击位置坐标的方法是利用figure的WindowButtonDownFcn属性定义一个坐标获取的回调函数。当在图上按下鼠标的时候,就会自动执行回调函数来获取坐标值。主函数的代码如下:
java function jigsaw(handles, rank_Tag, pic_data) % handles:图形句柄 % rank_Tag:拼图阶数 % pic_data:读入图片的全路径 %% 拼图主函数 Tag_A= Disrupt(rank_Tag);% 将标记矩阵的排列顺序打乱 drawmap(Tag_A,handles.axes_jigsaw,pic_data,rank_Tag);% 按照标记矩阵显示拼图 global flag;% flag决定在拼图区点击是否移动拼图 global Tag; % Tag是标记矩阵,定义成全局变量,方便传递参数 global steps;% steps用于计数,累计移动拼图的步数 % 初始化变量 flag=true; % 开始游戏后,可以点击移动拼图 steps=0; Tag=Tag_A; len_row=size(pic_data,1)/rank_Tag; % 每块拼图的宽度 len_col=size(pic_data,2)/rank_Tag; % 每块拼图的高度 mask_number(Tag,handles,len_row,len_col) % 改进选择决定是否显示数字标记 set(handles.text_steps,'String',num2str(steps)) % 显示初始步数 set(gcf,'windowButtonDownFcn',{@ButtonDownFcn,handles,pic_data,rank_Tag}); % 点击鼠标时调用ButtonDownFcn函数 function mask_number(tag,handles,len_row,len_col) %% 判断是否需要显示数字提示的函数 ismask=get(handles.checkbox_num,'Value');% 获取CheckBox的值(是否勾选) rank_Tag=size(tag,1);% 拼图阶数 axes(handles.axes_jigsaw);% 确定坐标轴 % 根据是否勾选决定是否显示text标识 for i=1:size(tag,1) for j=1:size(tag,2) if ismask text(len_col/2*(2*j-1)-10,len_row/2*(2*i-1),num2str(tag(i,j)),'FontSize',55-rank_Tag*5,'Color','c') else % 未勾选,在该位置设置text为空,实现不显示 text(len_col/2*(2*j-1)-10,len_row/2*(2*i-1),'','FontSize',55-rank_Tag*5,'Color','c') end end end
主函数中调用了mask_number其作用在于根据控件CheckBox的勾选状态决定是否在拼图块上显示数字提示,这个在前面的代码中已多次提及了。第25行是设置windowButtonDownFcn属性的回调函数,gcf表示当前图形窗口句柄,ButtonDownFcn是回调函数名,@ButtonDownFcn表示其函数句柄,后面handles, pic_data, rank_Tag是回调函数的输入参数,整条代码就是设置当在当前图形窗口中点击鼠标时就会转而执行ButtonDownFcn函数,该函数的代码在下节介绍。
4. 鼠标事件回调函数
每次点击鼠标时就会执行一次回调函数,因此可以在回调函数中编写程序获取当前鼠标位置并据此移动一次拼图,然后判断拼图是否完成。定义回调函数ButtonDownFcn( ),输入参数src、event为系统约定变量,handles, pic_data, rank_Tag分别为需要用到的控件句柄、拼图的图片数据、拼图阶数,函数代码如下:
java function ButtonDownFcn(src,event,handles,pic_data,rank_Tag) %% 回调函数,鼠标点击事件发生时调用 global flag;% flag声明,共用全局变量 global Tag; % 全局变量声明 if flag % 若flag为true,允许移动拼图 pt=get(gca,'CurrentPoint'); % 获取当前鼠标点击位置坐标 xpos=pt(1,1); % 鼠标点击处的横坐标实际值 ypos=pt(1,2); % 鼠标点击处的纵坐标实际值 len_row=size(pic_data,1)/rank_Tag; % 每块拼图的宽度 len_col=size(pic_data,2)/rank_Tag; % 每块拼图的高度 col = ceil(xpos/len_row); % 将横坐标值转换为列数 row = ceil(ypos/len_col); % 将纵坐标值转换为行数 % 判断鼠标点击位置是否在有效范围内 if(col<=rank_Tag && col>0)&&(row<=rank_Tag && row>0) Tag=movejig(Tag,row,col,handles.text_steps); % 按点击位置移动拼图 drawmap(Tag,handles.axes_jigsaw,pic_data,rank_Tag) % 显示拼图 mask_number(Tag,handles,len_row,len_col) % 是否显示数字提示 % order为顺序矩阵,以此判断拼图是否完成 order=[1:1:rank_Tag^2-1,0]; order=reshape(order,rank_Tag,rank_Tag); order=order'; zt = abs(Tag-order); % 比较两个矩阵 if sum(zt(:))==0 % 顺序已经完全吻合 axes(handles.axes_jigsaw) % 游戏完成,补全拼图 image(pic_data) % 显示全图 set(handles.axes_jigsaw,'Visible','off');% 隐藏坐标轴 msgbox('You did a good job ,恭喜完成!!!') % 提示完成信息 flag=false;% 游戏已经完成,设置flag为false end else % 点击在外部区域 return % 直接返回,不作处理 end end
【代码解释】
代码第6行先判断全局变量flag的值,决定是否执行下面的操作(是否相应鼠标点击),由前面的代码可知当只有在重新选择图片、选择难度(阶数)的时候flag的值会被置为false来防止误操作,而当点击运行游戏按钮时该值会被重新置为true使程序运行。第7-9行获取鼠标点击处的坐标实际值,我们需要转换成拼图块的行列数,所以第11-15行获得拼图块的长宽,并由此计算行列数。第18行判断鼠标点击位置是否在拼图的显示区域内,第19-20行调用上面的函数按点击位置移动拼图并显示。第25-29行产生一个顺序的矩阵,并将当前的标号矩阵与该矩阵进行比较,以判断拼图是否已拼好。第31-36行在拼图拼好后显示完整图像并提示完成。第37行重新将flag的值置为false在重新开始游戏前不能再移动拼图。