一 概述
先总体上简单点的描述一下这个小游戏吧,一切以简单为准,只是为了给读者一个概念性的东西。用一个五角星表示跳绳的人,用两个杆子表示挥绳子的人,用一条抛物线模拟一根绳子。
二 代码
直接给出代码,在代码注释中交流吧。
function part4(in)global handlers; % 用全局变量完成参数的传递,handlers为句柄集,将所有句柄以及所用数据存储在此
if nargin % 当有输入时,用eval函数完成回调函数的调用
eval(in);
elseif isempty(handlers) % 若无输入,那么检查handlers,若handlers为空,那么初始化小游戏
initialize; % 整个小游戏仅仅初始化一次,就是这里
else % 若无输入,同时handlers非空,那么考虑为程序意外出错,或者重复调用,此时完成界面的关闭
closefcn;
end % 这里都采用函数的形式来完成工作,不一定效率最高,但是便于理解
function initialize % 初始化函数
global handlers; % 句柄集,将所有句柄以及所用数据存储在此
handlers = [];
% 游戏状态机,游戏所有的操作都是通过切换游戏的状态或者各个对象的状态来完成的,而在每一次时钟调用时完成显示
handlers.state = 0; % 游戏状态变量: 0 标题界面, 5 游戏运行中, 7 游戏暂停, 10 游戏结束
% 创建游戏设置量,尽管没有自定义项
handlers.settings.axispos = [0,8,0,4]; % 坐标轴显示范围,也就是整个游戏空间的范围,空间大小为8x6
handlers.settings.resolution = 0.1; % 坐标轴分辨率,即X方向步进值
handlers.settings.figuresize = [800,600]; % 窗口大小
handlers.settings.titlefontsize = 25; % 标题字体大小
handlers.settings.fontsize = 20; % 字体大小
handlers.settings.mainmenubgcolor = [0.839, 0.839, 0.839]; % 主界面背景颜色
handlers.settings.runingbgcolor = [0,0,0]; % 游戏运行时的背景颜色
handlers.settings.jumpstrength = 7; % 跳跃时得到的速度
handlers.settings.gravity = -18; % 模拟重力的量,向下的加速度
% 创建窗口
handlers.fh = figure(... !!!创建图像界面
'numbertitle','off', ... 关闭数字标题
'name','第四部分 小游戏',... 窗口标题
'units','pixels',... 窗口位置等信息的单位,设置为像素
'position',[0,0,handlers.settings.figuresize],... 窗口位置,仅需要设置窗口大小,后面用MOVEGUI函数完成居中的移动
'resize','off',...
'menu','none', ...
'windowkeypressfcn','part4(''feedback'')', ... % 窗口按键回调函数,当在窗口中发生键盘事件时调用
'deletefcn','part4(''closefcn'')');
movegui(handlers.fh,'center'); % 用movegui函数完成将窗口调用至显示器中央位置
% 创建坐标系
handlers.ah = axes(... !!!创建坐标轴,作为游戏显示空间
'units','normalized',... 单位设置为归一化
'position',[0,0,1,1],... 利用归一化设置坐标轴铺满整个窗口
'color',handlers.settings.mainmenubgcolor,... 坐标轴背景色
'tickdir','out'); % 坐标轴小刻度朝向为外,也可以通过将其长度设置为极小值解决他占用界面的问题
hold on; % 打开图像保留,使得每次绘制不会删除前一次的绘制
% 创建主界面,进入游戏后的标题界面
handlers.mainmenu.title = uicontrol(handlers.fh, ... ! ! ! 创建标题,用文字完成,没有回调函数
'style','text', ... 设置风格为文字
'string','小游戏演示', ...
'fontsize',handlers.settings.titlefontsize, ...
'units','pixels', ...
'position', [100,handlers.settings.figuresize(2)-100,handlers.settings.titlefontsize*10,handlers.settings.titlefontsize*2], ...
'BackgroundColor',handlers.settings.mainmenubgcolor,...
'enable','on', ...
'visible','on');
handlers.mainmenu.begin = uicontrol(handlers.fh, ... ! ! ! 创建开始游戏选项,后面各个选项均采用按钮实现
'style','pushbutton', ... 设置风格为按钮
'string','开始游戏', ...
'fontsize',handlers.settings.fontsize, ...
'units','pixels', ...
'position', [100+handlers.settings.titlefontsize*2,handlers.settings.figuresize(2)-200,handlers.settings.fontsize*8,handlers.settings.fontsize*2], ...
'callback','part4(''startgame'')', ... 设置回调函数
'BackgroundColor',handlers.settings.mainmenubgcolor,...
'enable','on', ...
'visible','on');
handlers.mainmenu.exit = uicontrol(handlers.fh, ... ! ! ! 创建退出游戏选项
'style','pushbutton', ...
'string','退出游戏', ...
'fontsize',handlers.settings.fontsize, ...
'units','pixels', ...
'position', [100+handlers.settings.titlefontsize*2,handlers.settings.figuresize(2)-260,handlers.settings.fontsize*8,handlers.settings.fontsize*2], ...
'callback','part4(''closefcn'')', ...
'BackgroundColor',handlers.settings.mainmenubgcolor,...
'enable','on', ...
'visible','on');
% 创建死亡界面,游戏结束后的界面
handlers.deadmenu.restart = uicontrol(handlers.fh, ... ! ! ! 创建重新开始游戏选项
'style','pushbutton', ...
'string','重新开始', ...
'fontsize',handlers.settings.fontsize, ...
'units','pixels', ...
'position', [100+handlers.settings.fontsize,handlers.settings.figuresize(2)-100,handlers.settings.fontsize*8,handlers.settings.fontsize*2], ...
'callback','part4(''startgame'')', ...
'BackgroundColor',handlers.settings.mainmenubgcolor,...
'enable','off', ...
'visible','off');
handlers.deadmenu.backtomainmenu = uicontrol(handlers.fh, ... ! ! ! 创建退出到主界面游戏选项
'style','pushbutton', ...
'string','退到主界面', ...
'fontsize',handlers.settings.fontsize, ...
'units','pixels', ...
'position', [100,handlers.settings.figuresize(2)-170,handlers.settings.fontsize*10,handlers.settings.fontsize*2], ...
'callback','part4(''backtomain'')', ...
'BackgroundColor',handlers.settings.mainmenubgcolor,...
'enable','off', ...
'visible','off');
handlers.deadmenu.exit = uicontrol(handlers.fh, ... ! ! ! 创建退出游戏选项
'style','pushbutton', ...
'string','退出游戏', ...
'fontsize',handlers.settings.fontsize, ...
'units','pixels', ...
'position', [100+handlers.settings.fontsize,handlers.settings.figuresize(2)-240,handlers.settings.fontsize*8,handlers.settings.fontsize*2], ...
'callback','part4(''closefcn'')', ...
'BackgroundColor',handlers.settings.mainmenubgcolor,...
'enable','off', ...
'visible','off');
% 创建游戏对象,包括一个球和两个杆子还有一根绳子,分别模拟跳绳的人,舞绳的人和绳子
handlers.waveman.pos1 = [0,0]; % ! ! ! 舞绳人参数
handlers.waveman.pos2 = [handlers.settings.axispos(2),0]; % 前两个为舞绳人的位置参数
handlers.waveman.hight = 2; % 舞绳人的高度,体现在线长
handlers.waveman.width = 80; % 舞绳人的宽度,体现在线宽
handlers.waveman.color = [1,1,1]; % 舞绳人颜色
handlers.jumpman.shape = 'p'; % ! ! ! 跳绳人参数, 第一个参数为形状,这里设置为五角星
handlers.jumpman.width = 0.2; % 跳绳人宽度,在碰撞检测时使用
handlers.jumpman.size = 50; % 跳绳人大小,这里体现在绘制的五角星的大小
handlers.jumpman.edge = 0.2; % 跳绳人的边缘大小,判断落地和碰顶时使用
handlers.jumpman.pos = [handlers.settings.axispos(2)/2,handlers.jumpman.edge]; % 跳绳人的位置,体现在绘制的五角星位置
handlers.jumpman.speed = 0; % 跳绳人的实时速度
handlers.jumpman.color = [1,1,0]; % 跳绳人的颜色,体现在五角星颜色
handlers.jumpman.maxspeed = 13; % 跳绳人最快速度,判断落地和碰顶时使用,速度超过此,认为撞死了 - -
handlers.slope.width = 5; % ! ! ! 绳子参数, 第一个参数为绳子宽度
handlers.slope.maxhight = handlers.waveman.hight/2; % 绳子的最大高度,设置为舞绳人的高度的一半,当然,为了简便这里考虑为对称的
handlers.slope.hight = handlers.waveman.hight/2; % 绳子的实时高度,绳子用抛物线来模拟,这里就是抛物线极值点的高度
handlers.slope.pos = handlers.waveman.hight/2; % 绳子的位置,这个位置其实就是绳子两端的高度,为舞绳人的一半
handlers.slope.speed = 3; % 绳子的旋转速度,为绳子旋转角度的增量
handlers.slope.side = 0; % 绳子与人的位置关系,决定绘制顺序,当绳子在前面时,值为1
handlers.slope.angle = pi*1.3; % 绳子的实时角度,这里的角度angle设置方式如下,从右边舞绳人的角度来看,绳子顺时针方向为角度正向,正上方为0
handlers.slope.color = [1,0,1]; % 绳子颜色
% 创建计时器,控制对游戏每一帧的绘制
handlers.mainlooptimer = timer(...
'busymode','drop', ...
'ExecutionMode','fixedrate', ...
'period',0.02, ...
'timerfcn','part4(''mainloop'')');
start(handlers.mainlooptimer) % 创建完成后记得启动
function mainloop % 计时器调用游戏主循环
global handlers;
switch handlers.state
case 5 % 运行时,实际上,仅仅在运行时才有更新,在其他界面上可以不进行更新
% 获取即时周期
instant = get(handlers.mainlooptimer,'InstantPeriod'); % 获取即时周期
% 虽然计时器是按时间调用的,但是由于MATLAB效率不高,
% 可能会有两次调用不是标准的设置的时间,所以需要设置此参数保证每次调用的时候的时间
% 计算人的位置
newpos = handlers.jumpman.pos(2)+handlers.jumpman.speed*instant; % 计算位置
handlers.jumpman.speed = handlers.jumpman.speed + handlers.settings.gravity*instant; % 更新速度
% 这里选择后更新速度是因为如果本次操作有跳跃动作,那么先更新速度可能会抵消很大一部分效果
% 而且在后续过程中会对速度进行修正,所以速度不能先更新,会对程序造成影响的
% 计算绳子的位置,确定绳子在人的哪一侧,从而确定绘制顺序
newangle = mod(handlers.slope.angle+instant*handlers.slope.speed, 2*pi);
if newangle>pi
handlers.slope.side = 1; % 表示绳子在跳绳人的前面,优先绘制绳子
else
handlers.slope.side = 0; % 表示跳绳人在绳子的前面,优先绘制跳绳人
end
handlers.slope.hight = handlers.slope.maxhight * cos(handlers.slope.angle);
% 判断是否碰撞,从而确定位置
if newpos < handlers.jumpman.edge % 落地
if handlers.jumpman.speed<-handlers.jumpman.maxspeed % 太快了,撞死了
deadnow;
end
handlers.jumpman.pos(2) = handlers.jumpman.edge;
handlers.jumpman.speed = 0;
elseif newpos > handlers.settings.axispos(4) - handlers.jumpman.edge % 太高了,跳到屋顶
if handlers.jumpman.speed>handlers.jumpman.maxspeed % 太快了,撞死了
deadnow;
handlers.jumpman.speed = 0;
else
handlers.jumpman.speed = -handlers.jumpman.speed;
end
handlers.jumpman.pos(2) = handlers.settings.axispos(4) - handlers.jumpman.edge;
elseif (newangle<pi*0.2 || newangle>pi*1.8 || (newangle>pi*0.8&&newangle<pi*1.2)) ...
&& handlers.slope.hight+handlers.slope.pos-newpos<handlers.jumpman.width ...
&& handlers.slope.hight+handlers.slope.pos-newpos>-handlers.jumpman.width
% 撞绳子了,我们仅仅判断在顶上和下面各0.4 PI 的位置
deadnow;
handlers.jumpman.pos(2) = newpos;
handlers.jumpman.speed = 0;
else
handlers.jumpman.pos(2) = newpos;
end
handlers.slope.angle = newangle;
% 绘制,先还原坐标轴
cla; % 清空坐标轴,这一点非常重要,保证每次绘制的仅有我们希望的图元
set(handlers.ah, 'color', handlers.settings.runingbgcolor); % 清空坐标轴后,背景色会自动修改为默认值,修改回来即可
drawwaveman
if handlers.slope.side % 其实这个变量完全没有必要设置,只是为了逻辑更清晰;这个变脸决定绘制顺序,从而表现出绳子位置
drawslope
drawjumpman
else
drawjumpman
drawslope
end
% 重新调整坐标轴显示范围,MATLAB会在绘制后自动修改显示范围以将绘制内容放在图像中心,所以需要添加此步骤
axis(handlers.settings.axispos)
case {0,7,10} % 标题界面、暂停、死亡界面,没有操作
end
function closefcn % 关闭界面函数,删除所有建立的句柄,同时清空handlers
global handlers;
if isfield(handlers,'fh') && ishandle(handlers.fh)
delete(handlers.fh);
end
if isfield(handlers,'mainlooptimer') && isvalid(handlers.mainlooptimer)
stop(handlers.mainlooptimer);
delete(handlers.mainlooptimer);
end
handlers =[];
function feedback % 窗口按键的回调函数,根据按键完成相应操作
global handlers;
switch get(handlers.fh,'CurrentCharacter')
case 'q' % 按 'Q' 退出游戏
if handlers.state == 5 % 游戏状态变量: 0 标题界面, 5 游戏运行中, 7 游戏暂停, 10 游戏结束
backtomain;
else
closefcn;
end
case 'p' % 按 'P' 暂停和继续游戏
switch handlers.state
case 5 % 运行时
handlers.state = 7;
case 7 % 暂停
handlers.state = 5;
end
case 's' % 按 'S' 开始游戏
if handlers.state == 0
startgame;
end
case 'r' % 按 'R' 重新开始游戏
if handlers.state ~= 5
startgame;
end
case 'j'
handlers.jumpman.speed = handlers.jumpman.speed+handlers.settings.jumpstrength;
case 'h' % 按 'h' 获取游戏帮助
helpdlg(...
{'按 ''Q'' 退出游戏',...
'按 ''P'' 暂停和继续游戏',...
'按 ''S'' 开始游戏',...
'按 ''R'' 重新开始游戏',...
'按 ''h'' 获取游戏帮助',...
' ', ...
' BY DAV'}, ...
'操作说明')
end
function startgame % 开始游戏
global handlers;
handlers.state = 5;
% 关闭主界面
set(handlers.mainmenu.title,'visible','off')
set(handlers.mainmenu.begin,'enable','off','visible','off')
set(handlers.mainmenu.exit,'enable','off','visible','off')
% 关闭死亡界面
set(handlers.deadmenu.restart,'enable','off','visible','off')
set(handlers.deadmenu.backtomainmenu,'enable','off','visible','off')
set(handlers.deadmenu.exit,'enable','off','visible','off')
% 清空背景坐标轴
cla;
set(handlers.ah, 'color', handlers.settings.runingbgcolor);
% 重置绳子和人物参数
handlers.jumpman.pos = [handlers.settings.axispos(2)/2,handlers.jumpman.edge];
handlers.jumpman.speed = 0;
handlers.slope.angle = pi*1.3;
function backtomain % 退回至主界面
global handlers;
handlers.state = 0;
% 打开主界面
set(handlers.mainmenu.title,'visible','on')
set(handlers.mainmenu.begin,'enable','on','visible','on')
set(handlers.mainmenu.exit,'enable','on','visible','on')
% 关闭死亡界面
set(handlers.deadmenu.restart,'enable','off','visible','off')
set(handlers.deadmenu.backtomainmenu,'enable','off','visible','off')
set(handlers.deadmenu.exit,'enable','off','visible','off')
% 清空背景坐标轴
cla;
set(handlers.ah, 'color', handlers.settings.mainmenubgcolor);
function deadnow % 进入死亡界面
global handlers
handlers.state = 10;
% 关闭主界面
set(handlers.mainmenu.title,'visible','off')
set(handlers.mainmenu.begin,'enable','off','visible','off')
set(handlers.mainmenu.exit,'enable','off','visible','off')
% 打开死亡界面
set(handlers.deadmenu.restart,'enable','on','visible','on')
set(handlers.deadmenu.backtomainmenu,'enable','on','visible','on')
set(handlers.deadmenu.exit,'enable','on','visible','on')
% 最后是三个绘制函数,分别绘制绳子,跳绳人和舞绳人
function drawslope % 绘制绳子
global handlers;
x = handlers.settings.axispos(1):handlers.settings.resolution:handlers.settings.axispos(2);
y = - handlers.slope.hight*((x - (handlers.waveman.pos2(1)+handlers.waveman.pos1(1))*0.5)/(handlers.waveman.pos2(1)-handlers.waveman.pos1(1))*2).^2 ...
+ handlers.slope.pos + handlers.slope.hight;
plot(x, y, 'linewidth', handlers.slope.width, ...
'color', handlers.slope.color)
function drawjumpman % 绘制跳绳的人
global handlers;
plot(handlers.jumpman.pos(1),handlers.jumpman.pos(2), ...
'markersize',handlers.jumpman.size, ...
'marker',handlers.jumpman.shape, ...
'markerfacecolor',handlers.jumpman.color, ...
'color',handlers.jumpman.color)
function drawwaveman % 绘制舞绳人
global handlers;
plot([handlers.waveman.pos1(1), handlers.waveman.pos1(1)], ...
[handlers.waveman.pos1(2), handlers.waveman.pos1(2)+handlers.waveman.hight], ...
'linewidth',handlers.waveman.width, ...
'color',handlers.waveman.color)
plot([handlers.waveman.pos2(1), handlers.waveman.pos2(1)], ...
[handlers.waveman.pos2(2), handlers.waveman.pos2(2)+handlers.waveman.hight], ...
'linewidth',handlers.waveman.width, ...
'color',handlers.waveman.color)
三 小结
最后依然是小结部分。画面,非常简洁 - -, 概念最重要,正如标题所说,仅仅为了让读者对用函数实现GUI 有个概念。另外,碰撞检测液做的非常简单,可能很多bug,这里就请各位见谅了。有很多handlers.settings的部分可以完全不用设置为参数,这样虽然看代码简洁一些,但是不那么容易懂,所以还是用参数表示出来;参数表示的另一个好处就是修改起来非常容易,在后面调试参数时,很简便。
本节用之前介绍的一些想法来完成了一个非常简单的跳绳的游戏,虽然写出来有点复杂,但是逻辑应该还是比较清楚的,建议看的时候先看函数定义后的注释以及行内的注释,至于其他地方的注释则比较细节了,读者最应该关注的是整体结构以及MATLAB函数的使用,至于算法嘛,当然还有很多值得改进的地方。当你有了更好的想法,采用这里的想法,你就可以实现更好的东西。
最后,感谢各位耐心读完这么多废话~~~