用C语言写俄罗斯方块

C语言写俄罗斯方块

 

目录(需求):

1.  屏幕作图与窗口实现;

2.  方块的构造与产生;

3.  方块的移动与翻转;

4.  中断计时与方块自由下落;

5.  判断方块碰撞与消行;

6.  按键控制;

7.  扩展新的功能;

开发环境:

1.       编译器tc2.0

2.       编辑器wintc1.8

3.       运行环境xpdosbox

1. 屏幕作图与窗口实现

将整个屏幕划分成四部分:a、主游戏窗口;b、一个给预览下一个方块的4*4窗口;c、记录窗口SCORE和记录窗口LEVELd、提示信息窗口。

 

 

用C语言写俄罗斯方块_第1张图片

 

先来看看这些窗口是怎么绘制的。

程序中所有窗口绘制都抽象为一个窗口实现函数:

 

void DrawWin(char* title, int linecolor, int fillcolor, int x0, int y0, int x1, int y1);

参数:

     title: 窗口标题;

     linecolor:边框线颜色;

     fillcolor:窗口背景填充颜色;

     x0,y0,x1,y1: 窗口左上角和右下角的坐标。

 

     在了解这个函数的具体实现之前,先来看看TC2.0都给我们提供了些什么作图函数。用TC作图时,需要包含头文件 graphics.h ,这个头文件使用了扩展库Graphics.lib。所以在使用作图函数之前,先确定你的Includelib目录下是否有这两个文件。以下是图形函数介绍:

 

void far initgraph(int far *graphdriver,int far *graphmode,char far *pathtodriver);

参数:

     graphdriver: 指向图形驱动序号变量的指针;

     graphmode:指向图形显示模式序号变量的指针;

     pathtodriver:存放图形驱动文件的路径;

 

Tc2.0中用此函数可以切换到图形模式。代码实现格式比较固定,请看代码128~136行:

 

/* * @ 初始化图形环境 */ void Init_Graphics(void) { int gd, gm; gd=DETECT; /*自动检测驱动*/ initgraph(&gd, &gm, ""); /*初始化为图形界面,路劲为""表示驱动在当前目录下的 .BGI文件*/

 

 

其他一些重要的图形函数:

 

/*从(x,y)到(xx,yy)处以setcolor()决定的颜色画直线*/ void line( int x,int y,int xx,int yy); /*linestyle 变化范围为 0 ~ 4,分别表示实线,点线,中心线,点画线, 用户自定义线; upattern处一般写0,thickness只有1、3两个值,分别表示一个像素宽,三个像素宽*/ void setlinestyle(int linestyle,unsigned upattern ,int thickness ); /*决定前景和背景颜色,color 变化范围为 0 ~ 15 */ void setcolor (int color ); void setbkcolor(int color); /*以(x,y)为左上角,以(xx,yy)为右下角画矩形*/ void rectangle(int x,int y,int xx,int yy); /*以(x,y)为左上角,以(xx,yy)为右下角画条形*/ void bar(int x,int y,int xx,int yy); /*pattern可以是 0~12,指定填充图形,color为0~15,指定填充色 */ void setfillstyle(int pattern,int color); /*horiz变化范围为 0 ~ 2,分别代表左对齐,字符串中心对齐,右对齐;vert 变化范围为 0 ~ 2,分别代表底部对齐,中心对齐,顶部对齐*/ void settextjustify(int horiz,int vert); /*font变化范围为 0 ~ 5,表示字体;direction只能是0、1,0、1分别代表水平输出,垂直输出;charsize变化范围为 1 ~ 10,数值越大,字体越大*/ void settextstyle(int font,int direction,int charsize); /*按settextjustify()决定的对齐方式,以setcolor()决定的颜色,在(x,y)附近输出字符串*/ void outtextxy(int x,int y,char* str);

 

    

                                                                           

在来看DrawWin 的实现:

/*从(x,y)到(xx,yy)处以setcolor()决定的颜色画直线*/ void line( int x,int y,int xx,int yy); /*linestyle 变化范围为 0 ~ 4,分别表示实线,点线,中心线,点画线, 用户自定义线; upattern处一般写0,thickness只有1、3两个值,分别表示一个像素宽,三个像素宽*/ void setlinestyle(int linestyle,unsigned upattern ,int thickness ); /*决定前景和背景颜色,color 变化范围为 0 ~ 15 */ void setcolor (int color ); void setbkcolor(int color); /*以(x,y)为左上角,以(xx,yy)为右下角画矩形*/ void rectangle(int x,int y,int xx,int yy); /*以(x,y)为左上角,以(xx,yy)为右下角画条形*/ void bar(int x,int y,int xx,int yy); /*pattern可以是 0~12,指定填充图形,color为0~15,指定填充色 */ void setfillstyle(int pattern,int color); /*horiz变化范围为 0 ~ 2,分别代表左对齐,字符串中心对齐,右对齐;vert 变化范围为 0 ~ 2,分别代表底部对齐,中心对齐,顶部对齐*/ void settextjustify(int horiz,int vert); /*font变化范围为 0 ~ 5,表示字体;direction只能是0、1,0、1分别代表水平输出,垂直输出;charsize变化范围为 1 ~ 10,数值越大,字体越大*/ void settextstyle(int font,int direction,int charsize); /*按settextjustify()决定的对齐方式,以setcolor()决定的颜色,在(x,y)附近输出字符串*/ void outtextxy(int x,int y,char* str);

 

 

 

2.方块的构造与产生

在了解方块构造之前,先来看看程序中使用的几种坐标形式。

(1)    绝对坐标

(2)    相对坐标

(3)    像素坐标

绝对坐标:程序中默认分辨率是 640*480,单位是一个像素,以左上角为坐标(0,0)零点

像素坐标:即单位为一个像素。

相对坐标:程序中以绝对坐标(GS_X, GS_Y)为参考零点,以一个SSIZE*SSIZE方块为单位,水平轴为x,向右增加,垂直轴为y,向下增加的坐标。

方块坐标:即单位为一个SSIZE*SSIZE方块。

 

上面的SSIZEGS_X, GS_Y等均在头文件 square.h中定义:

/*每个块的大小和游戏空间的起始坐标(绝对坐标)*/ #define SSIZE 20 /*一个方块大小为20x20像素*/ #define GS_X 80 #define GS_Y -40 /*在顶端预留4行,形成壁厚为1个SSIZE,上不封顶的杯子状*/ #define GS_LEFT 0 /*"杯子"左壁相对坐标,单位SSIZE*/ #define GS_RIGHT 11 #define GS_TOP 0 #define GS_BOTTOM 24 #define DEAD_LINE 3 /*等于或高于它,证明挂掉了*/

 

 

在上面的代码注释中,提到了“杯子”。实际上,主窗口对应着一个全局的二维数组:

static unsigned char GameSpace[GS_RIGHT-GS_LEFT+1][GS_BOTTOM +1];

这个数组记录了主窗口主所有方块的堆叠情况,根据预定义不能看出它是一个12, 25列的数组,它就是上文提到的“杯子”的抽象。

初始化的时候,这个“杯子”将GS_LEFTGS_RIGHT作为左右壁,数组中对应都赋值为1GS_BOTTOM作为“杯子”的底,也赋值为1。“杯子”其他位置赋值为0,表示为,方块可以在这个区间活动。

有了这个杯子模型,方块的构造和移动实现都方便得多了。因为只要确定了方块的相对坐标(rxry),就很容易得出它在“杯子”中所占据的位置,它就是GameSpace[ry][rx]。正因为如此,方块形状的构造也用到了相对坐标,请看下面的方块形状结构:

 

/*4x4 方块结构*/ typedef struct shape { int xy[32]; /*xy坐标成对出现在数组中, 最多可表示16个小块的图形*/ int size; /*所表示图形的块数*/ int color; /*所表示图形的颜色*/ int next; /*用到翻转对应的下一个形状*/ }Shape;

 

 

现在主要关心前面三个成员,xy[32]size共同确定这个方块的形状。比如:

 

... /* 0 1 2 3 0□■□□ 1□■□□ 2□■□□ 3□■□□ */ { /*0*/ {1,0, 1,1, 1,2, 1,3}, 4, RED, 1 }, ...

  

 

方块都是结构,它也是相对坐标表示,参考点是4*4 SSIZE方块的左上角。如上面的长条形,它由4个小方块(SSIZE*SSIZE)构成,size4.

xy[32]赋值要遵循从左到右,从上到下的循序。所以这个形状的4个小方块坐标依次是(1,0)、(1,1)、(1,2)、(1,3),把它们循序记录到xy[32]中。

第三个结构体成员,表示方块的颜色。

我们如何在游戏时产生这些方块呢?

程序中使用了随机选取的方法,请看:

/* * 随机产生一个方块 * @ 返回这个方块形状的索引号 */ int CreatSquare(void) { int index; int j,i; /*初始化随机种子,time(NULL)返回1970.1.1至今的秒数*/ srand(time(NULL)); index = rand()%SHAPESIZE; /* rand 产生0至RAND_MAX 的随机数*/ return index; }

3.方块的移动与翻转

上面的方块形状结构还有一个成员 next,这个成员就是用来完成翻转的工作的。程序中所有的方块形状都定义在这个结构的数组中。请看:

 

Shape AllShape[]={ /* 0 1 2 3 0□■□□ 1□■□□ 2□■□□ 3□■□□ */ { /*0*/ {1,0, 1,1, 1,2, 1,3}, 4, RED, 1 }, /* 0 1 2 3 0□□□□ 1□□□□ 2■■■■ 3□□□□*/ { /*1*/ {0,2, 1,2, 2,2, 3,2}, 4, RED, 0 }, ...

 

这里只列出AllShape[1].next=0,这说明什么呢?

AllShape中元素在数组中的位置作为索引index,那么第一个长条的索引index=0,在它翻转后,形状变为第二个长条,它的索引值为1,所以index=AllShape[0].next=1。第二个长条翻转也是这个原理。

所以,要得到翻转后的图形,只需要通过索引index等于当前形状的next成员,再通过这个indexAllShape中获得即可。

 

方块的移动,实际上是个不断的画方块、擦除方块,再在新位置画方块的过程。先来看看程序中的实现函数:

void DrawShapeInMain(int rx0, int ry0,int index ) { int i, x, y; for (i=0; i<AllShape[index].size; i++) { if ((y=AllShape[index].xy[2*i+1]+ry0)<=DEAD_LINE) { /*DEAD_LINE以上的部分不用显示*/ continue; } /*通过相对坐标求出绝对坐标*/ x = GS_X+(AllShape[index].xy[2*i]+rx0)*SSIZE; y = GS_Y+y*SSIZE; setfillstyle(HATCH_FILL, AllShape[index].color); bar(x+1, y+1, x+SSIZE-1, y+SSIZE-1); /*画出一个小方块*/ } } void EraseShapeInMain(int rx0, int ry0, int index ) { int i, x, y; for (i=0; i<AllShape[index].size; i++) { if ((y=AllShape[index].xy[2*i+1]+ry0)<=DEAD_LINE) { continue; } x = GS_X+(AllShape[index].xy[2*i]+rx0)*SSIZE; y = GS_Y+y*SSIZE; setfillstyle(SOLID_FILL, BkColor); bar(x+1, y+1, x+SSIZE-1, y+SSIZE-1); } }

DrawShapeInMain是以相对坐标在主窗口中画出方块形状,两个参数rx0ry0代表4*4 SSIZE的方块形状左上角相对于“杯子”左上角的坐标,index代表要画的方块形状的索引。如果方块形状中某个小方块相对于本身4*4 SSIZE的大方块的相对坐标是(rx,ry),容易的算出它相对于杯子形状的相对坐标为(rx0+rxry0+ry),在通过这个相对坐标确定画出方块的绝对坐标。

EraseShapeInMainDrawShapeInMain的原理是一样的,不过填充颜色用的是背景色BkColor这也是一个全局变量。

4.中断计时与方块自由下落

传统的俄罗斯方块都有这样的特性,就是不管你的怎么移动或者翻转方块形状,每隔一定时间这个方块都会自由下落一格。而且,这个自由下落的速度还可能随你达到的分数或是等级而不断增加。这种功能是怎么实现的呢?

一种思路是通过延时来实现。例如,如果要求方块会每隔1s下落一格,就可以在循环中检测按键,响应,然后延时下落,之后再重复这个过程。这种方式有个弊端,就是如果延时的时间过长,按键响应就会变得很不灵敏。如果想用延时来实现方块自由下落,有一种较好的方式,就是将时间微分。比如要求每隔1s下落一格,我们可以在每次按键判断后延时10ms,然后再循环检测按键,当经过100次这样的循环后再执行自由下落。

一种更好的办法是引入中断。系统为我们提供了一个时钟中断,时钟中断大约每秒钟发生18.2次。我们可以设置一个全局的计时变量TimerCounter=0,这个变量会在每次发生中断后加1。在按键检测之后,判断这个变量是否大于我们设置的门限值,就可以确定方块是否该自由下落了。这种方法比起延时要节省系统资源,而且按键响应也会更好。

下面来看下怎么在程序中使用这个中断资源:

/* 时钟中断的中断号 */ #define TIMER 0x1c /* 指向原来时钟中断处理过程入口的中断处理函数指针(句柄) */ void interrupt ( *oldhandler)(void)=NULL; /*注意:tc2.0中只要是全局的都要初始化*/ int TimerCounter=0; /* 计时变量,每秒钟增加18。 */ /* 新的时钟中断处理函数,就是在原处理函数的基础上多了一个TimerCounter++*/ void interrupt newhandler(void) { /* increase the global counter */ TimerCounter++; /* call the old routine */ oldhandler(); } /* 设置新的时钟中断处理过程 */ void SetTimer(void interrupt (*IntProc)(void)) { oldhandler=getvect(TIMER); /* oldhandler 备份原处理函数*/ disable(); /* 设置新的时钟中断处理过程时,禁止所有中断 */ setvect(TIMER,IntProc); /*给TIMER 中断设置处理函数*/ enable(); /* 开启中断 */ } /* 恢复原有的时钟中断处理过程 */ void KillTimer(void) { disable(); setvect(TIMER,oldhandler); /*恢复原处理函数*/ enable(); }   

 

 下面是主程序中按键处理后,处理方块自由下落的一段代码:

 

 /*计时值超出,就会自动下落*/ if (TimerCounter>LvCount[Level]) { /*判断是否能再下落*/ if (MoveCheck(Sq_x, Sq_y+1, Cur_index)==-1) { /* UpdateGameSpace 函数用来更新“杯子”数组和主窗口显示 并返回消行数*/ ret=UpdateGameSpace(Sq_x, Sq_y, Cur_index); if (ret==-1) /*返回-1说明达到DEAD_LINE*/ { PrintGameOver(); return 0; } else if (ret>0) { Score += ret*LAYER_SCORE; Level = GetLvScore(Score); PrintScLv(Score, Level); /*打印分数和等级到对应窗口*/ } next=1; } else /*可以下落则下落一个,并将计数值清零*/ { EraseShapeInMain(Sq_x, Sq_y, Cur_index); Sq_y++; DrawShapeInMain(Sq_x, Sq_y, Cur_index); TimerCounter=0; }

 


5.判断方块碰撞与消行

俄罗斯方块还有另一个特性,当你左右移动,或是翻转遇到旁边正好有填充方块时,你的移动翻转操作就不会被执行;当你下移或者是方块自行下落,遇到下方有填充方块时,这个下落方块就会成为新的堆积物。你可以思考一下,要实现这种功能我们首先需要一个怎样的函数?

我想你很快会得出答案,没错,我们需要一个判断我们这次操作的结果会不会使我们移动或是翻转的方块与已有方块重合的函数。这个函数可以利用其相对坐标和“杯子”空间元素进行比较,得出是否这次移动或翻转是有效的。

这个判断过程在源码中是由MoveCheck这个函数来完成的,请看:

 

/* @ rx0, ry0 -- 待检测的相对坐标 @ index -- 待检测的方块索引*/ int MoveCheck(int rx0, int ry0, int index) { int i, rx, ry; for (i=0; i<AllShape[index].size; i++) { rx =AllShape[index].xy[2*i]+rx0; /*求的相对“杯子”左上角的坐标*/ ry =AllShape[index].xy[2*i+1]+ry0; if (GameSpace[ry][rx]!=0) /*如果这个坐标上已有填充方块,返回-1*/ { return -1; } } return 0; /*如果没有重叠,返回0*/ }

 

MoveCheck的具体用法可以分析第四节最后一段代码。下面来讨论消行的问题。首先思考:你直观理解的消行是什么概念呢?

方块消行应该是这个游戏最核心的问题之一,直观的理解是,当“杯子”一个整行都被方块填充后,这一行将被消除,同时,这一行以上所有行都会下移一行填充空出来的行。要实现这种效果,无疑需要我们扫描“杯子”空间的行,问题是我们该什么时候去扫描,又该如何去扫描呢?

前面说了MoveCheck可以判断方块是否可以再移动,如果当MoveCheck判断出方块不能下移后,活动方块就会成为新的堆积物。因此,如果判断得出这种结论,我们首先要做的工作就是根据新的堆积物更新“杯子”空间。请看:

int UpdateGameSpace(int rx0, int ry0, int index) { ... size= AllShape[index].size; /*将新图形加入 GameSpace*/ color=AllShape[index].color; for (i=0; i<size; i++) { rx =AllShape[index].xy[2*i]+rx0; ry =AllShape[index].xy[2*i+1]+ry0; GameSpace[ry][rx]=color; } ... }   

 

在更新杯子空间之后,我们就可以进行消行扫描了。同样在UpdateGameSpace中,我们采用了一种效率较高的算法,只扫描新的堆积物方块纵坐标范围内行,如果有满行,只更新这行和目前最高行之间的行:

int UpdateGameSpace(int rx0, int ry0, int index) { int i, j, rx, ry, size, color, h1, h2, h, hm; /*h1记录最高的方块y坐标*/ ... /* 由于形状是按从高到底扫描的,所以形状第一个方块y坐标表示形状的最高点,h1 表示形状的最高点,最后一个方块的y坐标就是形状的最低点;消行扫描只需要处理插入图形的最高点和最低点之间的行就行了。另外,y愈小代表方块越高*/ h1 = AllShape[index].xy[1]+ry0; h2 = AllShape[index].xy[2*size-1]+ry0; if (h1<Sq_h) /*更新全局最高点*/ { Sq_h = h1; } hm = Sq_h; /*将hm 作为Sq_h 的备份,后面要用到*/ /*扫描 h1 和 h2之间的GameSpace,如果为满行则消除*/ for (ry=h1; ry<=h2; ry++) /*从上往下扫描*/ { for (rx=GS_LEFT+1; rx<GS_RIGHT; rx++) { if (GameSpace[ry][rx]==0)/*在一行中扫描到一个方块为空,则转下一行*/ { break; } } if (rx<GS_RIGHT) { continue; } /*到这里说明ry 这一行为满行*/ for (j=ry; j>=Sq_h; j--) /*从下往上扫描*/ { for (i=GS_LEFT+1; i<GS_RIGHT; i++) { /*消一行,即让下一行等于它的上一行*/ GameSpace[j][i]=GameSpace[j-1][i]; } } Sq_h++; /*消一行对应Sq_h +1*/ h = ry; } /*for循环结束后 h将记录 最低消行的y坐标位置,更新主窗口图形用到*/ if (Sq_h<=DEAD_LINE) { return -1; } if ((Sq_h-hm)==0) { return 0; } /*PRINTGS;*/ /*更新主窗口*/ for (ry=hm; ry<=h; ry++) { for (rx=GS_LEFT+1; rx<GS_RIGHT; rx++) { color =GameSpace[ry][rx]; if (color==0) { color = BkColor; setfillstyle(SOLID_FILL, color); } else { setfillstyle(HATCH_FILL, color); } i = GS_X+rx*SSIZE; j = GS_Y+ry*SSIZE; bar(i+1, j+1, i+SSIZE-1, j+SSIZE-1); } } return Sq_h-hm; }

 

 

 

 

 

6.按键控制

游戏中是实现的按键功能有:(1)键盘方向键控制方块移动方向;(2)空格键控制翻转;(3)回车键控制暂停与继续;(4ESC键退出游戏。

程序运行时会在循环中不断检查键盘上这几个键的按下与否。在此获取键盘按键信息的函数,我们用的是库函数kbhit(),在执行时,如果按键有按下则该函数返回非0值,一般是1,没有按下则返回0。然后在判断有按键按下的情况下,利用函数bioskey(0)得到具体的按键值。我们将游戏中用到的按键在头文件square.h进行了宏定义:

/*键盘码*/ #define LEFT 0x4b00 #define RIGHT 0x4d00 #define DOWN 0x5000 #define UP 0x4800 #define SPACE 0x3920 #define ESC 0x011b #define ENTER 0x1c0d   


    然后我们可以通过一个switch-case结构对按键作出相应的动作响应。如下所示:

switch (key) { case LEFT: { /*左移操作语句;*/ ... } case RIGHT: { /*右移操作语句;*/ ... } ... default: { break; } }

 

 

 

请大家先自行思考各个按键功能的实现,在参考源码。

 

7.扩展新的功能

 

在游戏功能达到我们预先要求的情况下,我们还可以对该游戏进行扩展,比如可以增加方块的形状,实现具有穿透补空功能的小方块,类似炸弹功能对一定区域进行清除的小方块的功能等,可以充分发挥自己的想象力,并将自己的想法付诸实践,相信你可以做出更加好玩的游戏。在此我们对上述提到的扩展想法简要举出自己的例子:

 

1)添加新的方块:

这个过程其实很简单,在我们提供的源码中,你只需要在AllShape[ ]数组中增加相应的数组元素就可以了,例如游戏中用到的一个“巨大的累赘”方块你可以在AllShape[ ]中找到它:

/* 0 1 2 3 0□■□□ 1□■■■ 2■■■□ 3□□■□*/ { /*19*/ {1,0, 1,1, 2,1, 3,1, 0,2, 1,2, 2,2, 2,3}, 8, GREEN, 19 },

 

 

 

当然,想添加什么方块,这个形状完全由你决定。

 

2)计分与升级:

可以根据消行函数UpdateGameSpace的返回值计算出分数,再通过分数的当前的速度等级。这个过程比较简单,这里就不详述了。

 

3)穿越方块与炸弹方块:

要实现这两种特殊方块,首先在AllShape[]添加这两个方块的形状等信息。

由于是特殊方块,需要进行特殊处理。比如穿越方块,它与其他方块不同在于,在下落过程中遇到障碍物,如果这个障碍物下方还有空格,它不会堆积在它首先遇到的这个障碍物上面,而是会穿越这个障碍,直至新的障碍下方不在有空格为止。你可以先思考下,要到达这个功能我们需要扩展那个函数的功能?

没错,我们要扩展的正是MoveCheck这个函数,让我们来看看:

if (index==CROSS_INDEX) /*如果是穿梭方块*/ { rx =AllShape[index].xy[0]+rx0; ry =AllShape[index].xy[1]+ry0; for (i=ry; i<GS_BOTTOM; i++) { if (GameSpace[i][rx]==0) /*之下还有任意一个为空,返回0*/ { return 0; } } return -1; }...   


炸弹方块功能就留给大家自行分析了。

 

你可能感兴趣的:(游戏,c,timer,扩展,语言,图形)