一、工程声明
这篇文章包括搭建游戏背景、方块的建模、按键响应、方块的随机生成和下一块方块的生成等,以此记录一次C语言的练习。本次工程涉及到图形化,可以使用EasyX库来画出简单的图形,这个库的下载和使用均在百度可查询到。
本工程使用的软件为Visual Studio 2019,新建C++空项目编写代码。文件的工程结构如下图所示,一共有三个文件,一个头文件和两个C++文件。之所以采用cpp后缀,是因为EasyX库只可以在C++文件使用,但是工程代码都是C语言编写的。
基本概念:
- 方格:指的是游戏区域的小格子,大小为20*20;
- 方块:指的是俄罗斯方块,由4*4共16个小格子构成,大小为80*80;
- 游戏区域:背景为网格线为游戏区域,大小为300*500,存在25*15=375个方格;
- 图形区域:图形区域包括游戏区域和文字显示部分,大小为500*500;
- 方块的坐标点:定义方块左下角的坐标点为整个方块的坐标点;
- 方块的行序号和列序号:数格子的数量,第一个格子的行序号为0,列序号为0;序号与坐标点对应的关系为:行序号*20=坐标点的y,列序号*20=坐标点的x;
二、游戏背景的搭建
//game.c
void context(void)
{
initgraph(500, 500); // 初始化图形模式
setorigin(0, 500); //重新定义原点
setaspectratio(1, -1); //将Y上半部分设定为正半轴
setlinestyle(PS_SOLID); //设定画线样式为虚线
setlinecolor(WHITE); //线条颜色为白色
for (size_t i = 0; i <= 25; i++) //画出游戏区域网格线,宽300,高500
line(0, 0 + 20 * i, 300, 0 + 20 * i); //横线
for (size_t i = 0; i <= 15; i++)
line(0 + 20 * i, 0, 0 + 20 * i, 500); //竖线
for (size_t i = 0; i <= 4; i++) //画出下一个方块网格线,宽80,高80
line(370, 300 + 20 * i, 450, 300+20 * i); //横线
for (size_t i = 0; i <= 4; i++)
line(370 + 20 * i, 300, 370 + 20 * i, 380); //竖线
setaspectratio(1, 1);
settextstyle(25, 0, _T("Consolas"));
outtextxy(310, -430, _T("SCORE:0 points"));
outtextxy(310, -410, _T("下一个方块:"));
outtextxy(310, -270, _T("w 转换方向"));
outtextxy(310, -240, _T("s 加快下降速度"));
outtextxy(310, -210, _T("a 左移"));
outtextxy(310, -180, _T("d 右移"));
outtextxy(310, -150, _T("h 重启"));
setaspectratio(1, -1);
}
- 首先对图形区域进行了初始的设定,设定线的样式为白色虚线;
- 用两个循环画出游戏区域的横线和竖线,共25行15列,大小为300*500;
- 再用两个循环画出下一块区域的网格线,共4行4列,大小为80*80;
- 画出提示文本,主要写着分数和操作提示等;
- 效果如下图所示。
三、方块的建模
研究俄罗斯方块的各种形状,会发现他们都在在4*4的方格当中的,考虑到C语言的short数据类型刚好有2个字节,16bit的长度。所以我们可以用short数据类型来表示我们的方块。比如下面的俄罗斯方块可以表示为,1000 1000 1000 1000,转化为十六进制就是8888H。
考虑到每个俄罗斯方块有不同的方向,不同的方向对应不同的姿态,所以我新建一个二维数组来表示所有的俄罗斯方块。一共有7钟俄罗斯方块,每种有4个方向的变化,有一些变化还是原来的样子。在game.h头文件,声明展示俄罗斯方块和位置结构体,以方块左下角那个点的坐标为整个方块的坐标点。
//main.c
unsigned short Diamond[7][4] = {
{0x000f,0x8888,0x000f,0x8888},
{0x008E,0x0c88,0x00E2,0x044c},
{0x002E,0x088c,0x00E8,0x0c44},
{0x00CC,0x00CC,0x00CC,0x00CC},
{0x006C,0x08c4,0x006c,0x08c4},
{0x004E,0x08c8,0x00E4,0x04c4},
{0x00C6,0x04c8,0x00c6,0x04c8} };
//game.h
//方块的坐标
typedef struct LOCATE
{
int x;
int y;
} Location;
//俄罗斯方块展示函数
void DisplayDiamond(unsigned short diamond, Location Loca, int cur_color);
俄罗斯方块展示函数用到的主要是EasyX里面的fillrectangle函数,可以画有边框的填充矩形。
下面是俄罗斯方块展示函数具体的代码,判断short数据中每一位是否是1,是的话就填充一个小格子,不是就跳过,循环操作,进行四行四列共16次判断。擦除用的也是这个函数,只不过颜色换为了背景色(黑色),就变相达到了擦除效果。
int color[15][35] = { 0 }; //定义网格属性
//俄罗斯方块展示函数
void DisplayDiamond(unsigned short diamond, Location Loca, int draw_color)
{
int num_x, num_y;
int k = 0;
setfillcolor(draw_color); //设定填充颜色
for (int i = 3; i >= 0; i--) //四行填充
{
for (int j = 0; j < 4; j++) //四列填充
{
if (diamond & (0x8000 >> k))//判断格子是否存在1,20是格子边长
{
fillrectangle(Loca.x + 20 * j, Loca.y + 20 * i, Loca.x + 20 * (j + 1), Loca.y + 20 * (i + 1));
num_x = Loca.x / 20 + j;
num_y = Loca.y / 20 + i;
color[num_x][num_y] = draw_color;
}
k++;
}
}
}
- 设定填充的颜色,这是EasyX的内置函数,直接引用就行;
- 双重循环,进行四行、四列填充;
- 在函数内部进行判断,对short数据类型的diamond(俄罗斯方块)的每一位进行判断,为1则进行填充,为0则跳过;
- color是一个二维数组,用来记录格子对应的颜色,共25行15列,刚好对应格子的数量。该变量是一个全局变量,所以game.c文件的函数都可以引用或赋值该变量。
- 展示一下全部俄罗斯方块,验证一下前面建模是否正确,由下图可以看到,形状是正确的,符合我们的设想。
四、按键响应
移动和翻转需要键盘来进行选择,所以这涉及到与键盘的交互作用,所以需要包含头文件conio.h。conio.h不是C标准库中的头文件,是vc下的一个头文件。conio是Console Input/Output(控制台输入输出)的简写,其中定义了通过控制台进行数据输入和数据输出的函数,主要是一些用户通过按键盘产生的对应操作,比如getch()函数等等。
1.实现方块的自由下落
//main.c
while (MoveEnable(CurDiamond, CurLocation, EnableDown) == 0)
{
DisplayDiamond(CurDiamond, CurLocation, BLACK); //先擦除
CurLocation.y -= 20;
DisplayDiamond(CurDiamond, CurLocation, cur_color); //再更新
Sleep(speed);
if (_kbhit()) //如果敲击了键盘,就会返回1
{
userHit = _getch(); //获取敲击键盘字符
Keyboard(userHit, &row, &column, &CurLocation, &speed);
CurDiamond = Diamond[row][column];
}
}
- 用MoveEnable函数进行判断是否能下降,能下降就进入循环,不能下降就跳出循环。MoveEnable函数是自己定义的一个移动判断函数,后续会进行讲解;
- 循环内部第一步先擦除,即把原来有颜色的俄罗斯方块,填充为黑色,相当于擦除效果;
- 把坐标点的y减去20,然后重新画出该方块,进行一定的延时,实现下降效果;
- 接下来进行键盘的响应,_kbhit是EasyX的内部函数,主要用于检查是否有按键输入;
- 将获得的按键字符传入Keyboard字符处理函数,获得相应的效果。
//game.h
#define EnableDown 1
#define EnableLeft 2
#define EnableRight 3
//game.c
bool MoveEnable(unsigned short diamond, Location CurLocation, int direction)
{
bool stop=0;
int num_x,num_y;
switch (direction)
{
case EnableDown: //判断是否可以下移
{
for (int i = 0; i < GetWidth(diamond); i++) //对每一列进行判断
{
for (int j = 0; j < GetHigh(diamond); j++) //对该列每一行进行判断
{
num_x = CurLocation.x / 20 + i ; //小方格的列序号
num_y = CurLocation.y / 20 + j ; //小方格的行序号
if( diamond & (0x0001<<(3-i+4*j)) )
//如果该小方格是为1,即方块该列最底部
{
if ((color[num_x][num_y - 1] != BLACK) || (CurLocation.y == 0))
//如果下面那个小方格也是非黑色,就要停止
{
stop = 1;
}
break; //找到底部小格子即可跳出循环,进行下一列的判断
}
}
if (stop == 1) break; //既然已经要停止了,就不用去判断下一列
}
break;
}
}
return stop;
}
以上是字符处理函数,里面仅有判断是否可以下移部分代码,是否可以左移/右移代码类似,这里就不进行展示了。
swith语句选择究竟是判断下降、左移、右移;下降判断,其实就是找出每一列最下面那个有色小格子到底在第几行,找到之后就对其下面那个格子进行判断,如果下面那个格子也是有色小格子,就说明不能下降了;如果下面那个格子是黑色小格子,那就可以下降;接下来要找第二列的底部小格子,因为有可能第一列没被堵住,第二列被堵住了。当其中一列下面存在有色格子,就可以把stop变量置1,然后跳出循环,不用判断后面的列了。函数返回bool数据类型,stop这个变量。如果是1,代表存在阻碍,不能再移动了。如果是0,代表不存在阻碍,可以继续移动。以上便是函数的逻辑。
实现方块的改变方向和速度改变
//game.c
//响应键盘函数
void Keyboard(char userHit, int *row, int *column, Location *CurLocation, int *speed)
{
unsigned short Diamond[7][4] = {
{0x000f,0x8888,0x000f,0x8888},
{0x008E,0x0c88,0x00E2,0x044c},
{0x002E,0x088c,0x00E8,0x0c44},
{0x00CC,0x00CC,0x00CC,0x00CC},
{0x006C,0x08c4,0x006c,0x08c4},
{0x004E,0x08c8,0x00E4,0x04c4},
{0x00C6,0x04c8,0x00c6,0x04c8} };
unsigned short CurDiamond;
switch(userHit)
{
case 'w': //改变方向
{
CurDiamond = Diamond[*row][*column];
DisplayDiamond(CurDiamond, *CurLocation, BLACK); //先擦除
if ((*column) < 3)
(*column)++;
else
(*column) = 0;
CurDiamond = Diamond[*row][*column];
DisplayDiamond(CurDiamond, *CurLocation, cur_color); //再更新
break;
}
case 's': //增加下降速度
{
if ((*speed) > 50 )
{
(*speed) -= 50;
}
break;
}
case 'h': //重新开始
{
for (size_t i =0; i < 25; i++) //25行
{
for (size_t j = 0; j < 15; j++) //15列
{
color[j][i] = BLACK; //每一个格子填充黑色
setfillcolor(color[j][i]);
fillrectangle(20 * j, 20 * i, 20 * (j + 1), 20 * (i + 1));
}
}
break;
}
}
}
以上是响应键盘函数。首先对传入的字符进行判断,看到底应该进行左移、右移、加快下降速度、改变方向、重启操作。因为我们装有俄罗斯方块的模型是7*4的二维数组,对应是7种俄罗斯方块的四种方向。所以改变方向其实就是改变Dianmond的列号,我们只要在0-4循环就可以达到这种效果。因为要把行和列都保存下来,传出去函数外部,所以这里用的是指针。
速度是通过影响中间的间隔时间来改变的,只要间隔时间短,下降速度就快。所以每次按下s,speed变量减少50,速度就会变快。同样的,speed也要传出去函数外部,用的也是指针。按下‘h’是重新开始,就是清空已经下落的俄罗斯方块,所以只要把游戏区域的所有格子填充黑色就可以达到效果,同时要改变格子的属性color。
左右移动和方块的下落是同样的原理,只不过改变的是x,每次x改变20,就是一格子的长度。先进行擦除,后重新更新方块,就可以达到预期效果。
五、方块的随机生成和下一块方块的生成
//main.c
unsigned short CurDiamond; //下一个方块
unsigned short NewDiamond; //下一个方块
context(); //画出游戏背景
row = 0;
column = 0;
CurDiamond = Diamond[row][column];
cur_color = BLUE;
while(1)
{
RandomDiamond(&new_row, &new_column, &CurLocation, &speed); //随机生成方块
NewDiamond = Diamond[new_row][new_column];
DisplayDiamond(NewDiamond, { 370,300 }, new_color); //画出下一块方块
while (MoveEnable(CurDiamond, CurLocation, EnableDown) == 0)
{
//自由下落+按键响应代码
}
row = new_row;
column = new_column;
cur_color = new_color;
CurDiamond = Diamond[row][column];
DisplayDiamond(NewDiamond, { 370,300 }, BLACK); //先擦除
FullJudge();
}
方块的随机生成用的是自定义的RandomDiamond函数,生成方块在Diamond数组的行和列,将值赋给NewDiamond,然后在下一块方块区域画出来。while部分进行的curDiamond方块的自由下落和按键响应,当不能下落之后,就会跳出while循环。将上面随机生成的新行号和列号赋给现在的row和column,生成的颜色也赋给cur_color。然后就可以擦除下一块区域的方块,等待新的生成。
在最后,会进行满行判断,满行则消失,这个后面再讲。
//game.c
void RandomDiamond(int* row, int* column, Location* CurLocation, int* speed)
{
static int num = 0;
int color_Diamond[7] = { BLUE, GREEN, CYAN, RED, MAGENTA, BROWN, YELLOW };
*row = rand() % 7; //生成0到6随机数
*column = rand() % 4; //生成0到3随机数
//*row = 0;
*CurLocation = { 140,500 };
*speed = 200;
if (num == 6) num = 0;
new_color = color_Diamond[num++];
}
以上是RandomDiamond俄罗斯方块随机生成函数,一开始会生成随机的row和column,这个就是相当于随机的俄罗斯方块形状。同时也会生成颜色,颜色用的是数组内部循环,于是就生成了五颜六色的俄罗斯方块。
六、满行消除和更新
//game.c
void FullJudge()
{
int color_num=0;
for (size_t i = 0; i < 25; i++) //25行
{
for (size_t j = 0; j < 15; j++) //15列
if (color[j][i] != BLACK) color_num++;
if (color_num == 15) //遇到满行
{
setfillcolor(BLACK); //设定填充黑色
for (size_t j = 0; j < 15; j++) //填充该行15列,即消除该行
{
fillrectangle(j*20, i*20 ,(j+1)*20, (i+1)*20);
color[j][i] = BLACK;
}
Updata(i);//更新
i--; //重新判断该行
}
color_num = 0;
}
}
//消除满行后,上面往后降,重新判断该行
void Updata(int row)
{
//从满行的第row行开始
for (size_t i = row; i < 25; i++)
{
for (size_t j = 0; j < 15; j++) //15列
{
color[j][i] = color[j][i + 1]; //每一个格子等于上面格子的颜色
setfillcolor(color[j][i]);
fillrectangle( 20 * j, 20 * i, 20 * (j + 1), 20 * (i + 1));
}
}
static int game_point = 0;
game_point++; //更新分数
setaspectratio(1, 1);
TCHAR s1[5];
settextstyle(25, 0, _T("Consolas"));
swprintf_s(s1, _T("%d"), game_point);
outtextxy(380, -426, s1);
setaspectratio(1, -1);
}
满行判断就是一行行来判断,首先判断第一行,对每个格子的color进行判断,如果统计格子颜色为非黑色(即存在方块)的数量为列数量15,这就是遇到了满行情况。判断是满行情况,那就对该行所有格子填充黑色,就相当于消除。
消除之后,要把上面的格子落下来,相当于整体往下移了一格,这就是Updata函数的任务,其中还得更新一下分数,一行一分。整体下移之后要重新对该行进行判断,也许说不定下落的那一行又是满行。之后循环判断完25行,任务就完成了。
七、判断真实宽度和真实高度
//game.c
//计算方块的真实高度
int GetHigh(unsigned short diamond)
{
int a=0;
for (size_t i = 0; i < 4; i++)
{
if ( (diamond & (0x000f << 4 * i)) > 0)
a++;
}
return a;
}
//计算方块的真实宽度
int GetWidth(unsigned short diamond)
{
int a = 0;
for (size_t i = 0; i < 4; i++)
{
if ((diamond & (0x8888 >> i)) > 0)
a++;
}
return a;
}
从以下俄罗斯方块能看出,虽然每个俄罗斯方块都占据了4*4小格子的空间大小,但是实际上俄罗斯方块的真实高度和宽度并不一定是4。在进行左右移动的时候,就会需要判断俄罗斯方块的真实高度和宽度,也就是宽度为1,高度为4。判断的逻辑就是和0x000f、0x8888进行与逻辑运算,然后是否大于0。如果等于0,说明这一行/列都是0,那就是没被占用的空间。
以上就是俄罗斯方块代码的大部分内容,并不是全部代码,有一些只讲了逻辑,大家可以自由发挥。
八、运行效果