认识DirectX,快速建立DirectX环境。重点理解页面和学习页面内容的拷贝方法。
在 Windows 之前,个人电脑上的大部分游戏都是基于MS-DOS的,游戏开发人员可
以直接对计算机的硬件如显卡、声卡、键盘、游戏手柄等进行编程。
进入 Windows 后,Windows的设备管理程序对所有硬件都统一管理起来,这对品种
繁多又日新月异发展的硬件设备来说,这个统一的管理无疑是必需的,但同时带来的
问题就是游戏开发人员再不能直接对这些硬件设备编程了。例如在Windows下,我们要
在屏幕上显示一幅图形,用标准GDI函数就得经过无数的环节才能做到(现在计算机
的速度发展太快了,用标准GDI函数仿佛又不是什么问题了)。微软开发DirectX的首要
目的就是要提高Windows环境下游戏开发的容易性。
利用DirectX, 游戏开发人员就可以获得DirectX的硬件独立性的优点,同时又可以
直接访问硬件。DirectX的最主要的目标就是提供MS-DOS中直接的硬件访问特性,同时
去掉个人电脑中再添加新的硬件时所带来的硬件识别问题。
因为硬件的升级是不可避免的, 所以,DirectX实际上也提供了另一种即插即用的
方法。因此,利用DirectX可以开发出高性能实时的应用程序,可以直接访问计算机中
的硬件和将来系统中新的硬件设备。DirectX在硬件和应用之间提供了一致的接口以减
少安装和配置的复杂性,并且使硬件的利用达到最优。利用DirectX提供的接口,程序
员能充分利用硬件的特性而不需要考虑其具体细节。
DirectX主要包括以下几个部分。
DirectDraw通过支持访问屏外显示内存中位图的软硬件加速技术,快速直接存取,
利用硬件的位块传输和缓冲区翻转功能。
DirectSound提供软硬件声音混合和录音再生功能。
DirectMusic提供软硬件MID 音乐的播放功能。
DirectPlay使得游戏在调制解调器和网络之间的连接更加简单方便。
Direct3D 允许程序完成一个完全的三维图形系统和完全控制着色管道。
DirectInput提供了基于Windows游戏的输入的API 程序,包括键盘、鼠标和操
纵杆,以及将来的基于Windows新的输入设备。
DirectSetup提供了DirectX的一次性安装过程。
我们现在编制的是2D( 平面)游戏,主要是和DirectDraw打交道。
DirectX的版本发展很快,现在已到了9.0版;但是它的基本功能变化不大,这也是
微软在升级它的版本时一直没有抛弃它的向下兼容的原则,这恐怕也是人们能够放心
地学习微软技术的原因吧。
不少书上对建立DirectX环境讲述不少,但初学者能搞懂的不多。真有这么复杂吗?
在职业技术教育中有一个知识体系的最小够用原则( 就像中国汉字有几万,认识千多
字就可识字习文一样)。现在我们来建立最小的DirectX环境。根据需要我们在“game.cpp”
类文件中加入功能函数InitDDraw(void),作用是初始化DirectDraw环境。
在 InitDDraw(void)中我们的工作是初始化DirectDraw环境,并创建5个DirectDraw的工作页面。
主显页面 DXSPrimary
背景地图页面 DXSBack0
地图移动页面 DXSBack00
主缓存页面 DXSBack1
对象缓存页面 DXSgonh
BOOL game::InitDDraw(void)//功能:初始化DirectDraw 环境。
{//A.建立基本环境
1 ddrval=DirectDrawCreate( NULL, &DXW, NULL );
2 if(ddrval!= DD_OK ) return FALSE; //创建DirectDraw对象失败返回
3 ddrval=DXW→SetCooperativeLevel(GetActiveWindow(),DDSCL_NORMAL);
4 if(ddrval!= DD_OK) return FALSE; //设置窗口模式失败返回
5 ddrval=DXW→SetDisplayMode(800,600,16);
6 if(ddrval!= DD_OK) return FALSE; //设置显示模式失败返回
//B.创建主显页面
7 ddsd.dwSize = sizeof( ddsd );
8 ddsd.dwFlags = DDSD_CAPS;
9 ddsd.ddsCaps.dwCaps =DDSCAPS_PRIMARYSURFACE;
10 ddsd.dwBackBufferCount=1;
11 ddrval=DXW→CreateSurface( &ddsd,&DXSPrimary,NULL);
12 if( ddrval != DD_OK ) return FALSE; //创建主显页面失败返回
//C1.创建背景地图页面
13 Surface(WIDTH,HEIGHT); //创建背景地图页面,640X480
14 ddrval=DXW→CreateSurface(&ddsd,&DXSBack0,NULL);
15 if (ddrval!=DD_OK) return FALSE;
//C2.创建地图移动页面
Surface(WIDTH,HEIGHT); //创建地图移动页面,640X480
ddrval=DXW→CreateSurface(&ddsd,&DXSBack00,NULL);
if (ddrval!=DD_OK) return FALSE;
//C3.创建主缓存页面
Surface(SCRWI0,SCRHE0); //创建主缓存页面,800X600
ddrval=DXW→CreateSurface(&ddsd,&DXSBack1,NULL);
if (ddrval!=DD_OK) return FALSE;
//C4.创建对象缓存页面
Surface(210,200); //创建对象缓存页面, 大小按图片最大尺
寸。
ddrval=DXW→CreateSurface(&ddsd,&DXSgonh,NULL);
if (ddrval!=DD_OK) return FALSE;
//D.定义透明关键色
16 col.dwColorSpaceLowValue=RGB(255,255,255); //透明关键色
17 col.dwColorSpaceHighValue=RGB(255,255,255);//透明关键色
18 DXSgonh→SetColorKey(DDCKEY_SRCBLT,&col); //关键色
return TRUE;
}
InitDDraw()主要分为三部分。
A.建立基本环境
第 1、2行创建DirectDraw对象,失败返回。
第 3、4行设置窗口模式,失败返回。
第 5、6行设置显示模式,失败返回;这里我们设为16位色的800X600的模式。
B.创建主显页面(主表面)
第 7~12行我们在这里创建一个最简单的主显页面DXSPrimary,所谓的主显页面
(主表面)就是我们具体的屏幕显示区。
C1~C4.创建各个缓存页面。
第 13-15行是根据我们游戏的需要,调用Surface(int w,int h)函数创建一个指定高
宽的背景地图页面DXSBack0,用来存放那个用来拼接大地图的地面块。以下C2~C4
是根据不同要求分别创建其它页面。
为了移动大地图,我们还要创建一个地图移动页面DXSBack00。
和前面 GDI 模式一样,我们的游戏场景是先在一个暂存设备场景中生成,完成后
再拷贝到当前显示区。这样我们还要创建一个主缓存页面DXSBack1,也是在上面生成
好一幅场景后再翻转到主显页面上DXSPrimary。另外我们每次调入的图形还得有一个存
放点,那就再来一个对象缓存页面DXSgonh。
D.关键色
第 16~18行为了在 DirectDraw中得到透明显示效果,我们在这里定义对象缓存页
面DXSgonh的关键色。
void game::Surface(int w,int h)// 定义缓存页面的大小
1{ ZeroMemory(&ddsd,sizeof(ddsd));
2 ddsd.dwSize = sizeof(ddsd);
3 ddsd.dwFlags = DDSD_CAPS|DDSD_HEIGHT |DDSD_WIDTH;
4 ddsd.dwWidth = w; //
5 ddsd.dwHeight= h; //
6//ddsd.ddsCaps.dwCaps=DDSCAPS_OFFSCREENPLAIN|DDSCAPS_VIDEOMEMORY;
//定义显示内存为缓存区
7ddsd.ddsCaps.dwCaps=DDSCAPS_OFFSCREENPLAIN|DDSCAPS_SYSTEMMEMORY;
//定义系统内存为缓存区
}
第4、5行定义页面宽、高。
第 6行定义缓存页面在显示内存中。
第 7行定义缓存页面在系统内存中。(二者只能选一,这里是第7行有效)
//以下是DirectX变量、函数定义
public: //公有,外部可调用
LPDIRECTDRAWSURFACE DXSBack1; //主缓存页面1
LPDIRECTDRAWSURFACE DXSBack0; //背景地图缓存页面
LPDIRECTDRAWSURFACE DXSBack00; //地图移动缓存页面
RECT rect; //定义矩形
void FreeDDraw(void ); //释放所有的DirectDraw 对象
void Bk1ToPr(HDC scrhdc,CRect lpRect);//后页1 到前页
private: //私有,类内部使用
DDSURFACEDESC ddsd; //页面描述
HRESULT ddrval;
LPDIRECTDRAW DXW; // DXDraw 对象
DDCOLORKEY col; //透明关键色
LPDIRECTDRAWSURFACE DXSPrimary;// DXDraw 主页面
LPDIRECTDRAWSURFACE DXSgonh; //对象缓存页面
BOOL InitDDraw(void); //初始化DirectDraw 环境
void Surface(int w,int h); //定义缓存页面的大小
在“常数定义.h”的头文件上加入DirectDraw的说明和标明DirectDraw的连接库。
#include <ddraw.h>//DirectDraw 的引用
#pragma comment(lib,"ddraw.lib")//调用DirectDraw连接库
我们在 game.cpp 的init()初始化函数中, 调用InitDDraw()即可完成DirectX的初始化
工作。
if(!InitDDraw()) returnFALSE;//初始化DirectDraw环境。
完了?就这么简单?不是还要在VC中配置DirectX的环境吗?不需要,就这样一
个可以编译DirectX的最小配置就完成了,并且这是不依赖任何DirectX版本的。至于
DirectX 的其它复杂配置,在我们熟悉了DirectX 的基本应用后,需要用到它的其它功能
时,再给大家介绍。
现在主显页面和各缓存页面都建立了 ,剩下的工作就是如何按要求将图形在这些
页面上翻转拷贝了。
设备场景 MemDC 中宽w、高h的图形装入缓存页面DXSgonh(x,y)处:
HDC hdc;
if ( DXSgonh->GetDC(&hdc) != DD_OK)return FALSE;//
BitBlt(hdc,x,y,w,h, MemDC,0,0,SRCCOPY);
DXSgonh->ReleaseDC(hdc);
反之也可以将DX缓存页面图形装入GDI设备。
DXSgonh 中矩形rect 范围内的图形拷贝到DXSBack1(x,y)处:
rect.left=0,rect.top=0,rect.right=w,rect.bottom=h;
if(DXSBack1->BltFast(x,y,DXSgonh,&rect,dwTrans)!=DD_OK)return;
其中:
dwTrans =DDBLTFAST_WAIT为直接拷贝。
dwTrans=DDBLTFAST_WAIT|DDBLTFAST_SRCCOLORKEY为透明拷贝
这种在 DX 页面的拷贝速度是非常快的。我们在2D图形中引入DirectX技术,所寻
求的主要是这个拷贝速度的提升。
另外,着重说明的一点,就是缓存页面拷贝到主显页面。
rect.left=0,rect.top=0,rect.right=w,rect.bottom=h;
if(DXSPrimary->BltFast(x,y,DXSBack1,&rect, dwTrans)!=DD_OK)return;
这是我们将处理后的图形向屏幕输送显示的过程,在DX中有一个重要的技术叫页
面的翻转,缓存页面图形翻转到主显页面几乎是不需要额外时间的。但这必须在
DirectDraw初始化时,定义主页面为非窗口式的独占模式,这种模式留在以后再给大家介
绍。
由于前面我们引入DirectX时做了两个简化:一是在MFC中引入DirectX,二是保持
窗口模式。现在我们用DirectDraw的技术对游戏做一点小小的改动,标志游戏重要的指
标显示速度马上就有很大的提升。
这是对getpic(CStringcc,int p,int a)调压缩图形函数的修改,修改部分见加黑处。
保留 a=1 时的原调用功能,该功能在生成小地图时需要。
//**************************************************
// BOOL getpic(CString cc,int p,int a) 调压缩图形
// A. 调cc 指定的压缩图形包中第p 个图形和图形的尺寸w、h
// B. a=0 时取得的图形在对象缓存页面DXSgonh 中。
// C. a=1 时取得的图形在MemDC 中。
//**************************************************
BOOL game::getpic(CString cc,int p,int a)//调图片
{ int len;
if(p<0) return FALSE;
BYTE *tmp;
if(cc=="人")
{if(p>RBUF-1) return FALSE;
len=rbufadd[p+1]-rbufadd[p]; //取数据块长度
tmp=(BYTE *)new BYTE[len];
Memcpy0(tmp,rtmp+rbufadd[p],len);
}
if(cc=="兽")
⋯⋯
if(cc=="景")
⋯⋯
LPBITMAPINFOHEADER bm=(BITMAPINFOHEADER *)tmp; //图形数据转换
bm->biSize = sizeof(BITMAPINFOHEADER);
w=bm->biWidth; //图形高
h=bm->biHeight; //图形宽
if(a==0)
//DX-----------------------------------------------------------
{HDC hdc; //将图形装入动画缓存区DXSgonh
if ( DXSgonh->GetDC(&hdc) != DD_OK)return FALSE;//
StretchDIBits(hdc,0,0,w,h,0,0,w,h,tmp+256*sizeof(RGBQUAD)+bm->biSize,
(BITMAPINFO* )bm,DIB_RGB_COLORS,SRCCOPY);
DXSgonh->ReleaseDC(hdc);
}
//-------------------------------------------------------------
else
{StretchDIBits(MemDC,0,0,w,h,0,0,w,h,tmp+256*sizeof(RGBQUAD)+bm->biSize,
(BITMAPINFO* )bm,DIB_RGB_COLORS,SRCCOPY);
}
delete[] tmp;
return TRUE;
}
这是将各个对象显示到主缓存页面的关键部分,修改点就只有一行, 将原GDI的
透明显示换成DX的__________带透明显示的页面拷贝就行了。
TransparentBlt2(BkDC1,x,y,w,h,MemDC,0,0,w,h,RGB(255,255,255));//透明显
示
MemDC→ BkDC1 换成 DXSgonh→ DXSBack1;
rect.left=0,rect.top=0,rect.right=w,rect.bottom=h;
DXSBack1->BltFast(x+SCRW,y+SCRH,DXSgonh,&rect,DDBLTFAST_WAIT
|DDBLTFAST_SRCCOLORKEY);
修改部分见加黑处。
void game::Alphasetobj(int q,int a)//对象显示
{ if(a==1&&man[q].lb==2) return;
⋯⋯
rect.left=0,rect.top=0,rect.right=w,rect.bottom=h;
if(a==0)//DX
{if(DXSBack1->BltFast(x+SCRW,y+SCRH,DXSgonh,&rect,DDBLTFAST_WAIT
|DDBLTFAST_SRCCOLORKEY)!=DD_OK) return; //在缓存区透明显示
return;
}
else Alpha(x,y);
mans++;
}
⋯⋯
}
DX 的半透明显示函数构成稍微复杂一点,图形的通道合成本身算是一个比较复杂
的算法;这里我们还是使用Windows的GDI的AlphaBlend(⋯)半透明处理函数,在这
里构成DX中的半透明处理效果。我们先再看一下在GDI中的处理方法。
BitBlt(BK,0,0,w,h,BkDC1,x,y,SRCCOPY);
A.将要处理成半透明的对象区保存在BK中。
TransparentBlt2(BkDC1,x,y,w,h,MemDC,0,0,w,h,RGB(255,255,255));
B.再将活动对象在BkDC1中透明显示一次。
AlphaBlend(BkDC1,x,y,w,h,BK,0,0,w,h,rBlend);
C.半透明处理,将BK半透明拷贝到BkDC1上。
void game::Alpha(int x,int y)//DX 半透明显示
{ HDC hdc;
1 if ( DXSBack1→GetDC(&hdc) != DD_OK)return;
2 BitBlt(hDC0,0,0,w,h,hdc,x+SCRW,y+SCRH,SRCCOPY);
3 BitBlt(hDC1,0,0,w,h,hdc,x+SCRW,y+SCRH,SRCCOPY);
4 DXSBack1→ReleaseDC(hdc);
5 if ( DXSgonh→GetDC(&hdc) != DD_OK)return;
6TransparentBlt2(hDC1,0,0,w,h,hdc,0,0,w,h,RGB(255,255,255));//透明显示
7 AlphaBlend(hDC1,0,0,w,h,hDC0,0,0,w,h,rBlend); //半透明处理
8 BitBlt(hdc,0,0,w,h,hDC1,0,0,SRCCOPY);
9 DXSgonh→ReleaseDC(hdc);
10 if(DXSBack1→BltFast(x+SCRW,y+SCRH,DXSgonh,&rect,DDBLTFAST_WAIT
|DDBLTFAST_SRCCOLORKEY)!=DD_OK) return; //在主缓存页面上透明显
示
}
DX 半透明显示注释
这里新增加了两个设备场景用于DX半透明处理,hDC0、hDC1。
A.将要处理成半透明的对象区保存在hDC0中。
第 1行获取主缓存页面的设备场景hdc。
第 2行将要处理成半透明的对象区保存在hDC0中。
第 3行在hDC1 中再保存一次。
第 4行释放hdc。
B.再将活动对象在hDC1中透明显示一次。
第 5行获取对象缓存页面的设备场景hdc。
第 6行再将hdc→hDC1透明显示。
C.半透明处理,将hDC0半透明拷贝到hDC1上。
第 7行将hDC0 半透明拷贝到hDC1上。
第 8行透明显示结果hDC1 存回到hdc,实质上是拷贝到对象缓存页面DXSgonh中
了。
第 9行释放hdc。
第 10行对象缓存页面DXSgonh中的半透明图拷贝到主缓存页面DXSBack1上。
DX 半透明处理看起来好像是有些复杂,但需要半透明处理的只是当前显示区中的
活动对象,它的数量少、图形也比较小。所以增加DX半透明处理后,对整个系统的开
销没有多大的影响。
Bk1ToPr(⋯)是在“再快一点Dlg.cpp”的OnTimer(… ) 时钟函数中替换用 Bk1刷新
窗口。这是游戏显示品质因素提升的关键之处。
BitBlt(dc.m_hDC,2,10,WIDTH,HEIGHT,m_game.BkDC1,0,0,SRCCOPY);//用Bk1 刷新窗口
换成;
m_game.Bk1ToPr(dc.m_hDC,lpRect);//主缓存页面到主页面
void game::Bk1ToPr(HDC scrhdc,CRect lpRect)// 主缓存页面到主页面
{rect.left=SCRW,rect.top=SCRH,rect.right=WIDTH+SCRW,rect.bottom=HEIGHT+SCRH;
if( DXSPrimary→BltFast(lpRect.left+5,lpRect.top+13,
DXSBack1,&rect,DDBLTFAST_WAIT)!=DD_OK) return ;
}
对照原来GDI的地图块移动拼接函数,和在前面讲的DX缓存页面间的拷贝,这个
修改后的函数是容易理解的(这里再放上GDI设备场景和DX页面名称对照表)。
//**************************************************
// mlmap() 地图块移动拼接
// 这里使用的是单地图无缝拼接移动算法。
//**************************************************
void gamemap::mlmap()//地图块移动拼接DX
{if(movemap!=0) //地图有移动。
{int gx=scrx%WIDTH,gy=scry%HEIGHT;
//地图横向移动--------------------------------------------------------
rect.left=gx, rect.right=WIDTH, rect.top=0,rect.bottom=HEIGHT;
DXSBack00→BltFast(0,0,DXSBack0,&rect,DDBLTFAST_WAIT);
//--------------------------------------------------------------------
rect.left=0, rect.right=gx, rect.top=0,rect.bottom=HEIGHT;
DXSBack00→BltFast(WIDTH-gx,0,DXSBack0,&rect,DDBLTFAST_WAIT);
//地图纵向移动--------------------------------------------------------
rect.left=0, rect.right=WIDTH, rect.top=gy,rect.bottom=HEIGHT;
DXSBack1→BltFast(SCRW,SCRH,DXSBack00,&rect,DDBLTFAST_WAIT);
//--------------------------------------------------------------------
rect.left=0, rect.right=WIDTH, rect.top=0,rect.bottom=gy;
DXSBack1→BltFast(SCRW,SCRH+HEIGHT-gy,DXSBack00,&rect,DDBLTFAST_WAIT);
}
sort(); //按Y 坐标排序,用于在显示时分出前后位置
movemap=0; //地图移动否?
}
在生成小地图的getsmap()中,我们调入的用于拼接大地图的地面块,不要忘记拷
贝到背景地图页面DXSBack0中。
void gamemap::getsmap()//生成小地图
{//调地面块到BkDC0 地图设备场景
char name[256];
sprintf(name,"%s%s",dir,mapbak);
loadbmp(name);
SelectObject(MemDC,bitmap);
//DX 把位图装入背景地图页面///////////////////////////
HDC hdc;
if ( DXSBack0→GetDC(&hdc) != DD_OK) return;
BitBlt(hdc,0,0,WIDTH,HEIGHT,MemDC,0,0,SRCCOPY);
DXSBack0→ReleaseDC(hdc);
DeleteObject(bitmap); //删除上次的位图内存。
⋯⋯
}
好了,这几处修改完成后,我们的游戏又可以编译运行了。这样游戏在运行中有了
几个明显的变化。
一是,游戏运行时自动切换到800X600的16位色模式下;二是,显示速度明显加
快。在作者P3-800/128M内存的机器上,每屏显示时间从“快一点”的130ms降至本章
的47ms(这个时间是相对的,它与当前屏上的对象数有关)。
详细内容请看本章实例程序:“再快一点”。
在这一章里,我们学了以下知识和方法。
1.认识DirectX
2.建立DirectX环境和创建工作页面的方法
3.各种页面内容的拷贝方法。
4.GDI与DirectX图形处理方法的简单比较。