俄罗斯方块(C++)

2023-11-06

目录

一. 俄罗斯方块简易版的实现

1. 图形的存储

2. 图形的显示

3. 图形的操作

4. 碰撞检测

1. 碰撞检测:移动

2. 碰撞检测:旋转

5. 消除整行

6. 游戏结束

7. 完整代码

二. 俄罗斯方块简易版的升级

1. 新增属性

2. 更改初始化函数

3. 更改消除整行函数

4. 更改显示、清除图形辅助函数

5. 更改控制速度的语句

6. 完整代码

7. 运行效果


一. 俄罗斯方块简易版的实现

1. 图形的存储

        俄罗斯方块一共7种基础图形,旋转后会得到19种形态。那么可以认为我们有7种基础图形,通过旋转操作这7种基础图形又会得到不同的状态,我们可以只存储这7种基本的图形,但是讨论图形旋转的状态变化会比较麻烦。我们换一种思路,那就是存储每一种状态的坐标,这个时候我们旋转变换便是状态的变换,也就是说旋转后的状态我们已经记录下来了,在旋转的碰撞检测时就省去了不必要的计算。

        思路有了接下来就实现一下,假设我们使用4 * 4的网格来存放所有的状态,那么每种状态就会对应4个坐标对,假设从左到右依次为0~3,从上到下页依次为0~3,那么我们根据每种状态四个小方块的位置可以写出如下代码。

// 声明这19种图形
// 图形4个像素点的声明顺序无关紧要
// 但每种图形必须按旋转变换的顺序依次声明
Point From[19][4] = {
    // 条形
    {{0, 0}, {1, 0}, {2, 0}, {3, 0}},
    {{0, 0}, {0, 1}, {0, 2}, {0, 3}},
    // 方形
    {{0, 0}, {1, 0}, {0, 1}, {1, 1}},
    // L形
    {{0, 0}, {0, 1}, {0, 2}, {1, 2}},
    {{2, 0}, {1, 0}, {0, 0}, {0, 1}},
    {{2, 2}, {2, 1}, {2, 0}, {1, 0}},
    {{0, 2}, {1, 2}, {2, 2}, {2, 1}},
    // 镜像L形
    {{2, 0}, {2, 1}, {2, 2}, {1, 2}},
    {{2, 2}, {1, 2}, {0, 2}, {0, 1}},
    {{0, 2}, {0, 1}, {0, 0}, {1, 0}},
    {{0, 0}, {1, 0}, {2, 0}, {2, 1}},
    // T型
    {{1, 1}, {0, 2}, {1, 2}, {2, 2}},
    {{1, 1}, {0, 0}, {0, 1}, {0, 2}},
    {{1, 1}, {2, 0}, {1, 0}, {0, 0}},
    {{1, 1}, {2, 0}, {2, 1}, {2, 2}},
    // 闪电形
    {{0, 0}, {0, 1}, {1, 1}, {1, 2}},
    {{2, 0}, {1, 0}, {1, 1}, {0, 1}},
    // 镜像闪电形
    {{0, 0}, {1, 0}, {1, 1}, {2, 1}},
    {{1, 0}, {1, 1}, {0, 1}, {0, 2}}};

        4个像素点的声明顺序无关紧要,但每种图形必须按旋转变换的顺序依次声明,结合碰撞检测代码和旋转代码的实现想想为什么。

        这里我们需要存储的坐标对并不算多,并不需要在堆上申请内存,也没必要压缩存储。

2. 图形的显示

        我们首先思考一下我们需要哪些变量。按惯例,首先是场景的宽度BW、场景的高度BH,紧接着是图形的初始坐标HomeX、HomeY,然后是方块的显示数组Board。最后,针对俄罗斯方块这个游戏,我们需要声明俄罗斯方块的形状sharp,俄罗斯方块的坐标x、y。

const int BW(20);             // BW表示场景宽度
const int BH(27);             // BH表示场景高度
const int HomeX(BW - 1 >> 1); // HomeX表示图形初始横坐标
const int HomeY(1);           // HomeY表示图形初始纵坐标
bool Board[BW * BH];          // Board[x + y * BW]表示(x, y)处是否为小方块
int sharp;                    // 图形的形状(形状随机产生)
int x, y;                     // 图形的横纵坐标

        按惯例我们先完成main()函数。

int main()
{
    SetConsole("俄罗斯方块", 40, 27, "80");
    srand((int)time(0));
    run();
}

        紧接着我们按照之前的模版快速写出run()函数。Initialize()、Drop()、Left()、Right()、Ratote()这5个函数先声明不实现。

// 运行游戏
void run()
{
    Initialize();

    int t = 0;
    while (true)
    {
        // 时间复杂度O(BW * BH * BH)
        Sleep(1);
        if (t++ > 50)
        {
            Drop();
            t = 0;
        }
        if (kbhit())
        {
            int ch = getch();
            if (ch == 224)
            {
                switch (getch())
                {
                case 72:      // 按下键盘上键
                    Ratote(); // 旋转图形
                    break;
                case 80:    // 按下键盘下键
                    Drop(); // 下降图形
                    break;
                case 75:    // 按下键盘左键
                    Left(); // 左移图形
                    break;
                case 77:     // 按下键盘右键
                    Right(); // 右移图形
                    break;
                }
            }
            else if (ch == 32)
                Pause();
        }
    }
}

        接下来就要开始动一下脑子了。我们知道图形的坐标(x, y),图形的形状sharp,那么我们如何显示图形呢,我们依次打印表示图形的4个像素点即可,将我们存储的小方块的相对坐标转成绝对坐标,接下来我们将像素点绝对坐标处对应的Board值设置为true(该处存在小方块),然后用FillStr()函数来打印小方块即可。考虑到代码重复度较高,我们再写一个辅助函数ShowOrClear()来完成重复的工作。

// 相对坐标转绝对坐标
Point SiteChange(Point p)
{
    return {x + p.x, y + p.y};
}
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
    for (int i = 0; i < 4; ++i)
    {
        Point Pixel = SiteChange(From[sharp][i]);
        Board[Pixel.x + Pixel.y * BW] = exist;
        FillStr(Pixel.x, Pixel.y, fill);
    }
}
// 显示图形
void ShowGraph() { ShowOrClear(true, "■"); }
// 清除图形
void ClearGraph() { ShowOrClear(false, "  "); }

        实现一下Initialize()函数。

// 初始化游戏
void Initialize()
{
    system("cls");
    AddGraph();

    score = 0;
    speed = 1;
    MaxScore = 0;
    for (bool b : Board)
        b = false;
}

       实现一下AddGraph()函数。

// 添加图形
void AddGraph()
{
    sharp = rand() % 19;
    x = HomeX;
    y = HomeY;
    ShowGraph();
}

3. 图形的操作

        接下来我们实现一下基本操作。 

        下降函数Drop()。先清除屏幕上的图形,改变纵坐标的位置,重新打印屏幕上的图形。该图形无法继续下降了,先检测是否有满行可以消除,再添加新的图形到屏幕上。

// 图形下降
void Drop()
{
    if (!Drop_CD())
    {
        ClearGraph();
        ++y;
        ShowGraph();
    }
    else
    {
        LineRemove();
        AddGraph();
    }
}

        左移函数Left()。和下降函数同理。

// 图形左移
void Left()
{
    if (!Left_CD())
    {
        ClearGraph();
        --x;
        ShowGraph();
    }
}

        右移函数Right()。和下降函数同理。

// 图形右移
void Right()
{
    if (!Right_CD())
    {
        ClearGraph();
        ++x;
        ShowGraph();
    }
}

        旋转函数Ratote()。先记录旋转后的状态,再判断旋转后的状态是否可行。如果理解不了可以用switch()语句来表示旋转的状态转换,这里巧妙的避开了坐标的变换计算,因为每种状态都记录了相应的相对坐标。

        因为我们已经记录了每种状态对应的相对坐标了,我们这里只需要搞清楚每种状态的下一个状态是什么就行了,我最初并不是这么做的,而是分情况讨论了每种状态的旋转操作对应的坐标转换公式,也就是通过sharp重新计算了sharp旋转变换后对应的相对坐标。当然我们可以巧妙地避开这些繁琐的计算,于是我们得到了下面这个十分简洁的旋转函数。

// 图形旋转
void Ratote()
{
    int NextSharp = sharp;
    if (sharp == 6 || sharp == 10 || sharp == 14)
        NextSharp -= 3;
    else if (sharp == 1 || sharp == 16 || sharp == 18)
        --NextSharp;
    else
        ++NextSharp;
    if (!Ratote_CD(NextSharp))
    {
        ClearGraph();
        sharp = NextSharp;
        ShowGraph();
    }
}

        此时代码如下。

#include "../Engine/BrickEngine.h"

const int BW(20);             // BW表示场景宽度
const int BH(27);             // BH表示场景高度
const int HomeX(BW - 1 >> 1); // HomeX表示图形初始横坐标
const int HomeY(1);           // HomeY表示图形初始纵坐标
bool Board[BW * BH];          // Board[x + y * BW]表示(x, y)处是否为小方块
int sharp;                    // 图形的形状(形状随机产生)
int x, y;                     // 图形的横纵坐标

// 声明这19种图形
// 图形4个像素点的声明顺序无关紧要
// 但每种图形必须按旋转变换的顺序依次声明
Point From[19][4] = {
    // 条形
    {{0, 0}, {1, 0}, {2, 0}, {3, 0}},
    {{0, 0}, {0, 1}, {0, 2}, {0, 3}},
    // 方形
    {{0, 0}, {1, 0}, {0, 1}, {1, 1}},
    // L形
    {{0, 0}, {0, 1}, {0, 2}, {1, 2}},
    {{2, 0}, {1, 0}, {0, 0}, {0, 1}},
    {{2, 2}, {2, 1}, {2, 0}, {1, 0}},
    {{0, 2}, {1, 2}, {2, 2}, {2, 1}},
    // 镜像L形
    {{2, 0}, {2, 1}, {2, 2}, {1, 2}},
    {{2, 2}, {1, 2}, {0, 2}, {0, 1}},
    {{0, 2}, {0, 1}, {0, 0}, {1, 0}},
    {{0, 0}, {1, 0}, {2, 0}, {2, 1}},
    // T型
    {{1, 1}, {0, 2}, {1, 2}, {2, 2}},
    {{1, 1}, {0, 0}, {0, 1}, {0, 2}},
    {{1, 1}, {2, 0}, {1, 0}, {0, 0}},
    {{1, 1}, {2, 0}, {2, 1}, {2, 2}},
    // 闪电形
    {{0, 0}, {0, 1}, {1, 1}, {1, 2}},
    {{2, 0}, {1, 0}, {1, 1}, {0, 1}},
    // 镜像闪电形
    {{0, 0}, {1, 0}, {1, 1}, {2, 1}},
    {{1, 0}, {1, 1}, {0, 1}, {0, 2}}};

void run();
// 相对坐标转绝对坐标
Point SiteChange(Point p)
{
    return {x + p.x, y + p.y};
}
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
    for (int i = 0; i < 4; ++i)
    {
        Point Pixel = SiteChange(From[sharp][i]);
        Board[Pixel.x + Pixel.y * BW] = exist;
        FillStr(Pixel.x, Pixel.y, fill);
    }
}
// 显示图形
void ShowGraph() { ShowOrClear(true, "■"); }
// 清除图形
void ClearGraph() { ShowOrClear(false, "  "); }
// 添加图形
void AddGraph()
{
    sharp = rand() % 19;
    x = HomeX;
    y = HomeY;
    ShowGraph();
}
// 整行消除
void LineRemove() {}
// 碰撞检测:图形下降
bool Drop_CD() { return false; }
// 图形下降
void Drop()
{
    if (!Drop_CD())
    {
        ClearGraph();
        ++y;
        ShowGraph();
    }
    else
    {
        LineRemove();
        AddGraph();
    }
}
// 碰撞检测:图形左移
bool Left_CD() { return false; }
// 图形左移
void Left()
{
    if (!Left_CD())
    {
        ClearGraph();
        --x;
        ShowGraph();
    }
}
// 碰撞检测:图形右移
bool Right_CD() { return false; }
// 图形右移
void Right()
{
    if (!Right_CD())
    {
        ClearGraph();
        ++x;
        ShowGraph();
    }
}
// 碰撞检测:图形旋转
bool Ratote_CD(int NextSharp) { return false; }
// 图形旋转
void Ratote()
{
    int NextSharp = sharp;
    if (sharp == 6 || sharp == 10 || sharp == 14)
        NextSharp -= 3;
    else if (sharp == 1 || sharp == 16 || sharp == 18)
        --NextSharp;
    else
        ++NextSharp;
    if (!Ratote_CD(NextSharp))
    {
        ClearGraph();
        sharp = NextSharp;
        ShowGraph();
    }
}
// 初始化游戏
void Initialize()
{
    system("cls");
    AddGraph();
    for (bool b : Board)
        b = false;
}
// 运行游戏
void run()
{
    Initialize();

    int t = 0;
    while (true)
    {
        Sleep(1);
        if (t++ > 50)
        {
            Drop();
            t = 0;
        }
        if (kbhit())
        {
            int ch = getch();
            if (ch == 224)
            {
                switch (getch())
                {
                case 72:      // 按下键盘上键
                    Ratote(); // 旋转图形
                    break;
                case 80:    // 按下键盘下键
                    Drop(); // 下降图形
                    break;
                case 75:    // 按下键盘左键
                    Left(); // 左移图形
                    break;
                case 77:     // 按下键盘右键
                    Right(); // 右移图形
                    break;
                }
            }
            else if (ch == 32)
                Pause();
        }
    }
}

int main()
{
    SetConsole("俄罗斯方块", 40, 27, "80");
    srand((int)time(0));
    run();
}

        左移、右移、下降、旋转,4个基本功能就完成了,且达到了预期的效果。

4. 碰撞检测

        最重要也是最难实现的模块就是碰撞检测了。

1. 碰撞检测:移动

        下降、左移、右移的碰撞检测代码差不多。我们以下降为例。

        不管哪种碰撞检测,我们都需要判断图形中每一个小方块下一个位置是否有其他小方块(不能是图形本身的),另外还需要判断小方块是否会越界。在第一类碰撞检测的判断中,对于下降来说我们可以只判断该图形最下面的点下降后是否会碰撞到障碍物,因此大致上是两个步骤,先判断该小方块是否是最下面的点,再判断该点下一位置是否有小方块。不难发现对于是否越界的判断我们可以给出更加通用的判断条件,但是在整个程序中我们只有4处越界判断,所以我们可以只写每种情况对应的越界判断条件,以节省多余判断所带来的额外开销,复用性降低地同时提高了性能。

// 碰撞检测:图形下降
bool Drop_CD()
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点是否为该行下边的像素点
        Point Pixel0 = SiteChange(From[sharp][i]);

        // 是否越界
        if (Pixel0.y > BH - 2)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.x == Pixel0.x && Pixel1.y > Pixel0.y)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x + (Pixel0.y + 1) * BW])
            return true;
    }
    return false;
}

2. 碰撞检测:旋转

        旋转碰撞检测其实和移动的碰撞检测也差不多,唯一的差别是这里的Pixel0是旋转之后的小方块,而之前的Pixel0是移动前的小方块。相当于这里是先操作再看行不行,移动那里是先看行不行再操作,本质上是一样的。

// 碰撞检测:图形旋转
bool Ratote_CD(int NextSharp)
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点下一位置是否无其他像素点
        Point Pixel0 = SiteChange(From[NextSharp][i]);

        // 是否越界
        if (Pixel0.x < 0 || BW - 1 < Pixel0.x || Pixel0.y > BH - 1)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.x == Pixel0.x && Pixel1.y == Pixel0.y)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x + Pixel0.y * BW])
            return true;
    }
    return false;
}

5. 消除整行

        从最下面那一行开始扫描,如果发现满行就消除该行并重新扫描该行,否则就扫描上一行,直到扫描完所有行且没发现满行时退出函数。有一个细节是:因为我们要将消除的那一行上面的所有行下移,这里我们从右向左、从下到上依次移动就好;如果是从左上到右下遍历,则会覆盖还未操作的小方块,我之前为了解决这个问题,将所有符合条件的移动后的坐标缓存下来之后再取出,那样的话需要遍历2次。另外需要注意的是,消除该行后一定要退回到该行重新扫描。否则,该函数1次就只能消除1行。

// 整行消除
void LineRemove()
{
    int JudgeY = BH - 1; // 当前判断的行数
    while (JudgeY >= 0)
    {
        int cnt = 0;
        for (int i = 0; i < BW; ++i)
            if (Board[BW * JudgeY + i])
                ++cnt;
        if (cnt != BW) // 未满行, 判断上一行
            --JudgeY;
        else // 满行消除
        {
            // 移除JudgeY这一行
            for (int i = 0; i < BW; ++i)
            {
                Board[i + JudgeY * BW] = false;
                FillStr(i, JudgeY, "  ");
            }
            // 将JudgeY这行上方的所有方块下移一行
            for (int i = BW * JudgeY - 1; i >= 0; --i)
                if (Board[i])
                {
                    Board[i] = false;
                    FillStr(i % BW, i / BW, "  ");
                    Board[i + BW] = true;
                    FillStr(i % BW, i / BW + 1, "■");
                }
        }
    }
}

6. 游戏结束

        判断游戏结束的代码也十分巧妙。仅仅是我们在之前的函数里添加了一个if()语句。

// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
    for (int i = 0; i < 4; ++i)
    {
        Point Pixel = SiteChange(From[sharp][i]);
        if (Board[Pixel.x + Pixel.y * BW] == exist) // 该处已存在图形, 游戏结束
        {
            SetPos(BW - 3 >> 1, BH >> 1);
            std::cout << "Game Over!";
            Pause();
            run();
        }
        Board[Pixel.x + Pixel.y * BW] = exist;
        FillStr(Pixel.x, Pixel.y, fill);
    }
}

       不难理解,这里就是判断将要显示的位置是否已经存在图形,如果过存在则游戏结束。为什么这样可行呢,回想一下之前写的显示和清除语句,它们都包含在4个基本操作的函数里面了,也就是说我们已经进行了严格的碰撞检测,出现使上述if()成立的情况只有一种,那就是旧图形触底后发现没有添加新图形的空间,有就是俄罗斯方块中的游戏结束。

7. 完整代码

#include "../Engine/BrickEngine.h"

const int BW(20);             // BW表示场景宽度
const int BH(27);             // BH表示场景高度
const int HomeX(BW - 1 >> 1); // HomeX表示图形初始横坐标
const int HomeY(1);           // HomeY表示图形初始纵坐标
bool Board[BW * BH];          // Board[x + y * BW]表示(x, y)处是否为小方块
int sharp;                    // 图形的形状(形状随机产生)
int x, y;                     // 图形的横纵坐标

// 声明这19种图形
// 图形4个像素点的声明顺序无关紧要
// 但每种图形必须按旋转变换的顺序依次声明
Point From[19][4] = {
    // 条形
    {{0, 0}, {1, 0}, {2, 0}, {3, 0}},
    {{0, 0}, {0, 1}, {0, 2}, {0, 3}},
    // 方形
    {{0, 0}, {1, 0}, {0, 1}, {1, 1}},
    // L形
    {{0, 0}, {0, 1}, {0, 2}, {1, 2}},
    {{2, 0}, {1, 0}, {0, 0}, {0, 1}},
    {{2, 2}, {2, 1}, {2, 0}, {1, 0}},
    {{0, 2}, {1, 2}, {2, 2}, {2, 1}},
    // 镜像L形
    {{2, 0}, {2, 1}, {2, 2}, {1, 2}},
    {{2, 2}, {1, 2}, {0, 2}, {0, 1}},
    {{0, 2}, {0, 1}, {0, 0}, {1, 0}},
    {{0, 0}, {1, 0}, {2, 0}, {2, 1}},
    // T型
    {{1, 1}, {0, 2}, {1, 2}, {2, 2}},
    {{1, 1}, {0, 0}, {0, 1}, {0, 2}},
    {{1, 1}, {2, 0}, {1, 0}, {0, 0}},
    {{1, 1}, {2, 0}, {2, 1}, {2, 2}},
    // 闪电形
    {{0, 0}, {0, 1}, {1, 1}, {1, 2}},
    {{2, 0}, {1, 0}, {1, 1}, {0, 1}},
    // 镜像闪电形
    {{0, 0}, {1, 0}, {1, 1}, {2, 1}},
    {{1, 0}, {1, 1}, {0, 1}, {0, 2}}};

void run();
// 相对坐标转绝对坐标
Point SiteChange(Point p)
{
    return {x + p.x, y + p.y};
}
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
    for (int i = 0; i < 4; ++i)
    {
        Point Pixel = SiteChange(From[sharp][i]);
        if (Board[Pixel.x + Pixel.y * BW] == exist) // 该处已存在图形, 游戏结束
        {
            SetPos(BW - 3 >> 1, BH >> 1);
            std::cout << "Game Over!";
            Pause();
            run();
        }
        Board[Pixel.x + Pixel.y * BW] = exist;
        FillStr(Pixel.x, Pixel.y, fill);
    }
}
// 显示图形
void ShowGraph() { ShowOrClear(true, "■"); }
// 清除图形
void ClearGraph() { ShowOrClear(false, "  "); }
// 添加图形
void AddGraph()
{
    sharp = rand() % 19;
    x = HomeX;
    y = HomeY;
    ShowGraph();
}
// 整行消除
void LineRemove()
{
    int JudgeY = BH - 1; // 当前判断的行数
    while (JudgeY >= 0)
    {
        int cnt = 0;
        for (int i = 0; i < BW; ++i)
            if (Board[BW * JudgeY + i])
                ++cnt;
        if (cnt != BW) // 未满行, 判断上一行
            --JudgeY;
        else // 满行消除
        {
            // 移除JudgeY这一行
            for (int i = 0; i < BW; ++i)
            {
                Board[i + JudgeY * BW] = false;
                FillStr(i, JudgeY, "  ");
            }
            // 将JudgeY这行上方的所有方块下移一行
            for (int i = BW * JudgeY - 1; i >= 0; --i)
                if (Board[i])
                {
                    Board[i] = false;
                    FillStr(i % BW, i / BW, "  ");
                    Board[i + BW] = true;
                    FillStr(i % BW, i / BW + 1, "■");
                }
        }
    }
}
// 碰撞检测:图形下降
bool Drop_CD()
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点是否为该行下边的像素点
        Point Pixel0 = SiteChange(From[sharp][i]);

        // 是否越界
        if (Pixel0.y > BH - 2)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.x == Pixel0.x && Pixel1.y > Pixel0.y)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x + (Pixel0.y + 1) * BW])
            return true;
    }
    return false;
}
// 图形下降
void Drop()
{
    if (!Drop_CD())
    {
        ClearGraph();
        ++y;
        ShowGraph();
    }
    else
    {
        LineRemove();
        AddGraph();
    }
}
// 碰撞检测:图形左移
bool Left_CD()
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点是否为该行最左边的像素点
        Point Pixel0 = SiteChange(From[sharp][i]);

        // 是否越界
        if (Pixel0.x < 1)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.y == Pixel0.y && Pixel1.x < Pixel0.x)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x - 1 + Pixel0.y * BW])
            return true;
    }
    return false;
}
// 图形左移
void Left()
{
    if (!Left_CD())
    {
        ClearGraph();
        --x;
        ShowGraph();
    }
}
// 碰撞检测:图形右移
bool Right_CD()
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点是否为该行最右边的像素点
        Point Pixel0 = SiteChange(From[sharp][i]);

        // 是否越界
        if (Pixel0.x > BW - 2)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.y == Pixel0.y && Pixel1.x > Pixel0.x)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x + 1 + Pixel0.y * BW])
            return true;
    }
    return false;
}
// 图形右移
void Right()
{
    if (!Right_CD())
    {
        ClearGraph();
        ++x;
        ShowGraph();
    }
}
// 碰撞检测:图形旋转
bool Ratote_CD(int NextSharp)
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点下一位置是否无其他像素点
        Point Pixel0 = SiteChange(From[NextSharp][i]);

        // 是否越界
        if (Pixel0.x < 0 || BW - 1 < Pixel0.x || Pixel0.y > BH - 1)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.x == Pixel0.x && Pixel1.y == Pixel0.y)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x + Pixel0.y * BW])
            return true;
    }
    return false;
}
// 图形旋转
void Ratote()
{
    int NextSharp = sharp;
    if (sharp == 6 || sharp == 10 || sharp == 14)
        NextSharp -= 3;
    else if (sharp == 1 || sharp == 16 || sharp == 18)
        --NextSharp;
    else
        ++NextSharp;
    if (!Ratote_CD(NextSharp))
    {
        ClearGraph();
        sharp = NextSharp;
        ShowGraph();
    }
}
// 初始化游戏
void Initialize()
{
    system("cls");
    AddGraph();
    for (bool b : Board)
        b = false;
}
// 运行游戏
void run()
{
    Initialize();

    int t = 0;
    while (true)
    {
        Sleep(1);
        if (t++ > 50)
        {
            Drop();
            t = 0;
        }
        if (kbhit())
        {
            int ch = getch();
            if (ch == 224)
            {
                switch (getch())
                {
                case 72:      // 按下键盘上键
                    Ratote(); // 旋转图形
                    break;
                case 80:    // 按下键盘下键
                    Drop(); // 下降图形
                    break;
                case 75:    // 按下键盘左键
                    Left(); // 左移图形
                    break;
                case 77:     // 按下键盘右键
                    Right(); // 右移图形
                    break;
                }
            }
            else if (ch == 32)
                Pause();
        }
    }
}

int main()
{
    SetConsole("俄罗斯方块", 40, 27, "80");
    srand((int)time(0));
    run();
}

二. 俄罗斯方块简易版的升级

1. 新增属性

        ① 游戏得分score

        ② 图形下降速度speed

        ③ 场景左上角的横纵坐标BX、BY

const int BX(1);              // BX表示场景的横坐标
const int BY(0);              // BY表示场景的纵坐标
int score;                    // 游戏得分
int speed;                    // 图形下落的速度

2. 更改初始化函数

        给场景加上边框;在场景右边显示速度和分数。

// 初始化场景
FillRec(BX - 1, BY, 1, BH, "│");
FillRec(BX + BW, BY, 1, BH, "┃");
FillRec(BX, BY + BH, BW * 2 - 1, 1, "━");
FillStr(BX - 1, BY + BH, "╰");
FillStr(BX + BW, BY + BH, "╯");

FillStr(BW + 2, 3, " SCORE");
std::cout << std::setw(4);
FillStr(BW + 3, 4, std::to_string(score));
FillStr(BW + 2, 7, " SPEED");
std::cout << std::setw(2);
FillStr(BW + 4, 8, std::to_string(speed));

3. 更改消除整行函数

        更改填充函数的参数,使场景移动到指定的位置;添加更新玩家得分操作。

// 移除JudgeY这一行
for (int i = 0; i < BW; ++i)
{
    Board[i + JudgeY * BW] = false;
    FillStr(i + BX, JudgeY + BY, "  ");
}
// 将JudgeY这行上方的所有方块下移一行
for (int i = BW * JudgeY - 1; i >= 0; --i)
    if (Board[i])
    {
        Board[i] = false;
        FillStr(i % BW + BX, i / BW + BY, "  ");
        Board[i + BW] = true;
        FillStr(i % BW + BX, i / BW + 1 + BY, "■");
    }
++score;
std::cout << std::setw(4);
FillStr(BW + 3, 4, std::to_string(score));
std::cout << std::setw(2);
FillStr(BW + 4, 8, std::to_string(speed));

4. 更改显示、清除图形辅助函数

        更改填充函数的参数,使场景移动到指定的位置。

FillStr(Pixel.x + BX, Pixel.y + BY, fill);

5. 更改控制速度的语句

        稍作修改使得score决定speed,speed影响方块下降的速度。

speed = score / 5 + 1;
if (t++ > (30 - speed))
{
    Drop();
    t = 0;
}

6. 完整代码

#include "../Engine/BrickEngine.h"

const int BW(14);             // BW表示场景宽度
const int BH(24);             // BH表示场景高度
const int BX(1);              // BX表示场景的横坐标
const int BY(0);              // BY表示场景的纵坐标
const int HomeX(BW - 1 >> 1); // HomeX表示图形初始横坐标
const int HomeY(1);           // HomeY表示图形初始纵坐标
bool Board[BW * BH];          // Board[x + y * BW]表示(x, y)处是否为小方块
int sharp;                    // 图形的形状(形状随机产生)
int x, y;                     // 图形的横纵坐标
int score;                    // 游戏得分
int speed;                    // 图形下落的速度

// 声明这19种图形
// 图形4个像素点的声明顺序无关紧要
// 但每种图形必须按旋转变换的顺序依次声明
Point From[19][4] = {
    // 条形
    {{0, 0}, {1, 0}, {2, 0}, {3, 0}},
    {{0, 0}, {0, 1}, {0, 2}, {0, 3}},
    // 方形
    {{0, 0}, {1, 0}, {0, 1}, {1, 1}},
    // L形
    {{0, 0}, {0, 1}, {0, 2}, {1, 2}},
    {{2, 0}, {1, 0}, {0, 0}, {0, 1}},
    {{2, 2}, {2, 1}, {2, 0}, {1, 0}},
    {{0, 2}, {1, 2}, {2, 2}, {2, 1}},
    // 镜像L形
    {{2, 0}, {2, 1}, {2, 2}, {1, 2}},
    {{2, 2}, {1, 2}, {0, 2}, {0, 1}},
    {{0, 2}, {0, 1}, {0, 0}, {1, 0}},
    {{0, 0}, {1, 0}, {2, 0}, {2, 1}},
    // T型
    {{1, 1}, {0, 2}, {1, 2}, {2, 2}},
    {{1, 1}, {0, 0}, {0, 1}, {0, 2}},
    {{1, 1}, {2, 0}, {1, 0}, {0, 0}},
    {{1, 1}, {2, 0}, {2, 1}, {2, 2}},
    // 闪电形
    {{0, 0}, {0, 1}, {1, 1}, {1, 2}},
    {{2, 0}, {1, 0}, {1, 1}, {0, 1}},
    // 镜像闪电形
    {{0, 0}, {1, 0}, {1, 1}, {2, 1}},
    {{1, 0}, {1, 1}, {0, 1}, {0, 2}}};

void run();
// 相对坐标转绝对坐标
Point SiteChange(Point p)
{
    return {x + p.x, y + p.y};
}
// 显示、清除图形的辅助函数
void ShowOrClear(bool exist, const std::string &fill)
{
    for (int i = 0; i < 4; ++i)
    {
        Point Pixel = SiteChange(From[sharp][i]);
        if (Board[Pixel.x + Pixel.y * BW] == exist) // 该处已存在图形, 游戏结束
        {
            SetPos(7, BH + 1);
            std::cout << "Game Over!";
            Pause();
            run();
        }
        Board[Pixel.x + Pixel.y * BW] = exist;
        FillStr(Pixel.x + BX, Pixel.y + BY, fill);
    }
}
// 显示图形
void ShowGraph() { ShowOrClear(true, "■"); }
// 清除图形
void ClearGraph() { ShowOrClear(false, "  "); }
// 添加图形
void AddGraph()
{
    sharp = rand() % 19;
    x = HomeX;
    y = HomeY;
    ShowGraph();
}
// 整行消除
void LineRemove()
{
    int JudgeY = BH - 1; // 当前判断的行数
    while (JudgeY >= 0)
    {
        int cnt = 0;
        for (int i = 0; i < BW; ++i)
            if (Board[BW * JudgeY + i])
                ++cnt;
        if (cnt != BW) // 未满行, 判断上一行
            --JudgeY;
        else // 满行消除
        {
            // 移除JudgeY这一行
            for (int i = 0; i < BW; ++i)
            {
                Board[i + JudgeY * BW] = false;
                FillStr(i + BX, JudgeY + BY, "  ");
            }
            // 将JudgeY这行上方的所有方块下移一行
            for (int i = BW * JudgeY - 1; i >= 0; --i)
                if (Board[i])
                {
                    Board[i] = false;
                    FillStr(i % BW + BX, i / BW + BY, "  ");
                    Board[i + BW] = true;
                    FillStr(i % BW + BX, i / BW + 1 + BY, "■");
                }
            ++score;
            std::cout << std::setw(4);
            FillStr(BW + 3, 4, std::to_string(score));
            std::cout << std::setw(2);
            FillStr(BW + 4, 8, std::to_string(speed));
        }
    }
}
// 碰撞检测:图形下降
bool Drop_CD()
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点是否为该行下边的像素点
        Point Pixel0 = SiteChange(From[sharp][i]);

        // 是否越界
        if (Pixel0.y > BH - 2)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.x == Pixel0.x && Pixel1.y > Pixel0.y)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x + (Pixel0.y + 1) * BW])
            return true;
    }
    return false;
}
// 图形下降
void Drop()
{
    if (!Drop_CD())
    {
        ClearGraph();
        ++y;
        ShowGraph();
    }
    else
    {
        LineRemove();
        AddGraph();
    }
}
// 碰撞检测:图形左移
bool Left_CD()
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点是否为该行最左边的像素点
        Point Pixel0 = SiteChange(From[sharp][i]);

        // 是否越界
        if (Pixel0.x < 1)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.y == Pixel0.y && Pixel1.x < Pixel0.x)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x - 1 + Pixel0.y * BW])
            return true;
    }
    return false;
}
// 图形左移
void Left()
{
    if (!Left_CD())
    {
        ClearGraph();
        --x;
        ShowGraph();
    }
}
// 碰撞检测:图形右移
bool Right_CD()
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点是否为该行最右边的像素点
        Point Pixel0 = SiteChange(From[sharp][i]);

        // 是否越界
        if (Pixel0.x > BW - 2)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.y == Pixel0.y && Pixel1.x > Pixel0.x)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x + 1 + Pixel0.y * BW])
            return true;
    }
    return false;
}
// 图形右移
void Right()
{
    if (!Right_CD())
    {
        ClearGraph();
        ++x;
        ShowGraph();
    }
}
// 碰撞检测:图形旋转
bool Ratote_CD(int NextSharp)
{
    for (int i = 0; i < 4; ++i)
    {
        bool Point_CD = true; // 检测该点下一位置是否无其他像素点
        Point Pixel0 = SiteChange(From[NextSharp][i]);

        // 是否越界
        if (Pixel0.x < 0 || BW - 1 < Pixel0.x || Pixel0.y > BH - 1)
            return true;
        // 是否碰到障碍物
        for (int j = 0; j < 4; ++j)
        {
            Point Pixel1 = SiteChange(From[sharp][j]);
            if (Pixel1.x == Pixel0.x && Pixel1.y == Pixel0.y)
            {
                Point_CD = false;
                break;
            }
        }
        if (Point_CD && Board[Pixel0.x + Pixel0.y * BW])
            return true;
    }
    return false;
}
// 图形旋转
void Ratote()
{
    int NextSharp = sharp;
    if (sharp == 6 || sharp == 10 || sharp == 14)
        NextSharp -= 3;
    else if (sharp == 1 || sharp == 16 || sharp == 18)
        --NextSharp;
    else
        ++NextSharp;
    if (!Ratote_CD(NextSharp))
    {
        ClearGraph();
        sharp = NextSharp;
        ShowGraph();
    }
}
// 初始化游戏
void Initialize()
{
    system("cls");
    AddGraph();
    score = 0;
    speed = 1;
    for (bool b : Board)
        b = false;
    // 初始化场景
    FillRec(BX - 1, BY, 1, BH, "│");
    FillRec(BX + BW, BY, 1, BH, "┃");
    FillRec(BX, BY + BH, BW * 2 - 1, 1, "━");
    FillStr(BX - 1, BY + BH, "╰");
    FillStr(BX + BW, BY + BH, "╯");

    FillStr(BW + 2, 3, " SCORE");
    std::cout << std::setw(4);
    FillStr(BW + 3, 4, std::to_string(score));
    FillStr(BW + 2, 7, " SPEED");
    std::cout << std::setw(2);
    FillStr(BW + 4, 8, std::to_string(speed));
}
// 运行游戏
void run()
{
    Initialize();

    int t = 0;
    while (true)
    {
        // 时间复杂度O(BW * BH * BH)
        Sleep(1);
        speed = score / 5 + 1;
        if (t++ > (30 - speed))
        {
            Drop();
            t = 0;
        }
        if (kbhit())
        {
            int ch = getch();
            if (ch == 224)
            {
                switch (getch())
                {
                case 72:      // 按下键盘上键
                    Ratote(); // 旋转图形
                    break;
                case 80:    // 按下键盘下键
                    Drop(); // 下降图形
                    break;
                case 75:    // 按下键盘左键
                    Left(); // 左移图形
                    break;
                case 77:     // 按下键盘右键
                    Right(); // 右移图形
                    break;
                }
            }
            else if (ch == 32)
                Pause();
        }
    }
}

int main()
{
    SetConsole("俄罗斯方块", 40, 27, "80");
    srand((int)time(0));
    run();
}

7. 运行效果

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

俄罗斯方块(C++) 的相关文章

随机推荐

  • 禅道数据库异机访问,远程连接,navicat连接

    禅道数据库异机访问 远程连接 navicat连接 在使用xxj job做执行器时遇到的问题 数据库端口可以在这里修改 可以在这里修改 opt zbox app zentao config my php 禅道数据库官方文档 该链接也有修改数据
  • TP5+Fastadmin+Log日志

    Log 日志 1 引入类 use think Log 2 记录日志 通过Log类的record 方法 记录一条日志 Log record 这是一条普通日志 记录一个错误级别的日志 使用error 方法 Log error 这是一条错误日志
  • 2022届实习+秋招大厂面试经验(后端开发+java)

    个人情况 待补充 求职方向 后端开发 大致情况 20年底在字节实习到21年6月底 转正 21年7月初开始在阿里实习到8月中 转正 随后面试了百度 腾讯和美团 均拿到offer 中间有挂过 被其他部门又捞了 最终选择阿里 我是22年秋季毕业
  • C++函数中返回智能指针的裸指针问题

    注意 如果不是CSDN网站显示本篇文章 请于底部点击 阅读原文 来阅读本篇文章 C 函数中返回智能指针的裸指针问题 要点 原始代码 解决办法 作为一项案例分析下 C 函数中返回智能指针的裸指针问题 要点 不要在函数中返回智能指针的裸指针出去
  • Flutter之MaterialApp使用详解

    如需转载 请尊重作者 注明出处 谢谢配合 22个参数 字段 类型 navigatorKey 导航键 GlobalKey home 主页 Widget routes 路由 Map
  • 非常简单的无闪刷新验证码原理

    非常简单的无闪刷新验证码原理 只有这一段事件触发语句全搞定 nclick this src GifCode asp newDate getTime GifCode asp验证码的图片 事件 nclick this src GifCode a
  • jsp中request经常是的获取参数的方法总结

    我的个人博客开通了 地址 https timewentby com 欢迎访问 下面将获取参数的方法都列出来说一说 request getParameter String name 获取指定name的值 若name有多个相同值则获取第一个va
  • Devoloper warning for package "com..." Failed to post notification on channel "null" see log for det

    使用通知时屏幕显示 这是因为我们使用的是Android O Android 8 0 之前的通知写法 而Android O之后引入了通知通道 Notification channel 重新定义通知内容中的应用程序类别 可以让开发者给予用户更精
  • Kubernetes CKA考试和真题(下)

    前言 之前分享了CKA考试的1 10题 这篇文章继续分享CKA实操考试的最后7到题目 忘了告诉大家 考试的时候可以查看官网资料 监考官是允许这个行为的 但是你不能打开除了k8s官网的其它页面 第11题 启动多容器pod 任务 创建一个名为k
  • 计算巢实现大模型微调——如何发布一个微调服务

    前言 各位看官 最近是否被大模型相关信息刷屏了呢 铺天盖地的大模型预训练 微调等各种关键词是否让你眼花缭乱呢 在如此热度之下 你有没有想过自己亲自动手部署一个大模型执行训练或者微调呢 或许你曾经尝试过却被某个繁琐的步骤劝退了呢 又或者你是模
  • 关于java.lang.NullPointerException: Cannot invoke “org.springframework.data.redis.core.

    java lang NullPointerException Cannot invoke org springframework data redis core RedisTemplate opsForValue because this
  • 机器学习--特征缩放/均值归一化

    特征缩放 feature scaling 其中 特征缩放 feature scaling 大致的思路是这样的 梯度下降算法中 在有多个特征的情况下 如果你能确保这些不同的特征都处在一个相近的范围 这样梯度下降法就能更快地收敛 举个例子来说明
  • Linux中查看所有文件夹及包括文件大小之和

    如果想查看Linux中哪个文件夹最大 可以通过du sh 命令查询哪个文件夹最大 然后进入该文件夹继续执行该命令 直到找出最大的为止 1 Linux中查看所有文件夹和文件大小 这条命令将会计算该文件夹下所有文件大小总和 du sh 执行效果
  • 1、Reading Rasa Source Code —— main

    目录 main main 从 pypi 官方网站上下载 rasa 的最后一个释放版本 截止目前 最新版本为 2 2 1 的源码 解压后 在根目录下 找到 setup py 文件 这是安装引导程序 我们主要关注的是 entry points
  • TypeScript算法题实战——二叉搜索树篇

    二叉搜索树 也叫二叉查找树 二叉排序树 是具有下列性质的二叉树 若它的左子树不空 则左子树上所有结点的值均小于它的根结点的值 若它的右子树不空 则右子树上所有结点的值均大于它的根结点的值 注意 二叉搜索树中序遍历的结果是有序的 本系列博文将
  • 【Python】NMF非负矩阵分解算法(测试代码)

    目录 算法说明 百度百科 基本例程 总结 欢迎关注 Python 系列 持续更新中 欢迎关注 Python 系列 持续更新中 算法说明 百度百科 从多元统计的观点看 NMF是在非负性的限制下 在尽可能保持信息不变的情况下 将高维的随机模式简
  • 视频号5种提高曝光量的技巧

    在新开一个视频号 我们如何来尽可能吸引更多粉丝关注我们呢 在初期如何进行运营呢 我们先来看看现在视频号已知的一个机制 视频号虽然会依托位置 标签 话题等多维度信息进行智能分发 但目前最主 要传播还是靠基于微信生态形成的社交链 即当你的视频被
  • Hive和Hbase的对接

    引言 我们都知道hive数据存储在hdfs上 元数据可以存储在mysql中 计算框架采用mapreduce hive实际上只做分析工具 那么hive的数据是不是也可以存储在hbase呢 文章目录 一 配置hive 二 hive中数据与hba
  • C语言——创建文件

    创建文件 include
  • 俄罗斯方块(C++)

    目录 一 俄罗斯方块简易版的实现 1 图形的存储 2 图形的显示 3 图形的操作 4 碰撞检测 1 碰撞检测 移动 2 碰撞检测 旋转 5 消除整行 6 游戏结束 7 完整代码 二 俄罗斯方块简易版的升级 1 新增属性 2 更改初始化函数