推荐给好友 上一篇:C语言应用篇——用递归法解决商人渡河问题   下一篇:浅谈C语言的编程风格

TC2编写俄罗斯方块实例

  很多编程爱好者都编写过俄罗斯方块的游戏程序。很久以前,我用Tc2.0也做过一个;最近有好些朋友看见我以前的俄罗斯方块的程序后,
问我是怎么做的。我一直想把这个程序的整个过程写一份详细的东西,与各位编程爱好者分享,一直没空。正好现在放假了,而且离回家还有几天。于是我就把这个程序重新写了一遍,尽量使程序的结构比较清晰好懂一些。同时写了下面的这份东西。  俄罗斯方块游戏的程序中用到了一些方法。为了比较容易理解这些方法,我在讲述的同时写了些专门针对这些方法的示例程序。这些示例程序力求短小,目的是用最小的代码能够清楚的示例所用的方法。这些示例程序都经过tc2.0测试。最后还附了完整的俄罗斯方块游戏的源代码,和最终的可执行程序。如果你看了这份东东,有什么意见和想法,请发电子邮件告诉我。我将会继续更新这分东东,最新的版本可以在我的个人主页上下载。  下面的问题是有关俄罗斯方块程序的,其中有些是朋友问我的,有些是我认为可能会被问到的。我尽量按问题从易到难排列这些问题。关于俄罗斯方块程序的一些问题:
******************************************************
Tc2.0中怎么样设置图形显示?
Tc2.0中常用图形函数的用法?
怎样获取鍵盘输入?
怎样控制方块的移动?
怎样控制时间间隔(用于游戏中控制形状的下落)?
游戏中的各种形状及整个游戏空间怎么用数据表示?
游戏中怎么判断左右及向下移动的可能性?
游戏中怎么判断某一形状旋转的可能性?
按向下方向键时加速某一形状下落速度的处理?
怎么判断某一形状已经到底?
怎么判断某一已经被填满?
怎么消去已经被填满的一行?
怎么消去某一形状落到底后能够消去的所有的行?(如长条最多可以消去四行)
怎样修改游戏板的状态?
怎样统计分数?
怎样处理升级后的加速问题?
怎样判断游戏结束?
关于计分板设计的问题。
关于“下一个”形状取法的问题。
剩下的问题。******************************************************
新的问题:
 我想有一个最高记录的显示,应该怎么做呀?
 我想实现一个进度存储功能,应该怎么做呀?
Tc2.0中怎么样设置图形显示?  Tc2.0中有两种显示模式,一种是我们所熟知的字符模式,另一种是图形模式。在字符模式下只能显式字符,如ASCII字符。一般是显示25
行,每行80个字符。程序缺省的是字符模式。在字符模式下不能显式图形和进行绘图操作。要想进行图形显示和绘图操作,必须切换到图形模
式下。  Tc2.0中用initgraph()函数可以切换到图形模式,用closegraph()可以从图形模式切换回字符模式。initgraph()和closegraph()都是图形
函数,使用图形函数必须包括头文件"graphics.h"。  voidfarinitgraph(intfar*graphdriver,intfar*graphmode,charfar*pathtodriver);graphdriver是上涨指向图形驱动序号变量的指针;graphmode是在graphdriver选定后,指向图形显示模式序号变量的指针。pathtodriver表示存放图形驱动文件的路径。

  Tc2.0中有多种图形驱动,每种图形驱动下又有几种图形显示模式。在我的程序中图形驱动序号为VGA,图形显示模式序号为VGAHI。这是一种分辨率为640*480(从左到右坐标依次为0-639,从上到下坐标依次为0-479),能够显示16种颜色的图形模式。别的图形驱动序号和图形显示模式序号,可以从手册或联机帮助中找到。

  pathtodriver指示存放图形驱动文件的路径。图形驱动序号不同,图形驱动文件也不同。序号为VGA图形驱动对应"egavga.bgi"这个图形驱动文件。"egavga.bgi"一般在Tc目录下。voidfarclosegraph(void);
  没有参数,从图形模式直接返回字符模式。initgraph()和closegraph()的常用用法如下:
intgdriver=VGA,gmode=VGAHI,errorcode;

/*initializegraphicsmode*/
initgraph(&gdriver,&gmode,"e:\\tc2");/*readresultofinitialization*/
errorcode=graphresult();if(errorcode!=grOk)/*anerroroccurred*/
{
printf("Graphicserror:%s\n",grapherrormsg(errorcode));
printf("Pressanykeytohalt:");
getch();
exit(1);/*returnwitherrorcode*/
}/*returntotextmode*/
closegraph();
Tc2.0中常用图形函数的用法?在这里讲几个游戏中用到的绘图用的图形函数:
setcolor();
line();
rectangle();
settextjustify();
outtextxy();
setfillstyle();
bar();voidfarsetcolor(intcolor);
  设置画线、画框和在图形模式下显示文字的当前颜色。这个函数将影响line()、rectangle()和outtextxy()函数绘图的颜色。
color可以取常的颜色常量:
BLACK?0
BLUE?1
GREEN?2
CYAN?3
RED?4
MAGENTA?5
BROWN?6
LIGHTGRAY?7
DARKGRAY?8
LIGHTBLUE?9
LIGHTGREEN?10
LIGHTCYAN?11
LIGHTRED?12
LIGHTMAGENTA?13
YELLOW?14
WHITE?15voidfarline(intx1,inty1,intx2,inty2);
用当前颜色从(x1,y1)画一条到(x2,y2)的线段。voidfarrectangle(intleft,inttop,intright,intbottom);
用当前颜色画一个左上角为(left,top)、右下角为(right,bottom)的矩形框。voidfarsettextjustify(inthorz,intvert);
设置图形模式下文字输出的对齐方式。主要影响outtextxy()函数。
horiz和vert可取如下枚举常量:
horiz?LEFT_TEXT?0?Left-justifytext
?CENTER_TEXT?1?Centertext
?RIGHT_TEXT?2?Right-justifytext
vert?BOTTOM_TEXT?0?Justifyfrombottom
?CENTER_TEXT?1?Centertext
?TOP_TEXT?2?Justifyfromtopvoidfarouttextxy(intx,inty,char*textstring);
在(x,y)处用当前字体(缺省的字体是DEFAULT_FONT)显示字符串textstring,字符串的对齐方式由settextjustify()指定。voidfarsetfillstyle(intpattern,intcolor);
设置图形的填充模式和填充颜色,主要影响bar()等函数。
pattern一般取枚举常量值SOLID_FILL,color的取值与setcolor(intcolor)中color的取值范围相同。  介绍完了前面两个问题,现在来写一个程序。这个程序演示前了面所介绍的几个图形函数。
程序prog1.c
怎样获取鍵盘输入?  在Tc2.0中有一个处理键盘输入的函数bioskey();
intbioskey(intcmd);
  当cmd为1时,bioskey()检测是否有键按下。没有键按下时返回0;有键按下时返回按键码(任何按键码都不为0),但此时并不将检测到的按
键码从键盘缓冲队列中清除。
  当cmd为0时,bioskey()返回键盘缓冲队列中的按键码,并将此按键码从键盘缓冲队列中清除。如果键盘缓冲队列为空,则一直等到有键按
下,才将得到的按键码返回。for(;;)
{
key=bioskey(0);/*waitforakeystroke*/
printf("0x%x\n",key);
if(key==0x11b)break;/*Escape*/
}#defineVK_LEFT0x4b00
#defineVK_RIGHT0x4d00
#defineVK_DOWN0x5000
#defineVK_UP0x4800
#defineVK_HOME0x4700
#defineVK_END0x4f00
#defineVK_SPACE0x3920
#defineVK_ESC0x011b
#defineVK_ENTER0x1c0d
  完整的程序请参见prog2.c、prog3.c。
prog2.c获取按键的按键码,按Escape键退出程序。
prog3.c根据不同的按键进行不同的操作,按Escape键退出程序。
怎样控制方块的移动?
  方块移动的实现很简单,将方块原来的位置用背景色画一个同样大小的方块,将原来的方块涂去。然后在新的位置上重新绘制方块就可以
了。这样就实现了方块的移动。完整的程序请参见prog4.c。这个用方向键控制一个黄色的小方块在屏幕上上、下、左、右移动。这个程序用到了前面几个问题讲的内容,如果你有点忘了,还要回头看看哦。:)
怎样控制时间间隔(用于游戏中控制形状的下落)?
  解决这个问题要用到时钟中断。时钟中断大约每秒钟发生18.2次。截获正常的时钟中断后,在处理完正常的时钟中断后,将一个计时变量
加1。这样,每秒钟计时变量约增加18。需要控控制时间的时候,只需要看这个计时变量就行了。
  截获时钟中断要用到函数getvect()和setvect()。
两个函数的声明如下:
?voidinterrupt(*getvect(intinterruptno))();
?voidsetvect(intinterruptno,voidinterrupt(*isr)());getvect()根据中断号interruptno获取中断号为interruptno的中断处理函数的入口地址。
setvect()将中断号为interruptno的中断处理函数的入口地址改为isr()函数的入口地址。即中断发生时,将调用isr()函数。
  在程序开始的时候截获时钟中断,并设置新的中断处理。在程序结束的时候,一定要记着恢复时钟中断哦,不然系统的计时功能会出问题
的。/*prog5.c*/
Thisisaninterruptserviceroutine.YoucanNOTcompilethis
programwithTestStackOverflowturnedonandgetanexecutable
filewhichwilloperatecorrectly.*//*这个程序每隔1秒钟输出一个整数,10秒钟后结束程序。
按escape键提前退出程序。*/#include<stdio.h>
#include<dos.h>
#include<conio.h>/*Escapekey*/
#defineVK_ESC0x11b/*中断处理函数在C和C++中的表示略有不同。
如果定义了_cplusplus则表示在C++环境下,否则是在C环境下。*/#ifdef__cplusplus
#define__CPPARGS...
#else
#define__CPPARGS
#endif/*指向原来时钟中断处理过程入口的中断处理函数指针(句柄)*/
voidinterrupt(*oldhandler)(__CPPARGS);/*新的时钟中断处理函数*/
voidinterruptnewhandler(__CPPARGS)
{
/*increasetheglobalcounter*/
TimerCounter++;/*calltheoldroutine*/
oldhandler();
}/*设置新的时钟中断处理过程*/
voidSetTimer(voidinterrupt(*IntProc)(__CPPARGS))
{
oldhandler=getvect(TIMER);
disable();/*设置新的时钟中断处理过程时,禁止所有中断*/
setvect(TIMER,IntProc);
enable();/*开启中断*/
}/*恢复原有的时钟中断处理过程*/
voidKillTimer()
{
disable();
setvect(TIMER,oldhandler);
enable();
}
voidmain(void)
{
intkey,time=0;for(;;)
{
if(bioskey(1))
{
key=bioskey(0);
if(key==VK_ESC)/*按escape键提前退出程序*/
{
printf("Usercancel!\n");
break;
}
}
if(TimerCounter>18)/*1秒钟处理一次*/
{
/*恢复计时变量*/
TimerCounter=0;
time++;
printf("%d\n",time);
if(time==10)/*10秒钟后结束程序*/
{
printf("Programterminatednormally!\n");
break;
}
}
}
KillTimer();/*恢复时钟中断*/
游戏中的各种形状及整个游戏空间怎么用数据表示?□□□□□□□□□□□□□□□□
□■□□□■■□□□□□□□□□
□■□□□■□□□■□□□■■□
□■■□□■□□■■■□■■□□□□□□□■□□□□□□
□□□□□■□□□□□□
■■□□□■□□□■■□
□■■□□■□□□■■□我定义了一个结构来表示形状。
structshape
{
intxy[8];
intcolor;
intnext;
}
-1012
-3□□□□
-2□□□□
-1□□□□
0□■□□  所有的各种形状都可以放在4x4的格子里。假定第二列,第四行的格子坐标为(0,0)(如上图中黑块所示),则每个形状的四个方块都可以用4
个数对来表示。坐标x从左向右依次增加,y从上到下依次增加。表示的时候,组成该形状的四个方块从左到右,从上到下(不一定非要按这个顺
序)。如上面七种形状的第一个用数对来表示就是(-2,0)、(-1,0)、(0,0)、(1,0)。结构shape中的xy就是用来表示这4个数对的。为了简化程序,用一维数组xy[8]来表示。xy[0]、xy[1]表示第一个数对,xy[2]、xy[3]表示第二个数对,依次类推。
  shape中的color表示形状的颜色,不同的形状有不同的颜色。七种形状及它们旋转后的变形体一共有19种形状,用一个全局数组表示。假定旋转的方向是逆时针方向(顺时针方向道理一样)。shape中的next就表示当前形状逆时针旋转后的下一个形状的序号。例如:第一种形状及其旋
转变形的形状用结构表示如下。□□□□□□□□□□□□□□□□
□■□□□□□□□■■□□□□□
□■□□□□■□□□■□■■■□
□■■□■■■□□□■□■□□□structshapeshapes[19]=
{
/*{x1,y1,x2,y2,x3,y3,x4,y4,color,next}*/
{0,-2,0,-1,0,0,1,0,CYAN,1},/**/
{-1,0,0,0,1,-1,1,0,CYAN,2},/*#*/
{0,-2,1,-2,1,-1,1,0,CYAN,3},/*#*/
{-1,-1,-1,0,0,-1,1,-1,CYAN,0},/*##*/  游戏空间指的是整个游戏主要的界面(呵呵,这个定义我实在想不出更准确的,还请哪位大虾指点)。实际上是一个宽10格子、高20格子的
游戏板。用一个全局数组board[12][22]表示。表示的时候:board[x][y]为1时表示游戏板上(x,y)这个位置上已经有方块占着了,board[x][y]
为0表示游戏板上这位置还空着。为了便于判断形状的移动是否到边、到底,初始的时候在游戏板的两边各加一列,在游戏板的下面加一行,全
部填上1,表示不能移出界。即board[0][y],board[11][y](其中y从0到21)初始都为1,board[x][21](其中x从1到10)初始都为1。
12345678910
1□□□□□□□□□□
2□□□□□□□□□□
3□□□□□□□□□□
4□□□□□□□□□□
5□□□□□□□□□□
6□□□□□□□□□□
7□□□□□□□□□□
8□□□□□□□□□□
9□□□□□□□□□□
10□□□□□□□□□□
11□□□□□□□□□□
12□□□□□□□□□□
13□□□□□□□□□□
14□□□□□□□□□□
15□□□□□□□□□□
16□□□□□□□□□□
17□□□□□□□□□□
18□□□□□□□□□□
19□□□□□□□□□□
20□□□□□□□□□□prog6.c演示了用结构表示各种形状的方法。虽然程序稍长一些,但并不是特别复杂。其中游戏板初始化部分并没有真正用到,但是后面的程
序会用到的。新的坐标系如下图所示:
-8-7-6-5-4-3-2-1012345678910111213141516171819202122232425262728293031
-4□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
-3□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
-2□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
-1□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
0□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
1□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
2□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
3□□□□□□□□□■■■■■■■■■■□□□■■■■□□□□□□□□□□□□□□
4□□□□□□□□□■■■■■■■■■■□□□■■■■□□□□□□□□□□□□□□
5□□□□□□□□□■■■■■■■■■■□□□■■■■□□□□□□□□□□□□□□
6□□□□□□□□□■■■■■■■■■■□□□■■■■□□□□□□□□□□□□□□
7□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
8□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
9□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
10□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
11□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
12□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
13□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
14□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
15□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
16□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
17□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
18□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
19□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
20□□□□□□□□□■■■■■■■■■■□□□□□□□□□□□□□□□□□□□□□
21□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
22□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
23□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
24□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
25□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
26□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□□
游戏中怎么判断左右及向下移动的可能性?  看懂了前面的各种形状和游戏板等的表示,接下来的东西就都好办多了。先来看一下某个形状如何显示在游戏板当中。假设要在游戏板中
显示第一个形状。第一个形状在结构中的表示如下:structshapeshapes[19]=
{
/*{x1,y1,x2,y2,x3,y3,x4,y4,color,next}*/
{0,-2,0,-1,0,0,1,0,CYAN,1},  那么这个组成形状四个方块的坐标表示为(0,-2)、(0,-1)、(0,0)和(1,0)。这实际上是相对坐标。假形状的实际坐标指的是4x4方块中的第
二列、第三行的方块的位置,设这个位置为(x,y)。那么组成这个形状的四个小方块的实际坐标(以第一个形状为例)就是(x+0,y-2)、(x+0,y-1)、(x+0,y+0)和(x+1,y+0)。由于所有的形状都可以在4x4的方块阵列中表示,这样就找到了一种统一的方法来表示所有的形状了。-1012
-3□□□□相对坐标
-2□■□□
-1□■□□组成第一种形状的四个方块的相对坐标为(0,-2)、(0,-1)、(0,0)和(1,0)。
0□■■□12345678910
1□■□□□□□□□□形状的坐标为(2,3)。组成形状的四个方块的坐标由形状的
2□■□□□□□□□□坐标加上这四个小方块各自的相对坐标得出。它们分别是:
3□■■□□□□□□□(2+0,3-2)、(2+0,3-1)、(2+0,3-0)和(2+1,3-0)。即:
4□□□□□□□□□□(2,1)、(2,2)、(2,3)和(3,3)。如左图所示。
5□□□□□□□□□□
6□□□□□□□□□□
7■□□□□□□□□□形状的坐标为(1,9)。组成形状的四个方块的坐标分别是:
8■□□□□□□□□□(1+0,9-2)、(1+0,9-1)、(1+0,9-0)和(1+1,9-0)。即:
9■■□□□□□□□□(1,7)、(1,8)、(1,9)和(2,9)。如左图所示。
10□□□□□□□□□□
11□□□□□□□□□□
12□□□□□□□□□□
13□□□□□□□□□□
14□□□□□□□□□□
15□□□□□□□□□□
16□□□□□□□□□□
17□□□□□□□□□□
18□□□□□□□□■□形状的坐标为(9,20)。组成形状的四个方块的坐标分别是:
19□□□□□□□□■□(9+0,20-2)、(9+0,20-1)、(9+0,20-0)和(9+1,20-0)。即:
20□□□□□□□□■■(9,18)、(9,19)、(9,20)和(10,20)。如左图所示。enumboolConfilict(intShapeIndex,intx,inty)
{
inti;/*对组成索引号为ShapeIndex的形状的四个方块依次判断*/
for(i=0;i<=7;i++,i++)/*i分别取0,2,4,6*/
{
/*如果四个方块中有任何一个方块的x坐标小于1或大于10,表示超出左边界或右边界。
此时,发生冲突。*/
if(shapes[ShapeIndex].xy[i]+x<1||
shapes[ShapeIndex].xy[i]+x>10)returnTrue;/*如果四个方块中某个方块的y坐标小于1,表示整个形状还没有完全落入游戏板中。
此时,没有必要对这个方块进行判断。*/
if(shapes[ShapeIndex].xy[i+1]+y<1)continue;/*如果四个方块中有任何一个方块与游戏板当前状态发生冲突,则整个形状在(x,y)处
与游戏板当前状态冲突*/
if(board[shapes[ShapeIndex].xy[i]+x][shapes[ShapeIndex].xy[i+1]+y])
returnTrue;
}/*四个方块中没有任何一个方块与游戏板当前状态发生冲突,则整个形状在(x,y)处
没有与游戏板当前状态冲突*/
returnFalse;
}对以上代码附加说明如下:
  shapes[ShapeIndex].xy[i](其中i等于0,2,4,6)表示组成索引号为ShapeIndex的形状的某个方块的x相对坐标。(i等于0时,表示第1个方块的x相对坐标;i等于2时,表示第2个方块的x相对坐标;i等于4时,表示第3个方块的x相对坐标;i等于6时,表示第4个方块的x相对坐标。)  shapes[ShapeIndex].xy[i]+y(其中i等于1,3,5,7)表示索引号为ShapeIndex的形状的坐标为(x,y)时,组成该形状的某个方块的y实际坐
标。(i等于1时,表示第1个方块的y实际坐标;i等于3时,表示第2个方块的y实际坐标;i等于5时,表示第3个方块的y实际坐标;i等于7时,表示第4个方块的y实际坐标。)现在来看看这句是什么意思吧。
board[shapes[ShapeIndex].xy[i]+x][shapes[ShapeIndex].xy[i+1]+y]ActualX=shapes[ShapeIndex].xy[i]+x;/*其中x为0,2,4,6*/
表示某个方块实际的x坐标。ActualY=[shapes[ShapeIndex].xy[i+1]+y;
表示某个方块实际的y坐标。
仔细看过这段代码后,你可能会提一个问题:不是已经在游戏板的左右两边都加了“边”了吗,为什么还要加下面这个对x坐标的判断呢?/*如果四个方块中有任何一个方块的x坐标小于1或大于10,表示超出左边界或右边界。
此时,发生冲突。*/
if(shapes[ShapeIndex].xy[i]+x<1||
shapes[ShapeIndex].xy[i]+x>10)returnTrue;■■
■2345678910
1■□□□□□□□□□这在当前形状刚出来的时候,是可能发生的。但是我们只给游戏板
2□□□□□□□□□□加了一层“边”。对于这个形状的最左边的那个方块将失去判断,
3□□□□□□□□□□如果不予理会,这个形状将会“挂”在游戏板的左上角!当初我也
4□□□□□□□□□□没有想到这一点,后来发现会有形状“挂”在最顶层,而导致游戏
5□□□□□□□□□□提前退出。发现了这个问题。
6□□□□□□□□□□
7□□□□□□□□□□
8□□□□□□□□□□加了这个判断后,游戏板的左右两个“边”对冲突的判断就是去意
9□□□□□□□□□□义了。因为没有这两个“边”,对于冲突的判断也不会出错。不过
10□□□□□□□□□□为了程序易于理解,还是保留了游戏板的左右两个“边”。
11□□□□□□□□□□
12□□□□□□□□□□
13□□□□□□□□□□
14□□□□□□□□□□
15□□□□□□□□□□
16□□□□□□□□□□
17□□□□□□□□□□
18□□□□□□□□□□
19□□□□□□□□□□
20□□□□□□□□□□  如果你对我上面提出的新问题及对于这个问题的解释不太明白,没关系,这并不重要。因为现在才刚刚开始,而且刚才所说的这个问题只
有在特殊情况下才出现(当然,一旦发生上面说的问题,游戏就出错啦!^_^),对于理解整个程序的思路影响不大。看多了就会明白了(你
会说:原来就这么简单!)。

TAG:

我来说两句