「C/C++」有手就能学会系列——经典俄罗斯方块

小白手写游戏经典-俄罗斯方块

儿时经典的掌机游戏,你还记得吗?

花式消行技巧,荣耀伙伴的最高纪录,仿佛又回到了纯真的战斗时代。

致敬俄罗斯方块之父"阿列克谢·帕基特诺夫",一起实现这个传奇经典游戏吧!

当时他耗费了6天时间,今天的你需要几天呢?

在我们的B站中有详细的讲解视频,以及相关的素材,只需90分钟就能实现这个经典!

https://www.bilibili.com/video/BV1uK4y187zT?from=search&seid=5164603845757173126

先看一下游戏效果:

搭建环境

首先,我们来搭建开发环境:

1. VC++2010或以上版本,推荐使用VS2015以上版本。

2. 下载SFML库,可以到SFML官网下载,或者联系作者。

SFML是一个非常方便的工具库,我下载的是SFML2.5.1 32位版本(32位可以在32位和64位平台运行)

下载后解压到电脑的任意位置,比如:C:\SFML\SFML-2.5.1_32bit

使用VC++或者VS创建一个空项目,然后配置头文件以及库文件。

配置使用的库文件:

sfml-window-d.lib

sfml-system-d.lib

sfml-graphics-d.lib

sfml-audio-d.lib

准备游戏素材

准备好游戏的素材(图片,背景音乐,音效音乐, 字体文件)。

背景图片如下,也可以更换成自己喜欢的其他图片。

游戏区域四周的方框图片,这个图片可以让游戏界面更好看哦,也可以省略不要。

俄罗斯方块的纹理图片如下。在代码中,我们会按照小方块的大小进行切割,得到多个不同颜色的小方块。

游戏逻辑结构

搭建游戏框架

先写好游戏框架:

 
#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include <time.h>
using namespace sf;

void keyEvent(RenderWindow * window) {
	Event e;

	// pollEvent 从事件队列中取出一个事件
	while (window->pollEvent(e))
	{
		if (e.type == Event::Closed)
			window->close();

		if (e.type == Event::KeyPressed) {
			switch (e.key.code) {
			case Keyboard::Up:
				break;
			case Keyboard::Left:
				break;
			case Keyboard::Right:
				break;
			default:
				break;
			}
		}
	}  
}

int main()
{	
    srand(time(0));

    // 创建窗口
    RenderWindow window(
        VideoMode(320, 416),   //窗口大小
        "Rock");   //标题

    // 创建表示图片的精灵
    Texture t2, t3;
    t2.loadFromFile("images/bg2.jpg");
    t3.loadFromFile("images/frame.png");
    Sprite spriteBg(t2);
    Sprite spriteFrame(t3);
    
    while (window.isOpen())
    {
       
        keyEvent(&window);

        window.clear(Color::White);
        window.draw(spriteBg);
        window.draw(spriteFrame);

        window.display();
    }
	
    return 0;
}

运行后,效果如下:

俄罗斯方块的设计

俄罗斯方块的实现,有很多实现方式,最简单的方式是使用多个二位数组,每个二位数组来表示一种方块。不过有更高效的实现方式,使用一个二维数组来表示多种俄罗斯方块。

const int ROW_COUNT = 20;
const int COL_COUNT = 10;

int blocks[7][4] = {
    1,3,5,7, // I
    2,4,5,7, // Z 1型
    3,5,4,6, // Z 2型
    3,5,4,7, // T
    2,3,5,7, // L
    3,5,7,6, // J
    2,3,4,5, // 田
};

把方块信息转换成位置坐标

所以对于"I"字型的方块,{1,3,5,7} 就使用4个坐标来表示,(1,0),(1,1),(1,2),(1,3)

struct Point {
    int x, y;
} curBlock[4], BakBlock[4];
......
for (int i = 0; i < 4; i++)
{
   curBlock[i].x = blocks[n][i] % 2;
   curBlock[i].y = blocks[n][i] / 2;
}

俄罗斯方块的旋转

旋转效果,最简单的方式是,为每一种方向使用一个二位数组。我们使用最灵活的方式,对俄罗斯方块进行旋转处理。

直接使用通用的数学公式,就可以直接得到旋转后的坐标位置。

直接使用数学公式,平面中,一个点(x,y)绕任意点(dx,dy)顺时针旋转a度后的坐标

xx= (x - dx)*cos(-a) - (y - dy)*sin(-a) + dx ;

yy= (x - dx)*sin(-a) + (y - dy)*cos(-a) +dy ;

平面中,一个点(x,y)绕任意点(dx,dy)逆时针旋转a度后的坐标

xx= (x - dx)*cos(a) - (y - dy)*sin(a) + dx ;

yy= (x - dx)*sin(a) + (y - dy)*cos(a) +dy ;

我们需要的是逆时针旋转90度代入公式,得:

Point p = curBlock[1]; //center of rotation
    for (int i = 0; i < 4; i++)
    {
        struct Point tmp = curBlock[i];
        curBlock[i].x = p.x - tmp.y + p.y;
        curBlock[i].y = p.y + tmp.x - p.x;
    }

使用时钟控制自由下落

每次循环开始的时候,就累加时间,如果时间超过延时间隔,就调用自定义的降落函数drop()

......
while (window.isOpen())
    {
        float time = clock.getElapsedTime().asSeconds();
        clock.restart(); //计时器重启及时
        timer += time;

        keyEvent(&window);

        if (timer > delay) {
            drop(); //下降一个位置
            timer = 0;
        }
        .....
    }

游戏方块的渲染

void drawBlocks(RenderWindow *window, Sprite *spriteBlock) {
    // 绘制已降落完毕的方块
    for (int i = 0; i < ROW_COUNT; i++)
        for (int j = 0; j < COL_COUNT; j++)
        {
            if (table[i][j] == 0) continue;
            spriteBlock->setTextureRect(IntRect(table[i][j] * 18, 0, 18, 18));
            spriteBlock->setPosition(j * 18, i * 18);
            spriteBlock->move(28, 31); //offset
            window->draw(*spriteBlock);
        }

    // 绘制当前方块
    for (int i = 0; i < 4; i++)
    {
        spriteBlock->setTextureRect(IntRect(blockIndex * 18, 0, 18, 18));
        spriteBlock->setPosition(curBlock[i].x * 18, curBlock[i].y * 18);
        spriteBlock->move(28, 31); //offset
        window->draw(*spriteBlock);
    }
}

消行的实现

从底部向上逐行扫描。

void clearLine() {
    int k = ROW_COUNT - 1;
    for (int i = ROW_COUNT - 1; i > 0; i--)
    {
        int count = 0;
        for (int j = 0; j < COL_COUNT; j++)
        {
            if (table[i][j]) count++;

            table[k][j] = table[i][j];
        }

        // 如果没有满行,就继续扫描上一行
        // 如果已经满行,k不变,在这个满行内,存放下一次的扫描结果
        if (count < COL_COUNT) k--;
        else {
            score += 10;
            sou.play();
        }
    }

    char tmp[16];
    sprintf_s(tmp, "%d", score);
    textScore.setString(tmp);
}

撸码捷径

越是优化代码,越是演练,越是思考,就越能发现C/C++的优势所在。

边看视频边写代码,遇到问题,私信小编。

详细教程:https://www.bilibili.com/video/BV1uK4y187zT

学习交流群:782648055

猜你喜欢

转载自blog.csdn.net/xiangxin1030/article/details/106503168
今日推荐