MATLAB GUI游戏设计——俄罗斯方块
MATLAB GUI游戏设计——俄罗斯方块
本教程旨记录笔者学习使用 MATLAB App Designer 工具设计和实现一个基本的俄罗斯方块游戏的过程。我们将通过以下几个步骤来创建游戏:界面设计、方块属性和移动逻辑、游戏循环与计时器、边界判断与消除逻辑、游戏结束判断、提示区域与分数显示、以及帮助按钮的实现。
一、界面设计
游戏界面是玩家与游戏互动的首要媒介。我们将创建两个面板:一个主游戏区域,用于显示和操作下落的方块;另一个提示区域,用于显示下一个将要出现的方块。此外,我们还将添加一个显示玩家当前分数的编辑字段。
1、创建面板
首先,初始化两个面板 (uipanel
) 对象,分别命名为 p1 和 p2。其中 p1 作为游戏区域,p2 作为提示区域。
% 创建一个面板,用于存放400个文本框
p1 = uipanel('Parent', app.UIFigure, 'Position', [45, 30, 240, 480]);
p2 = uipanel('Parent', app.UIFigure, 'Position', [330, 385, 240, 125]);
2、初始化文本框
接下来,定义两个文本框数组 app.TextArea1
和 app.TextArea2
,它们分别用于表示游戏区域和提示区域。
% 初始化文本框属性
app.TextArea1 = cell(20, 10);
app.TextArea2 = cell(5, 10);
3、创建游戏区域
接下来,我们需要为游戏区域创建文本框,每个文本框代表游戏界面上的一个单元格。
% 遍历行和列,创建游戏区域
for row = 1:20
for col = 1:10
% 计算文本框的位置
position = [(col - 1) * 23 + 5, (row - 1) * 23 + 10, 23, 23];
% 创建文本框,并将其句柄存储在 app.TextArea 单元数组中
app.TextArea1{row, col} = uitextarea(p1, ...
'Position', position, ...
'Value', '', ...
'Editable', 'off', ...
'BackgroundColor', [1, 1, 1] ...
... % 这里可以添加更多的属性,如 'FontName', 'FontSize', 等
);
end
end
4、创建提示区域
类似地,我们为提示区域创建文本框,显示预告的下一个方块。
% 遍历行和列,创建提示区域
for row = 1:5
for col = 1:10
% 计算文本框的位置
position = [(col - 1) * 23 + 5, (row - 1) * 23 + 5, 23, 23];
% 创建文本框,并将其句柄存储在 app.TextArea 单元数组中
app.TextArea2{row, col} = uitextarea(p2, ...
'Position', position, ...
'Value', '', ...
'Editable', 'off', ...
'BackgroundColor', [1, 1, 1] ...
... % 这里可以添加更多的属性,如 'FontName', 'FontSize', 等
);
end
end
5、添加分数显示和控制面板
我们在界面上添加一个用于显示玩家分数的数字编辑字段组件(uieditfield
)。调整附带的文本框的位置为[327,300,87,60],数字编制框的位置为[410,300,160,60],为了让显示清晰,我们调大字号为36,并设置为不可编辑(取消Enable)。
我们在添加一个面板用来存放“开始游戏”按钮,“帮助”按钮,“难度”下拉框,位置可以灵活调整。
6、结果
运行这些代码后,您的界面应该看起来像这样:
这只是创建俄罗斯方块游戏的第一步。在接下来的教程中,我们将介绍如何添加游戏逻辑和控制。
二、方块属性与移动逻辑
1、方块的形状
俄罗斯方块通常有O型,Z型,S型,I型,L型,J型,T型这七种形状,通常我们可以把方块包括旋转之后的所有形状,储存在同一个元胞数组中,这里我们利用MATLAB高效率的矩阵运算来表示旋转之后的形状。
因此,我们把方块的旋转中心放在坐标原点上,每个小文本框右上角在直角坐标系中的坐标就是文本框在我们建立的20行10列的游戏区域中坐标。
代码如下
% 初始化形状数组
app.Shapes = cell(1, 7);
% 定义各种基本形状
app.Shapes{1} = [0,0; 0,1; 1,0; 1,1]; % O型
app.Shapes{2} = [-0.5,0; 0.5,0; 0.5,1; 1.5,1]; % S型
app.Shapes{3} = [-0.5,1; 0.5,1; 0.5,0; 1.5,0]; % Z型
app.Shapes{4} = [2,0.5;1,0.5;0,0.5;-1,0.5]; % I型
app.Shapes{5} = [0.5,1.5; 0.5,0.5; 0.5,-0.5; 1.5,-0.5]; % L型
app.Shapes{6} = [0.5,-0.5;0.5,0.5;0.5,1.5;1.5,1.5]; % J型
app.Shapes{7} = [0.5,1.5;0.5,0.5;1.5,0.5;0.5,-0.5]; % T型
2、方块的初始状态
% 随机产生方块的形状和相对于基础的旋转方向
app.ShapeId = randi([1,7]); % 表示方块编号
app.Shape = app.Shapes{app.ShapeId};
app.Direction = randi([-1,2]);
% 定义颜色
app.Colors = [1, 0, 0; % 红色
1, 0.647, 0; % 橙色
1, 1, 0; % 黄色
0, 1, 0; % 绿色
0, 0, 1; % 蓝色
0.502, 0, 0.502; % 紫色
1, 0.753, 0.796]; % 粉红色
% 随机产生颜色
app.Color = app.Colors(randi([1, 7]),:);
3、方块的初始位置
我们新建一个函数来原本我们设想以原点为中心的方块移动到游戏区域开始的位置。
function PositionStart(app)
app.BlockId = app.Shape;
switch app.ShapeId
case 1
app.BlockId = app.BlockId + [19,5;19,5;19,5;19,5];
case 2
app.BlockId = app.BlockId + [18.5,5;18.5,5;18.5,5;18.5,5];
case 3
app.BlockId = app.BlockId + [18.5,5;18.5,5;18.5,5;18.5,5];
case 4
app.BlockId = app.BlockId + [18,5.5;18,5.5;18,5.5;18,5.5];
case 5
app.BlockId = app.BlockId + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
case 6
app.BlockId = app.BlockId + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
case 7
app.BlockId = app.BlockId + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
end
end
我们再在初始界面上显示这个方块。
% 遍历数组,改变对应文本区域的背景颜色
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = [1, 0, 0]; % 红色
end
再修改代码中的颜色,让他呈现我们的随机生成的颜色值。
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
三、实现移动逻辑
1、添加键盘回调函数
我们点击UIFigure
,在键盘回调里边选择添加一个KeyPress函数,这样,我们就可以通过键盘添加移动逻辑了
key = event.Key;
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = [1, 1, 1]; % 修改颜色
end
switch key
case 'rightarrow'
app.BlockId = app.BlockId + [0,1;0,1;0,1;0,1];
case 'leftarrow'
app.BlockId = app.BlockId - [0,1;0,1;0,1;0,1];
case 'downarrow'
app.BlockId = app.BlockId - [1,0;1,0;1,0;1,0];
end
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
end
2、实现旋转逻辑
线性代数告诉我们,矩阵与矩阵的乘积实质上可以表示线性空间中的伸缩和旋转。特别地,下面这个矩阵:
[
\begin{pmatrix}
0 & 1 \
-1 & 0
\end{pmatrix}
]
是一个旋转矩阵,它用于实现二维空间中的点相对于原点的90度顺时针旋转。当我们将这个矩阵与一个二维向量相乘时,得到的结果是原向量旋转90度后的新位置。
另外我们发现,对方块形状Shape进行旋转之后,原本用来定义坐标的位置统统向上平移了一个单位,故新的方块形状可以表示为:
Shape
×
(
0
1
−
1
0
)
+
(
1
0
1
0
1
0
1
0
)
\text{Shape} \times \begin{pmatrix} 0 & 1 \\ -1 & 0 \end{pmatrix} + \begin{pmatrix} 1 & 0 \\ 1 & 0 \\ 1 & 0 \\ 1 & 0 \end{pmatrix}
Shape×(0−110)+
11110000
我们可以使用代码实现为:
function BlockDirection(app)
A = [0,1;-1,0];
B = [1,0;1,0;1,0;1,0];
app.Shape = app.Shape*A+B;
end
我们可以实现键盘回调函数中的旋转。为了实现这个,我们需要计算在游戏区域的方块位置与原来设置在原点的位置差,注意到S型,Z型,I型的这个位置差中与其他的不同,直接变化形状再与位置差相加会变成不是整数的坐标,所以我们对他进行修正:
Distance = app.BlockId - app.Shape;
if any(fix(Distance(:,1)) ~= Distance(:,1)) && any(fix(Distance(:,2)) == Distance(:,2)) % 检查是否有小数部分
Distance(:,1) = Distance(:,1) - 0.5;
Distance(:,2) = Distance(:,2) + 0.5;
elseif any(fix(Distance(:,2)) ~= Distance(:,2)) && any(fix(Distance(:,1)) == Distance(:,1)) % 检查是否有小数部分
Distance(:,1) = Distance(:,1) + 0.5;
Distance(:,2) = Distance(:,2) - 0.5;
end
switch key
case 'rightarrow'
app.BlockId = app.BlockId + [0,1;0,1;0,1;0,1];
case 'leftarrow'
app.BlockId = app.BlockId - [0,1;0,1;0,1;0,1];
case 'downarrow'
app.BlockId = app.BlockId - [1,0;1,0;1,0;1,0];
case 'uparrow'
app.BlockDirection()
app.BlockId = app.Shape + Distance;
end
四、实现游戏循环
1、创造游戏的计时器
游戏循环是游戏运行的核心,计时器能定时更新游戏状态。
我们在初始的代码中添加上计时器对象:
app.game = timer('ExecutionMode','fixedRate','Period',0.3,'TimerFcn',@app.tetrisGame);
之后,我们开始创建计时器回调函数:
function tetrisGame(app,~,~)
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = [1, 1, 1]; % 修改颜色
end
app.BlockId = app.BlockId - [1,0;1,0;1,0;1,0];
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
end
end
2、整理代码以及开始游戏
我们对于最开始设置的“开始游戏”按钮,添加一个按钮回调函数,整理一下代码。
下面是按钮回调函数的内容:
% 随机产生方块的形状和相对于基础的旋转方向
app.ShapeId = randi([1,7]);
app.Shape = app.Shapes{app.ShapeId};
app.Direction = randi([-1,2]);
app.flag = 1;
app.PositionStart()
% 随机产生颜色
app.Color = app.Colors(randi([1, 7]),:);
% 遍历数组,改变对应文本区域的背景颜色
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
end
start(app.game);
focus(app.UIFigure);
我们再创建一个游戏开始的标志flag,然后把移动逻辑封装成函数moveid:
function moveid(app,key)
Distance = app.BlockId - app.Shape;
if any(fix(Distance(:,1)) ~= Distance(:,1)) && any(fix(Distance(:,2)) == Distance(:,2)) % 检查是否有小数部分
Distance(:,1) = Distance(:,1) - 0.5;
Distance(:,2) = Distance(:,2) + 0.5;
elseif any(fix(Distance(:,2)) ~= Distance(:,2)) && any(fix(Distance(:,1)) == Distance(:,1)) % 检查是否有小数部分
Distance(:,1) = Distance(:,1) + 0.5;
Distance(:,2) = Distance(:,2) - 0.5;
end
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = [1, 1, 1]; % 修改颜色
end
switch key
case 'rightarrow'
app.BlockId = app.BlockId + [0,1;0,1;0,1;0,1];
case 'leftarrow'
app.BlockId = app.BlockId - [0,1;0,1;0,1;0,1];
case 'downarrow'
app.BlockId = app.BlockId - [1,0;1,0;1,0;1,0];
case 'uparrow'
app.BlockDirection()
app.BlockId = app.Shape + Distance;
end
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
end
end
3、修改键盘回调函数
由于键盘回调函数与计时器分开,在极限情况下,有可能会出现,同时旋转和下落,这个时候,图形显示会出现错误,于是,我们修改函数,使他调用按钮回调函数时,停止计时器。
function UIFigureKeyPress(app, event)
key = event.Key;
if app.flag == 0
return
end
stop(app.game);
app.moveid(key);
start(app.game);
end
五、实现边界判断
在MATLAB中制作俄罗斯方块游戏时,确保方块在游戏区域内正确移动和旋转是非常重要的。这要求实现一个有效的边界检测逻辑,以防止方块超出屏幕边缘或穿透已经放置的方块。以下是实现这一逻辑的关键步骤:
1、更新 tetris
类属性
首先,我们需要在 tetris
类中添加一个新属性来存储游戏区域的状态。
properties (Access = public)
GameArea = zeros(20, 10); % 20行10列的游戏区域
end
2、修改 moveid
方法
接下来,修改 moveid
方法来包含边界检查,以确保方块不会超出游戏区域。
function moveid(app, key)
...
% 边界检查逻辑
if any(app.newBlockId(:, 2) < 1) || any(app.newBlockId(:, 2) > 10) || ...
any(app.newBlockId(:, 1) < 1) || any(app.newBlockId(:, 1) > 20) || ...
any(app.GameArea(sub2ind(size(app.GameArea), app.newBlockId(:, 1), app.newBlockId(:, 2))) == 1)
return; % 如果移动后超出边界或与其他方块重叠,则不执行移动
end
...
end
3、 修改 tetrisGame
方法
tetrisGame
方法控制方块的下落。在这里,我们添加逻辑以处理方块到达底部的情况。
function tetrisGame(app,~,~)
...
% 假设方块将要下移一行
app.newBlockId = app.BlockId - [1,0;1,0;1,0;1,0];
% 检查是否到达底部或有障碍物
if any(app.newBlockId(:, 1) < 1) || any(app.newBlockId(:, 1) > 20) || ...
any(app.GameArea(sub2ind(size(app.GameArea), app.newBlockId(:, 1), app.newBlockId(:, 2))) == 1)
...
% 方块固定和产生新方块的逻辑
...
else
...
% 方块正常下移
...
end
end
通过这些步骤,我们可以确保游戏中的方块在移动和旋转时遵守游戏区域的边界,并且不会与其他方块发生冲突。这是创建一个功能完整且用户体验良好的俄罗斯方块游戏的关键部分。
4、生成新的方块
通过前面的步骤,我们已经基本上完成了单个方块的游戏逻辑,现在我们开始解决方块落下后生成新的方块的逻辑。
为此我们把生成方块的代码封装成一个函数。
function generateNewBlock(app)
% 随机产生方块的形状和相对于基础的旋转方向
app.ShapeId = randi([1,7]);
app.Shape = app.Shapes{app.ShapeId};
app.Direction = randi([-1,2]);
app.flag = 1;
app.PositionStart()
% 随机产生颜色
app.Color = app.Colors(randi([1, 7]),:);
% 遍历数组,改变对应文本区域的背景颜色
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
end
end
于是我们可以修改开始按钮回调函数的代码为:
app.generateNewBlock();
start(app.game);
focus(app.UIFigure);
修改游戏主函数tetrisGame里的代码为:
function tetrisGame(app,~,~)
% ... 现有的游戏逻辑 ...
if % 方块无法进一步下落的条件 ...
% 将方块固定在当前位置
% ... 现有的固定方块的代码 ...
% 生成新方块
app.generateNewBlock();
else
% ... 现有的下落逻辑 ...
end
end
六、实现消除逻辑
在这一部分的教程中,我们将专注于实现俄罗斯方块游戏中的一个核心功能:消除逻辑。我们将详细介绍如何检测并消除填满的行,以及在消除行后如何处理游戏区域的更新。
1、 初始化颜色存储
首先,我们需要一个属性来存储游戏区域中每个单元格的颜色。这将帮助我们在消除行后正确处理颜色的变化。
properties (Access = public)
GameAreaColors % 存储游戏区域中每个单元格的颜色
end
在 startupFcn
方法中初始化这个颜色存储:
function startupFcn(app)
% ... 现有初始化代码 ...
% 初始化游戏区域颜色
app.GameAreaColors = repmat({[1, 1, 1]}, size(app.GameArea));
end
2、 检测完整的行
当方块固定后,我们需要检查游戏区域中是否有任何行被完全填满。
function checkCompleteRows(app)
completedRows = [];
for row = 1:size(app.GameArea, 1)
if all(app.GameArea(row, :) == 1)
completedRows = [completedRows, row];
end
end
% 如果存在完成的行,同时闪烁这些行
if ~isempty(completedRows)
app.blinkRow(completedRows);
end
% 然后一次性消除这些行
for i = length(completedRows):-1:1
app.eliminateRow(completedRows(i));
end
end
3、 实现闪烁效果
在消除行之前,我们先让这些行闪烁,以提供视觉反馈。
function blinkRow(app, row)
% 设置闪烁次数和持续时间
numBlinks = 3;
blinkDuration = 0.3; % 秒
% 循环闪烁
for i = 1:numBlinks
% 将行颜色设置为不同颜色(例如,黑色)
for r = row
app.setRowColor(r, [0, 0, 0]);
end
pause(blinkDuration);
% 恢复原来的颜色
for r = row
app.setRowColor(r, [1, 1, 1]);
end
pause(blinkDuration);
end
end
4、 消除行并更新游戏区域
一旦行闪烁完成,我们需要消除这些行,并更新游戏区域。
function eliminateRow(app, row)
% 消除指定行
app.GameArea(row, :) = 0;
% 下移上方的所有行
for r = row+1:20
app.GameArea(r-1, :) = app.GameArea(r, :);
app.GameAreaColors(r-1, :) = app.GameAreaColors(r, :);
end
% 将顶部行设置为0
app.GameArea(20, :) = 0;
app.GameAreaColors(20, :) = {[1, 1, 1]};
% 更新显示
app.updateDisplay();
end
5、 更新显示
最后,我们需要更新游戏区域的显示以反映消除行后的新状态。
function updateDisplay(app)
for row = 1:size(app.GameArea, 1)
for col = 1:size(app.GameArea, 2)
app.TextArea1{row, col}.BackgroundColor = app.GameAreaColors{row, col};
end
end
end
通过以上步骤,俄罗斯方块游戏将能够正确检测和消除完整的行,并在消除行之前提供一个闪烁效果,增加游戏的互动性和视觉吸引力。
六、失败判定
在这一部分教程中,我们将探讨如何在MATLAB中实现俄罗斯方块游戏的游戏结束逻辑。这包括检测游戏结束的条件、处理游戏结束事件以及提供用户交互选项。
1、实现游戏结束判断
游戏结束通常发生在新生成的方块无法放置在游戏区域顶部时。为此,我们需要在游戏逻辑中嵌入一个检测机制。
function tetrisGame(app, ~, ~)
% ... 现有的方块下移逻辑 ...
% 假设方块将要下移一行
app.newBlockId = app.BlockId - [1,0;1,0;1,0;1,0];
% 检查是否到达底部或有障碍物
if % ... 检查逻辑 ...
% 将方块固定在当前位置
% ... 固定方块的代码 ...
% 检查并消除完整的行
app.checkCompleteRows();
% 生成新方块
app.generateNewBlock();
% 检查游戏结束条件
if any(app.GameArea(1, :) == 1)
% 游戏失败
app.gameOver();
return;
end
else
% ... 方块下落逻辑 ...
end
end
2、 修改 gameOver
方法
游戏结束时,我们需要停止计时器、重置游戏状态,并向用户提供一系列选项,如重新开始、返回主界面或退出游戏。
function gameOver(app)
% 停止游戏计时器
stop(app.game);
delete(app.game);
%初始化游戏参数
app.GameAreaColors = repmat({[1, 1, 1]}, size(app.GameArea));
app.GameArea = zeros(20, 10);
for row = 1:20
for col = 1:10
app.TextArea1{row, col}.BackgroundColor = [1, 1, 1]; % 修改颜色
end
end
% 显示游戏结束消息
msg = '游戏结束';
title = '游戏结束';
selection = uiconfirm(app.UIFigure,msg,title, ...
'Options',{'重新开始','返回主界面','退出游戏'}, ...
'DefaultOption',1,'CancelOption',2);
switch selection
case '重新开始'
app.game = timer('ExecutionMode','fixedRate','Period',1,'TimerFcn',@app.tetrisGame);
start(app.game);
focus(app.UIFigure);
case '返回主界面'
app.game = timer('ExecutionMode','fixedRate','Period',1,'TimerFcn',@app.tetrisGame);
case '退出游戏'
delete(app.UIFigure);
end
end
通过在 gameOver
方法中实现这些功能,这个俄罗斯方块游戏将能够在游戏结束时提供清晰的反馈,并给玩家提供选择的自由,从而增强游戏体验。
七、更新提示区域和分数显示
1、更新提示区域
为了在提示区域显示下一个方块,我们需要先清空提示区域,然后根据下一个方块的形状在提示区域绘制方块。这可以通过修改 generateNewBlock 方法来实现。
我们先创建一个函数来得到下一个方块。
代码如下:
function block = NewBlock(app)
NewShapeId = randi([1,7]);
NewShape1 = app.Shapes{NewShapeId};
NewColor = app.Colors(randi([1, 7]),:);
NewShape = app.PositionStart(NewShape1,NewShapeId);
% 清空提示区域
for row = 1:5
for col = 1:10
app.TextArea2{row, col}.BackgroundColor = [1, 1, 1];
end
end
% 在提示区域显示下一个方块
for i = 1:size(NewShape, 1)
row = NewShape(i, 1) - 16;
col = NewShape(i, 2);
app.TextArea2{row, col}.BackgroundColor = NewColor;
end
block = {NewShape1,NewColor,NewShape};
end
基于上面的代码,我们需要修改PositionStart函数,使他可以输入参数。
function Id = PositionStart(app,shape,shapeid)
Id = shape;
switch shapeid
case 1
Id = shape + [19,5;19,5;19,5;19,5];
case 2
Id = shape + [18.5,5;18.5,5;18.5,5;18.5,5];
case 3
Id = shape + [18.5,5;18.5,5;18.5,5;18.5,5];
case 4
Id = shape + [18,5.5;18,5.5;18,5.5;18,5.5];
case 5
Id = shape + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
case 6
Id = shape + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
case 7
Id = shape + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
end
end
我们修改 generateNewBlock 方法如下:
function generateNewBlock(app)
% 随机产生方块的形状和相对于基础的旋转方向
if app.flag == 0
app.ShapeId = randi([1,7]);
app.Shape = app.Shapes{app.ShapeId};
app.flag = 1;
% 随机产生颜色
app.Color = app.Colors(randi([1, 7]),:);
app.BlockId = app.PositionStart(app.Shape,app.ShapeId);
app.block = app.NewBlock();
else
app.Shape = app.block{1};
app.BlockId = app.block{3};
app.Color = app.block{2};
app.block = app.NewBlock();
end
% 遍历数组,改变对应文本区域的背景颜色
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
end
end
2、实现难度选择
我们可以通过修改计时器的时间来实现。
function DropDownValueChanged(app, event)
value = app.DropDown.Value;
switch value
case '简单'
app.game.Period = 1;
case '中等'
app.game.Period = 0.8;
case '困难'
app.game.Period = 0.5;
otherwise
app.game.Period = 1;
end
focus(app.UIFigure);
end
为了防止游戏过程中,因为计时器参数改变而造成的报错,我们在开始游戏之后,禁用难度选项。我们可以修改开始按钮的回调函数。
function ButtonPushed(app, event)
app.generateNewBlock();
start(app.game);
app.DropDown.Enable = 'off';
focus(app.UIFigure);
end
注 :记得在初始化的函数中添加
app.flag = 0
app.DropDown.Enable = 'on';
等代码。
3、实现分数显示
我们先在属性中添加Score,然后我们在初始化代码中添加
app.Score = 0
我们设置游戏逻辑为:每消除一行,增加一次分数,难度越高,增加越多。所以我们需要修改checkCompleteRows
方法,来更新分数。
function checkCompleteRows(app)
% ...其余代码...
% 然后一次性消除这些行
for i = length(completedRows):-1:1
app.eliminateRow(completedRows(i));
% 更新分数
app.EditField.Value = app.EditField.Value + 80/app.game.Period;
end
end
八、帮助按钮
为了让这个游戏更加完整,我们为这个游戏创建帮助信息。
function Button_2Pushed(app, event)
% 创建一个对话框显示帮助信息
msg = {'游戏玩法说明:', ...
'使用左右键移动方块,上键旋转方块,下键加速方块下落。', ...
'', ...
'分数规则:', ...
'每消除一行,得8分。难度越高,分数增加越快。', ...
'', ...
'控制难度:', ...
'在控制面板中选择游戏难度。'};
uialert(app.UIFigure, msg, '游戏帮助','Icon','question','Interpreter','html');
focus(app.UIFigure);
end
总结
通过以上步骤,我们基本上成功的创建了一个俄罗斯方块游戏,当然,由于笔者刚刚开始学习,如有错误,欢迎指正。
最后附上游戏的全部代码,以及最后的运行结果。
classdef tetris < matlab.apps.AppBase
% Properties that correspond to app components
properties (Access = public)
UIFigure matlab.ui.Figure
Panel matlab.ui.container.Panel
DropDown matlab.ui.control.DropDown
Button_2 matlab.ui.control.Button
Button matlab.ui.control.Button
EditField matlab.ui.control.NumericEditField
Label_2 matlab.ui.control.Label
Label matlab.ui.control.Label
end
properties (Access = public)
TextArea1 % 游戏区域
TextArea2 % 提示区域
BlockId % 方块位置
Shapes % 方块所有形状
Shape % 本次方块
ShapeId % 方块编号
Colors % 方块的所有颜色
Color % 方块目前的颜色
game % 游戏计时器
flag % 游戏开始的标志
GameArea = zeros(20, 10);
% 20行10列的游戏区域
newBlockId% 方块下一个位置
GameAreaColors
% 存储游戏区域中每个单元格的颜色
block % 下一个方块的属性
Score % 游戏分数
end
methods (Access = public)
function Id = PositionStart(app,shape,shapeid)
Id = shape;
switch shapeid
case 1
Id = shape + [19,5;19,5;19,5;19,5];
case 2
Id = shape + [18.5,5;18.5,5;18.5,5;18.5,5];
case 3
Id = shape + [18.5,5;18.5,5;18.5,5;18.5,5];
case 4
Id = shape + [18,5.5;18,5.5;18,5.5;18,5.5];
case 5
Id = shape + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
case 6
Id = shape + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
case 7
Id = shape + [18.5,5.5;18.5,5.5;18.5,5.5;18.5,5.5];
end
end
function BlockDirection(app)
A = [0,1;-1,0];
B = [1,0;1,0;1,0;1,0];
app.Shape = app.Shape*A+B;
end
function tetrisGame(app,~,~)
% 假设方块将要下移一行
app.newBlockId = app.BlockId - [1,0;1,0;1,0;1,0];
% 检查是否到达底部或有障碍物
if any(app.newBlockId(:, 1) < 1) || any(app.newBlockId(:, 1) > 20) || ...
any(app.GameArea(sub2ind(size(app.GameArea), app.newBlockId(:, 1), app.newBlockId(:, 2))) == 1)
% 将方块固定在当前位置
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.GameArea(row, col) = 1; % 更新游戏区域
app.GameAreaColors{row, col} = app.Color; % 更新游戏区域颜色
end
% 检查是否有任何一列完全被填满
if any(app.GameArea(20, :) == 1)
% 游戏失败
app.gameOver();
return;
end
% 检查并消除完整的行
app.checkCompleteRows();
% 这里添加产生新方块的代码
app.generateNewBlock();
else
% 清除当前方块的颜色
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = [1, 1, 1];
end
% 更新方块位置
app.BlockId = app.newBlockId;
% 绘制新位置的方块
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color;
end
end
end
function moveid(app,key)
Distance = app.BlockId - app.Shape;
if any(fix(Distance(:,1)) ~= Distance(:,1)) && any(fix(Distance(:,2)) == Distance(:,2)) % 检查是否有小数部分
Distance(:,1) = Distance(:,1) - 0.5;
Distance(:,2) = Distance(:,2) + 0.5;
elseif any(fix(Distance(:,2)) ~= Distance(:,2)) && any(fix(Distance(:,1)) == Distance(:,1)) % 检查是否有小数部分
Distance(:,1) = Distance(:,1) + 0.5;
Distance(:,2) = Distance(:,2) - 0.5;
end
switch key
case 'rightarrow'
app.newBlockId = app.BlockId + [0,1;0,1;0,1;0,1];
case 'leftarrow'
app.newBlockId = app.BlockId - [0,1;0,1;0,1;0,1];
case 'downarrow'
app.newBlockId = app.BlockId - [1,0;1,0;1,0;1,0];
case 'uparrow'
app.BlockDirection()
app.newBlockId = app.Shape + Distance;
end
% 边界检查逻辑
if any(app.newBlockId(:, 2) < 1) || any(app.newBlockId(:, 2) > 10) || ...
any(app.newBlockId(:, 1) < 1) || any(app.newBlockId(:, 1) > 20) || ...
any(app.GameArea(sub2ind(size(app.GameArea), app.newBlockId(:, 1), app.newBlockId(:, 2))) == 1)
return; % 如果移动后超出边界或与其他方块重叠,则不执行移动
end
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = [1, 1, 1]; % 修改颜色
end
app.BlockId = app.newBlockId;
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
end
end
function block = NewBlock(app)
NewShapeId = randi([1,7]);
NewShape1 = app.Shapes{NewShapeId};
NewColor = app.Colors(randi([1, 7]),:);
NewShape = app.PositionStart(NewShape1,NewShapeId);
% 清空提示区域
for row = 1:5
for col = 1:10
app.TextArea2{row, col}.BackgroundColor = [1, 1, 1];
end
end
% 在提示区域显示下一个方块
for i = 1:size(NewShape, 1)
row = NewShape(i, 1) - 16;
col = NewShape(i, 2);
app.TextArea2{row, col}.BackgroundColor = NewColor;
end
block = {NewShape1,NewColor,NewShape};
end
function generateNewBlock(app)
% 随机产生方块的形状和相对于基础的旋转方向
if app.flag == 0
app.ShapeId = randi([1,7]);
app.Shape = app.Shapes{app.ShapeId};
app.flag = 1;
% 随机产生颜色
app.Color = app.Colors(randi([1, 7]),:);
app.BlockId = app.PositionStart(app.Shape,app.ShapeId);
app.block = app.NewBlock();
else
app.Shape = app.block{1};
app.BlockId = app.block{3};
app.Color = app.block{2};
app.block = app.NewBlock();
end
% 遍历数组,改变对应文本区域的背景颜色
for i = 1:4
row = app.BlockId(i, 1);
col = app.BlockId(i, 2);
app.TextArea1{row, col}.BackgroundColor = app.Color; % 修改颜色
end
end
function checkCompleteRows(app)
completedRows = [];
for row = 1:size(app.GameArea, 1)
if all(app.GameArea(row, :) == 1)
completedRows = [completedRows, row];
end
end
% 如果存在完成的行,同时闪烁这些行
if ~isempty(completedRows)
app.blinkRow(completedRows);
end
% 然后一次性消除这些行
for i = length(completedRows):-1:1
app.eliminateRow(completedRows(i));
app.EditField.Value = app.EditField.Value + 80/app.game.Period;
end
end
function eliminateRow(app, row)
% 消除指定行
app.GameArea(row, :) = 0;
% 下移上方的所有行
for r = row+1:20
app.GameArea(r-1, :) = app.GameArea(r, :);
app.GameAreaColors(r-1, :) = app.GameAreaColors(r, :);
end
% 将顶部行设置为0
app.GameArea(20, :) = 0;
app.GameAreaColors(20, :) = {[1, 1, 1]};
% 更新显示
app.updateDisplay();
end
function updateDisplay(app)
% 遍历游戏区域的每个单元格
for row = 1:size(app.GameArea, 1)
for col = 1:size(app.GameArea, 2)
if app.GameArea(row, col) == 1
% 如果单元格被占用,则显示对应的颜色
app.TextArea1{row, col}.BackgroundColor = app.GameAreaColors{row, col};
else
% 如果单元格空闲,则显示为白色
app.TextArea1{row, col}.BackgroundColor = [1, 1, 1];
end
end
end
end
function blinkRow(app, row)
% 设置闪烁次数和持续时间
numBlinks = 3;
blinkDuration = 0.3; % 秒
% 循环闪烁
for i = 1:numBlinks
% 将行颜色设置为不同颜色(例如,黑色)
for r = row
app.setRowColor(r, [0, 0, 0]);
end
pause(blinkDuration);
% 恢复原来的颜色
for r = row
app.setRowColor(r, [1, 1, 1]);
end
pause(blinkDuration);
end
end
function setRowColor(app, row, color)
for col = 1:10
app.TextArea1{row, col}.BackgroundColor = color;
end
end
function gameOver(app)
% 停止游戏计时器
stop(app.game);
delete(app.game);
%初始化游戏参数
app.GameAreaColors = repmat({[1, 1, 1]}, size(app.GameArea));
app.GameArea = zeros(20, 10);
app.flag = 0;
app.DropDown.Enable = 'on';
for row = 1:20
for col = 1:10
app.TextArea1{row, col}.BackgroundColor = [1, 1, 1]; % 修改颜色
end
end
% 显示游戏结束消息
msg = '游戏结束';
title = '游戏结束';
selection = uiconfirm(app.UIFigure,msg,title, ...
'Options',{'重新开始','返回主界面','退出游戏'}, ...
'DefaultOption',1,'CancelOption',2);
switch selection
case '重新开始'
app.EditField.Value = 0;
app.DropDown.Enable = "off";
app.flag = 1;
app.game = timer('ExecutionMode','fixedRate','Period',1,'TimerFcn',@app.tetrisGame);
start(app.game);
focus(app.UIFigure);
case '返回主界面'
app.game = timer('ExecutionMode','fixedRate','Period',1,'TimerFcn',@app.tetrisGame);
app.Button.Enable = "on";
app.DropDown.Enable = 'on';
app.Button_2.Enable = "on";
case '退出游戏'
delete(app.UIFigure);
end
end
end
% Callbacks that handle component events
methods (Access = private)
% Code that executes after component creation
function startupFcn(app)
% 创建一个面板,用于存放400个文本框
p1 = uipanel('Parent', app.UIFigure, 'Position', [45, 30, 240, 480]);
p2 = uipanel('Parent', app.UIFigure, 'Position', [330, 385, 240, 125]);
% 初始化文本框属性
app.TextArea1 = cell(20, 10);
app.TextArea2 = cell(5, 10);
% 遍历行和列,创建游戏区域
for row = 1:20
for col = 1:10
% 计算文本框的位置
position = [(col - 1) * 23 + 5, (row - 1) * 23 + 10, 23, 23];
% 创建文本框,并将其句柄存储在 app.TextArea 单元数组中
app.TextArea1{row, col} = uitextarea(p1, ...
'Position', position, ...
'Value', '', ...
'Editable', 'off', ...
'BackgroundColor', [1, 1, 1] ...
... % 这里可以添加更多的属性,如 'FontName', 'FontSize', 等
);
end
end
% 遍历行和列,创建提示区域
for row = 1:5
for col = 1:10
% 计算文本框的位置
position = [(col - 1) * 23 + 5, (row - 1) * 23 + 5, 23, 23];
% 创建文本框,并将其句柄存储在 app.TextArea 单元数组中
app.TextArea2{row, col} = uitextarea(p2, ...
'Position', position, ...
'Value', '', ...
'Editable', 'off', ...
'BackgroundColor', [1, 1, 1] ...
... % 这里可以添加更多的属性,如 'FontName', 'FontSize', 等
);
end
end
% 初始化形状数组
app.Shapes = cell(1, 7);
app.flag = 0;
app.Score = 0;
% 定义各种基本形状
app.Shapes{1} = [0,0; 0,1; 1,0; 1,1]; % O型
app.Shapes{2} = [-0.5,0; 0.5,0; 0.5,1; 1.5,1]; % S型
app.Shapes{3} = [-0.5,1; 0.5,1; 0.5,0; 1.5,0]; % Z型
app.Shapes{4} = [2,0.5;1,0.5;0,0.5;-1,0.5]; % I型
app.Shapes{5} = [0.5,1.5; 0.5,0.5; 0.5,-0.5; 1.5,-0.5]; % L型
app.Shapes{6} = [0.5,-0.5;0.5,0.5;0.5,1.5;1.5,1.5]; % J型
app.Shapes{7} = [0.5,1.5;0.5,0.5;1.5,0.5;0.5,-0.5]; % T型
% 定义颜色
app.Colors = [1, 0, 0; % 红色
1, 0.647, 0; % 橙色
1, 1, 0; % 黄色
0, 1, 0; % 绿色
0, 0, 1; % 蓝色
0.502, 0, 0.502; % 紫色
1, 0.753, 0.796]; % 粉红色
% 初始化游戏区域颜色
app.GameAreaColors = repmat({[1, 1, 1]}, size(app.GameArea));
% 创建计时器对象
app.game = timer('ExecutionMode','fixedRate','Period',1,'TimerFcn',@app.tetrisGame);
end
% Key press function: UIFigure
function UIFigureKeyPress(app, event)
key = event.Key;
if app.flag == 0
return
end
stop(app.game);
app.moveid(key);
start(app.game);
end
% Button pushed function: Button
function ButtonPushed(app, event)
app.EditField.Value = 0;
app.generateNewBlock();
start(app.game);
app.Button.Enable = "off";
app.DropDown.Enable = 'off';
app.Button_2.Enable = 'off';
focus(app.UIFigure);
end
% Value changed function: DropDown
function DropDownValueChanged(app, event)
value = app.DropDown.Value;
switch value
case '简单'
app.game.Period = 1;
case '中等'
app.game.Period = 0.8;
case '困难'
app.game.Period = 0.5;
otherwise
app.game.Period = 1;
end
focus(app.UIFigure);
end
% Button pushed function: Button_2
function Button_2Pushed(app, event)
% 创建一个对话框显示帮助信息
msg = {'游戏玩法说明:', ...
'使用左右键移动方块,上键旋转方块,下键加速方块下落。', ...
'', ...
'分数规则:', ...
'每消除一行,得10分。难度越高,分数增加越快。', ...
'', ...
'控制难度:', ...
'在控制面板中选择游戏难度。'};
uialert(app.UIFigure, msg, '游戏帮助','Icon','question','Interpreter','html');
focus(app.UIFigure);
end
end
% Component initialization
methods (Access = private)
% Create UIFigure and components
function createComponents(app)
% Create UIFigure and hide until all components are created
app.UIFigure = uifigure('Visible', 'off');
app.UIFigure.Position = [340 60 600 600];
app.UIFigure.Name = 'MATLAB App';
app.UIFigure.KeyPressFcn = createCallbackFcn(app, @UIFigureKeyPress, true);
% Create Label
app.Label = uilabel(app.UIFigure);
app.Label.HorizontalAlignment = 'center';
app.Label.FontName = '宋体';
app.Label.FontSize = 40;
app.Label.FontWeight = 'bold';
app.Label.FontColor = [1 0 0];
app.Label.Position = [0 525 600 75];
app.Label.Text = '俄罗斯方块';
% Create Label_2
app.Label_2 = uilabel(app.UIFigure);
app.Label_2.HorizontalAlignment = 'center';
app.Label_2.FontName = '宋体';
app.Label_2.FontSize = 36;
app.Label_2.FontWeight = 'bold';
app.Label_2.Position = [327 300 87 60];
app.Label_2.Text = '分数';
% Create EditField
app.EditField = uieditfield(app.UIFigure, 'numeric');
app.EditField.ValueDisplayFormat = '%.0f';
app.EditField.HorizontalAlignment = 'center';
app.EditField.FontSize = 36;
app.EditField.Enable = 'off';
app.EditField.Position = [410 300 160 60];
% Create Panel
app.Panel = uipanel(app.UIFigure);
app.Panel.TitlePosition = 'centertop';
app.Panel.Title = '控制面板';
app.Panel.FontName = '宋体';
app.Panel.Position = [330 45 240 240];
% Create Button
app.Button = uibutton(app.Panel, 'push');
app.Button.ButtonPushedFcn = createCallbackFcn(app, @ButtonPushed, true);
app.Button.FontName = '宋体';
app.Button.FontSize = 20;
app.Button.Position = [45 156 150 34];
app.Button.Text = '开始游戏';
% Create Button_2
app.Button_2 = uibutton(app.Panel, 'push');
app.Button_2.ButtonPushedFcn = createCallbackFcn(app, @Button_2Pushed, true);
app.Button_2.FontName = '宋体';
app.Button_2.FontSize = 20;
app.Button_2.Position = [45 41 150 34];
app.Button_2.Text = '玩法帮助';
% Create DropDown
app.DropDown = uidropdown(app.Panel);
app.DropDown.Items = {'简单', '中等', '困难'};
app.DropDown.ValueChangedFcn = createCallbackFcn(app, @DropDownValueChanged, true);
app.DropDown.FontName = '宋体';
app.DropDown.FontSize = 20;
app.DropDown.Position = [45 100 150 30];
app.DropDown.Value = '简单';
% Show the figure after all components are created
app.UIFigure.Visible = 'on';
end
end
% App creation and deletion
methods (Access = public)
% Construct app
function app = tetris
% Create UIFigure and components
createComponents(app)
% Register the app with App Designer
registerApp(app, app.UIFigure)
% Execute the startup function
runStartupFcn(app, @startupFcn)
if nargout == 0
clear app
end
end
% Code that executes before app deletion
function delete(app)
% Delete UIFigure when app is deleted
delete(app.UIFigure)
end
end
end