/*********************************************************************************************
*
* 本文取自网络,其中有些不正确的地方,如第九节(控制游戏的速度),本人对不正确的地方进行
*
* 了改正,有些不足的地方依照<计算机基本原理>一书增加了一些相关的内容。
*
* 注:本文可能还有不正确的地方,请留意。
*
*********************************************************************************************/
电脑游戏编程入门 (DOS)
这里以电脑VGA 13H视频模式为例,系统介绍制作电脑游戏的基本方法。VGA INT 13H模式是一种工作
在图形方式下的模式,它具有320X200的屏幕分辩率,同屏上可以显示256种颜色(超级任天堂和世嘉5代游
戏机也只有256种颜色),早期的许多游戏都是在这一图形方式下开发的(如大家非常熟悉的仙剑奇侠传、
红色警戒、DOOM等),在这种模式下开发游戏具有编程简单、运行速度快、颜色丰富等特点。
虽然以今天的标准来说320X200的分辩率已经不算什么,但是它仍然是一个学习电脑游戏编程的很好的
入门环境,我们可以通过开发VGA 13H模式下的游戏,逐步地由简单到复杂地学习游戏设计的基本方法,只
要掌握了游戏设计的基本方法和理论,再利用其它软件工具(象WATCOM C、DJGPP、VC、DIRECT X等32位的C
编译器等)来设计游戏将变得非常容易。函数用TURBO C 2.0编译,也可以稍作修改用其它的C编译器来编译
运行。我们先来了解一下电脑显示器的工作原理和显示卡的结构。
一.显示器的工作原理
目前在个人计算机上广泛使用的是采用阴极射线管(CRT)的光栅扫描显示器,我们在屏幕上所看到的
颜色是由电子枪发出的电子束打在CRT屏幕背面的荧光层上的点形成的,通过控制点的亮度可以产生不同的
颜色。电子束不断地从左到右、从上到下扫描整个屏幕,使屏幕显示出图案,电子束以大约每秒70次的速
率在屏幕上重画这一图案,这个过程称为显示刷新或屏幕刷新,具体的扫描频率依赖于所用的显示适配器
(又称为显示卡)。电子束从屏幕的左上角开始向右扫描,到达屏幕的右边缘后,电子束被关闭(水平断开),
接着它又迅速地返回到屏幕的左边缘(水平回扫)开始进行下一行水平方向的扫描,在完成全部的水平方
向的扫描后,电子束在屏幕的右下角结束,此时电子束被关闭(垂直断开),接着又迅速地返回到屏幕的
左上角(垂直回扫),开始下一屏扫描。电子束就是这样周而复始地扫描整个屏幕。显示器在两种方式下
工作:文本方式和图形方式,电脑游戏一般在图形方式下进行。
二.显示器的坐标系统
计算机屏幕上的坐标与我们通常使用的直角坐标系不同,坐标原点(0,0)在屏幕的左上角,向右是
水平方向的坐标,向下是垂直方向的坐标,且坐标没有负值。
三.显示卡的结构
显示器上的显示卡负责将图形显示在屏幕上。显示存储器中存放着在屏幕上显示的图像数据,显示卡
硬件不停地将显存中的内容显示在屏幕上。显示存储器实际上是安装在显示卡上的一块或几块大规模集成
电路,其容量有1M、2M、4M、8M等,在DOS下我们可以访问的内存只有1MB空间(这就是DOS的局限性所在),
地址从00000H到FFFFFH,这段内存根据用途又分为不同的块,系统分配给图形缓冲区(显示存储器)的地
址在A0000H到BFFFFH之间,大小为128KB,其中,VGA占用了A0000H到AFFFFH段,共64KB,这段地址是内存
映射地址,供我们访问显示存储器用。在VGA 13H图形模式下,显示内存使用A0000H到AF9FFH的一段线性内
存空间,每个字节表示一个点,对应屏幕上的一个像点,320*200的屏幕分辨率共需要64000个字节,刚好
64KB,因为一个字节可以表示的最大整数值为256,所以每个像点就可以表示256种颜色。
四.设置视频模式
画图以前必须使屏幕工作在图形方式,这就要设置屏幕的视频模式。设置视频模式有许多种方法,其
中调用视频BIOS功能是最简单的一种,通过调用BIOS中断0x10的服务程序,可以很方便地设置屏幕模式。
调用方法是将值0放入ah寄存器,显示模式放入al寄存器中,然后调用int86()函数。
设置视频模式的函数:
void SetVideoMode(int mode)
{
union REGS r;
r.h.ah = 0;
r.h.al = mode;
int86(0x10,&r,&r);
}
其中mode是视频模式。
五.在屏幕上画点
由于显示存储器是线性排列的,每个像点用一个字节来表示,所以对像点寻址非常容易,像点在显示
内存中的偏移地址可由这个公式确定:y*320+x,其中,y是像点在垂直方向的坐标,x是水平方向的坐标,
320是屏幕的宽度。有了像点的偏移地址,然后加上显示内存的首地址即可得到像点在显示内存中的绝对地
址。只要将表示点的颜色值放到这个地址处,就可以在屏幕上画点了。首先建立一个指针VideoBufferPtr,
使它指向显示内存的首地址:
char far *VideoBufferPtr=(char far *)0xA0000000L;
将这个指针加上像点的偏移地址,像点的最终地址就确定了,它等于:
VideoBufferPtr + y * 320 + x;
把颜色值color写到这个地址:
*(VideoBufferPtr + y * 320 + x) = color;
画点的函数:
void DrawPoint(int x,int y,unsigned char color)
{
*(VideoBufferPtr + y * 320 + x ) = color;
}
六.显示字符
游戏经常在屏幕上打印出显示游戏状态的文本,使游戏者了解当前游戏的状态,这是游戏与游戏者进
行交互的必不可少的。然而在图形模式下显示文本与在文本模式下有很大区别,在图形模式下文本必须使
用基于画位图的方法来显示。
这里介绍一种简便的方法:利用计算机只读存储器中的ASCII字体数据。在计算机只读存储器(ROM)中
固化有ASCII字体数据,这可在基址F000:FA6E上找到。我们只需了解数据是如何存储的,然后再得出存取
该数据的算法,就可以用任意颜色在屏幕上用画位图的方法把字符画出来。
在ROM中,字符按照ASCII字符编码的顺序放置,由于每个字符为8*8的点阵,所以每个字符占据8个字
节的存储空间,要找出某个字符,我们只需将这个字符的ASCII码与8相乘,然后将结果加到基址F000:FA6E
上。由于字符用画位图的方法来显示,因此我们可以随意给它们指定颜色(前景或背景颜色)。
图形下字符显示原理(以字符A为例): 0 0 0 1 0 0 0 0 -ROM中A字符第一个字节
0 0 1 0 1 0 0 0 -第二个字节
0 1 0 0 0 1 0 0 -第三个字节
0 1 0 0 0 1 0 0 .
0 1 0 0 0 1 0 0 .
0 1 1 1 1 1 0 0 .
0 1 0 0 0 1 0 0 .
0 1 0 0 0 1 0 0 -第八个字节
其中1所组成的图像为A.
在图形模式下,我们只需要两个函数就能够打印文本:一个用于显示单个字符,另一个用来显示字符
串。 定义一个远指针指向ROM字符集的开始位置:
char far *RomCharPtr=(char far *)0xF000FA6E;
下面是在屏幕上打印字符和字符串的函数:
1.打印字符的函数。
void PrintChar(int cx,int cy,char c,unsigned char Fcolor,unsigned char Bcolor,int flag)
{
int offset; /* 用来储存显存偏移地址 */
int x,y; /* 循环变量,输出8*8点陈字符 */
char far *TempPtr; /* BIOS中的字符段地址 */
unsigned char bit_mask; /* 与运算变量,用来确定字符点的显示位置 */
TempPtr = RomCharPtr + (c << 3); /* 得到ASCII码的地址,(c << 3)=(c * 8) */
offset = (cy << 8) + (cy << 6) + cx; /* 横坐标320=256+64,所以偏移地址cy*320+x */
/* 可写成(cy<<8)+(cy<<6)+cx */
for(y = 0;y < 8;y ++) /* 行画点,显示字符 */
{
bit_mask = 0x80; /* 1000 0000B */
for(x = 0;x < 8;x ++ ) /* 列画点,显示字符 */
{
if((*TempPtr & bit_mask)) /* 当前位&后为真 */
*(VideoBufferPtr + offset + x) = Fcolor;/* 画点 */
else if(flag == 1) /* flag=1显示背景色 */
*(VideoBufferPtr + offset + x) = Bcolor;/* 画点 */
bit_mask=(bit_mask >> 1); /* 左移一位后与TempPtr相& */
}
offset += 320; /* 换行 */
TempPtr ++; /* 后移一位 */
}
}
说明:
1.cx,cy 是字符在屏幕上的坐标。
2.c是字符的ASCII码。
3.Fcolor,Bcolor 分别是字符的前景和背景颜色。
4.flag 打印标志,当flag=1时显示字符的背景色,否则打印的字符具有透明效果。
2. 打印字符串的函数。
void PrintString(int x,int y,char *string,unsigned char Fcolor,unsigned char Bcolor,int flag)
{
int index;
for(index = 0; string[index] != 0;index ++)
PrintChar(x + (index << 3),y,string[index],Fcolor,Bcolor,flag);
}
说明:
1.x,y 是字符串在屏幕上的坐标。
2.*string 字符串指针。
3.Fcolor,Bcolor 分别是字符串的前景和背景颜色。
4.flag 打印标志,当flag=1时显示字符串的背景色,否则打印的字符串具有透明效果。
七.设置颜色寄存器
我们知道VGA显示卡具有显示256种颜色的能力,每种颜色能够用一个0-255之间的数值来表示,那么这
些数值与我们在屏幕上实际见到的颜色之间有什么关系呢?其实这些数值只是VGA显示卡上的颜色寄存器的
索引值,颜色寄存器里才保存了屏幕上颜色的真实值。VGA显示卡上有一个包含256个单元的颜色寄存器(又
称为调色板),每个单元由三部份组成,这三部份分别代表颜色中的红、绿、蓝三种成份(显示器就是用这
三种成份来组成任何我们所看到的颜色),用三个字节表示,颜色寄存器一共有768个字节(3*256=768)。
当我们要在屏幕上显示某种颜色时,显示卡硬件就根据颜色的索引值在颜色寄存器中查找,找到后再
从相应单元中取出颜色值显示在屏幕上,这个过程与画家使用调色板相似,颜色寄存器相当于调色板,颜
色寄存器中的单元相当于调色板上的色格,在色格中装有预先调好的颜色,当画家需要用某种颜色作画时,
就从装有那种颜色的色格中把颜色取出来。例如,我们要显示颜色索引值为30的颜色,显示卡硬件就去查
找颜色寄存器的第30单元,30单元位于距颜色寄存器首址3*30=90处(因为每个单元有三个字节),然后取出
90处记录有红、绿、蓝三种成份的三个字节作为在屏幕上显示的色彩信号。但是实际上每个字节只用了六
位来表示颜色,其它两位没用,这六位表示的数的值域为0-63,所以每种颜色(红、绿、蓝)成份具有64种
亮度的表现能力,三种颜色成份组合共可以产生64*64*64=262144种颜色(VGA 13H模式从这262144种颜色
中取出256种在同一屏幕上显示)。我们可以通过事先设置颜色寄存器的值来使用我们自己的颜色。
设置颜色寄存器有多种方法,如调用BIOS功能,但是这种方法速度比较慢,游戏设计中通常采用直接
访问VGA显示卡的I/O端口的方法来快速设置颜色寄存器,我们只需访问四个I/O端口就可以完成设置颜色
寄存器的工作。这四个端口分别是: 0x3c6、0x3c7、0x3c8和0x3c9。
端口0x3c6称为调色板屏蔽寄存器,用来屏蔽所要求的调色板寄存器的位,如果你在这个寄存器中放入
0xff,你就可以通过调色板索引寄存器0x3c7和0x3c8(一个用于读,一个用于写)访问任何你希望访问的颜色
寄存器,端口0x3c9称为调色板数据寄存器,红、绿、蓝三种成份就是通过它进行读写(颜色值要读或写三次)。
我们定义一个结构来方便处理颜色寄存器:
typedef struct RGB_COLOR
{
unsigned char red;
unsigned char green;
unsigned char blue;
}RGBColor,*RGBColorPtr;
结构中的red、green和blue变量用来保存颜色的红、绿、蓝三种成份。
设置颜色寄存器值的函数:
void SetPaletteRegister(int index,RGBColorPtr color)
{
outportb(0x3c6,0xff);
outportb(0x3c8,index);
outportb(0x3c9,color->red);
outportb(0x3c9,color->green);
outportb(0x3c9,color->blue);
}
获取颜色寄存器值的函数:
void GetPaletteRegister(int index,RGBColorPtr color)
{
outportb(0x3c6,0xff);
outportb(0x3c7,index);
color->red = inportb(0x3c9);
color->green = inportb(0x3c9);
color->blue = inportb(0x3c9);
}
八.在屏幕上画位图
计算机绘制图像通常采用一种称为位映射图(BITMAP)的图形处理方法进行,位映射图是一个矩形的点
阵结构(二维矩阵),显示在屏幕上时,对应屏幕上一个矩形区域,组成位图的数据储存在内存中一段连续
的区间。我们比较常见的位图文件有:BMP、PCX、GIF、JPG等。位图通常存储在外部文件中,使用以前必
须将其从磁盘文件调入内存。下面介绍将一个256色PCX图形文件读入内存的方法:
1.定义PCX文件头结构:
typedef struct PCX_HEADER
{
char menufactrue; /* 厂家标识编号 0x0a */
char version; /* 文件版本编号 */
char packing_type; /* 压缩模式 */
char bits_per_pixel; /* 每点占用的位数 */
int minx; /* 最小X坐标值 */
int miny; /* 最小Y坐标值 */
int maxx; /* 最大X坐标值 */
int maxy; /* 最大Y坐标值 */
int hres; /* 水平分辨率 */
int vres; /* 垂直分辨率 */
char palette[48]; /* 颜色调色板 */
char unused; /* 未使用 */
char bit_plance; /* 位平面个数 */
int bytes; /* 单一水平线占用的字节数 */
int palette_type; /* 调色板类型 */
char unused2[58]; /* 未使用 */
}PCXHeader,*PCXHeaderPtr;
2.定义用来存放PCX图像数据的结构:
typedef struct PCX_PICTURE
{
int width;
int height;
char far *buffer;
RGBColor palette[256];
}PCXPicture,*PCXPicturePtr;
3.初始化图像数据的函数:
int InitPCX(PCXPicturePtr image,int w,int h)
{
unsigned size = w * h;
image->width = w;
image->height = h;
image->buffer=(char far *)farmalloc(size);
if(image->buffer == NULL) return 0;
return 1;
}
4.从外部文件读入数据的函数:
int LoadPCX(char *filename,PCXPicturePtr image,int flag)
{
FILE *fp;
unsigned num_bytes,count,size;
int index;
unsigned char data;
PCXHeader PcxHeader;
size = image->width * image->height;
if((fp = fopen(filename,"rb")) == NULL)
return 0;
fread(&PcxHeader,sizeof(PCXHeader),1,fp);
count = 0;
while(count <= size)
{
data = fgetc(fp);
if(data >= 192 && data <= 255)
{
num_bytes = data - 192;
data = fgetc(fp);
while(num_bytes >= 0)
{
*(image->buffer + count) = data;
++ count;
}
}
else
{
*(image->buffer + count) = data;
++ count;
}
}
fseek(fp,-768L,SEEK_END);
for(index = 0;index < 256;index ++)
{
image->palette[index].red = ((fgetc(fp)) >> 2);
image->palette[index].green = ((fgetc(fp)) >> 2);
image->palette[index].blue = ((fgetc(fp)) >> 2);
}
fclose(fp);
if(flag == 1)
for(index = 0;index < 256;index ++)
SetPaletteRegister(index,(RGBColorPtr) & image->palette[index]);
return 1;
}
其中参数flag用来指明调入文件的同时是否设置颜色寄存器(flag=1设置)。
5.画位图的函数:
void DrawImage(int x,int y,int width,int height,char far *image)
{
int i,j;
for(i = 0;i < height;i ++)
{
for(j = 0;j < width;j ++)
{
if(*image != 0 && (x + j) >= 0 && (x + j) < 320 && (y + i) >= 0 && (y + i) < 200)
DrawPoint(x + j,y + i,*image);
image ++;
}
}
}
x,y是图像在屏幕上的左上角坐标,width,height是图像的宽度和高度,image是指向内存中图像的指针。
我们对if(*image != 0 && (x + j) >= 0 && (x + j) < 320 && (y + i) >= 0 && (y + i) < 200)语句
进行一下分析:
*image != 0用来检查所画的颜色值是否是透明色,如果是,则不画出来,这样我们就可以画出有透
明效果的图象,即透过图象可以看到背景,透明色通常取值0,也可以用其他的颜色值表示。
(x + j) >= 0 && (x + j) < 320 && (y + i) >= 0 && (y + i) < 200语句用来判断所画点的坐标是
否超出屏幕显示的范围,这样可以画出具有裁剪效果的图象,如图象的一部份在屏幕外。
这个函数并不是最快的,因为它要执行width X height次判断,更快的函数请看VGA13H函数库中的绘
图函数。