莱哥 发表于 2006-5-27 17:57

[转载]电脑游戏编程入门

<P>/*********************************************************************************************<BR>*<BR>* 本文取自网络,其中有些不正确的地方,如第九节(控制游戏的速度),本人对不正确的地方进行<BR>*<BR>*       了改正,有些不足的地方依照&lt;计算机基本原理&gt;一书增加了一些相关的内容。<BR>*<BR>* 注:本文可能还有不正确的地方,请留意。<BR>*<BR>*********************************************************************************************/</P>
<P>       电脑游戏编程入门 (DOS) </P>
<P><BR>    这里以电脑VGA 13H视频模式为例,系统介绍制作电脑游戏的基本方法。VGA INT 13H模式是一种工作</P>
<P>在图形方式下的模式,它具有320X200的屏幕分辩率,同屏上可以显示256种颜色(超级任天堂和世嘉5代游</P>
<P>戏机也只有256种颜色),早期的许多游戏都是在这一图形方式下开发的(如大家非常熟悉的仙剑奇侠传、</P>
<P>红色警戒、DOOM等),在这种模式下开发游戏具有编程简单、运行速度快、颜色丰富等特点。</P>
<P>    虽然以今天的标准来说320X200的分辩率已经不算什么,但是它仍然是一个学习电脑游戏编程的很好的</P>
<P>入门环境,我们可以通过开发VGA 13H模式下的游戏,逐步地由简单到复杂地学习游戏设计的基本方法,只</P>
<P>要掌握了游戏设计的基本方法和理论,再利用其它软件工具(象WATCOM C、DJGPP、VC、DIRECT X等32位的C</P>
<P>编译器等)来设计游戏将变得非常容易。函数用TURBO C 2.0编译,也可以稍作修改用其它的C编译器来编译</P>
<P>运行。我们先来了解一下电脑显示器的工作原理和显示卡的结构。 </P>
<P><BR>一.显示器的工作原理</P>
<P>    目前在个人计算机上广泛使用的是采用阴极射线管(CRT)的光栅扫描显示器,我们在屏幕上所看到的</P>
<P>颜色是由电子枪发出的电子束打在CRT屏幕背面的荧光层上的点形成的,通过控制点的亮度可以产生不同的</P>
<P>颜色。电子束不断地从左到右、从上到下扫描整个屏幕,使屏幕显示出图案,电子束以大约每秒70次的速</P>
<P>率在屏幕上重画这一图案,这个过程称为显示刷新或屏幕刷新,具体的扫描频率依赖于所用的显示适配器</P>
<P>(又称为显示卡)。电子束从屏幕的左上角开始向右扫描,到达屏幕的右边缘后,电子束被关闭(水平断开),</P>
<P>接着它又迅速地返回到屏幕的左边缘(水平回扫)开始进行下一行水平方向的扫描,在完成全部的水平方</P>
<P>向的扫描后,电子束在屏幕的右下角结束,此时电子束被关闭(垂直断开),接着又迅速地返回到屏幕的</P>
<P>左上角(垂直回扫),开始下一屏扫描。电子束就是这样周而复始地扫描整个屏幕。显示器在两种方式下</P>
<P>工作:文本方式和图形方式,电脑游戏一般在图形方式下进行。</P>

<P>二.显示器的坐标系统</P>
<P>    计算机屏幕上的坐标与我们通常使用的直角坐标系不同,坐标原点(0,0)在屏幕的左上角,向右是</P>
<P>水平方向的坐标,向下是垂直方向的坐标,且坐标没有负值。</P>

<P>三.显示卡的结构</P>
<P>    显示器上的显示卡负责将图形显示在屏幕上。显示存储器中存放着在屏幕上显示的图像数据,显示卡</P>
<P>硬件不停地将显存中的内容显示在屏幕上。显示存储器实际上是安装在显示卡上的一块或几块大规模集成</P>
<P>电路,其容量有1M、2M、4M、8M等,在DOS下我们可以访问的内存只有1MB空间(这就是DOS的局限性所在),</P>
<P>地址从00000H到FFFFFH,这段内存根据用途又分为不同的块,系统分配给图形缓冲区(显示存储器)的地</P>
<P>址在A0000H到BFFFFH之间,大小为128KB,其中,VGA占用了A0000H到AFFFFH段,共64KB,这段地址是内存</P>
<P>映射地址,供我们访问显示存储器用。在VGA 13H图形模式下,显示内存使用A0000H到AF9FFH的一段线性内</P>
<P>存空间,每个字节表示一个点,对应屏幕上的一个像点,320*200的屏幕分辨率共需要64000个字节,刚好</P>
<P>64KB,因为一个字节可以表示的最大整数值为256,所以每个像点就可以表示256种颜色。</P>

<P>四.设置视频模式</P>
<P>    画图以前必须使屏幕工作在图形方式,这就要设置屏幕的视频模式。设置视频模式有许多种方法,其</P>
<P>中调用视频BIOS功能是最简单的一种,通过调用BIOS中断0x10的服务程序,可以很方便地设置屏幕模式。</P>
<P>调用方法是将值0放入ah寄存器,显示模式放入al寄存器中,然后调用int86()函数。</P>
<P>设置视频模式的函数:</P>
<P>void SetVideoMode(int mode)<BR>{<BR>    union REGS r;</P>
<P>    r.h.ah = 0;<BR>    r.h.al = mode;<BR>    int86(0x10,&amp;r,&amp;r);<BR>}</P>
<P>其中mode是视频模式。</P>

<P>五.在屏幕上画点</P>
<P>    由于显示存储器是线性排列的,每个像点用一个字节来表示,所以对像点寻址非常容易,像点在显示</P>
<P>内存中的偏移地址可由这个公式确定:y*320+x,其中,y是像点在垂直方向的坐标,x是水平方向的坐标,</P>
<P>320是屏幕的宽度。有了像点的偏移地址,然后加上显示内存的首地址即可得到像点在显示内存中的绝对地</P>
<P>址。只要将表示点的颜色值放到这个地址处,就可以在屏幕上画点了。首先建立一个指针VideoBufferPtr,</P>
<P>使它指向显示内存的首地址:</P>
<P>char far *VideoBufferPtr=(char far *)0xA0000000L;</P>
<P>将这个指针加上像点的偏移地址,像点的最终地址就确定了,它等于:</P>
<P>VideoBufferPtr + y * 320 + x;</P>
<P>把颜色值color写到这个地址:</P>
<P>*(VideoBufferPtr + y * 320 + x) = color;</P>
<P>画点的函数:</P>
<P>void DrawPoint(int x,int y,unsigned char color)<BR>{<BR>    *(VideoBufferPtr + y * 320 + x ) = color;<BR>}</P>

<P>六.显示字符</P>
<P>    游戏经常在屏幕上打印出显示游戏状态的文本,使游戏者了解当前游戏的状态,这是游戏与游戏者进</P>
<P>行交互的必不可少的。然而在图形模式下显示文本与在文本模式下有很大区别,在图形模式下文本必须使</P>
<P>用基于画位图的方法来显示。</P>
<P>    这里介绍一种简便的方法:利用计算机只读存储器中的ASCII字体数据。在计算机只读存储器(ROM)中</P>
<P>固化有ASCII字体数据,这可在基址F000:FA6E上找到。我们只需了解数据是如何存储的,然后再得出存取</P>
<P>该数据的算法,就可以用任意颜色在屏幕上用画位图的方法把字符画出来。</P>
<P>    在ROM中,字符按照ASCII字符编码的顺序放置,由于每个字符为8*8的点阵,所以每个字符占据8个字</P>
<P>节的存储空间,要找出某个字符,我们只需将这个字符的ASCII码与8相乘,然后将结果加到基址F000:FA6E</P>
<P>上。由于字符用画位图的方法来显示,因此我们可以随意给它们指定颜色(前景或背景颜色)。</P>
<P>    图形下字符显示原理(以字符A为例):        0 0 0 1 0 0 0 0 -ROM中A字符第一个字节<BR>         0 0 1 0 1 0 0 0 -第二个字节<BR>         0 1 0 0 0 1 0 0 -第三个字节<BR>         0 1 0 0 0 1 0 0 .<BR>         0 1 0 0 0 1 0 0 .<BR>         0 1 1 1 1 1 0 0 .<BR>         0 1 0 0 0 1 0 0 .<BR>         0 1 0 0 0 1 0 0 -第八个字节<BR>        其中1所组成的图像为A.</P>
<P>    在图形模式下,我们只需要两个函数就能够打印文本:一个用于显示单个字符,另一个用来显示字符 </P>
<P>串。 定义一个远指针指向ROM字符集的开始位置:</P>
<P>char far *RomCharPtr=(char far *)0xF000FA6E;</P>
<P>下面是在屏幕上打印字符和字符串的函数:</P>
<P>1.打印字符的函数。</P>
<P>void PrintChar(int cx,int cy,char c,unsigned char Fcolor,unsigned char Bcolor,int flag)<BR>{<BR>    int offset;      /*  用来储存显存偏移地址   */<BR>    int x,y;      /*  循环变量,输出8*8点陈字符   */<BR>    char far *TempPtr;     /*  BIOS中的字符段地址    */<BR>    unsigned char bit_mask;    /*  与运算变量,用来确定字符点的显示位置  */</P>
<P>    TempPtr = RomCharPtr + (c &lt;&lt; 3);   /*  得到ASCII码的地址,(c &lt;&lt; 3)=(c * 8)  */<BR>    offset = (cy &lt;&lt; 8) + (cy &lt;&lt; 6) + cx;  /*  横坐标320=256+64,所以偏移地址cy*320+x */<BR>       /*  可写成(cy&lt;&lt;8)+(cy&lt;&lt;6)+cx   */<BR>    for(y = 0;y &lt; 8;y ++)    /*  行画点,显示字符    */<BR>    {<BR>        bit_mask = 0x80;    /*  1000 0000B     */</P>
<P>        for(x = 0;x &lt; 8;x ++ )    /*  列画点,显示字符    */<BR>        {<BR>            if((*TempPtr &amp; bit_mask))   /*  当前位&amp;后为真    */<BR>                *(VideoBufferPtr + offset + x) = Fcolor;/*  画点     */<BR>            else if(flag == 1)    /*  flag=1显示背景色    */<BR>                *(VideoBufferPtr + offset + x) = Bcolor;/*  画点     */<BR>            bit_mask=(bit_mask &gt;&gt; 1);   /*  左移一位后与TempPtr相&amp;   */<BR>        }<BR>        offset += 320;     /*  换行     */<BR>        TempPtr ++;     /*  后移一位     */<BR>    }<BR>}</P>
<P>说明:</P>
<P>1.cx,cy 是字符在屏幕上的坐标。</P>
<P>2.c是字符的ASCII码。</P>
<P>3.Fcolor,Bcolor 分别是字符的前景和背景颜色。</P>
<P>4.flag 打印标志,当flag=1时显示字符的背景色,否则打印的字符具有透明效果。</P>
<P><BR>2. 打印字符串的函数。</P>
<P>void PrintString(int x,int y,char *string,unsigned char Fcolor,unsigned char Bcolor,int flag)<BR>{<BR>    int index;</P>
<P>    for(index = 0; string[index] != 0;index ++)<BR>        PrintChar(x + (index &lt;&lt; 3),y,string[index],Fcolor,Bcolor,flag);<BR>}</P>
<P>说明:</P>
<P>1.x,y 是字符串在屏幕上的坐标。</P>
<P>2.*string 字符串指针。</P>
<P>3.Fcolor,Bcolor 分别是字符串的前景和背景颜色。</P>
<P>4.flag 打印标志,当flag=1时显示字符串的背景色,否则打印的字符串具有透明效果。</P>

<P>七.设置颜色寄存器</P>
<P>    我们知道VGA显示卡具有显示256种颜色的能力,每种颜色能够用一个0-255之间的数值来表示,那么这</P>
<P>些数值与我们在屏幕上实际见到的颜色之间有什么关系呢?其实这些数值只是VGA显示卡上的颜色寄存器的</P>
<P>索引值,颜色寄存器里才保存了屏幕上颜色的真实值。VGA显示卡上有一个包含256个单元的颜色寄存器(又</P>
<P>称为调色板),每个单元由三部份组成,这三部份分别代表颜色中的红、绿、蓝三种成份(显示器就是用这</P>
<P>三种成份来组成任何我们所看到的颜色),用三个字节表示,颜色寄存器一共有768个字节(3*256=768)。</P>
<P>    当我们要在屏幕上显示某种颜色时,显示卡硬件就根据颜色的索引值在颜色寄存器中查找,找到后再</P>
<P>从相应单元中取出颜色值显示在屏幕上,这个过程与画家使用调色板相似,颜色寄存器相当于调色板,颜</P>
<P>色寄存器中的单元相当于调色板上的色格,在色格中装有预先调好的颜色,当画家需要用某种颜色作画时,</P>
<P>就从装有那种颜色的色格中把颜色取出来。例如,我们要显示颜色索引值为30的颜色,显示卡硬件就去查</P>
<P>找颜色寄存器的第30单元,30单元位于距颜色寄存器首址3*30=90处(因为每个单元有三个字节),然后取出</P>
<P>90处记录有红、绿、蓝三种成份的三个字节作为在屏幕上显示的色彩信号。但是实际上每个字节只用了六</P>
<P>位来表示颜色,其它两位没用,这六位表示的数的值域为0-63,所以每种颜色(红、绿、蓝)成份具有64种</P>
<P>亮度的表现能力,三种颜色成份组合共可以产生64*64*64=262144种颜色(VGA 13H模式从这262144种颜色</P>
<P>中取出256种在同一屏幕上显示)。我们可以通过事先设置颜色寄存器的值来使用我们自己的颜色。 </P>
<P>    设置颜色寄存器有多种方法,如调用BIOS功能,但是这种方法速度比较慢,游戏设计中通常采用直接</P>
<P>访问VGA显示卡的I/O端口的方法来快速设置颜色寄存器,我们只需访问四个I/O端口就可以完成设置颜色</P>
<P>寄存器的工作。这四个端口分别是: 0x3c6、0x3c7、0x3c8和0x3c9。</P>
<P>    端口0x3c6称为调色板屏蔽寄存器,用来屏蔽所要求的调色板寄存器的位,如果你在这个寄存器中放入</P>
<P>0xff,你就可以通过调色板索引寄存器0x3c7和0x3c8(一个用于读,一个用于写)访问任何你希望访问的颜色</P>
<P>寄存器,端口0x3c9称为调色板数据寄存器,红、绿、蓝三种成份就是通过它进行读写(颜色值要读或写三次)。</P>
<P>我们定义一个结构来方便处理颜色寄存器:</P>
<P>typedef struct RGB_COLOR<BR>{<BR>    unsigned char red;<BR>    unsigned char green;<BR>    unsigned char blue;</P>
<P>}RGBColor,*RGBColorPtr;</P>
<P>结构中的red、green和blue变量用来保存颜色的红、绿、蓝三种成份。</P>
<P>设置颜色寄存器值的函数:</P>
<P>void SetPaletteRegister(int index,RGBColorPtr color)<BR>{<BR>    outportb(0x3c6,0xff);<BR>    outportb(0x3c8,index);</P>
<P>    outportb(0x3c9,color-&gt;red);<BR>    outportb(0x3c9,color-&gt;green);<BR>    outportb(0x3c9,color-&gt;blue);<BR>}</P>
<P>获取颜色寄存器值的函数:</P>
<P>void GetPaletteRegister(int index,RGBColorPtr color)<BR>{<BR>    outportb(0x3c6,0xff);<BR>    outportb(0x3c7,index);</P>
<P>    color-&gt;red   = inportb(0x3c9);<BR>    color-&gt;green = inportb(0x3c9);<BR>    color-&gt;blue  = inportb(0x3c9);<BR>}</P>

<P>八.在屏幕上画位图</P>
<P>    计算机绘制图像通常采用一种称为位映射图(BITMAP)的图形处理方法进行,位映射图是一个矩形的点</P>
<P>阵结构(二维矩阵),显示在屏幕上时,对应屏幕上一个矩形区域,组成位图的数据储存在内存中一段连续</P>
<P>的区间。我们比较常见的位图文件有:BMP、PCX、GIF、JPG等。位图通常存储在外部文件中,使用以前必</P>
<P>须将其从磁盘文件调入内存。下面介绍将一个256色PCX图形文件读入内存的方法:</P>
<P>1.定义PCX文件头结构:</P>
<P>typedef struct PCX_HEADER<BR>{<BR>    char menufactrue;     /*  厂家标识编号 0x0a       */<BR>    char version;         /*  文件版本编号            */<BR>    char packing_type;    /*  压缩模式                */<BR>    char bits_per_pixel;  /*  每点占用的位数          */<BR>    int  minx;            /*  最小X坐标值             */<BR>    int  miny;            /*  最小Y坐标值             */<BR>    int  maxx;            /*  最大X坐标值             */<BR>    int  maxy;            /*  最大Y坐标值             */<BR>    int  hres;            /*  水平分辨率              */<BR>    int  vres;            /*  垂直分辨率              */<BR>    char palette[48];     /*  颜色调色板              */<BR>    char unused;          /*  未使用                  */<BR>    char bit_plance;      /*  位平面个数              */<BR>    int  bytes;           /*  单一水平线占用的字节数  */<BR>    int  palette_type;    /*  调色板类型              */<BR>    char unused2[58];     /*  未使用                  */</P>
<P>}PCXHeader,*PCXHeaderPtr;</P>
<P>2.定义用来存放PCX图像数据的结构:</P>
<P>typedef struct PCX_PICTURE<BR>{<BR>    int width;<BR>    int height;<BR>    char far *buffer;<BR>    RGBColor palette[256];</P>
<P>}PCXPicture,*PCXPicturePtr;</P>
<P>3.初始化图像数据的函数:</P>
<P>int InitPCX(PCXPicturePtr image,int w,int h)<BR>{<BR>    unsigned size = w * h;</P>
<P>    image-&gt;width  = w;<BR>    image-&gt;height = h;</P>
<P>    image-&gt;buffer=(char far *)farmalloc(size);</P>
<P>    if(image-&gt;buffer == NULL)  return 0;</P>
<P>    return 1;<BR>}</P>
<P>4.从外部文件读入数据的函数:</P>
<P>int LoadPCX(char *filename,PCXPicturePtr image,int flag)<BR>{<BR>    FILE *fp;<BR>    unsigned num_bytes,count,size;<BR>    int index;<BR>    unsigned char data;<BR>    PCXHeader PcxHeader;</P>
<P>    size = image-&gt;width * image-&gt;height;</P>
<P>    if((fp = fopen(filename,"rb")) == NULL)<BR>        return 0;</P>
<P>    fread(&amp;PcxHeader,sizeof(PCXHeader),1,fp);</P>
<P>    count = 0;</P>
<P>    while(count &lt;= size)<BR>    {<BR>        data = fgetc(fp);<BR>        if(data &gt;= 192 &amp;&amp; data &lt;= 255)<BR>        {<BR>            num_bytes = data - 192;<BR>            data = fgetc(fp);<BR>            while(num_bytes &gt;= 0)<BR>            {<BR>                *(image-&gt;buffer + count) = data;<BR>                ++ count;<BR>            }<BR>        }<BR>        else<BR>        {<BR>            *(image-&gt;buffer + count) = data;<BR>            ++ count;<BR>        }<BR>    }</P>
<P>    fseek(fp,-768L,SEEK_END);<BR>    for(index = 0;index &lt; 256;index ++)<BR>    {<BR>        image-&gt;palette[index].red   = ((fgetc(fp)) &gt;&gt; 2);<BR>        image-&gt;palette[index].green = ((fgetc(fp)) &gt;&gt; 2);<BR>        image-&gt;palette[index].blue  = ((fgetc(fp)) &gt;&gt; 2);<BR>    }<BR>    fclose(fp);</P>
<P>    if(flag == 1)<BR>        for(index = 0;index &lt; 256;index ++)<BR>            SetPaletteRegister(index,(RGBColorPtr) &amp; image-&gt;palette[index]);</P>
<P>    return 1;<BR>}</P>
<P>其中参数flag用来指明调入文件的同时是否设置颜色寄存器(flag=1设置)。</P>
<P>5.画位图的函数:</P>
<P>void DrawImage(int x,int y,int width,int height,char far *image)<BR>{<BR>    int i,j;</P>
<P>    for(i = 0;i &lt; height;i ++)<BR>    {<BR> for(j = 0;j &lt; width;j ++)<BR> {<BR>     if(*image != 0 &amp;&amp; (x + j) &gt;= 0 &amp;&amp; (x + j) &lt; 320 &amp;&amp; (y + i) &gt;= 0 &amp;&amp; (y + i) &lt; 200)<BR>  DrawPoint(x + j,y + i,*image);</P>
<P>     image ++;<BR> }<BR>    }<BR>}</P>
<P>x,y是图像在屏幕上的左上角坐标,width,height是图像的宽度和高度,image是指向内存中图像的指针。</P>
<P>我们对if(*image != 0 &amp;&amp; (x + j) &gt;= 0 &amp;&amp; (x + j) &lt; 320 &amp;&amp; (y + i) &gt;= 0 &amp;&amp; (y + i) &lt; 200)语句</P>
<P>进行一下分析:</P>
<P>    *image != 0用来检查所画的颜色值是否是透明色,如果是,则不画出来,这样我们就可以画出有透</P>
<P>明效果的图象,即透过图象可以看到背景,透明色通常取值0,也可以用其他的颜色值表示。</P>
<P>    (x + j) &gt;= 0 &amp;&amp; (x + j) &lt; 320 &amp;&amp; (y + i) &gt;= 0 &amp;&amp; (y + i) &lt; 200语句用来判断所画点的坐标是</P>
<P>否超出屏幕显示的范围,这样可以画出具有裁剪效果的图象,如图象的一部份在屏幕外。</P>
<P>    这个函数并不是最快的,因为它要执行width X height次判断,更快的函数请看VGA13H函数库中的绘</P>
<P>图函数。</P>

莱哥 发表于 2006-5-27 17:58

<P>九.控制游戏的速度</P>
<P>    初学游戏编程的人常常会遇到这样的问题,设计出来的游戏在一台机子上速度运行正常,为什么把游</P>
<P>戏在另一台运算速度比较快的机子上运行时,游戏速度也变得很快呢?是为什么呢?问题就在于游戏中没</P>
<P>有一个时间基准值在控制它。那么如何控制游戏的速度呢?这就得用电脑时钟计数来控制游戏。</P>
<P>    首先我们来了解一下PC机的时钟是如何工作的,PC机采用一块8253定时器芯片计算系统时钟的脉冲,</P>
<P>若干个系统时钟周期转换成一个脉冲,这些脉冲序列可以用以计时,也可以送入计算机的扬声器产生特定</P>
<P>频率的声音。8253定时器芯片独立于CPU运行,它可以象实时时钟那样,CPU的工作状态对它没有任何影响。</P>
<P>    8253芯片有三个独立的通道,每个通道的功能各不相同,三个通道的功能如下:通道0:为系统时钟</P>
<P>所用,在启动时由BIOS置入初值,每秒钟约发出18.2个脉冲,脉冲的计数值存放在BIOS数据区的0040:006c</P>
<P>存储单元中(注意,这个单元的内容对我们非常有用!),通道0的输出脉冲作为申请定时器中断的请求信</P>
<P>号,还用于磁盘的某些定时操作,如果改变了通道0的计数值,必须确保在CPU每次访问磁盘以前恢复原来</P>
<P>的读数,否则将使磁盘读写产生错误。通道1:用于控制计算机的动态RAM刷新速率,一般情况下不要去改</P>
<P>变它。通道2:连接计算机的扬声器,产生单一的方波信号控制扬声器发声。8253定时器芯片的每一个通道</P>
<P>含有3个寄存器,CPU通过访问3个端口(通道0为40h,通道1为41h,通道2为42h)来访问各个端口的3个寄存</P>
<P>器,8253每个端口有6种工作模式,当通道0用于定时或通道2用于定时或发声时,一般用模式3。在模式3下,</P>
<P>计数值被置入锁存器后立即复制到计数器,计数器在每次系统时钟到来时减1,减至0后一方面马上从锁存</P>
<P>器中重新读取计数值,另一方面向CPU发出一个中断请求(INT 1CH中断,很有用),如此循环在输出线上高</P>
<P>低电平的时间各占计数时间的一半,从而产生方波输出。</P>
<P>    对8253定时器芯片编程是通过命令端口寄存器(控制字寄存器)43h来实现,它决定选用的通道、工作模</P>
<P>式、送入锁存器的计数值是一字节还是两字节、是二进制码还是BCD码等工作参数,端口43h各位的组合形式</P>
<P>如下:</P>
<P>                                 控制字寄存器各位的功能表示</P>
<P>                        D7   D6      D5   D4       D3  D2  D1     D0<BR>                     ---------------------------------------------------<BR>                     |  SC1  SC0  |  RL1  RL0   |  M2  M1  M0  |  BCD  |<BR>                     ---------------------------------------------------</P>
<P><BR>D0   位____若为0则采用二进制表示,否则用BCD码表示计数值。</P>
<P>D3-D1位____工作模式号,其值(0-5)对应6种模式。</P>
<P>D5-D4位____操作的类型:00:把通道中当前数据寄存器的值送入16位锁存器中,供CPU读取该值;</P>
<P>         01:表示只读/写低8位字节数据,只写入低8位时,高8位自动置为0;</P>
<P>         10:表示只读/写高8位字节数据,只写入高8位时,低8位自动置为0;</P>
<P>         11:允许读/写16位字数据,8253的数据线只有8位,读/写时必须分两次进行,先<BR>                           读/写低8位字节,再读/写高8位字节;</P>
<P>D7-D6位____决定选用的通道号,其值为0-2。</P>
<P>对8253芯片编程的三个步骤:</P>
<P>1.设置命令端口43h,</P>
<P>2.向端口发送一个工作状态字节,</P>
<P>3.确定定时器的工作方式;</P>
<P>    若是通道2,给端口61h(8255芯片接口)的第0位和第1位置数,启动时钟信号,当第1位置1时,通道2</P>
<P>驱动扬声器,置0时用于定时操作;将一个字的计数值,按先低字节后高字节的顺序送入通道的I/O端口寄</P>
<P>存器(通道0为40h,通道1为41h,通道2为42h)。当第3个步骤完成后被编程的通道马上在新的状态下开始</P>
<P>工作。由于8253的三个通道都独立于CPU运行,所以在程序结束以前要恢复各通道的正常状态值。</P>
<P>    在游戏中我们只使用通道0,怎样对其进行编程呢?首先得确定放入锁存器的16位的计数值:</P>
<P>16位的计数值=1.19318MB/希望的频率</P>
<P>其中1.19318MB是系统振荡器的频率。</P>
<P>由上式可知,计数器所能产生的值是18.2Hz-1.19318MHz,这已经足够了,可以满足我们游戏的要求。</P>
<P>以下是对8253定时器芯片通道0编程的函数:</P>
<P>#define T60HZ 0x4dae<BR>#define T50HZ 0x5d37<BR>#define T40HZ 0x7468<BR>#define T30HZ 0x965c<BR>#define T20HZ 0xe90b<BR>#define T18HZ 0xffff</P>
<P>#define LOW_BYTE(n) (   n    &amp; 0x00ff)<BR>#define HI_BYTE(n) ((n &gt;&gt; 8) &amp; 0x00ff)</P>
<P>int far *clk = (int far *)0x0000046c; </P>
<P><BR>1.改变时间定时器值的函数:</P>
<P>void ChangTime(unsigned cnt)<BR>{<BR>    outportb(0x43,0x3c);<BR>    outportb(0x40,LOW_BYTE(cnt));<BR>    outportb(0x40,HI_BYTE(cnt));<BR>}</P>
<P>2.延时函数:</P>
<P>void Delay(int d)<BR>{<BR>    int tm = *clk;<BR>    while(*clk - tm &lt; d);<BR>}</P>
<P>    上面定义的指针*clk指向BIOS数据区的0040:006c存储单元,该单元中存放着定时器的计数值,我们</P>
<P>可以根据该单元的内容计算差值来达到延时的目的。需要注意的是,由于改变定时器计数值的操作会与保</P>
<P>护模式下的代码发生冲突,所以不能在WINDOWS的DOS仿真环境下改变定时器,必须在纯DOS环境里使用。</P>
<P>    改变了定时器值以后,程序结束以前必须恢复原来的值,如果不恢复原来的18.2次的计数值,在读</P>
<P>写磁盘操作时将引起读写错误甚至死机。</P>
<P>我们将时钟设置成每秒60HZ这样调用函数:ChangTime(T60HZ);</P>
<P>在游戏中延时一定时间这样调用函数:Delay(10);</P>

<P>十.在后台运行程序</P>
<P>    所谓后台运行就是在游戏运行当中“同时“运行其它的程序,如在游戏中演奏背景音乐、游戏计时等,</P>
<P>这些工作看起来好象与游戏过程“同时“在进行。这里的“同时“其实就是利用中断的方法来实现的,即</P>
<P>在游戏中每隔一定时间产生一次中断去做其他工作,完成后再回到游戏中。</P>
<P>    在BIOS中包含了一个特殊的伪中断----INT 1CH中断,这一中断在BIOS初始化时没有任何作用,它的中</P>
<P>断处理程序只有一条中断返回语句----IRET,因此该中断一经调用立即返回。INT 1CH中断是在BIOS中断</P>
<P>INT 8H修正日历计数后由该中断调用,和INT 8H中断程序一起以定时器中断频率(正常情况下为每秒钟18.2次)</P>
<P>不停地执行而与CPU无关,我们可以改变这一中断,使它指向我们的中断处理程序,当我们改变了时间定时</P>
<P>器的值以后,INT 1CH程序将按改变后的频率被调用。</P>
<P>    重置INT 1CH中断向量前必须先保存好原来的INT 1CH中断向量,以便在我们的程序运行结束时再恢复它,</P>
<P>否则将引起系统崩溃。另外,中断处理程序的处理时间不能超过定时器确定的时间,否则将死机,所以在</P>
<P>中断处理程序里不能运行太多的代码,也不能调用与C相关的函数,如sound,delay等。</P>
<P>编写新的INT 1CH中断程序的方法如下:</P>
<P>1. 设置一个中断类型的函数指针用来保存原先的INT 1CH中断向量:</P>
<P>void interrupt far (*OldInt1chHandler)();</P>
<P>2. 编写新的INT 1CH中断程序:</P>
<P>void far interrupt NewInt1ch(void)</P>
<P>{</P>
<P>    /*我们的中断程序*/</P>
<P>}</P>
<P>3. 保存原来的INT 1CH中断向量:</P>
<P>OldInt1chHandler = getvect(0x1c);</P>
<P>4. 设置新的INT 1CH中断向量:</P>
<P>setvect(0x1c,NewInt1ch);</P>
<P>5. 恢复旧的INT 1CH中断程序:</P>
<P>setvect(0x1c, OldInt1chHandler);</P>

<P>十一.如何处理按键</P>
<P>    PC的键盘是一个智能化的键盘,它相当于一部完整的计算机,键盘内有一片Intel 8048(或8049)单片</P>
<P>机(处理器)对整个键盘上的字符键、功能键、控制键和组合键进行管理,当在键盘上按下一个键时,键盘</P>
<P>上的处理器首先向计算机主机发出硬件中断请求,然后将该键的扫描码以串行的方式传送给计算机主机,</P>
<P>计算机主机在硬件中断的作用下,调用INT 09H硬件中断把键盘送来的扫描码读入,并转换为ASCII码存入</P>
<P>键盘缓冲区中。</P>
<P>    按下一个键,送出一个闭合码,键被释放时送出一个断开码,键盘处理中断程序从键盘I/O端口(端口</P>
<P>地址为60H)读取一个字节的数据,如果读取的数据的第7位为1时表示按键已放开(送出断开码),如第7位</P>
<P>为0表示键按下(送出闭合码),数据的第0-6位则为按键的扫描码。</P>
<P>    键盘上的每一个键都对应一个扫描码,根据扫描码就能唯一的确定一个键。键盘缓冲区位于0040:001EH</P>
<P>-4000:003EH之间的BIOS数据区,长度为34个字节,是一个先进后出的循环队列。使用PC机原有的键盘处理程</P>
<P>序可以很方便地处理键盘,但是因为它是调用BIOS,所以反应比较慢,另外当我们要同时处理几个按键时(例</P>
<P>如同时按下Up箭头键和Left箭头键沿对角线运动),原有的键盘中断程序就不能满足要求,这时就需要编写一</P>
<P>个适合我们要求的键盘中断程序。</P>
<P>编写新的键盘中断程序要做以下几项工作:</P>
<P>1.进入键盘中断程序。</P>
<P>2.从键盘I/O端口60H读取一个字节的按键码,并将它存入一个全局变量中供main程序处理,或者将按键</P>
<P>    码存入一个数据表中。</P>
<P>3.读取控制寄存器61H,并用80h完成一个OR操作。</P>
<P>4.将结果写回控制寄存器端口61H。</P>
<P>5.在控制寄存器上用7fh完成一个AND操作,以便复位键盘触发器,告诉硬件一个按键已被处理,可以读</P>
<P>   下一个键了。</P>
<P>6.复位中断控制器8259,向端口20h写一个20h。</P>
<P>7.退出键盘中断程序。</P>

莱哥 发表于 2006-5-27 17:59

<P>我们先定义一组宏常量记录键值,它包括128个键盘扫描码:</P>
<P>#define KEY_A   0x1E</P>
<P>#define KEY_B   0x30</P>
<P>#define KEY_C   0x2e</P>
<P>#define KEY_D   0x20</P>
<P>#define KEY_E   0x12</P>
<P>#define KEY_F   0x21</P>
<P>#define KEY_G   0x22</P>
<P>#define KEY_H   0x23</P>
<P>#define KEY_I   0x17</P>
<P>#define KEY_J   0x24</P>
<P>#define KEY_K   0x25</P>
<P>#define KEY_L   0x26</P>
<P>#define KEY_M   0x32</P>
<P>#define KEY_N   0x31</P>
<P>#define KEY_O   0x18</P>
<P>#define KEY_P   0x19</P>
<P>#define KEY_Q   0x10</P>
<P>#define KEY_R   0x13</P>
<P>#define KEY_S   0x1f</P>
<P>#define KEY_T  0x14</P>
<P>#define KEY_U   0x16</P>
<P>#define KEY_V   0x2f</P>
<P>#define KEY_W   0x11</P>
<P>#define KEY_X  0x2d</P>
<P>#define KEY_Y   0x15</P>
<P>#define KEY_Z   0x2c</P>
<P>#define KEY_1   0x02</P>
<P>#define KEY_2   0x03</P>
<P>#define KEY_3   0x04</P>
<P>#define KEY_4   0x05</P>
<P>#define KEY_5   0x06</P>
<P>#define KEY_6  0x07</P>
<P>#define KEY_7   0x08</P>
<P>#define KEY_8   0x09</P>
<P>#define KEY_9   0x0a</P>
<P>#define KEY_0   0x0b</P>
<P>#define KEY_DASH  0x0c /* _- */</P>
<P>#define KEY_EQUAL  0x0d  /* += */</P>
<P>#define KEY_LBRACKET  0x1a  /* {[ */</P>
<P>#define KEY_RBRACKET  0x1b  /* }] */</P>
<P>#define KEY_SEMICOLON  0x27  /* :; */</P>
<P>#define KEY_RQUOTE  0x28  /* "' */</P>
<P>#define KEY_LQUOTE  0x29  /* ~` */</P>
<P>#define KEY_PERIOD  0x33    /* &gt;. */</P>
<P>#define KEY_COMMA  0x34 /* &lt;, */</P>
<P>#define KEY_SLASH 0x35 /* ?/ */</P>
<P>#define KEY_BACKSLASH 0x2b  /* |\ */</P>
<P>#define KEY_F1   0x3b</P>
<P>#define KEY_F2   0x3c</P>
<P>#define KEY_F3   0x3d</P>
<P>#define KEY_F4   0x3e</P>
<P>#define KEY_F5   0x3f</P>
<P>#define KEY_F6   0x40</P>
<P>#define KEY_F7  0x41</P>
<P>#define KEY_F8  0x42</P>
<P>#define KEY_F9  0x43</P>
<P>#define KEY_F10  0x44</P>
<P>#define KEY_ESC  0x01</P>
<P>#define KEY_BACKSPACE 0x0e</P>
<P>#define KEY_TAB  0x0f</P>
<P>#define KEY_ENTER 0x1c</P>
<P>#define KEY_CONTROL 0x1d</P>
<P>#define KEY_LSHIFT 0x2a</P>
<P>#define KEY_RSHIFT 0x36</P>
<P>#define KEY_PRTSC 0x37</P>
<P>#define KEY_ALT  0x38</P>
<P>#define KEY_SPACE 0x39</P>
<P>#define KEY_CAPSLOCK 0x3a</P>
<P>#define KEY_NUMLOCK 0x45</P>
<P>#define KEY_SCROLLLOCK 0x46</P>
<P>#define KEY_HOME 0x47</P>
<P>#define KEY_UP   0x48</P>
<P>#define KEY_PGUP  0x49</P>
<P>#define KEY_MINUS  0x4a</P>
<P>#define KEY_LEFT  0x4b</P>
<P>#define KEY_CENTER  0x4c</P>
<P>#define KEY_RIGHT  0x4d</P>
<P>#define KEY_PLUS  0x4e</P>
<P>#define KEY_END  0x4f</P>
<P>#define KEY_DOWN  0x50</P>
<P>#define KEY_PGDOWN  0x51</P>
<P>#define KEY_INS  0x52</P>
<P>#define KEY_DEL  0x53</P>
<P>然后定义两个字符型数组来保存键盘状态:</P>
<P>char key_state[128],key_pressed[128];</P>
<P>    其中key_state[128]用来表示键的当前状态,key_pressed[128]里保存的值表示哪些键被按下,值1表</P>
<P>示按下,0表示放开。</P>
<P>    在挂上新的键盘中断以前,将原来的键盘中断程序地址保存好,以便在程序运行结束后恢复它,我们</P>
<P>定义一个中断指针来存放原来的地址:</P>
<P>void interrupt far (*OldInt9Handler)();</P>
<P>1.安装新的键盘中断程序的函数:</P>
<P>void InstallKeyboard(void)<BR>{<BR>    int i;<BR>    for(i = 0;i &lt; 128;i ++)<BR>        key_state[i] = key_pressed[i] = 0;<BR>    OldInt9Handler = getvect(9);<BR>    setvect(9,NewInt9);<BR>}</P>
<P>2.恢复旧的键盘中断程序的函数:</P>
<P>void ShutDownKeyboard(void)<BR>{<BR>    setvect(9,OldInt9Handler);<BR>}</P>
<P>3.新的键盘中断程序:</P>
<P>void far interrupt NewInt9(void)<BR>{<BR>    unsigned char ScanCode,temp;</P>
<P>    ScanCode = inportb(0x60);<BR>    temp = inportb(0x61);</P>
<P>    outportb(0x61,temp | 0x80);<BR>    outportb(0x61,temp &amp; 0x7f);</P>
<P>    if(ScanCode &amp; 0x80)<BR>    {<BR>        ScanCode &amp;= 0x7f;<BR>        key_state[ScanCode] = 0;<BR>    }<BR>    else<BR>    {<BR>        key_state[ScanCode]   = 1;<BR>        key_pressed[ScanCode] = 1;<BR>    }<BR>    outportb(0x20,0x20);<BR>}</P>
<P>4.读取按键状态的函数(游戏中调用来确定按了哪些键):</P>
<P>int GetKey(int ScanCode)<BR>{<BR>    int res;</P>
<P>    res = key_state[ScanCode] | key_pressed[ScanCode];</P>
<P>    key_pressed[ScanCode] = 0;</P>
<P>    return res;<BR>}</P>
<P>例如:</P>
<P>if(GetKey(KEY_UP))</P>
<P>{</P>
<P>    ....</P>
<P>}</P>
<P>用来判断是否按下UP键。</P>

<P>十二.消除屏幕闪烁</P>
<P>    当我们直接在显示缓冲区内绘图时,屏幕上往往会产生闪烁或类似裁剪的现象,这是为什么呢?这是</P>
<P>因为人的眼睛有惰性,当人眼看东西时,看过的东西的影像会在人的眼睛里停留一段很短的时间,大约为</P>
<P>1/24秒,当图像在一个时间间隔中被更新时,如果更新的时间长于1/24秒,看到的图像就会产生闪烁,</P>
<P>1/24秒是一个人能够看出视觉变化的最少时间量。产生屏幕闪烁还有另外一个原因,就是我们绘制图像时</P>
<P>没有与视频显示完全同步,VGA视频硬件以大约每秒70次的速率刷新屏幕,绘制图像过程如果不在一个刷新</P>
<P>周期内完成的话,屏幕也会产生闪烁现象,因为可能会出现一幅图像还没有绘制完就被显示出来的情况。</P>
<P>    如何避免屏幕出现闪烁呢?一般有二种方法:一是使图像的绘制过程在屏幕的一个刷新周期内进行。</P>
<P>二是采用双缓冲的方法。</P>
<P>    在垂直回扫周期内视频缓冲区不会被视频硬件访问,如果我们改变了视频缓冲区,也要等到下一帧才</P>
<P>能看到结果,更新视频缓冲的最佳时机是它没有被访问的时候----垂直回扫周期内。为此我们必须找到检</P>
<P>测垂直回扫开始的方法,并且用检测的结果使视频缓冲更新和屏幕刷新同步。在VGA显示卡上有一个寄存器</P>
<P>可以被我们用来监视垂直回扫,这个寄存器我们称为VGA输入状态字寄存器,它在端口0x3da上,是个八位</P>
<P>的寄存器,我们只对其中第四位感兴趣,当该位为1时,屏幕正在进行回扫,该位为0时,没有回扫。</P>
<P>等待垂直回扫的函数:</P>
<P>void WaitForVsync(void)<BR>{<BR>    while(!inportb(0x3da) &amp; 0x08);<BR>}</P>
<P>函数从端口0x3da取出一个字节,然后将这个字节与值0x08进行与运算,运算结果如果为1则回扫过程开始。</P>
<P>对垂直回扫计数也可用来给程序定时。</P>
<P>    那么,什么是双缓冲?所谓双缓冲就是在内存中开辟一段与视频缓冲区大小一样的内存空间,把图形</P>
<P>先画在这段内存中,然后快速地把内存的内容复制到视频缓冲区,由于复制的速度非常快,人眼感觉不到</P>
<P>绘图的过程,也避免了屏幕出现闪烁现象。 </P>
<P>在创建双缓冲区前先建立一个指针指向双缓冲区的开始位置:</P>
<P>char far *DoubleBufferPtr;</P>
<P>因为有时我们需要访问视频缓冲区,所以还要建立一个指针用来记录当前活动缓冲区:</P>
<P>char far *ActiveBufferPtr;</P>
<P>以下是与双缓冲有关的函数:</P>
<P>1.创建双缓冲的函数:</P>
<P>int CreateDoubleBuffer(void)<BR>{<BR>    DoubleBufferPtr = (char far *)farmalloc(64000L);</P>
<P>    if(DoubleBufferPtr == NULL)  return 0;</P>
<P>    return 1;<BR>}</P>
<P>2.撤消双缓冲区的函数:</P>
<P>void DeleteDoubleBuffer(void)<BR>{<BR>    if(DoubleBufferPtr != NULL)<BR> farfree(DoubleBufferPtr);<BR>}</P>
<P>3.显示双缓冲区的函数:</P>
<P>void ShowDoubleBuffer(void)<BR>{<BR>    memcpy(VideoBufferPtr,DoubleBufferPtr,64000L);<BR>}</P>
<P>4.设置当前活动缓冲区的函数:</P>
<P>void SetActiveBuffer(int n)<BR>{<BR>    switch(n)<BR>    {<BR>        case VIDEO_BUFFER:   ActiveBufferPtr = VideoBufferPtr;   break;</P>
<P>        case DOUBLE_BUFFER:  ActiveBufferPtr = DoubleBufferPtr;  break;<BR>    }<BR>}</P>

<P>十三.游戏动画对象</P>
<P>    游戏中的活动体都可以看成为一个动画对象。动画对象由一定数量的静态图像(子画面)组成,按一定</P>
<P>的时间间隔把这些图像在屏幕上显示出来给人以运动的感觉,游戏中的各种活动的人物、精灵怪物、活动</P>
<P>的背景等都可以当成动画对象来处理。为了方便我们处理动画对象,我们定义一个动画对象结构用来存放</P>
<P>有关信息。 动画对象结构:</P>
<P>typedef struct OBJECT<BR>{<BR>    int x,y,h,v,x_old,y_old,width,height;<BR>    int anim_clock,anim_speed;<BR>    int motion_clock,motion_speed;<BR>    char far *frame;<BR>    int cur_frame;<BR>    int state;<BR>    char far *mask;</P>
<P>}Object,*ObjectPtr;</P>
<P>其中: x,y 是对象在屏幕上的坐标(左上角坐标);</P>
<P>h,v 对象在水平和垂直方向的移动速率;</P>
<P>x_old,y_old 对象移动以前的屏幕位置;</P>
<P>width,height 对象的宽度和高度;</P>
<P>anim_clock 对象子画面更新计数值;</P>
<P>anim_speed 对象子画面更新计数阀值,当anim_clock累加值等于anim_speed时,显示对象下一个子画面,</P>
<P>           anim_clock同时复位为初始值;</P>
<P>motion_clock 对象位置更新计数值;</P>
<P>motion_speed 对象位置更新计数阀值,当motion_clock的值累加等于motion_speed的值时,对象变换位置,</P>
<P>             otion_clock的值复位为初始值;</P>
<P>*frame 指向当前显示对象子画面的指针;</P>
<P>cur_frame 当前显示对象子画面号;</P>
<P>state 对象的当前状态(是否活动,是否存活等);</P>
<P>*mask 对象图形掩模指针。掩模的大小与对象相同,用来保存屏幕上即将被对象覆盖掉的位置上的图形,</P>
<P>      当对象位置移动后再用来恢复原来位置的图形,以避免对象破坏屏幕。</P>

wonshing 发表于 2006-6-8 09:04

好东西,收藏了,虽不详细但是脑子里已经有了点轮廓

kernel 发表于 2006-6-10 18:39

顶<br>正在仔细欣赏中<br><br>

心动音符 发表于 2006-6-11 11:13

完全看不 懂

灭火的风 发表于 2006-6-29 23:35

应该置顶,对于我这样的初学很合适。

hjj1123 发表于 2006-8-11 09:49

<P>你能不能在 五.在屏幕上画点 给一个画点的原代码?</P>

一笔苍穹 发表于 2006-8-11 13:57

楼上说的看不懂啊

hjj1123 发表于 2006-8-11 17:48

其实在"五.在屏幕上画点"中讲的就是直接写屏的内容,可惜和书上的差不多,只有关键点,没有完整程序,周围的搞不定关键的也就没什么意义.具体实现过程没有,也没有细节.

一笔苍穹 发表于 2006-8-12 11:38

结合上下文,也能弄懂的。

hjj1123 发表于 2006-8-12 13:09

我上次的哪个直接写屏的程序就是这篇文章结合上下文写的 ,结果问题一大堆,我觉得好象我和斑竹用的图形库不一样,程序有一些差别.

一笔苍穹 发表于 2006-8-13 00:03

是吧?这只是一篇理论性的文章,要将其中的东西实现还是有一些差异的,我入门时也用过这篇文章里的东西,不过它主要是针对320X200X256这种低分辩率模式的,实用价值不高,用来上手倒还不错。

偷着乐 发表于 2006-8-16 11:54

好东西

programer 发表于 2006-12-19 22:34

俺这个菜鸟只能看懂汉字。<BR>[em08]<BR>悲哀,我真笨!<BR><BR>[URL=Qzone]http://376480358.qzone.qq.com[/URL]

zm9456 发表于 2007-5-2 22:38

<P>期待着一天变成高手啊~~~</P>

奔跑的鸟 发表于 2007-5-3 11:16

有启发,收了,有空仔细看看,呵呵

大白免 发表于 2008-1-2 19:52


正在仔细欣赏中

页: [1]

编程论坛