MATLAB GUI游戏设计——俄罗斯方块

本教程记录了使用MATLAB App Designer设计俄罗斯方块游戏的过程。涵盖界面设计,包括游戏和提示区域、分数显示;方块属性与移动逻辑;实现游戏循环、边界判断、消除逻辑、失败判定;更新提示区域和分数显示,还添加了帮助按钮,最终完成游戏设计。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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.TextArea1app.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×(0110)+ 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

俄罗斯方块

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值