一、项目介绍

这是一个可以单人进行的俄罗斯方块小游戏。

按左右键移动方块,按上键可以旋转方块,按下键可以加速方块的下落(需要控制好按下的时长,否则下一个方块也会加速落下)。方块碰到屏幕底部,或者碰到已经堆积的方块就会停下,此时上方会落下下一个方块。右侧会对下一个方块的种类进行提示。

 编译环境:visual c++ 6.0

第三方库:Easyx2022  注意需要提前安装easyX,如没有基础可以先了解easyX图形编程


二、运行截图

俄罗斯方块1


俄罗斯方块2


三、源码解析

游戏逻辑:

1.     生成界面,初始化程序

2.     游戏开始循环。每次循环检测输入按键做出反应,之后睡眠50毫秒,再开始下一次循环。

3.     按住上键调用旋转的函数。

4.     按住左右键,改变方块的横坐标,调用对应函数。

5.     Esc 键退出程序。

6.     在循环当中,利用计时器判断是否过了500毫秒,如果过了则下落一格。下落一格则检测底部是否碰撞,碰撞则重新生成方块,改变对下一个方块的提示,清除连成一行的方块并得分,检测游戏是否结束。

7.     检测是否按住下键,按住则加速下落。

8.     失败后循环结束,退出游戏。

 

根据这些我们需要完成的功能,我们定义两个类。其一为游戏类,内置地图,分数,时间等整局游戏的变量,以及设置地图,判读满行,清除行等操作函数。

另一个类是方块类,设置方块的坐标,类型,旋转方向,颜色等特征,以及对应的初始化,添加,移动,旋转等操作函数。


接下来是相关步骤的详细解析。

  1. 界面生成以及初始化

SetWindowText(initgraph(350, 440), "俄罗斯方块dotcpp.com");
 
         // 设置绘图颜色
         setbkcolor(WHITE);
         cleardevice();
         setlinecolor(BLACK);
 
         // 生成游戏界面和数据
         srand(time(NULL));
         Block::generateBlockData();
 
         Game game;
         game.drawMap();
         game.drawPrompt();
 
         Block b(game);
         Block nextBlock(game, 11, 2); // 下一方块
         clock_t start = 0;                                  // 时钟开始时间
         clock_t end;                                          // 时钟结束时间
 
         ExMessage msg;
 
         nextBlock.draw();

这里用到了一些Easyx当中的函数。如无注明,后文中非本程序定义的函数也都位于Easyx当中。
setbkcolor()用于设置当前设备绘图背景色。
Cleardevice()使用当前背景色清空绘图设备.
Setlinecolor()用于设置当前设备画线颜色。
ExMessage结构体用于保存鼠标信息。
在此处我们还定义了几个相关的函数。
 
Block::generateBlockData()用于设定不同种类方块的信息。blockData是一个三维数组,用于保存所有方块的数据,第一位是方块种类,二三位则是横纵坐标,调节这些数据与游戏内容一致。

roundrect用于画无填充的圆角矩形。

rectangle用于画无填充的矩形。

setfillcolor用于设置当前设备填充颜色。

fillrectangle用于画有边框的填充矩形。

game.drawPrompt()用于绘制提示界面,主要是各种提示。

其中最主要用到的outtextxy是在x,y坐标处输出文字。

其余的还有settextstyle设置当前文字样式。

gettextstyle获取当前文字样式。

settextcolor设置当前文字颜色。

Block::draw()用来绘制方块。

还是用到了setfillcolor和fillrectangle,根据方块数据对应的xy坐标,用双层循环的方式输出结果。注意在Y坐标为负时不绘制。

 

2. 游戏循环

while (true)
         {
                   b.clear();
                   clearrectangle(20, 20, 220, 420);
                   game.drawMap();
        ……
           b.draw();
              game.clearLine();
              FlushBatchDraw();      // 刷新缓冲区
              Sleep(50);                   // 每 50 毫秒接收一次按键
       }

b是Block类的一个对象。

clearrectangle用于清空矩形区域。

FlushBatchDraw用于执行未完成的绘制任务。

其中也用到了Block类中定义的两个函数Block::draw和Block::clear。前者已经在前文中提到过,而后者则是把绘制函数换成了clearrectangle,以此来清除绘制的方块。通过调用clear再调用draw,可以及时地显示出方块的变化。

在之后还调用了Game::clearLine()这个函数。其目的是判断哪一行已经满了,并将其清除以及增加得分。

void Game::clearLine()
{
       int line = -1;
       // 判断哪一行满行
       for (int j = 0; j < MAP_HEIGHT; j++)
       {
              if (checkLine(j))
              {
                     line = j;
                     break;
              }
       }
 
       if (line != -1)
       {
              // 将上一行移至满行
              for (int j = line; j > 0; j--)
              {
                     for (int i = 0; i < MAP_WIDTH; i++)
                     {
                            map[i][j] = map[i][j - 1];
                     }
              }
              score += 10; // 将游戏分数加 10
       }
       drawPrompt();
}

首先用循环判定哪一行是满的,其中用到了Game::checkLine(),其内部也是一个循环,依次检测某一y坐标对应的所有x坐标是否都为1。如果检测到某行是满的,跳出循环,继续函数的后续部分。不用担心没有判断所有行是否填满,因为在50毫秒之后,该函数会再次被调用。

然后,将被消除那一行上方的所有行都向下移动一行。循环从j行开始,以y坐标递减的方式执行,让map变量(所有方块的位置)对应坐标位置的数据等于其y坐标减一的数据。最后如果消除,则将游戏分数加10。调用绘制提示界面的函数drawPrompt(),将得分显示在旁边。

 

3.    按下上键

这里先看我们如何获取键盘信息。

while (peekmessage(&msg, EM_KEY) && msg.message == WM_KEYDOWN)

              {

                     switch (msg.vkcode)

                     {

peekmessage用于获取一个消息,并立即返回。其中参数&msg表示用指针的形式保存获取到的信息,而 EM_KEY意味这是键盘信息。这个函数获取消息的返回值为True,如果没有获取到,则返回False。

右边的msg为ExMessage结构体的对象,msg.message == WM_KEYDOWN表明获取到的信息为键盘按下。上面的代码意味着,如果你向程序发出了指令,并且该指令是键盘按下时,循环才会执行。

msg.vkcode代表按键的虚拟键码。很显然,

case 'W':

                     case VK_UP:

                            b.rotate();

                            break;

意味着如果你按下上键或者W键,才会执行b.rotate()。

b.rotate()用于改变b(Block)类型的数据。执行会改变其中block[4][4]这个二维数组,这个数组用1,0来表示在对应位置是否存在方块。对每种情况分类讨论,得出旋转之后的结果。

 

4.按下左右键,方块左右移动

                            // 左键移动

                     case 'A':

                     case VK_LEFT:

                            b.move(1);

                            break;

 

                            // 右键移动

                     case 'D':

                     case VK_RIGHT:

                            b.move(2);

                            break;

在Block类中,我们定义了move函数。其中只有一个参数,0 表示下移一格,1 表示左移一格,2 表示右移一格,当下移检测到碰撞时返回 true。

switch (direction)

       {

       case 0:

              y++;

              if (checkCollision())

              {

                     y--;

                     return true;

              }

              break;

其逻辑很简单,就是根据输入的不同情况,改变x或者y的坐标(x,y是整个大方块的坐标)。之后进行碰撞检测,如果碰撞,取消移动。上面是其中下方碰撞情况,注意如果是向左或向右移动,则需要返回False,因为返回True时,方块便落到地图上了。

Block::checkCollision()用于碰撞检测。
bool Block::checkCollision() const
{
       for (int i = 0; i < 4; i++)
       {
              for (int j = 0; j < 4; j++)
              {
                     // 判断方块是否与地图发生碰撞,顶部不判断
                     if ((game.getMap(x + i, y + j) || 20 + BLOCK_WIDTH * (x + i) < 20 || 20 + BLOCK_WIDTH * (x + i) + BLOCK_WIDTH > 220 || 20 + BLOCK_WIDTH * (j + y) + BLOCK_WIDTH > 420) && block[i][j])
                     {
                            return true;
                     }
              }
       }
       return false;
}

用双重循环,判定每一个小方块是否超出地图边界,或者与地图上方块重叠。

 


四、完整源码

俄罗斯方块C++完整源码(easyX版)

点赞(0)

C语言网提供由在职研发工程师或ACM蓝桥杯竞赛优秀选手录制的视频教程,并配有习题和答疑,点击了解:

一点编程也不会写的:零基础C语言学练课程

解决困扰你多年的C语言疑难杂症特性的C语言进阶课程

从零到写出一个爬虫的Python编程课程

只会语法写不出代码?手把手带你写100个编程真题的编程百练课程

信息学奥赛或C++选手的 必学C++课程

蓝桥杯ACM、信息学奥赛的必学课程:算法竞赛课入门课程

手把手讲解近五年真题的蓝桥杯辅导课程

Dotcpp在线编译      (登录可减少运行等待时间)