C++【EasyX】俄罗斯方块

文章目录

  • 一、前言
  • 二、效果
  • 三、实现流程
    • 1、界面设计
    • 2、方块元素设计
    • 3、方块颜色
    • 4、俄罗斯方块设计
    • 5、当前俄罗斯方块与下一俄罗斯方块
      • (1).俄罗斯方块的随机生成
      • (2).俄罗斯方块的迭代
      • (3).俄罗斯方块的初始化
    • 6、当前俄罗斯方块的位置
    • 7、地图中俄罗斯方块的记录
    • 8、移动控制指令
      • (1).所有指令
      • (2).接收指令
      • (3).执行指令
        • [1].旋转指令
        • [2].左/右移指令
        • [3].下移指令
    • 9、整个游戏的绘制
      • (1).背景绘制
      • (2).游戏区域的绘制
        • [1].游戏区域背景绘制
        • [2].“尘埃落定”的方块绘制
        • [3].当前俄罗斯方块绘制
      • (3).侧边栏区域的绘制
    • 10、主函数中的游戏流程
      • (1).初始化环境
      • (2).每大局游戏的初始化
      • (3).每小轮游戏的迭代(俄罗斯方块的迭代)
      • (4).主函数
    • 11、其他注意点
      • (1).绘图闪屏问题
  • 三、整体代码
  • 四、后话

一、前言

说起来真是很巧,写这个俄罗斯方块前一天晚上突然在B站看到一条视频,说是33行代码实现俄罗斯方块。然后脑海里想起了EasyX村长的话,大概是说一直写到能写出俄罗斯方块才能算勉强合格。

于是晚上睡之前心血来潮,心想,我也要写出来一个俄罗斯方块!

二、效果

效果如下图所示:
C++【EasyX】俄罗斯方块_第1张图片

三、实现流程

1、界面设计

界面部分 大小/位置
整体区域大小 600 × 540 600 \times 540 600×540(单位:像素)
游戏区域大小 360 × 540 360 \times 540 360×540(单位:像素)、 10 × 15 10 \times 15 10×15(单位:方块)
侧边区域大小 240 × 540 240 \times 540 240×540(单位:像素)
侧边预览框大小 144 × 144 144 \times 144 144×144(单位:像素)、 4 × 4 4 \times 4 4×4(单位:方块)
侧边预览框位置 ( 360 + 48 , 36 ) (360 + 48, 36) (360+48,36)
按键提示文字大小 30 30 30(单位:像素)
按键提示文字位置 ( 360 + 48 , 300 + ( i − 1 ) ∗ 30 ) , i = 1 , 2 , 3 , 4 (360 + 48, 300 + (i - 1) * 30), i=1,2,3,4 (360+48,300+(i1)30),i=1,2,3,4
得分显示文字大小 30 30 30(单位:像素)
得分显示文字位置 ( 360 + 48 , 200 ) (360 + 48, 200) (360+48,200)
方块元素大小 36 × 36 36 \times 36 36×36(单位:像素)

注:绘图的位置都是指绘制时左上角的坐标,字体都是选用了“华文楷体”,颜色没什么就不用多说了

2、方块元素设计

设计 大小(单位:像素)
占据部分 36 × 36 36 \times 36 36×36
内边距 2 2 2
边框圆角 6 6 6
实体大小 32 × 32 32 \times 32 32×32

设计图纸如下(其中px代表像素):
C++【EasyX】俄罗斯方块_第2张图片

3、方块颜色

用一个颜色(COLORREF)数组存放所有的颜色,比如我这里就是:红橙黄绿青蓝紫

COLORREF colors[7] = { RGB(255, 0, 0), RGB(255, 165, 0), RGB(255, 255, 0), RGB(0, 128, 0), RGB(0, 255, 255), RGB(0, 0, 255), RGB(128, 0, 128) };

方块的颜色的绘制只需要用EasyX提供的floodfill函数,以其边框线颜色(白色)为边界,进行填充即可。

单个方块的绘制函数代码如下:

void drawItem(int x, int y, COLORREF c)
{
	// 方块设计
	// 实际尺寸:32 * 32(单位:像素)
	// 边框颜色:白色
	// 圆角半径:4(单位:像素)
	// 内部间距:2(单位:像素)
	tmp_fill_color = getfillcolor();

	const int r = 6;
	const int p = 2;

	int up_l_x = x + p + r;
	int up_r_x = x + 36 - p - r;
	int up___y = y + p;

	int down_l_x = x + p + r;
	int down_r_x = x + 36 - p - r;
	int down___y = y + 36 - p;

	int left_u_y = y + p + r;
	int left_d_y = y + 36 - p - r;
	int left___x = x + p;

	int right_u_y = y + p + r;
	int right_d_y = y + 36 - p - r;
	int right___x = x + 36 - p;

	line(up_l_x, up___y, up_r_x, up___y);
	line(down_l_x, down___y, down_r_x, down___y);
	line(left___x, left_u_y, left___x, left_d_y);
	line(right___x, right_u_y, right___x, right_d_y);
	arc(x + p, y + p, x + p + 2 * r, y + p + 2 * r, pi / 2, pi);
	arc(x + 36 - p, y + p, x + 36 - p - 2 * r, y + p + 2 * r, 0, pi / 2);
	arc(x + p, y + 36 - p, x + p + 2 * r, y + 36 - p - 2 * r, -pi, -pi / 2);
	arc(x + 36 - p, y + 36 - p, x + 36 - p - 2 * r, y + 36 - p - 2 * r, -pi / 2, 0);

	setfillcolor(c);
	floodfill(x + p + r + 1, y + p + r + 1, WHITE);

	setfillcolor(tmp_fill_color);
}

4、俄罗斯方块设计

俄罗斯方块类型大致分为7类,如下表所列举:

俄罗斯方块类型 实际形状个数
山型 4 4 4
L型 4 4 4
反L型 4 4 4
Z型 2 2 2
反Z型 2 2 2
I型 2 2 2
田型 1 1 1

由此可见,一个俄罗斯方块应该具有特征【形状】,一个俄罗斯方块最多有4个形状,每个形状都需要描述4个点,每个点需要用2个坐标值来描述坐标,因此我们需要一个 4 × 4 × 2 4 \times 4 \times 2 4×4×2的数组来储存俄罗斯方块的形状,得到俄罗斯方块的结构体如下:

struct Square
{
	int dir[4][4][2];
};

结构体定义好了,我们就需要根据7种俄罗斯方块的结构来实例化7个Square,我的实例化方式如下:

Square squares[7] = { 
	{ 0, -2, 0, -1, 0, 0, 1, 0, 0, -1, 1, -1, 2, -1, 0, 0, 0, -2, 1, -2, 1, -1, 1, 0, 0, 0, 1, 0, 2, 0, 2, -1 },       //  L 型
	{ 0, 0, 1, 0, 1, -1, 1, -2, 0, -1, 0, 0, 1, 0, 2, 0, 0, -2, 0, -1, 0, 0, 1, -2, 0, -1, 1, -1, 2, -1, 2, 0 },       //  L 型(反)
	{ 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0 },        // 田 型
	{ 0, 0, 1, -1, 1, 0, 2, 0, 1, -2, 1, -1, 1, 0, 2, -1, 0, -1, 1, -1, 1, 0, 2, -1, 0, -1, 1, -2, 1, -1, 1, 0 },      // 山 型
	{ 0, -3, 0, -2, 0, -1, 0, 0, 0, -3, 1, -3, 2, -3, 3, -3, 0, -3, 0, -2, 0, -1, 0, 0, 0, -3, 1, -3, 2, -3, 3, -3 },  //  | 型
	{ 0, -1, 1, -1, 1, 0, 2, 0, 0, -1, 0, 0, 1, -2, 1, -1, 0, -1, 1, -1, 1, 0, 2, 0, 0, -1, 0, 0, 1, -2, 1, -1 },      //  Z 型
	{ 0, 0, 1, -1, 1, 0, 2, -1, 1, -2, 1, -1, 2, -1, 2, 0, 0, 0, 1, -1, 1, 0, 2, -1, 1, -2, 1, -1, 2, -1, 2, 0 }       //  Z 型(反)
};

Square结构体里面,坐标(地图中)的定义自然都需要相对于同一个地图点而言。

这里我单独拉出来一个图形来说,就比如squares[0].dir[0]的四个坐标 ( 0 , − 2 ) (0, -2) (0,2) ( 0 , − 1 ) (0, -1) (0,1) ( 0 , 0 ) (0, 0) (0,0) ( 1 , 0 ) (1, 0) (1,0),这些坐标的值代表了相对于当前位置的偏移量。

假设当前位置的地图点坐标为 ( 0 , 0 ) (0, 0) (0,0),那么我们根据上述偏移量可以绘制出以下图形:
C++【EasyX】俄罗斯方块_第3张图片

类似地,我们根据上面定义的7种形状squares,同样假设当前位置的地图点坐标为 ( 0 , 0 ) (0, 0) (0,0),整理出来就可以得到如下图所示的所有图形(省略了重复的):
C++【EasyX】俄罗斯方块_第4张图片
到此,俄罗斯方块的所有形状就设计完成了。

5、当前俄罗斯方块与下一俄罗斯方块

(1).俄罗斯方块的随机生成

在如上所述的设计中,一个俄罗斯方块的全部属性仅仅由以下三者共同唯一决定:

  • 俄罗斯方块类型索引s_idx
  • 俄罗斯方块形状索引d_idx
  • 俄罗斯方块颜色索引c_idx

以上三者确定后,可以确定俄罗斯方块的四个关键绘制点在squares[s_idx].dir[d_idx]中,其颜色为colors[c_idx].

因此,如果要随机生成一个方块,只需要令其 { s _ i d x = r a n d ( ) % 7 d _ i d x = r a n d ( ) % 4 c _ i d x = r a n d ( ) % 7 \begin{cases} s\_idx = rand() \% 7 \\ d\_idx = rand() \%4 \\ c\_idx = rand() \% 7 \end{cases} s_idx=rand()%7d_idx=rand()%4c_idx=rand()%7

要确定当前俄罗斯方块和下一个俄罗斯方块,我们需要下面6个变量 { n o w _ s _ i d x 、 n o w _ d _ i d x 、 n o w _ c _ i d x n e x t _ s _ i d x 、 n e x t _ d _ i d x 、 n e x t _ c _ i d x \begin{cases} now\_s\_idx、now\_d\_idx、now\_c\_idx \\ next\_s\_idx、next\_d\_idx、next\_c\_idx \end{cases} {now_s_idxnow_d_idxnow_c_idxnext_s_idxnext_d_idxnext_c_idx

(2).俄罗斯方块的迭代

当需要切换到下一个俄罗斯方块时,将下一个俄罗斯方块的全部索引赋值给当前俄罗斯方块,然后重新随机生成下一个俄罗斯方块。

局部代码如下:

now_c_idx = next_c_idx;
now_s_idx = next_s_idx;
now_d_idx = next_d_idx;
next_c_idx = rand() % 7;
next_s_idx = rand() % 7;
next_d_idx = rand() % 4;

(3).俄罗斯方块的初始化

由于迭代过程中,当前俄罗斯方块是直接从把下一个俄罗斯方块拿来用,因此初始时,只需要初始化下一个俄罗斯方块即可。

局部代码如下:

next_c_idx = rand() % 7;
next_s_idx = rand() % 7;
next_d_idx = rand() % 4;

6、当前俄罗斯方块的位置

当前俄罗斯方块的地图位置需要设置两个变量now_mp_xnow_mp_y来确认,然后只要根据当前俄罗斯方块的dir属性,就可以知道当前俄罗斯方块的4个方块的地图位置,从而绘制出来。

(在上方超出地图外的部分有时候需要稍微处理一下,因为可能会有下标越界的可能;绘制的时候可以不用管,因为绘制超出了窗口范围也不会报错)

7、地图中俄罗斯方块的记录

用一个二维的 10 × 15 10 \times 15 10×15int数组mp来存储地图中的俄罗斯方块、COLORREF的数组mp_c来存储地图中俄罗斯方块的颜色。

由于当前俄罗斯方块需要频繁地移动,因此不宜把我们看到的场面都存入mp,只有当确定俄罗斯方块已经“沉底”、不能移动了,我们才将它的数据存入地图mp中。因此,正在移动的当前俄罗斯方块是不会被记入地图mp中的

8、移动控制指令

(1).所有指令

用了enum枚举类型枚举了几种控制指令,如下所示:

enum Cmd
{
	Cmd_rotate,  // 方块旋转
	Cmd_left,    // 方块左移
	Cmd_right,   // 方块右移
	Cmd_down     // 方块下移
};

(2).接收指令

通过一个函数接收按键消息,实现获取指令的功能。

为了不让俄罗斯因为输入了移动指令而一直卡在某一层,我们需要增加一个计时器,每隔一段时间就自动执行下降指令。

接受指令的函数如下:

Cmd getCmd()
{
	while (true)
	{
		// 指令超时
		DWORD time_tmp = GetTickCount();
		if (time_tmp - time_now >= 1000)
		{
			time_now = time_tmp;
			return Cmd_down;
		}

		// 接受指令
		if (_kbhit())
		{
			switch (_getch())
			{
			case 72:
				return Cmd_rotate;
			case 75:
				return Cmd_left;
			case 77:
				return Cmd_right;
			case 80:
				return Cmd_down;
			}
		}

		// 降低CPU占用
		Sleep(20);
	}
}

(3).执行指令

[1].旋转指令

当触发了旋转指令以后,我们需要依次尝试该俄罗斯方块的其他三种形状,如果出现了以下情况之一,就能说明这样旋转后的放置是不合理的

  • 俄罗斯方块的某个方块跨越左边界
  • 俄罗斯方块的某个方块跨越右边界
  • 俄罗斯方块的某个方块跨越下边界
  • 俄罗斯方块的某个方块的位置在地图mp上已经存在方块

据此,我们写出了以下校检函数,用于检查当前俄罗斯方块根据属性dir的形状索引dir_idx在地图坐标 ( m p _ x , m p _ y ) (mp\_x, mp\_y) (mp_x,mp_y)位置是否可以被成功放置:

bool checkPut(int mp_x, int mp_y, int dir_idx)
{
	int sq_x[4];
	int sq_y[4];
	for (int i = 0; i < 4; ++i)
	{
		sq_x[i] = mp_x + squares[now_s_idx].dir[dir_idx][i][0];
		sq_y[i] = mp_y + squares[now_s_idx].dir[dir_idx][i][1];
	}

	// 【左右越界、下方越界、重复占格】
	for (int i = 0; i < 4; ++i)
	{
		if (sq_x[i] < 0 || sq_x[i] > 9 || sq_y[i] > 14)
			return false;
		if (sq_y[i] < 0) // 检查坐标合法性
			continue;
		if (mp[sq_x[i]][sq_y[i]])
			return false;
	}
	return true;
}

如果某一种旋转放置不合理,那么我们尝试下一种旋转。

如果存在某种旋转放置合理,那么我们更新当前俄罗斯方块的形状索引now_d_idx;如果所有旋转放置都不合理,那么说明不能旋转,什么都不用去改变。

(需要注意的地方是,由于每一种俄罗斯方块的形状索引只能为 0 ∼ 3 0 \sim 3 03,为了避免下标越界这种尴尬的情况出现,一定要将形状索引对4取余)

旋转指令执行函数代码如下:

// 尝试剩余所有形状,可以旋转即调整旋转状态
for (int i = 1; i <= 3; ++i)
	if (checkPut(now_mp_x, now_mp_y, (now_d_idx + i) % 4))
	{
		now_d_idx = (now_d_idx + i) % 4;
		break;
	}

[2].左/右移指令

和旋转指令一样,只要尝试看看左/右移后的方块能否被成功放置,然后再决定是否执行即可。

左/右移指令执行函数代码如下:

void moveLeft()
{
	// 尝试能否左移
	if (checkPut(now_mp_x - 1, now_mp_y, now_d_idx))
		--now_mp_x;
}

void moveRight()
{
	// 尝试能否右移
	if (checkPut(now_mp_x + 1, now_mp_y, now_d_idx))
		++now_mp_x;
}

[3].下移指令

和上面稍微有点不同,因为不能下移说明方块“沉底”了,因此也就有了下面几种需求:

  • 提示可以开始下一个俄罗斯方块
  • 将当前俄罗斯方块记录到地图mp
  • 进行消行
  • 判断游戏是否结束

提示可以开始下一个方块我们可以用一个变量flag_next来标识。当前俄罗斯方块还能移动时,保持值为0(也是每一小轮的初始值);当前俄罗斯方块不能再移动了,设置值为1,等待开启下一小轮。

将当前俄罗斯方块记录也很简单,只要根据当前位置的地图坐标 ( n o w _ m p _ x , n o w _ m p _ y ) (now\_mp\_x, now\_mp\_y) (now_mp_x,now_mp_y)以及当前俄罗斯方块的三个索引,将mp中对应位置标记为1、mp_c中对应位置颜色标记为对应颜色即可。
代码如下:

void recordSquareNow()
{
	int sq_x[4];
	int sq_y[4];
	for (int i = 0; i < 4; ++i)
	{
		sq_x[i] = now_mp_x + squares[now_s_idx].dir[now_d_idx][i][0];
		sq_y[i] = now_mp_y + squares[now_s_idx].dir[now_d_idx][i][1];
	}
	for (int i = 0; i < 4; ++i)
		if (sq_y[i] >= 0)
		{
			mp[sq_x[i]][sq_y[i]] = 1;
			mp_c[sq_x[i]][sq_y[i]] = colors[now_c_idx];
		}
}

进行消行也不难实现,只要将地图mp x x x方向上值全部为1的某一层删除即可。我觉得反正都要遍历,不如遍历的时候把符合条件的复制到临时数组里,然后再从临时数组拷贝过来就行了。
代码如下:

void execClear()
{
	memset(mp_tmp, 0, sizeof(mp_tmp));
	memset(mp_c_tmp, 0, sizeof(mp_c_tmp));
	int cnt_j = 14;
	for (int j = 14; j >= 0; --j)
	{
		int cnt = 0;
		for (int i = 0; i < 10; ++i)
			if (mp[i][j])
				++cnt;
		if (cnt != 10)
		{
			for (int i = 0; i < 10; ++i)
			{
				mp_tmp[i][cnt_j] = mp[i][j];
				mp_c_tmp[i][cnt_j] = mp_c[i][j];
			}
			--cnt_j;
		}
		else
			++score;
	}
	for (int j = 0; j < 15; ++j)
		for (int i = 0; i < 10; ++i)
		{
			mp[i][j] = mp_tmp[i][j];
			mp_c[i][j] = mp_c_tmp[i][j];
		}
}

然后就是判断游戏是否结束,这个就很简单了,只要看看地图mp中的最上层是否存在值为1的某一块即可。我们也需要设置一个变量flag_over来标识游戏是否结束。游戏没有结束时,保持值为0(也是每一大局的初始值);游戏已经结束时,设置值为1,询问是否再来亿局。
代码如下:

bool checkOver()
{
	for (int i = 0; i < 10; ++i)
		if (mp[i][0])
			return true;
	return false;
}

当上述函数实现后,我们就可以完整地执行下移了

下移移指令执行函数代码如下:

void moveDown()
{
	// 尝试能否下移
	if (checkPut(now_mp_x, now_mp_y + 1, now_d_idx))
	{
		++now_mp_y;
		return;
	}
	// 不能下移则说明这块方块“触礁”了,执行以下操作
	// 1、提示可以开始下一个方块
	// 2、将方块记录到map地图中
	// 3、判断是否可以消行
	// 4、判断消行后游戏是否结束
	flag_next = 1;
	recordSquareNow();
	execClear();
	if (checkOver())
		flag_over = 1;
}

9、整个游戏的绘制

(1).背景绘制

因为我不是很喜欢很单调的颜色,所以用了线性渐变的方法,不停画线,做出了线性渐变的背景样式。

局部代码:

for (int i = 0; i < 541; ++i)
{
	setlinecolor(RGB(135, 206, 250 - i / 5));
	line(0, 540 - i, 360, 540 - i);
}
for (int i = 0; i < 541; ++i)
{
	setlinecolor(RGB(224, 178, 220 - i / 15));
	line(361, i, 600, i);
}

(2).游戏区域的绘制

[1].游戏区域背景绘制

就是一个简单的线性渐变

void drawGameBG()
{
	// 划分区域(游戏区域、计分区域)
	// 方块尺寸——36 * 36(单位:像素)
	// 游戏尺寸——10 * 15(单位:方块)
	// 下一个方块显示区域——4 * 4(单位:方块)
	COLORREF tmp = getlinecolor();

	for (int i = 0; i < 541; ++i)
	{
		setlinecolor(RGB(135, 206, 250 - i / 5));
		line(0, 540 - i, 360, 540 - i);
	}

	setlinecolor(tmp);
}

[2].“尘埃落定”的方块绘制

然后需要根据地图数组mp绘制已经“尘埃落定”的方块:

void drawMap()
{
	for (int i = 0; i < 10; ++i)
		for (int j = 0; j < 15; ++j)
			if (mp[i][j])
				drawItem(i * 36, j * 36, mp_c[i][j]);
}

[3].当前俄罗斯方块绘制

void drawSquareNow()
{
	drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][0][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][0][1]) * 36, colors[now_c_idx]);
	drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][1][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][1][1]) * 36, colors[now_c_idx]);
	drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][2][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][2][1]) * 36, colors[now_c_idx]);
	drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][3][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][3][1]) * 36, colors[now_c_idx]);
}

(3).侧边栏区域的绘制

主要是还需要将预览效果弄上去,这里封装了一个drawSquareNext函数来实现,在drawSide函数里被调用。

void drawSquareNext()
{
	int tmp_x = 360 + 48;
	int tmp_y = 36 + 108;
	COLORREF c = colors[next_c_idx];
	drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][0][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][0][1] * 36, c);
	drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][1][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][1][1] * 36, c);
	drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][2][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][2][1] * 36, c);
	drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][3][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][3][1] * 36, c);
}

void drawSide()
{
	tmp_line_color = getlinecolor();
	getlinestyle(&tmp_line_style);
	tmp_text_color = getlinecolor();
	gettextstyle(&tmp_text_style);

	for (int i = 0; i < 541; ++i)
	{
		setlinecolor(RGB(224, 178, 220 - i / 15));
		line(361, i, 600, i);
	}
	settextcolor(RGB(65, 105, 225));
	settextstyle(30, 0, L"华文楷体");
	outtextxy(360 + 48, 300, L"左移:←");
	outtextxy(360 + 48, 330, L"右移:→");
	outtextxy(360 + 48, 360, L"变形:↑");
	outtextxy(360 + 48, 390, L"下落:↓");

	setlinecolor(WHITE);
	rectangle(360 + 48, 36, 600 - 48, 36 + 144);
	drawSquareNext();
	setlinecolor(0x7FFFAA);
	rectangle(360 + 48, 36, 600 - 48, 36 + 144);

	swprintf(score_tips, 29, L"得分:%d", score * 100);
	outtextxy(360 + 48, 200, score_tips);

	setlinecolor(tmp_line_color);
	setlinestyle(&tmp_line_style);
	settextcolor(tmp_text_color);
	settextstyle(&tmp_text_style);
}

10、主函数中的游戏流程

(1).初始化环境

主要包括

  • 窗口设置
  • 绘图模式设置
  • 随机数种子设置
void initEnvironment()
{
	// 窗口设置
	initgraph(600, 540);
	HWND hwnd = GetHWnd();
	SetWindowText(hwnd, L"俄罗斯方块");
	SetWindowPos(hwnd, HWND_TOP, 700, 20, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW);

	// 绘图模式设置
	setbkmode(TRANSPARENT);

	// 随机数种子
	srand(time(NULL));
}

(2).每大局游戏的初始化

主要包括

  • 清空地图mp与其对应颜色mp_c
  • 初始化计时器(不初始化也就是开局直接落下去一格,感觉不是很重要)
  • 随机初始化下一个俄罗斯方块
  • 标记游戏状态为“未结束”
  • 重置计分器
void initDatasPerRound()
{
	memset(mp, 0, sizeof(mp));
	memset(mp, 0, sizeof(mp_c));
	time_now = GetTickCount();
	next_c_idx = rand() % 7;
	next_s_idx = rand() % 7;
	next_d_idx = rand() % 4;
	flag_over = 0;
	score = 0;
}

(3).每小轮游戏的迭代(俄罗斯方块的迭代)

主要包括

  • 初始化当前俄罗斯方块位置的地图坐标
  • 设置当前小轮状态为“未结束”
  • 将当前俄罗斯方块换成下一个俄罗斯方块
  • 随机初始化下一个俄罗斯方块
void initDatasPerSquare()
{
	now_mp_x = 5;
	now_mp_y = -1;
	flag_next = 0;
	now_c_idx = next_c_idx;
	now_s_idx = next_s_idx;
	now_d_idx = next_d_idx;
	next_c_idx = rand() % 7;
	next_s_idx = rand() % 7;
	next_d_idx = rand() % 4;
}

(4).主函数

int main()
{
	initEnvironment();
	// 开始游戏
	while (true)
	{
		initDatasPerRound();
		while (!flag_over)
		{
			initDatasPerSquare();
			while (!flag_next)
			{
				BeginBatchDraw();
				drawGameBG();
				drawSide();
				//drawLines();
				drawMap();
				drawSquareNow();
				EndBatchDraw();
				Cmd cmd = getCmd();
				execCmd(cmd);
			}
		}
		// 一局结束后的统计
		swprintf(over_tips, 39, L"游戏结束\n你的最终得分:%d\n是否再来亿局?", score * 100);
		if (MessageBox(GetHWnd(), over_tips, L"再来亿局?", MB_ICONQUESTION | MB_YESNO) == IDNO)
			break;
	}

	return 0;
}

11、其他注意点

(1).绘图闪屏问题

由于绘图循环比较多,一开始频繁地直接绘制导致游戏过程中老是闪屏,后来,在EasyX村长和村民的友情提醒下,总算是成功优化了。

只需要在绘图前使用BeginBatchDraw这个函数标识一下,在绘图完成时再用EndBatchDraw这个函数标识一下,就可以将其中的所有绘图内容批量绘制,这样可以有效避免闪屏问题。

三、整体代码

整个程序的代码如下(没有任何资源文件):

#include 
#include 
#include 
#include 
#include 

struct Square
{
	int dir[4][4][2];
};

enum Cmd
{
	Cmd_rotate,  // 方块旋转
	Cmd_left,    // 方块左移
	Cmd_right,   // 方块右移
	Cmd_down     // 方块下移
};

Square squares[7] = { 
	{ 0, -2, 0, -1, 0, 0, 1, 0, 0, -1, 1, -1, 2, -1, 0, 0, 0, -2, 1, -2, 1, -1, 1, 0, 0, 0, 1, 0, 2, 0, 2, -1 },       //  L 型
	{ 0, 0, 1, 0, 1, -1, 1, -2, 0, -1, 0, 0, 1, 0, 2, 0, 0, -2, 0, -1, 0, 0, 1, -2, 0, -1, 1, -1, 2, -1, 2, 0 },       //  L 型(反)
	{ 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0, 0, -1, 0, 0, 1, -1, 1, 0 },        // 田 型
	{ 0, 0, 1, -1, 1, 0, 2, 0, 1, -2, 1, -1, 1, 0, 2, -1, 0, -1, 1, -1, 1, 0, 2, -1, 0, -1, 1, -2, 1, -1, 1, 0 },      // 山 型
	{ 0, -3, 0, -2, 0, -1, 0, 0, 0, -3, 1, -3, 2, -3, 3, -3, 0, -3, 0, -2, 0, -1, 0, 0, 0, -3, 1, -3, 2, -3, 3, -3 },  //  | 型
	{ 0, -1, 1, -1, 1, 0, 2, 0, 0, -1, 0, 0, 1, -2, 1, -1, 0, -1, 1, -1, 1, 0, 2, 0, 0, -1, 0, 0, 1, -2, 1, -1 },      //  Z 型
	{ 0, 0, 1, -1, 1, 0, 2, -1, 1, -2, 1, -1, 2, -1, 2, 0, 0, 0, 1, -1, 1, 0, 2, -1, 1, -2, 1, -1, 2, -1, 2, 0 }       //  Z 型(反)
};

const double pi = acos(-1);

COLORREF tmp_line_color;
LINESTYLE tmp_line_style;
COLORREF tmp_text_color;
LOGFONT tmp_text_style;
COLORREF tmp_fill_color;

COLORREF colors[7] = { RGB(255, 0, 0), RGB(255, 165, 0), RGB(255, 255, 0), RGB(0, 128, 0), RGB(0, 255, 255), RGB(0, 0, 255), RGB(128, 0, 128) };
int mp[10][15];
int mp_tmp[10][15];
COLORREF mp_c[10][15];
COLORREF mp_c_tmp[10][15];
wchar_t score_tips[30];
wchar_t over_tips[50];

DWORD time_now;
int flag_next;
int flag_over;
int now_c_idx;
int now_s_idx;
int now_d_idx;
int next_c_idx;
int next_s_idx;
int next_d_idx;
int now_mp_x;
int now_mp_y;
int score;

void initEnvironment();
void initDatasPerRound();
void initDatasPerSquare();
void drawGameBG();
void drawSide();
//void drawLines();
void drawItem(int x, int y, COLORREF c);
void drawSquareNow();
void drawSquareNext();
void drawMap();
Cmd getCmd();
bool checkPut(int mp_x, int mp_y, int dir_idx);
void execClear();
bool checkOver();
void execCmd(Cmd cmd);
void moveRotate();
void moveLeft();
void moveRight();
void moveDown();
void recordSquareNow();

int main()
{
	initEnvironment();
	// 开始游戏
	while (true)
	{
		initDatasPerRound();
		while (!flag_over)
		{
			initDatasPerSquare();
			while (!flag_next)
			{
				BeginBatchDraw();
				drawGameBG();
				drawSide();
				//drawLines();
				drawMap();
				drawSquareNow();
				EndBatchDraw();
				Cmd cmd = getCmd();
				execCmd(cmd);
			}
		}
		// 一局结束后的统计
		swprintf(over_tips, 39, L"游戏结束\n你的最终得分:%d\n是否再来亿局?", score * 100);
		if (MessageBox(GetHWnd(), over_tips, L"再来亿局?", MB_ICONQUESTION | MB_YESNO) == IDNO)
			break;
	}

	return 0;
}

void initEnvironment()
{
	// 窗口设置
	initgraph(600, 540);
	HWND hwnd = GetHWnd();
	SetWindowText(hwnd, L"俄罗斯方块");
	SetWindowPos(hwnd, HWND_TOP, 700, 20, 0, 0, SWP_NOSIZE | SWP_SHOWWINDOW);

	// 绘图模式设置
	setbkmode(TRANSPARENT);

	// 随机数种子
	srand(time(NULL));
}

void initDatasPerRound()
{
	memset(mp, 0, sizeof(mp));
	memset(mp, 0, sizeof(mp_c));
	time_now = GetTickCount();
	next_c_idx = rand() % 7;
	next_s_idx = rand() % 7;
	next_d_idx = rand() % 4;
	flag_over = 0;
	score = 0;
}

void initDatasPerSquare()
{
	now_mp_x = 5;
	now_mp_y = -1;
	flag_next = 0;
	now_c_idx = next_c_idx;
	now_s_idx = next_s_idx;
	now_d_idx = next_d_idx;
	next_c_idx = rand() % 7;
	next_s_idx = rand() % 7;
	next_d_idx = rand() % 4;
}

void drawGameBG()
{
	// 划分区域(游戏区域、计分区域)
	// 方块尺寸——36 * 36(单位:像素)
	// 游戏尺寸——10 * 15(单位:方块)
	// 下一个方块显示区域——4 * 4(单位:方块)
	COLORREF tmp = getlinecolor();

	for (int i = 0; i < 541; ++i)
	{
		setlinecolor(RGB(135, 206, 250 - i / 5));
		line(0, 540 - i, 360, 540 - i);
	}

	setlinecolor(tmp);
}

void drawSide()
{
	tmp_line_color = getlinecolor();
	getlinestyle(&tmp_line_style);
	tmp_text_color = getlinecolor();
	gettextstyle(&tmp_text_style);

	for (int i = 0; i < 541; ++i)
	{
		setlinecolor(RGB(224, 178, 220 - i / 15));
		line(361, i, 600, i);
	}
	settextcolor(RGB(65, 105, 225));
	settextstyle(30, 0, L"华文楷体");
	outtextxy(360 + 48, 300, L"左移:←");
	outtextxy(360 + 48, 330, L"右移:→");
	outtextxy(360 + 48, 360, L"变形:↑");
	outtextxy(360 + 48, 390, L"下落:↓");

	setlinecolor(WHITE);
	rectangle(360 + 48, 36, 600 - 48, 36 + 144);
	drawSquareNext();
	setlinecolor(0x7FFFAA);
	rectangle(360 + 48, 36, 600 - 48, 36 + 144);

	swprintf(score_tips, 29, L"得分:%d", score * 100);
	outtextxy(360 + 48, 200, score_tips);

	setlinecolor(tmp_line_color);
	setlinestyle(&tmp_line_style);
	settextcolor(tmp_text_color);
	settextstyle(&tmp_text_style);
}

//void drawLines()
//{
//	for (int i = 0; i <= 10; ++i)
//		line(36 * i, 0, 36 * i, 540);
//	for (int j = 0; j <= 15; ++j)
//		line(0, j * 36, 360, j * 36);
//
//	for (int i = 1; i < 4; ++i)
//		line(360 + 48 + i * 36, 36, 360 + 48 + i * 36, 36 + 144);
//	for (int j = 1; j < 4; ++j)
//		line(360 + 48, 36 + j * 36, 600 - 48, 36 + j * 36);
//}

void drawItem(int x, int y, COLORREF c)
{
	// 方块设计
	// 实际尺寸:32 * 32(单位:像素)
	// 边框颜色:白色
	// 圆角半径:4(单位:像素)
	// 内部间距:2(单位:像素)
	tmp_fill_color = getfillcolor();

	const int r = 6;
	const int p = 2;

	int up_l_x = x + p + r;
	int up_r_x = x + 36 - p - r;
	int up___y = y + p;

	int down_l_x = x + p + r;
	int down_r_x = x + 36 - p - r;
	int down___y = y + 36 - p;

	int left_u_y = y + p + r;
	int left_d_y = y + 36 - p - r;
	int left___x = x + p;

	int right_u_y = y + p + r;
	int right_d_y = y + 36 - p - r;
	int right___x = x + 36 - p;

	line(up_l_x, up___y, up_r_x, up___y);
	line(down_l_x, down___y, down_r_x, down___y);
	line(left___x, left_u_y, left___x, left_d_y);
	line(right___x, right_u_y, right___x, right_d_y);
	arc(x + p, y + p, x + p + 2 * r, y + p + 2 * r, pi / 2, pi);
	arc(x + 36 - p, y + p, x + 36 - p - 2 * r, y + p + 2 * r, 0, pi / 2);
	arc(x + p, y + 36 - p, x + p + 2 * r, y + 36 - p - 2 * r, -pi, -pi / 2);
	arc(x + 36 - p, y + 36 - p, x + 36 - p - 2 * r, y + 36 - p - 2 * r, -pi / 2, 0);

	setfillcolor(c);
	floodfill(x + p + r + 1, y + p + r + 1, WHITE);

	setfillcolor(tmp_fill_color);
}

void drawSquareNow()
{
	drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][0][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][0][1]) * 36, colors[now_c_idx]);
	drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][1][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][1][1]) * 36, colors[now_c_idx]);
	drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][2][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][2][1]) * 36, colors[now_c_idx]);
	drawItem((now_mp_x + squares[now_s_idx].dir[now_d_idx][3][0]) * 36, (now_mp_y + squares[now_s_idx].dir[now_d_idx][3][1]) * 36, colors[now_c_idx]);
}

void drawSquareNext()
{
	int tmp_x = 360 + 48;
	int tmp_y = 36 + 108;
	COLORREF c = colors[next_c_idx];
	drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][0][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][0][1] * 36, c);
	drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][1][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][1][1] * 36, c);
	drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][2][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][2][1] * 36, c);
	drawItem(tmp_x + squares[next_s_idx].dir[next_d_idx][3][0] * 36, tmp_y + squares[next_s_idx].dir[next_d_idx][3][1] * 36, c);
}

void drawMap()
{
	for (int i = 0; i < 10; ++i)
		for (int j = 0; j < 15; ++j)
			if (mp[i][j])
				drawItem(i * 36, j * 36, mp_c[i][j]);
}

Cmd getCmd()
{
	while (true)
	{
		// 指令超时
		DWORD time_tmp = GetTickCount();
		if (time_tmp - time_now >= 1000)
		{
			time_now = time_tmp;
			return Cmd_down;
		}

		// 接受指令
		if (_kbhit())
		{
			switch (_getch())
			{
			case 72:
				return Cmd_rotate;
			case 75:
				return Cmd_left;
			case 77:
				return Cmd_right;
			case 80:
				return Cmd_down;
			}
		}

		// 降低CPU占用
		Sleep(20);
	}
}

void execCmd(Cmd cmd)
{
	switch (cmd)
	{
	case Cmd_down:
		moveDown();
		break;
	case Cmd_left:
		moveLeft();
		break;
	case Cmd_right:
		moveRight();
		break;
	case Cmd_rotate:
		moveRotate();
		break;
	}
}

bool checkPut(int mp_x, int mp_y, int dir_idx)
{
	int sq_x[4];
	int sq_y[4];
	for (int i = 0; i < 4; ++i)
	{
		sq_x[i] = mp_x + squares[now_s_idx].dir[dir_idx][i][0];
		sq_y[i] = mp_y + squares[now_s_idx].dir[dir_idx][i][1];
	}

	// 【左右越界、下方越界、重复占格】
	for (int i = 0; i < 4; ++i)
	{
		if (sq_x[i] < 0 || sq_x[i] > 9 || sq_y[i] > 14)
			return false;
		if (sq_y[i] < 0) // 检查坐标合法性
			continue;
		if (mp[sq_x[i]][sq_y[i]])
			return false;
	}
	return true;
}

void execClear()
{
	memset(mp_tmp, 0, sizeof(mp_tmp));
	memset(mp_c_tmp, 0, sizeof(mp_c_tmp));
	int cnt_j = 14;
	for (int j = 14; j >= 0; --j)
	{
		int cnt = 0;
		for (int i = 0; i < 10; ++i)
			if (mp[i][j])
				++cnt;
		if (cnt != 10)
		{
			for (int i = 0; i < 10; ++i)
			{
				mp_tmp[i][cnt_j] = mp[i][j];
				mp_c_tmp[i][cnt_j] = mp_c[i][j];
			}
			--cnt_j;
		}
		else
			++score;
	}
	for (int j = 0; j < 15; ++j)
		for (int i = 0; i < 10; ++i)
		{
			mp[i][j] = mp_tmp[i][j];
			mp_c[i][j] = mp_c_tmp[i][j];
		}
}

bool checkOver()
{
	for (int i = 0; i < 10; ++i)
		if (mp[i][0])
			return true;
	return false;
}

void moveRotate()
{
	// 尝试剩余所有形状,可以旋转即调整旋转状态
	for (int i = 1; i <= 3; ++i)
		if (checkPut(now_mp_x, now_mp_y, (now_d_idx + i) % 4))
		{
			now_d_idx = (now_d_idx + i) % 4;
			break;
		}
}

void moveLeft()
{
	// 尝试能否左移
	if (checkPut(now_mp_x - 1, now_mp_y, now_d_idx))
		--now_mp_x;
}

void moveRight()
{
	// 尝试能否右移
	if (checkPut(now_mp_x + 1, now_mp_y, now_d_idx))
		++now_mp_x;
}

void moveDown()
{
	// 尝试能否下移
	if (checkPut(now_mp_x, now_mp_y + 1, now_d_idx))
	{
		++now_mp_y;
		return;
	}
	// 不能下移则说明这块方块“触礁”了,执行以下操作
	// 1、提示可以开始下一个方块
	// 2、将方块记录到map地图中
	// 3、判断是否可以消行
	// 4、判断消行后游戏是否结束
	flag_next = 1;
	recordSquareNow();
	execClear();
	if (checkOver())
		flag_over = 1;
}

void recordSquareNow()
{
	int sq_x[4];
	int sq_y[4];
	for (int i = 0; i < 4; ++i)
	{
		sq_x[i] = now_mp_x + squares[now_s_idx].dir[now_d_idx][i][0];
		sq_y[i] = now_mp_y + squares[now_s_idx].dir[now_d_idx][i][1];
	}
	for (int i = 0; i < 4; ++i)
		if (sq_y[i] >= 0)
		{
			mp[sq_x[i]][sq_y[i]] = 1;
			mp_c[sq_x[i]][sq_y[i]] = colors[now_c_idx];
		}
}

四、后话

做这个的前一天,我一直在想,应该挺简单的吧。一早上一个半小时基本都在考虑绘图方面的内容。

到了下午,真正写逻辑的时候,我开始慌了,问题还是比较多的,比如“7种俄罗斯方块的不同形状要怎么弄啊”、“接受按键命令的同时就算不操作也要执行下移要怎么实现”、“移动命令要怎么实现”……

后来看这看那,现学现卖,一整天花了9个多小时,总算是写完了(如此看来我还是比较菜的吧23333 )

不过也确实让我颇感实践项目经验的重要性。人家熟练的人可以用5分钟、以33行的代码成功实现,而我却花了整整9小时、写了460行代码才实现出来(虽然可视化的效果还是可以比的23333 )。

如此看来,小的项目经验也是十分重要的,只有多写多实践,以后才能得心应手。

PS:本博客的编写总计耗时5小时50分钟,之所以写的这么详细是因为昨天在写这个的时候到CSDN上想找一些点子,但是很无奈找不着一些很细化的思路,因此也花了大把精力整理了这么一篇博客,也算是一次小结吧。

你可能感兴趣的:(C++——EasyX,c++,游戏开发,界面设计)