前言
相信大家一定玩过俄罗斯方块这款小游戏,简单容易上手是老少皆宜的小游戏,今天大家就跟着我来实现这个小游戏吧!让自己学的C语言有用武之地。
为了让俄罗斯方块的开发更为简单些,图像更为丰富,在这里就利用了Easyx库来实现,大家如果没了解过Easyx库,可以先到官网了解下,我们用到的函数不多,大家也可以在代码中用到那个就查看文档中这个函数的用法。官网如下EasyX 文档 - flushmessage。
由于Easyx库必须在C++文件中才能用,我们的源文件必须命名为.cpp,但不用担心,C++是兼容C的,我们依然可以使用C的语法进行操作。所以这篇博客不需要太大的C++知识,总体而言是用C语言写的,学过C语言便可以了。当然如果想简单了解下C++,也可以看我写的这几篇博客。
【从C到C++过渡知识上 - CSDN App】http://t.csdnimg.cn/eRs9m
【从C到C++过渡知识 中(为什么C++支持函数重载,而C不支持函数重载) - CSDN App】http://t.csdnimg.cn/bPaCC
【从C到C++过渡知识 下(深入理解引用与指针的关系) - CSDN App】http://t.csdnimg.cn/NAkzO
我们最终的实现结果如下。
俄罗斯方块
目录
源码
源码如下,大家在下载好Easyx后,配置好图片就可以运行了。
可以看百度网盘或者我的资源下载源码图片。
链接: https://pan.baidu.com/s/1ZRwoI9d-53BIIfCwxNIcmg 提取码: 0000
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<graphics.h>
#include<time.h>
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
enum block_status
{
EXIST,//存在
NO,//不存在
LAND,
ROATE//旋转点
};
struct block
{
IMAGE img;
enum block_status flag;
int x;
int y;
};
enum STATUS
{
OK,
ESC,
GAME_OVER
};
typedef struct project
{
struct block BackGropund[20][10];//背景
IMAGE original[7];//原始六色方块图片,最后一个为背景
struct block Bag[7][4][4];//原始方块包
struct block Down[4][4];//下落方块
enum STATUS status;//状态
int SleepTime;//速度
int Score;//分数
int max;//最高分
ExMessage m;
}SP;
//加载方块包
void LoadImg(SP* p)
{
IMAGE img0;
loadimage(&img0, L"../橙色方块.jpg", 40, 40);
p->original[0] = img0;
IMAGE img1;
loadimage(&img1, L"../紫色方块.jpg", 40, 40);
p->original[1] = img1;
IMAGE img2;
loadimage(&img2, L"../红色方块.jpg", 40, 40);
p->original[2] = img2;
IMAGE img3;
loadimage(&img3, L"../黄色方块.jpg", 40, 40);
p->original[3] = img3;
IMAGE img4;
loadimage(&img4, L"../绿色方块.jpg", 40, 40);
p->original[4] = img4;
IMAGE img5;
loadimage(&img5, L"../蓝色方块.jpg", 40, 40);
p->original[5] = img5;
IMAGE img6;
loadimage(&img6, L"../背景.jpg", 40, 40);
p->original[6] = img6;
}
//加载方块的模板
void LoadBag(SP* p)
{
int i1 = 0, i2 = 0;
//长形
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i2 == 1)
p->Bag[0][i1][i2].flag = EXIST;
else
p->Bag[0][i1][i2].flag = NO;
if(i1==2 && i2==1)
p->Bag[0][i1][i2].flag = ROATE;
p->Bag[0][i1][i2].x = i2 + 1;
p->Bag[0][i1][i2].y = i1 - 3;
}
}
//正方形
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((i2 == 1 || i2 == 2) && (i1==1||i1==2))
p->Bag[1][i1][i2].flag = EXIST;
else
p->Bag[1][i1][i2].flag = NO;
if (i1 == 1 && i2 == 1)//正方形特殊处理
p->Bag[1][i1][i2].flag = ROATE;
p->Bag[1][i1][i2].x = i2 + 1;
p->Bag[1][i1][i2].y = i1 - 3;
}
}
//山形
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 3 && i2 < 3 || i1 == 2 && i2 == 1)
p->Bag[2][i1][i2].flag = EXIST;
else
p->Bag[2][i1][i2].flag = NO;
if (i1 == 2 && i2 == 1)
p->Bag[2][i1][i2].flag = ROATE;
p->Bag[2][i1][i2].x = i2 + 1;
p->Bag[2][i1][i2].y = i1 - 3;
}
}
//右七
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 3 && i2 < 3 || i1 == 2 && i2 == 2)
p->Bag[3][i1][i2].flag = EXIST;
else
p->Bag[3][i1][i2].flag = NO;
if (i1 == 2 && i2 == 2)
p->Bag[3][i1][i2].flag = ROATE;
p->Bag[3][i1][i2].x = i2 + 1;
p->Bag[3][i1][i2].y = i1 - 3;
}
}
//左七
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 3 && i2 > 0 || i1 == 2 && i2 == 1)
p->Bag[4][i1][i2].flag = EXIST;
else
p->Bag[4][i1][i2].flag = NO;
if (i1 == 2 && i2 == 1)
p->Bag[4][i1][i2].flag = ROATE;
p->Bag[4][i1][i2].x = i2 + 1;
p->Bag[4][i1][i2].y = i1 - 3;
}
}
//右Z
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 2 && i2 < 2 || i1 == 1 && (i2 == 1 || i2 == 2))
p->Bag[5][i1][i2].flag = EXIST;
else
p->Bag[5][i1][i2].flag = NO;
if (i1 == 2 && i2 == 1)
p->Bag[5][i1][i2].flag = ROATE;
p->Bag[5][i1][i2].x = i2 + 1;
p->Bag[5][i1][i2].y = i1 - 3;
}
}
//左Z
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 2 && i2 > 1 || i1 == 1 && (i2 == 1 || i2 == 2))
p->Bag[6][i1][i2].flag = EXIST;
else
p->Bag[6][i1][i2].flag = NO;
if (i1 == 2 && i2 == 2)
p->Bag[6][i1][i2].flag = ROATE;
p->Bag[6][i1][i2].x = i2 + 1;
p->Bag[6][i1][i2].y = i1 - 3;
}
}
}
//字符转换
void Change(WCHAR* des, char* src)
{
while (*des++ = *src++);
}
//打印背景
void PrintBackGround(SP* p)
{
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
}
}
FlushBatchDraw();
}
//打印分数信息
void PrintMessage(SP* p)
{
char arr1[50];
WCHAR arr2[50];
//打印当前分数
RECT r = { 400, 200, 600, 300 };
drawtext(_T("当前分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 300, 600, 350 };
sprintf(arr1, "%d", p->Score);
Change(arr2, arr1);
drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//打印最高分数
r = { 400, 0, 600, 200 };
drawtext(_T("最高分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 100, 600, 200 };
sprintf(arr1, "%d", p->max);
Change(arr2, arr1);
drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 400, 600, 450 };
drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 450, 600, 500 };
drawtext(_T("↑旋转↓快速下落"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 500, 600, 550 };
drawtext(_T("Esc退出 空格暂停"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//打印版权
r = { 400, 700, 600, 800 };
drawtext(_T("版权所有CSDN卫胡迪"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
//打印下落方块
void PrintDown(SP* p)
{
int i1, i2;
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)
{
putimage(p->Down[i1][i2].x * 40, p->Down[i1][i2].y * 40, &p->Down[i1][i2].img);
}
}
}
}
//打印全部内容
void Print(SP* p)
{
PrintBackGround(p);
PrintDown(p);
solidrectangle(400, 0, 600, 800);
PrintMessage(p);
FlushBatchDraw();
}
//创造下落方块
void CreatBag(SP* p)
{
int t = rand() % 7;
int colour = (t + rand()) % 6;
int i1, i2;
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2] = p->Bag[t][i1][i2];
//不同方块不同
if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
{
// p->Down[i1][i2].flag != NO
p->Down[i1][i2].img = p->original[colour];
}
}
}
}
//初始化
void Init(SP *p)
{
initgraph(600, 800);// EX_SHOWCONSOLE 控制台
//初始配置
setlinecolor(BLACK);
setlinestyle(PS_SOLID | PS_JOIN_BEVEL, 3);
setbkcolor(WHITE);
settextcolor(BLACK);
settextstyle(20, 0, L"楷体");
setfillcolor(WHITE);
setbkmode(TRANSPARENT);//透明文字
cleardevice();
srand((unsigned int)time(NULL));
BeginBatchDraw();//防止闪屏
//结构体初始化
p->Score = 0;
p->SleepTime = 500;
p->status = OK;
FILE* pf=fopen("../date.text","a+");
if (pf == NULL)
{
perror("Init:fopen");
return;
}
if (fscanf(pf, "%d", &p->max) == EOF)
{
p->max = 0;
}
fclose(pf);
//加载图片,基础方块
LoadImg(p);
//加载七个方块
LoadBag(p);
//创建方块
CreatBag(p);
//设置边界线
line(400, 0, 400, 800);
//初始化背景
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
p->BackGropund[i1][i2].img = p->original[6];
p->BackGropund[i1][i2].flag = NO;
}
}
//打印背景
PrintBackGround(p);
//打印提示版权信息
PrintMessage(p);
FlushBatchDraw();
}
void LeftMove(SP* p)
{
int i1 = 0, i2 = 0;
//全部检查是否合法
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)&&
(p->Down[i1][i2].x-1<0 ||p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x - 1].flag==LAND))
return;
}
}
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2].x -= 1;
}
}
}
void RightMove(SP*p)
{
int i1 = 0, i2 = 0;
//全部检查一遍
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE) &&
(p->Down[i1][i2].x +1 >9|| p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x +1].flag == LAND))
return;
}
}
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2].x += 1;
}
}
}
//旋转方块
void Rorate(SP* p)
{
int i1, i2;
int dx, dy;
struct block tmp[4][4];
//找到旋转点
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (p->Down[i1][i2].flag == ROATE)
{
dx = p->Down[i1][i2].x;
dy = p->Down[i1][i2].y;
goto end;
}
}
}
end:
//正方形直接退出
if (i1 == 1 && i2 == 1)
return;
//临时旋转数组
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
tmp[i1][i2] = p->Down[i1][i2];
tmp[i1][i2].x = dx+dy- p->Down[i1][i2].y;
tmp[i1][i2].y = dy-dx+ p->Down[i1][i2].x;
}
}
//判断是否合法
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((tmp[i1][i2].flag == EXIST || tmp[i1][i2].flag == ROATE) &&
(
tmp[i1][i2].x > 9 || tmp[i1][i2].x < 0 || tmp[i1][i2].y>19
|| p->BackGropund[tmp[i1][i2].y][tmp[i1][i2].x].flag == LAND
)
)
return;
}
}
//合法则复制
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2]= tmp[i1][i2];
}
}
}
void CheckKey(SP*p)
{
peekmessage(&p->m);
if (p->m.vkcode==(VK_DOWN))
{
p->SleepTime = 60;
}
else
{
p->SleepTime = 500;
if (p->m.vkcode == (VK_UP))
{
Rorate(p);
Print(p);
Sleep(200);
}
else if (p->m.vkcode == (VK_LEFT))
{
LeftMove(p);
Print(p);
Sleep(200);
}
else if (p->m.vkcode == (VK_RIGHT))
{
RightMove(p);
Print(p);
Sleep(200);
}
else if (p->m.vkcode == (VK_ESCAPE))
{
p->status = ESC;
}
else if (KEY_PRESS(VK_SPACE))
{
while (!KEY_PRESS(VK_SPACE))
{
Sleep(50);
}
}
}
//消除影响
p->m.vkcode = 0;
flushmessage();
}
//检查删除方块
void CheckDelste(SP* p)
{
int arr[20] = {0};
int i1 = 0, i2 = 0;
int flag = 0;
int sum = 0;
//查找消除行
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
if (p->BackGropund[i1][i2].flag == LAND)
{
flag = 1;
}
else
{
flag = 0;
break;
}
}
arr[i1] = flag;
sum += arr[i1];
}
//检查有无消除行
if (sum == 0)
return;
p->Score += 50 * sum;//加分数,一行50分
//闪烁功能
for (int j = 0; j < 3; j++)
{
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
if(arr[i1]== 0)
putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
else
putimage(i2 * 40, i1 * 40, &p->original[6]);
}
}
FlushBatchDraw();
Sleep(150);
PrintBackGround(p);
Sleep(150);
FlushBatchDraw();
}
//建立临时数组
struct block tmp[20][10];
int t = 19;
for (i1 = 19; i1 >=0; i1--)
{
if (arr[i1] == 0)
{
for (i2 = 0; i2 < 10; i2++)
{
tmp[t][i2] = p->BackGropund[i1][i2];
}
t--;
}
}
//多余赋值为背景
for (i1 = t; i1 >= 0; i1--)
{
for (i2 = 0; i2 < 10; i2++)
{
tmp[t][i2].flag = NO;
tmp[t][i2].img = p->original[6];
}
}
//转换
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
p->BackGropund[i1][i2] = tmp[i1][i2];
}
}
Print(p);
}
void DownJudge(SP*p)
{
int i1, i2;
//检查是否到底
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
//行为y列为x
if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)&&
(p->BackGropund[p->Down[i1][i2].y+1][p->Down[i1][i2].x].flag == LAND || p->Down[i1][i2].y == 19))
{
//复制方块,变为地
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{//
if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
{
if (p->Down[i1][i2].y < 0)
{
p->status = GAME_OVER;
return;
}
p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].img = p->Down[i1][i2].img;
p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].flag = LAND;
}
}
}
//加分数
p->Score += 10;
//检测是否可以消除
CheckDelste(p);
//创建新的下落方块
CreatBag(p);
//打印图片
Print(p);
return;
}
}
}
//正常则下降一格
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2].y += 1;
}
}
//打印图片
Print(p);
}
//游戏结束
void GameOver(SP*p)
{
settextcolor(RED);
settextstyle(50, 0, L"楷体");
//创造最高记录
if (p->max < p->Score)
{
RECT r = { 0, 300, 600, 400 };
drawtext(_T("恭喜你创造了新的记录!"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
FILE* pf = fopen("../date.text", "w+");
fprintf(pf, "%d", p->Score);
fclose(pf);
}
RECT r = { 0, 500, 600, 600 };
drawtext(_T("欢迎下次再玩。"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 0, 700,600, 800 };
drawtext(_T("请按Enter结束……"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
FlushBatchDraw();
//不断检测信息,直到按下enter
ExMessage m = getmessage();
while (m.vkcode != VK_RETURN)
{
m = getmessage();
}
closegraph();
}
int main()
{
SP a;
//初始化
Init(&a);
while (a.status == OK)
{
//检测按键
CheckKey(&a);
//下降处理
DownJudge(&a);
//休眠
Sleep(a.SleepTime);
}
//游戏结束
GameOver(&a);
return 0;
}
图片如下,记得在.cpp上一级目录保存,名字正确,否则会报错。可以用qq截图保存。也可以下载压缩包。
Easyx下载地址EasyX Graphics Library for C++
图片配置
方法一下载资源包
下载文章绑定免费压缩包(如果收费,可能为CSDN默认设置,私信我改为免费),下载后解压。如下图
如果有VS,直接点击 俄罗斯方块.sln就可以跳转到VS开始运行了。
方法二自己配置
注意此时的配置以我文章中的代码为主,如果更改 加载六色方块目录代码内容,要与之对应才可以。分析如下图
这里图片可以用截图保存在.cpp上一级目录,也可以下载文章绑定资源直接复制过去。
分析
我们可以先在网上搜索一些俄罗斯方块的视频游戏,然后再根据自己的想法设计游戏。最终我们可以发现整个的游戏页面可以划分为功能区和游戏区。
游戏区主要进行方块的下落,消除,功能区主要进行分数的显示,操作的介绍,版权等信息。
我们想要的游戏结果便是方块不断地下落,消除记录分数,游戏介绍保存最大的分数。于是我们可以将游戏的整体逻辑分为三大块,游戏的初始化,游戏进行中,游戏的结尾,显然第二个是整个游戏的核心。一次直接写出很难做到,我们可以利用C语言面向过程的特点,逐步解决每个小问题,最终解决整个问题。
游戏初始化
在这里大家遇到不知道的函数,可以在Easyx文档中查看用法。EasyX 文档 - flushmessage为了尽量精简文章不会过多的讲解函数的用法,主要注重在程序的设计思想。
功能区初始化
首先我们就要创建一个窗口,这个窗口的大小,大家可以自己调试设计,我在这里采用的是600*800的窗口。然后我们可以现在画图软件上设计我们的功能区文字,内容。标注出大致的位置,方块的设计。最终设计如下。
功能区划分在(400,0) (600,800)组成的矩形中。方块的大小设置为40*40,于是便是最终的游戏区长20个方块,宽10个方块。当然这是我们在纸上的设计。下一步便是要实现。
设置背景
initgraph是Easyx中的函数,用来创建窗口,我们刚创建的窗口是黑色的,为了为了美观,我们可以将背景设置为白色。然后用背景色刷新屏幕。效果图如下
initgraph(600, 800);
setbkcolor(WHITE);
cleardevice();
注意我们在这里加了个getchar,否则程序运行完就会结束,不会停留在这个界面。
接下来我们便可以画区分线。我们可以设置线为3像素,否则太细了。我们知道两点确定一条直线,我们画直线也十分的简单,只需要用给line函数提供两个坐标即可。代码如下
//设置边界线
line(400, 0, 400, 800);
打印文字
接下来我们便可以设计具体的操作了,我们可以利用←向左移动→向右移动↑旋转↓快速下落,光有这些还不够,我们还可以增加空格暂停,Esc退出的功能。←可以利用输入法中的特殊字符打出。
然后就是打印字符了,但我们不可以用printf,他是在控制台输出文字,我们要在窗口中输出文字要用到drawtext来实现。他是在一个矩形框中输出文字,所以我们要定义一个矩形的位置信息这个位置可以用RECT类型变量实现。只要我们给出矩形左上顶点与右下顶点,那么这个矩形就确定了。于是便有如下的代码。_T()是将里面的内容当成宽字符存储,与我们平时使用的单字节不同。
RECT r = { 400, 400, 600, 450 };
drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
由此通过不断地调试文字的位置,我们便可以写出如下的代码。
r = { 400, 400, 600, 450 };
drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 450, 600, 500 };
drawtext(_T("↑旋转↓快速下落"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 500, 600, 550 };
drawtext(_T("Esc退出 空格暂停"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//打印版权
r = { 400, 700, 600, 800 };
drawtext(_T("版权所有CSDN卫胡迪"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
打印分数
分数背我们以整型的形式进行存储,但要想使用drawtext必须为宽字符,我们便需要将分数先转换为单字符,然后再转换为宽字符。将数字转为单字符我们可以利用C语言的函数sprintf,格式化输出,转换到一个临时数组中存储,然后将单字节转为多字节。
接下来我们来实现Change函数,让单字节变为多字节。实现方式也十分的简单,多字节采用的Unicode编码兼容ASCII,数字在ASCII表中,那么数字在多字节与单字节的唯一区别便是存储大小不一样。我们只需要将单字节的内容一个个拷贝过来就行。类似于strcpy的实现,只不过类型不同罢了。
//字符转换
void Change(WCHAR* des, char* src)
{
while (*des = *src)
{
des++;
src++;
}
}
注意while判断中是一个=,赋值操作,当遇到字符串的结尾时,就停止。'\0'在对于的值为0,上面的代码还可以简化为如下
void Change(WCHAR* des, char* src)
{
while (*des++ = *src++);
}
有了上面的铺垫我们就可以输出当前的分数了。具体位置信息还需要调试。
char arr1[50];
WCHAR arr2[50];
//打印当前分数
RECT r = { 400, 200, 600, 300 };
drawtext(_T("当前分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 300, 600, 350 };
sprintf(arr1, "%d", Score);
Change(arr2, arr1);
drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
读取历史最高分
我们在这里便可以用C语言的文件处理函数。首先我们要理解../表示上一级目录,默认当前目录为.cpp所在的目录。当我们第一次运行游戏的时候,可能还没有存储历史最高分数的文件,便可以采用fopen("../date.text","a+")方式,不存在就创建一个。
然后fscanf(pf, "%d", &max)读取数据,第一次可能会读取失败,就初始化为0.最终的代码如下。
FILE* pf=fopen("../date.text","a+");
if (pf == NULL)
{
perror("Init:fopen");
return;
}
if (fscanf(pf, "%d", &max) == EOF)
{
max = 0;
}
fclose(pf);
一定记得fclose,关闭文件,否则后续的文件操作就不会成功。
综上而言,我们整个的代码如下。
//初始化
void Init(SP *p)
{
initgraph(600, 800);// EX_SHOWCONSOLE 控制台
setbkcolor(WHITE);
cleardevice();
score=0;
//设置边界线
line(400, 0, 400, 800);
char arr1[50];
WCHAR arr2[50];
//打印当前分数
RECT r = { 400, 200, 600, 300 };
drawtext(_T("当前分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 300, 600, 350 };
sprintf(arr1, "%d", Score);
Change(arr2, arr1);
drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//打印最高分数
r = { 400, 0, 600, 200 };
drawtext(_T("最高分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 100, 600, 200 };
sprintf(arr1, "%d", max);
Change(arr2, arr1);
drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 400, 600, 450 };
drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 450, 600, 500 };
drawtext(_T("↑旋转↓快速下落"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 500, 600, 550 };
drawtext(_T("Esc退出 空格暂停"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//打印版权
r = { 400, 700, 600, 800 };
drawtext(_T("版权所有CSDN卫胡迪"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
struct project
然后我们可以定义一个结构体 struct project,用这个结构体来保存我们所需要的变量,在这里肯定会有许多人疑惑为什么要定义结构体,我一个个变量用的十分好?在这里我想说的是,定义结构体简化了我们后序设计函数的复杂度。我们设计函数参数的时候就不需要过多的考虑参数,只需要传递一个结构体指针,便可以在任何地方修改结构体内的值。这个只有当自己写完一次的时候才会恍然大悟,我刚开始也是不理解,直到自己写过几个项目后才恍然大悟,如果你目前不接受这个观点,等到写完后接受的话,就在评论区打出我悟了!
typedef struct project
{
struct block BackGropund[20][10];//背景
IMAGE original[7];//原始六色方块图片,最后一个为背景
struct block Bag[7][4][4];//原始方块包
struct block Down[4][4];//下落方块
enum STATUS status;//状态
int SleepTime;//速度
int Score;//分数
int max;//最高分
ExMessage m;
}SP;
这里面有几个变量,max:历史最高分,Score :当前分数, SleepToime: 速度,第一次写游戏的读者可能不太清楚,速度和休眠时间有什么关系? 在我们运行俄罗斯方块游戏的时候,我们必定会写一个循环来不断地执行程序,直到游戏结束。那方块下落的速度如何体现呢? 我们便可以设定一定的时间间隔来实现,暂停500ms(ms为毫秒)后,方块下落一格打印,暂停500ms后,方块下落一个打印,如此循环就实现了方块的下落操作。说个题外话,为什么我们要主动的暂停500ms,程序的运行需要消耗时间,即使我们不主动暂停,下次打印也会有一定时间。但现在电脑的运行太快了,运行完我们写的几百行代码可能才几毫秒,远远超过了正常人的反应速度。
由上面的分析我们可以明白,我们核心游戏下落的代码是个循环,具体为while还是for根据个人习惯,那么我们便可以设计一个循环判断变量,当这个数为0时就结束,为1的时候就继续,但这种代码的可读性较差,我们只知道0的时候结束,却不理解0时什么含义.于是我们便可以利用枚举常量来定义状态。如下代码。这样我们判断循环的结束便可以写为 while(status == OK),这样写本质还是与0,1进行比较,但他的代码可读性大大提升,代码的健壮性更好。
enum STATUS
{
OK,
ESC,
GAME_OVER
};
ExMessage是用来存储键盘信息的,我们上下左右的实现离不开他。可以用peekmessage函数读取键盘信息并存储在m中。
方块结构体
相信大家一定看到了上面的多个数组,他们都是用来存储方块信息的。下面我们来一一介绍。
首先我们可以看出他都是struct block结构体的数组,我们来看下这个结构体。
struct block
{
IMAGE img;
enum block_status flag;
int x;
int y;
};
每个方块都有其对应的位置,所以x,y便是其位置信息。IMAGE是Easyx中的一种变量,用来表示图片的信息,然后就是flag表示当前方块的状态,是下落,还是已经下落完,成为背景。同理,我们可以枚举来表示状态。
这里的LAND表示已经成为背景,其他几种要在后面讲解。
enum block_status
{
EXIST,//存在
NO,//不存在
LAND,
ROATE//旋转点
};
首先根据我们刚才的分析,可以将游戏输出屏幕用struct block BackGropund[20][10];数组来表示。这样我们后续的消除就方便许多。
加载六色方块
接着我们看下最重要的下落方块。我们知道最初的方块有七个如下。
首先我们可以了解上面的图像是不同的方块组成的,我们因此想要绘制方块首先要将方块加载如文件。此时就保存在IMAGE original[7]里面。
应为我们每个的方块大小为40*40,我们就可以在加载图片的时候缩放大小,用loadimage函数,loadimage(&img0, L"../橙色方块.jpg", 40, 40);格式。在这里../表明在.cpp上一级目录下的橙色方块.jpg图片,这个要配置好否则就会报错。接下来就是依次加载7种方块了,特殊的第七种方块是背景方块。
在这里要记住IMAGE img0;变量只能加载一个图片,加载多个图片会出现黑屏,每加载一个图片,创建一个变量名。
于是总的加载代码如下,在这里我们采用了一个LoadImg函数进行封装,防止主函数过长,不利于观察主题逻辑。
//加载方块包
void LoadImg(SP* p)
{
IMAGE img0;
loadimage(&img0, L"../橙色方块.jpg", 40, 40);
p->original[0] = img0;
IMAGE img1;
loadimage(&img1, L"../紫色方块.jpg", 40, 40);
p->original[1] = img1;
IMAGE img2;
loadimage(&img2, L"../红色方块.jpg", 40, 40);
p->original[2] = img2;
IMAGE img3;
loadimage(&img3, L"../黄色方块.jpg", 40, 40);
p->original[3] = img3;
IMAGE img4;
loadimage(&img4, L"../绿色方块.jpg", 40, 40);
p->original[4] = img4;
IMAGE img5;
loadimage(&img5, L"../蓝色方块.jpg", 40, 40);
p->original[5] = img5;
IMAGE img6;
loadimage(&img6, L"../背景.jpg", 40, 40);
p->original[6] = img6;
}
初始化背景
我们的背景是10*20的方格,所以我们可以用数组进行保存。对背景进行初始化也十分简单。只需要将每一个方块的背景改为original[6],我们刚才加载的背景方块就可以了。然后方块的状态设置为NO。
在这里我们要注意的是[i1][i2]表示第i1行,第i2列的方块,与坐标对应就是(i2*40,i1*40),注意数组的表示方式与我们日常坐标的表示方式不一样。i2对应X,i1对应Y。
//初始化背景
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
p->BackGropund[i1][i2].img = p->original[6];
p->BackGropund[i1][i2].flag = NO;
}
}
我们可以简单的打印背景看看效果如何。我们要用到Easyx中的putimage。将对应下标输入如下。FlushBatchDraw();是刷新缓存区,让画面到屏幕上。
//打印背景
void PrintBackGround(SP* p)
{
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
}
}
FlushBatchDraw();
}
效果图如下
加载七个方块
我们可以将他们都放在4*4的方格中,这样有利于我们统一的操作。然后我们就是存一份七个方块的数据,方便我们每次使用就不用在创建了,直接复制即可。
接着要做的就是在4*4的方块中规划每个方块的位置。大家可以自行画。经过分析七个方块的位置如下。
然后就是加载这七个的位置坐标。我们以第一个长条为例。
我们遍历4*4方块内的每一个方块,如果为我们要的红色区域就把方块的状态改为EXIST存在,否则就改为NO,同时我们把每个方块的坐标进行更改。因为这是用来初始化每一次下落方块的,我们的y坐标一开始不能全为正,要有部分为负才可以。具体可以根据设计调。我这里采用的是全部为负,往下降落一次出现一行。
//长形
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i2 == 1)
p->Bag[0][i1][i2].flag = EXIST;
else
p->Bag[0][i1][i2].flag = NO;
p->Bag[0][i1][i2].x = i2 + 1;
p->Bag[0][i1][i2].y = i1 - 3;
}
}
同理我们可以将七个方块都加载如Bag中,在这里我们先不急着加载剩下的方块,我们来讨论下方块旋转的话题。这是我们游戏核心的存在,也是最难做的一块。
方块旋转
方法一
我们要做的就是将对应的方块旋转90度,但这有很多做法。我们首先了解最为简单的。
如果我们将4*4的方块旋转90度,其内部的图像不就旋转了90度么。我们以下面的图像为例。
那么如何做呢?我换个图让大家更清楚些。
旋转后就是将行变列,列变行。我们可以创建个历史数组,然后在赋值给原来的数组。注意坐标不是原来的坐标,要进行一定的变换。
1的坐标为(x,y),那么1旋转后的坐标为(x+3,y).
2的坐标为(x,y),那么2旋转后的坐标为(x+2,y+1).
3的坐标为(x,y),那么3旋转后的坐标为(x+1,y+2).
4的坐标为(x,y),那么4旋转后的坐标为(x,y+3).
以此类推我们不难发现新的坐标和原来的坐标存在等差数列的关系,于是我们便可以得到如下的代码。
struct block tmp[4][4];
int x = p->Down[0][0].x, y = p->Down[0][0].y;
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
tmp[i2][3-i1] = p->Down[i1][i2];
tmp[i2][3 - i1].x = x + 3 - i1;
tmp[i2][3 - i1].y = y + i2;
}
}
这样写比较方便但有个问题是旋转幅度比较大。我们仔细看上面的旋转图可以发现有些不自然,第二到第三张图片,图片整体的位置提高了。相对而言有些不自然。
方法二
这个方法十分简单,既然要旋转后的位置图片信息,我提前准备好不就可以了么。
我们不用想他如何变换的,旋转一次,一次选第几个图片,不够这种方法要增加额外的变量保存旋转的次数, t=++t%4;r然后根据t来选取图像,再来初始化下降方块的坐标和图片信息。
这种方法的旋转显然是可以尽最大努力达到自然的状态,但工作量有些大了!4*7我们要制作28个4*4方块的信息。感兴趣的读者也可以自行尝试,在这里就不赘述了。
方法三
这种方法要用到数学中旋转的公式,一个点绕另一个点顺时针旋转90度,其坐标是确定的。假设我们绕点a(x0,y0)顺时针旋转90度,b(a,b)将变为(x0+y0-b,y0-x0+a)。感兴趣的读者可以看以下数学证明。
于是我们便可以设置旋转点,然后将方块都绕这个点旋转90度即可。对于长条方块而言,选择的旋转点是(1,2),我们便可以将这个方块状态标为ROATE。于是我们在初始化的时候假设ROATE,于是旋转的函数便可以采用如下的方法。为了方便统一将旋转点放在要下落的方块内,而不是NO中
//长形
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i2 == 1)
p->Bag[0][i1][i2].flag = EXIST;
else
p->Bag[0][i1][i2].flag = NO;
if(i1==2 && i2==1)
p->Bag[0][i1][i2].flag = ROATE;
p->Bag[0][i1][i2].x = i2 + 1;
p->Bag[0][i1][i2].y = i1 - 3;
}
}
//找到旋转点
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (p->Down[i1][i2].flag == ROATE)
{
dx = p->Down[i1][i2].x;
dy = p->Down[i1][i2].y;
goto end;
}
}
}
end:
//正方形直接退出
if (i1 == 1 && i2 == 1)
return;
//临时旋转数组
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
tmp[i1][i2] = p->Down[i1][i2];
tmp[i1][i2].x = dx+dy- p->Down[i1][i2].y;
tmp[i1][i2].y = dy-dx+ p->Down[i1][i2].x;
}
}
当然这种方法的自然度不如第二种,但代码量减少了许多。
同理可以写出剩下6个的Bag,每个方块。在这里就不一一展开了。
下落方块
我们在此之前完成了7个方块的准备,在这了创造一个下落方块便十分简单。这个功能在其他的地方会用到,我们就将他封装成一个函数。
在这里我们要用到rand()函数,我们可以在主函数中初始化一次种子srand((unsigned int)time(NULL));然后再获得随机数。我们可以定义两个变量。t代表是那个方块,colour代表什么颜色。但注意颜色这里只加载了6个,方块有7种,后序读者可以根据自己设计自行添加。于是便可以得到如下代码。
//创造下落方块
void CreatBag(SP* p)
{
int t = rand() % 7;
int colour = (t + rand()) % 6;
int i1, i2;
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2] = p->Bag[t][i1][i2];
//不同方块不同
if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)
{
p->Down[i1][i2].img = p->original[colour];
}
}
}
}
循环中的if判断也可以写为p->Down[i1][i2].flag != NO,两种方式都可以。
我们可以将上述的代码封装在一个初始化Init的函数中,传入struct project结构体指针,然后进行操作,在Init中还可以再次封装,这里就不一一叙述了,总体而言处理后的代码如下。
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<graphics.h>
#include<time.h>
enum block_status
{
EXIST,//存在
NO,//不存在
LAND,
ROATE//旋转点
};
struct block
{
IMAGE img;
enum block_status flag;
int x;
int y;
};
enum STATUS
{
OK,
ESC,
GAME_OVER
};
typedef struct project
{
struct block BackGropund[20][10];//背景
IMAGE original[7];//原始六色方块图片,最后一个为背景
struct block Bag[7][4][4];//原始方块包
struct block Down[4][4];//下落方块
enum STATUS status;//状态
int SleepTime;//速度
int Score;//分数
int max;//最高分
ExMessage m;
}SP;
//加载方块包
void LoadImg(SP* p)
{
IMAGE img0;
loadimage(&img0, L"../橙色方块.jpg", 40, 40);
p->original[0] = img0;
IMAGE img1;
loadimage(&img1, L"../紫色方块.jpg", 40, 40);
p->original[1] = img1;
IMAGE img2;
loadimage(&img2, L"../红色方块.jpg", 40, 40);
p->original[2] = img2;
IMAGE img3;
loadimage(&img3, L"../黄色方块.jpg", 40, 40);
p->original[3] = img3;
IMAGE img4;
loadimage(&img4, L"../绿色方块.jpg", 40, 40);
p->original[4] = img4;
IMAGE img5;
loadimage(&img5, L"../蓝色方块.jpg", 40, 40);
p->original[5] = img5;
IMAGE img6;
loadimage(&img6, L"../背景.jpg", 40, 40);
p->original[6] = img6;
}
//加载方块的模板
void LoadBag(SP* p)
{
int i1 = 0, i2 = 0;
//长形
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i2 == 1)
p->Bag[0][i1][i2].flag = EXIST;
else
p->Bag[0][i1][i2].flag = NO;
if(i1==2 && i2==1)
p->Bag[0][i1][i2].flag = ROATE;
p->Bag[0][i1][i2].x = i2 + 1;
p->Bag[0][i1][i2].y = i1 - 3;
}
}
//正方形
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((i2 == 1 || i2 == 2) && (i1==1||i1==2))
p->Bag[1][i1][i2].flag = EXIST;
else
p->Bag[1][i1][i2].flag = NO;
if (i1 == 1 && i2 == 1)//正方形特殊处理
p->Bag[1][i1][i2].flag = ROATE;
p->Bag[1][i1][i2].x = i2 + 1;
p->Bag[1][i1][i2].y = i1 - 3;
}
}
//山形
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 3 && i2 < 3 || i1 == 2 && i2 == 1)
p->Bag[2][i1][i2].flag = EXIST;
else
p->Bag[2][i1][i2].flag = NO;
if (i1 == 2 && i2 == 1)
p->Bag[2][i1][i2].flag = ROATE;
p->Bag[2][i1][i2].x = i2 + 1;
p->Bag[2][i1][i2].y = i1 - 3;
}
}
//右七
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 3 && i2 < 3 || i1 == 2 && i2 == 2)
p->Bag[3][i1][i2].flag = EXIST;
else
p->Bag[3][i1][i2].flag = NO;
if (i1 == 2 && i2 == 2)
p->Bag[3][i1][i2].flag = ROATE;
p->Bag[3][i1][i2].x = i2 + 1;
p->Bag[3][i1][i2].y = i1 - 3;
}
}
//左七
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 3 && i2 > 0 || i1 == 2 && i2 == 1)
p->Bag[4][i1][i2].flag = EXIST;
else
p->Bag[4][i1][i2].flag = NO;
if (i1 == 2 && i2 == 1)
p->Bag[4][i1][i2].flag = ROATE;
p->Bag[4][i1][i2].x = i2 + 1;
p->Bag[4][i1][i2].y = i1 - 3;
}
}
//右Z
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 2 && i2 < 2 || i1 == 1 && (i2 == 1 || i2 == 2))
p->Bag[5][i1][i2].flag = EXIST;
else
p->Bag[5][i1][i2].flag = NO;
if (i1 == 2 && i2 == 1)
p->Bag[5][i1][i2].flag = ROATE;
p->Bag[5][i1][i2].x = i2 + 1;
p->Bag[5][i1][i2].y = i1 - 3;
}
}
//左Z
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (i1 == 2 && i2 > 1 || i1 == 1 && (i2 == 1 || i2 == 2))
p->Bag[6][i1][i2].flag = EXIST;
else
p->Bag[6][i1][i2].flag = NO;
if (i1 == 2 && i2 == 2)
p->Bag[6][i1][i2].flag = ROATE;
p->Bag[6][i1][i2].x = i2 + 1;
p->Bag[6][i1][i2].y = i1 - 3;
}
}
}
//字符转换
void Change(WCHAR* des, char* src)
{
while (*des++ = *src++);
}
//打印背景
void PrintBackGround(SP* p)
{
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
}
}
FlushBatchDraw();
}
//打印分数信息
void PrintMessage(SP* p)
{
char arr1[50];
WCHAR arr2[50];
//打印当前分数
RECT r = { 400, 200, 600, 300 };
drawtext(_T("当前分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 300, 600, 350 };
sprintf(arr1, "%d", p->Score);
Change(arr2, arr1);
drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//打印最高分数
r = { 400, 0, 600, 200 };
drawtext(_T("最高分数:"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 100, 600, 200 };
sprintf(arr1, "%d", p->max);
Change(arr2, arr1);
drawtext(arr2, &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 400, 600, 450 };
drawtext(_T("←向左移动→向右移动"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 450, 600, 500 };
drawtext(_T("↑旋转↓快速下落"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 400, 500, 600, 550 };
drawtext(_T("Esc退出 空格暂停"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
//打印版权
r = { 400, 700, 600, 800 };
drawtext(_T("版权所有CSDN卫胡迪"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
}
//打印下落方块
void PrintDown(SP* p)
{
int i1, i2;
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)
{
putimage(p->Down[i1][i2].x * 40, p->Down[i1][i2].y * 40, &p->Down[i1][i2].img);
}
}
}
}
//打印全部内容
void Print(SP* p)
{
PrintBackGround(p);
PrintDown(p);
solidrectangle(400, 0, 600, 800);
PrintMessage(p);
FlushBatchDraw();
}
//创造下落方块
void CreatBag(SP* p)
{
int t = rand() % 7;
int colour = (t + rand()) % 6;
int i1, i2;
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2] = p->Bag[t][i1][i2];
//不同方块不同
if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
{
// p->Down[i1][i2].flag != NO
p->Down[i1][i2].img = p->original[colour];
}
}
}
}
//初始化
void Init(SP *p)
{
initgraph(600, 800);// EX_SHOWCONSOLE 控制台
//初始配置
setlinecolor(BLACK);
setlinestyle(PS_SOLID | PS_JOIN_BEVEL, 3);
setbkcolor(WHITE);
settextcolor(BLACK);
settextstyle(20, 0, L"楷体");
setfillcolor(WHITE);
setbkmode(TRANSPARENT);//透明文字
cleardevice();
srand((unsigned int)time(NULL));
BeginBatchDraw();//防止闪屏
//结构体初始化
p->Score = 0;
p->SleepTime = 500;
p->status = OK;
FILE* pf=fopen("../date.text","a+");
if (pf == NULL)
{
perror("Init:fopen");
return;
}
if (fscanf(pf, "%d", &p->max) == EOF)
{
p->max = 0;
}
fclose(pf);
//加载图片,基础方块
LoadImg(p);
//加载七个方块
LoadBag(p);
//创建方块
CreatBag(p);
//设置边界线
line(400, 0, 400, 800);
//初始化背景
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
p->BackGropund[i1][i2].img = p->original[6];
p->BackGropund[i1][i2].flag = NO;
}
}
//打印背景
PrintBackGround(p);
//打印提示版权信息
PrintMessage(p);
FlushBatchDraw();
}
int main()
{
SP a;
//初始化
Init(&a);
return 0;
}
对于这种代码可以采用逐步深入的方式,看好注释,然后从上往下读,遇到函数往上找定义。相信经过上面的分析,这段代码看起来是十分简单的。
接下来是游戏的核心,循环处理了。读者可以休息会再看。
游戏进行
首先我们进行循环判断的条件就十分的清楚,只需要判断,结构体a的状态就可以了。然后每次循环我们主动的休眠500ms,大家也可以自行调试时间。时间越短所需要的反应越快。整体的逻辑代码如下。
int main()
{
SP a;
//初始化
Init(&a);
while (a.status == OK)
{
//休眠
Sleep(a.SleepTime);
}
return 0;
}
下降判断
这段代码显然不会是几行就能解决的,我们就可以将他封装在一个函数内。
遇到LAND或者到达底部
当我们的方块一直往下掉落的时候,有两者情况停止下落,一种是到达底部,另一种是遇到LAND,LAND就是其他方块到达底部后的状态。
于是我们便可以遍历检查。
void DownJudge(SP*p)
{
int i1, i2;
//检查是否到底
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
//行为y列为x
if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)&&
(p->BackGropund[p->Down[i1][i2].y+1][p->Down[i1][i2].x].flag == LAND || p->Down[i1][i2].y == 19))
{
}
}
}
}
这个的判断条件比较长我们逐步来看,首先判断的是状态为EXIST或者为ROATE,因为我们选取原来EXIST中一点当作旋转点,所以ROATE也是需要判断的一点,然后是后半段判断判断下落方块的y是不是19,或者当前方块的下一个为LAND,如果满足上述判断就代表要进行。复制方块,把EXIST变为LAND的操作。
复制的操作也十分的简单,如果这个方块的状态满足要求,就复制到背景中,并将背景方块的状态改为LAND,注意如果在某些方块存在,并且y小于0,便代表了游戏结束,把游戏状态改为GAME_OVER,并且直接返回,不进行后序操作。
//复制方块,变为地
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
{
if (p->Down[i1][i2].y < 0)
{
p->status = GAME_OVER;
return;
}
p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].img = p->Down[i1][i2].img;
p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].flag = LAND;
}
}
}
如果没有游戏结束,那么便要考虑接下来的操作了。下落一个方块,分数加10分,并且要创建新的方块,我们可以直接调用前面写的CreatBag(p);函数。但我们还有个重要的判断,假如一行全是LAND方块,我们就可以进行消除。所以在这里我们在封装一个函数,检测消除。
消除检测
我们消除的条件是一行全为LAND,便可以定义一个数组表示每一行是否要消除。于是便有如下代码。需要消除的行赋值为1,不需要赋值为0.如果没有要消除的直接返回,否则进行下面的代码。
//检查删除方块
void CheckDelste(SP* p)
{
int arr[20] = {0};
int i1 = 0, i2 = 0;
int flag = 0;
int sum = 0;
//查找消除行
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
if (p->BackGropund[i1][i2].flag == LAND)
{
flag = 1;
}
else
{
flag = 0;
break;
}
}
arr[i1] = flag;
sum += arr[i1];
}
//检查有无消除行
if (sum == 0)
return;
p->Score += 50 * sum;//加分数,一行50分
}
接下来是处理消除行。我们如何做呢?创建一个临时数组,从后往前遍历,不是消除的复制这一行,是消除的跳过,最后临时数组还有几行全部赋值为背景,最后再打印。完整代码如下。
//建立临时数组
struct block tmp[20][10];
int t = 19;
for (i1 = 19; i1 >=0; i1--)
{
if (arr[i1] == 0)
{
for (i2 = 0; i2 < 10; i2++)
{
tmp[t][i2] = p->BackGropund[i1][i2];
}
t--;
}
}
//多余赋值为背景
for (i1 = t; i1 >= 0; i1--)
{
for (i2 = 0; i2 < 10; i2++)
{
tmp[t][i2].flag = NO;
tmp[t][i2].img = p->original[6];
}
}
//转换
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
p->BackGropund[i1][i2] = tmp[i1][i2];
}
}
Print(p);
这样可以完成我们的消除操作,但消除行直接一闪而过,为了消除效果更加好,我们增加一个闪烁的效果。整体闪烁三次。
如果消除行就打印背景方块,然后再打印PrintBackGround(p);泽里的背景方块还未处理,所以消除行还会被打印。暂停150ms,然后刷新缓存区。
//闪烁功能
for (int j = 0; j < 3; j++)
{
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
if(arr[i1]== 0)
putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
else
putimage(i2 * 40, i1 * 40, &p->original[6]);
}
}
FlushBatchDraw();
Sleep(150);
PrintBackGround(p);
Sleep(150);
FlushBatchDraw();
}
正常下落
如果没有检测到底,就正常下落,y坐标加一。代码如下。
//正常则下降一格
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2].y += 1;
}
}
//打印图片
Print(p);
总体而言这块的总代码如下。
//检查删除方块
void CheckDelste(SP* p)
{
int arr[20] = {0};
int i1 = 0, i2 = 0;
int flag = 0;
int sum = 0;
//查找消除行
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
if (p->BackGropund[i1][i2].flag == LAND)
{
flag = 1;
}
else
{
flag = 0;
break;
}
}
arr[i1] = flag;
sum += arr[i1];
}
//检查有无消除行
if (sum == 0)
return;
p->Score += 50 * sum;//加分数,一行50分
//闪烁功能
for (int j = 0; j < 3; j++)
{
int i1 = 0, i2 = 0;
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
if(arr[i1]== 0)
putimage(i2 * 40, i1 * 40, &p->BackGropund[i1][i2].img);
else
putimage(i2 * 40, i1 * 40, &p->original[6]);
}
}
FlushBatchDraw();
Sleep(150);
PrintBackGround(p);
Sleep(150);
FlushBatchDraw();
}
//建立临时数组
struct block tmp[20][10];
int t = 19;
for (i1 = 19; i1 >=0; i1--)
{
if (arr[i1] == 0)
{
for (i2 = 0; i2 < 10; i2++)
{
tmp[t][i2] = p->BackGropund[i1][i2];
}
t--;
}
}
//多余赋值为背景
for (i1 = t; i1 >= 0; i1--)
{
for (i2 = 0; i2 < 10; i2++)
{
tmp[t][i2].flag = NO;
tmp[t][i2].img = p->original[6];
}
}
//转换
for (i1 = 0; i1 < 20; i1++)
{
for (i2 = 0; i2 < 10; i2++)
{
p->BackGropund[i1][i2] = tmp[i1][i2];
}
}
Print(p);
}
void DownJudge(SP*p)
{
int i1, i2;
//检查是否到底
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
//行为y列为x
if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)&&
(p->BackGropund[p->Down[i1][i2].y+1][p->Down[i1][i2].x].flag == LAND || p->Down[i1][i2].y == 19))
{
//复制方块,变为地
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE)
{
if (p->Down[i1][i2].y < 0)
{
p->status = GAME_OVER;
return;
}
p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].img = p->Down[i1][i2].img;
p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x].flag = LAND;
}
}
}
//加分数
p->Score += 10;
//检测是否可以消除
CheckDelste(p);
//创建新的下落方块
CreatBag(p);
return;
}
}
}
//正常则下降一格
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2].y += 1;
}
}
//打印图片
Print(p);
}
检测按键
写完上面的代码后,我们的游戏一句完成一半了,可以正常的下落。视频如下。
俄罗斯方块半成品
接下来我们再次封装一个检测按钮的函数,用来实现左移右移与旋转。
int main()
{
SP a;
//初始化
Init(&a);
while (a.status == OK)
{
//检测按键
CheckKey(&a);
//下降处理
DownJudge(&a);
//休眠
Sleep(a.SleepTime);
}
return 0;
}
在这里我们要用到peekmessage函数,注意不能用getmessage,这个函数用于获取一个消息。如果当前消息队列中没有,就一直等待。我们要立即返回的就用peekmessage。
于是我们便可以写出如下的结构。然后对应处理每个按钮即可,为了防止这个代码过于臃肿。可以将每个按钮的操作再分装在一个函数内。
注意我们在和函数的最后加了两条语句,一个是刷新消息缓存区,一个是初始化当前的虚拟键码,这两步都是为了消除上一次循环的影响。
void CheckKey(SP*p)
{
peekmessage(&p->m);
if (p->m.vkcode==(VK_DOWN))
{
}
else if (p->m.vkcode == (VK_UP))
{
}
else if (p->m.vkcode == (VK_LEFT))
{
}
else if (p->m.vkcode == (VK_RIGHT))
{
}
else if (p->m.vkcode == (VK_ESCAPE))
{
}
else if (p->m.vkcode == (VK_SPACE))
{
}
p->m.vkcode = 0;
flushmessage();
}
左移
这个操作十分简单只需要将方块的x值减一就可以了,但我们还要检测左移是否合法,不合法就不支持左移。VK_LEFT是虚拟键码,大家可以在官网查询虚拟键码 (Winuser.h) - Win32 apps | Microsoft Learn
如下,我们就可以完成一次左移。
void LeftMove(SP* p)
{
int i1 = 0, i2 = 0;
//全部检查是否合法
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag==ROATE)&&
(p->Down[i1][i2].x-1<0 ||p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x - 1].flag==LAND))
return;
}
}
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2].x -= 1;
}
}
}
为了效果更加的好,我们在左移后就立即打印,并且停留200ms
else if (p->m.vkcode == (VK_LEFT))
{
LeftMove(p);
Print(p);
Sleep(200);
}
右移
右移与左移十分的相似,都是要检测是否合法。
void RightMove(SP*p)
{
int i1 = 0, i2 = 0;
//全部检查一遍
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((p->Down[i1][i2].flag == EXIST || p->Down[i1][i2].flag == ROATE) &&
(p->Down[i1][i2].x +1 >9|| p->BackGropund[p->Down[i1][i2].y][p->Down[i1][i2].x +1].flag == LAND))
return;
}
}
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2].x += 1;
}
}
}
下落
此时我们要的是方块快速下落,我们就可以将SleepTime 改为60ms,就可以达到快速下落的操作,但我们要在不按↓键的时候将SleepTime改为500;我们可以在其他的分支语句中加上p->SleepTime = 500;也可以将上述的结构简单修改。如下代码。
void CheckKey(SP*p)
{
peekmessage(&p->m);
if (p->m.vkcode==(VK_DOWN))
{
p->SleepTime = 60;
}
else
{
p->SleepTime = 500;
if (p->m.vkcode == (VK_UP))
{
}
else if (p->m.vkcode == (VK_LEFT))
{
LeftMove(p);
Print(p);
Sleep(200);
}
else if (p->m.vkcode == (VK_RIGHT))
{
RightMove(p);
Print(p);
Sleep(200);
}
else if (p->m.vkcode == (VK_ESCAPE))
{
}
else if (p->m.vkcode == (VK_SPACE))
{
}
}
p->m.vkcode = 0;
flushmessage();
}
退出
这个功能也十分简单就可以实现,将状态改为ESC即可
else if (p->m.vkcode == (VK_ESCAPE))
{
p->status = ESC;
}
暂停
在这里我用不论我用getmessage,还是peekmessage总会出现BUG,我也找不到具体原因,不得已之下换了一种检测按钮的方式.利用其他的函数检测。当对于的虚拟键码存在时便为真。
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
当我们第一次按下暂停进入分支,第二次按下退出循环。
else if (KEY_PRESS(VK_SPACE))
{
while (!KEY_PRESS(VK_SPACE))
{
Sleep(50);
}
}
在这里我只能说抱歉,Easyx对于API进行了再次封装,里面的细节看不到,我也找不出问题所在,如果读者有什么解决办法可以发在评论区,不胜感激。当然也不排除这个函数本身的问题。
旋转
接下来是我们的最重要的操作,将方块进行旋转。首先我们还是可以创建一个临时数组,然后检测合法性,最后在进行复制。
首先我们要找到旋转点,求出dx,dy,其中正方形要特殊处理。应为旋转点无论旋转几次,他在4*4方格中的位置不变。我们由此就可以判断是否为正方形,如果为正方形就直接退出。
int i1, i2;
int dx, dy;
struct block tmp[4][4];
//找到旋转点
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (p->Down[i1][i2].flag == ROATE)
{
dx = p->Down[i1][i2].x;
dy = p->Down[i1][i2].y;
goto end;
}
}
}
end:
//正方形直接退出
if (i1 == 1 && i2 == 1)
return;
接下来就是创建临时数组,每个点的坐标关系如上文方块旋转所说。在这里我们不需要将临时数组在4*4方格中的位置变换,因为我们最终看的是方块的坐标,而不是在4*4方格中的位置。
//临时旋转数组
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
tmp[i1][i2] = p->Down[i1][i2];
tmp[i1][i2].x = dx+dy- p->Down[i1][i2].y;
tmp[i1][i2].y = dy-dx+ p->Down[i1][i2].x;
}
}
接下来就是判断是否合法,不合法直接退出,合法就复制,
//判断是否合法
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((tmp[i1][i2].flag == EXIST || tmp[i1][i2].flag == ROATE) &&
(
tmp[i1][i2].x > 9 || tmp[i1][i2].x < 0 || tmp[i1][i2].y>19
|| p->BackGropund[tmp[i1][i2].y][tmp[i1][i2].x].flag == LAND
)
)
return;
}
}
//合法则复制
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2]= tmp[i1][i2];
}
}
完整代码如下
//旋转方块
void Rorate(SP* p)
{
int i1, i2;
int dx, dy;
struct block tmp[4][4];
//找到旋转点
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if (p->Down[i1][i2].flag == ROATE)
{
dx = p->Down[i1][i2].x;
dy = p->Down[i1][i2].y;
goto end;
}
}
}
end:
//正方形直接退出
if (i1 == 1 && i2 == 1)
return;
//临时旋转数组
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
tmp[i1][i2] = p->Down[i1][i2];
tmp[i1][i2].x = dx+dy- p->Down[i1][i2].y;
tmp[i1][i2].y = dy-dx+ p->Down[i1][i2].x;
}
}
//判断是否合法
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
if ((tmp[i1][i2].flag == EXIST || tmp[i1][i2].flag == ROATE) &&
(
tmp[i1][i2].x > 9 || tmp[i1][i2].x < 0 || tmp[i1][i2].y>19
|| p->BackGropund[tmp[i1][i2].y][tmp[i1][i2].x].flag == LAND
)
)
return;
}
}
//合法则复制
for (i1 = 0; i1 < 4; i1++)
{
for (i2 = 0; i2 < 4; i2++)
{
p->Down[i1][i2]= tmp[i1][i2];
}
}
}
最终我们的按钮检测就可以如下表示。
void CheckKey(SP*p)
{
peekmessage(&p->m);
if (p->m.vkcode==(VK_DOWN))
{
p->SleepTime = 60;
}
else
{
p->SleepTime = 500;
if (p->m.vkcode == (VK_UP))
{
Rorate(p);
Print(p);
Sleep(200);
}
else if (p->m.vkcode == (VK_LEFT))
{
LeftMove(p);
Print(p);
Sleep(200);
}
else if (p->m.vkcode == (VK_RIGHT))
{
RightMove(p);
Print(p);
Sleep(200);
}
else if (p->m.vkcode == (VK_ESCAPE))
{
p->status = ESC;
}
else if (KEY_PRESS(VK_SPACE))
{
while (!KEY_PRESS(VK_SPACE))
{
Sleep(50);
}
}
}
//消除影响
p->m.vkcode = 0;
flushmessage();
}
到这里我们就完成了游戏的大部分了,现在游戏就可以正常的运行了,等不下的读者可以先玩了。下面是最后一部分结尾工作了。
游戏结束
同理我们可以封装成一个函数解决,减少主函数的复杂度。
int main()
{
SP a;
//初始化
Init(&a);
while (a.status == OK)
{
//检测按键
CheckKey(&a);
//下降处理
DownJudge(&a);
//休眠
Sleep(a.SleepTime);
}
//游戏结束
GameOver(&a);
return 0;
}
保存分数
首先我们要做的就是看看是否创造记录,如果创造记录就保存。
在这了我们用的是w+刚好可以将原来的文件删除,创建新的文件,保存最高记录。同时为了庆祝,还可以打印些文字,读者可以自行安排。
settextcolor(RED);
settextstyle(50, 0, L"楷体");
//创造最高记录
if (p->max < p->Score)
{
RECT r = { 0, 300, 600, 400 };
drawtext(_T("恭喜你创造了新的记录!"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
FILE* pf = fopen("../date.text", "w+");
fprintf(pf, "%d", p->Score);
fclose(pf);
}
然后可以打印处游戏结束语,为了不直接闪过,我们加个循环检测Enter,让用户主动结束页面。当然在最后不要忘了结束界面 closegraph();。
RECT r = { 0, 500, 600, 600 };
drawtext(_T("欢迎下次再玩。"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 0, 700,600, 800 };
drawtext(_T("请按Enter结束……"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
FlushBatchDraw();
//不断检测信息,直到按下enter
ExMessage m = getmessage();
while (m.vkcode != VK_RETURN)
{
m = getmessage();
}
closegraph();
读者可自行添加其他文字,总的代码如下
//游戏结束
void GameOver(SP*p)
{
settextcolor(RED);
settextstyle(50, 0, L"楷体");
//创造最高记录
if (p->max < p->Score)
{
RECT r = { 0, 300, 600, 400 };
drawtext(_T("恭喜你创造了新的记录!"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
FILE* pf = fopen("../date.text", "w+");
fprintf(pf, "%d", p->Score);
fclose(pf);
}
RECT r = { 0, 500, 600, 600 };
drawtext(_T("欢迎下次再玩。"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
r = { 0, 700,600, 800 };
drawtext(_T("请按Enter结束……"), &r, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
FlushBatchDraw();
//不断检测信息,直到按下enter
ExMessage m = getmessage();
while (m.vkcode != VK_RETURN)
{
m = getmessage();
}
closegraph();
}
到这里我们的代码就写完了。十分的不容易,感谢你可以读到此处。