深入directdraw

我知道,自从上次那篇《加入DirectDraw》以后,我已经很久没有再谈论DirectDraw编程了。对此我很抱歉。这虽然有N多个原因,但归根结底是我堕落了。所以从现在起,我会戒掉大麻,继续这项工作。

再次阅读一下《加入DirectDraw》。你应该已经发现,里面的程序简直愚蠢透顶。我只是创建了一个全屏模式下的主页面,但根本没让DirectDraw做任何事情。在你知道怎样把屏幕切换到全屏模式后,你还需要做很多很多的事情才会看到真正的很酷的东西。但这些大多都是些看不见的后台操作,而且枯燥乏味。所以,我会用我一贯夸张的习惯,尽量使你能睁着眼睛读完本文——不会睡着。

现在是时候知道一些高级的C语言编程技巧了,这也许很简单,也许很复杂,无论如何,我希望你能看完它。

如果有人问你什么是计算机内存,你应该对此非常熟悉,就是那块有几个芯片的绿色小电路板,把它插到主板上,就能使你的电脑跑的疯快。你知道内存就象美女——越多越好。也许你已经是情场高手,但你肯定也会常常苦恼,不知道这些姑娘在想些什么。但做一个合格的C++程序员,你永远都要清楚,你完全可以只手遮天,随心所欲地控制内存的工作。

程序员眼中的内存很奇怪,它是一长串的格子,每个格子可放入一个字节的数据,每个格子都有一个唯一的地址。你也许无数次地听过专家们得意洋洋地谈论16位操作系统和32位操作系统,它们的根本区别就是,16位操作系统一次只读出两个格子的数据,32位操作系统则可读出4个格子也就是4个字节。因为每个字节为8位二进制数,4个字节就是32位,所以叫做32位操作系统。从WIN98开始,所有的WINDOWS操作系统都完全是32位的了。而且从386开始,所有的CPU都是32位,这就是说CPU一次处理4个字节的数据。因此,在程序里你最好尽量把变量定义为长整形,因为32位的变量不管寻址还是计算都是最快的。除非你在使用286计算机,或者DOS操作系统。

这或许是个很基本的概念,但如果你象我一样——没考上大学,你可能仍然不明白什么是8位,16位,32位?这就是说8个二进制数,16个二进制数,32个二进制数。任何10进制数短除2后就得到二进制数。我给你个简单的例子。

比如10进制数255,短除2转换成二进制后是11111111。(如果你不知道怎样短除,就用WINDOWS上的计算器来转换,用科学型)它同16进制的关系是这样的:

二进制:1111 1111

16进制: F  F

把任何二进制数以四位分组,求出每组的16进制值就是16进数值。此例为FF,所以一个8位二进制数的最大值是255,而它的16进制值是FF,它是一个字节能存放的最大数。如果是256你就得用2个字节来存放它。但有一点你要非常清楚,1个字节有256种值,因为0-255是256个数。还有一点非常容易让初学者陷入迷途,如果你在程序中定义一个“255”字符串的话,记住,它占用的是3个字节。因为每一个字符都占一个字节,而一个汉字占用2个字节。

变量一旦申明过后,就会在内存中分配相应类型长度的字节数,当你为变量赋值时,便把这个值储存到这段内存里。知道这点,你就可以用很多种方法来改变你的变量的值——只要改变这段内存的数据就行。C语言里有一种独特的数据类型——指针。我相信很多程序员都憎恨它。指针变量里存储的都是各种各样的地址,你可以让它指向任何位置,而通过这个指针你就可以轻易地访问你要修改的内存位置。指针操作是电脑能做的最快的运算,如果你有足够的勇气,你完全可以用指针来代替所有的变量。但你得非常小心,因为这个指针并非总是指向天堂,更多时候,它指着的是地狱!如果你试图修改某段操作系统钟爱并养在深闺中的内存——比如WINDOWS 所在的内存,这时就很可能发生灾难。对于大多数开发应用软件的程序员来说,指针太危险了,他们更喜欢用数组。你应该知道数组,这在Basic语言里相当常用。其实数组就是指针,它是为了使指针使用更加安全而设定的。这听起来不错,但数组的运算要比指针慢得多,我要开发的是游戏软件,我可以舍弃生命来追求速度,这正如我会毫不忧郁地冲进山洞去寻找宝物,而不会在乎里面的敌人是多少级。

我很想写一个简单的使用指针的小程序给你看看,但如果你已经清楚指针的概念,你就会对此不肖一顾,如果你是第一次接触指针的话,这也对你没有太大用处。你必须去参考C语言的书籍,然后去把你的电脑弄死机,以便你能深刻地理解指针这把双刃剑。

很多人说电子游戏比毒品还恶毒,你的父母也许为此剥夺过你玩游戏的权力,但如果你曾用整人专家等工具修改游戏的话,你就已经很熟悉角色的生命力等是怎样存储在内存中的了。也就是说你其实已经理解了内存的工作方式。对我来说整人专家就是指针,我用它找到了我要修改的内存,然后我手工进行修改,甚至锁定它。大多数时候这样做会使我无敌于天下,但很多时候我也不得不去重新启动计算机。你也许对此深有体会,但我讲这些的真正目的是——你现在可以理直气壮地告诉你父母,玩游戏对你了解电脑是多么重要!

好了,我知道我偏题已经很远了。现在你已经了解了内存的工作方式,也知道怎样使用指针了。但我还是要唠叨一句:不管是真正的物理内存还是硬盘上的虚拟内存,甚至显卡上的显存,对于程序员来说它们的使用都是完全一样的。唯一的区别是速度,你知道内存非常快,比硬盘快N 多倍。不过,我们在写游戏软件,我们希望画面绚丽而且速度流畅,所以——把数据写到显存里去,因为它最快,比内存快N多倍。

但如何知道我们写的是显存呢?WINDOWS最可恶也是最可爱的一点就是它把硬件都藏在了操作系统背后。你根本无法直接访问任何硬件设备。要直接写显存,只有通过DirectDraw。(感谢上帝,我终于回到正题上了!)

尽管我非常想再次对DirectDraw进行一次更加深入的解释。但某种神秘的感觉制止了我这不明智之举。已经没有必要再说些什么了,如果我真要把它解释清楚,就不可避免地要介绍COM技术。COM技术(Component Object Model)是面向对象编程的精华,也可以说是终极境界。可能你很早就听说过COM但一直对它充满敬畏,但如果你用Public定义过一个C++类的话,实际上那个类就是你写的第一个COM。多数时候,COM就是.DLL 文件。(什么嘛,原来是动态连接库。)对。用COM编写的.DLL,.OCX 等文件可以支持C++程序,VB等。你还可以在任何平台上调用它,甚至可以用来开发PS2游戏。目前很多编程已经简化到了只是对这些动态库的拼凑而已。这就是COM技术!DirectX也是这样。


就此打住,我不能过多地谈论COM, 我知道这会给我带来麻烦,也会让你糊涂得一塌糊涂。这好比一个中学物理教师在高中物理课上讲广义相对论——自寻烦恼。

一切知识都来自于实践,如果我再不写一点程序的话,我知道,你会发疯的。

上次我写的DirectDraw程序实际上只调用了DirectX的1.0版本!别急着操家伙揍我,现在我就给你最新的7.0版的代码。如果你问我为什么不是8.0版的,那是因为8.0根本就没有DirectDraw!

LPDIRECTDRAW7 lpdd7=NULL; //说明一个DirectDraw7.0对象。

if(DirectDrawCreateEx(NULL, (void **)&lpdd7, IID_IDirectDraw7, NULL)!=DD_OK)

  return 0;

此函数是DirectDrawCreate的扩展,如果你要创建一个7.0版的DirectDraw对象,就可以用这个函数。先替换掉原来的DirectDrawCreate 函数。编译……

如果编译成功,那么你简直就是上帝!

如果出现了N多个错误,那么欢迎回到凡人的世界。别担心,我告诉你怎么做。在VC6里选择Tools/options,弹出一个对话框,选择Directories,然后在Show directories中选择Include,你是否看到了../mssdk/include?如果没有,我很遗憾地告诉你,你必须安装DirectX7.0或DirectX8.0的 SDK(注意:是SDK,而不是游戏光盘里的DirectX运行库,mssdk就是你安装DirectX SDK 的目录)。如果有的话,确保../mssdk/include和../mssdk/lib在最上面。然后在主菜单中选择Project/Settings,在对话框中选择Link,加入ddraw.lib,如果已经有了就不必管它。我知道有很多人不能编译DirectDraw程序都是因为没有加入ddraw.lib,其实有一个简单的办法,在工程里添加../mssdk/lib下的ddraw.lib文件。还有,加入dxguid.lib,或者在主程序上方加上 #define INITGUID。

好,编译……

现在应该成功了,如果还有问题,把编译错误信息拷到邮箱,发给我。

运行程序……

好象没什么改变,这真让人扫兴。但本文要介绍的所有程序,几乎都看不到什么结果,但下次,我们就可以开始制作游戏引擎了!所以,多点耐心吧。

同样,我希望你看看函数原型:

HRESULT WINAPI DirectDrawCreateEx(GUID FAR *lpGUID, LPVOID *lplpDD, REFIID iid, IUnknown FAR *pUnkOuter);

GUID FAR lpGUID 同样定义了一个显示驱动程序的唯一标识符,如果为NULL则是默认的主显示驱动程序。除非你打算使用两台以上的显示器,一般没必要管它。LPVOID *lplpDD 就是lpdd7的指针,这是个无类型的指针变量,所以要在前面做类型转换操作。现在这里出现了个奇怪的类型:REFIID iid?REFIID是什么?我先告诉你它的英语名称 Reference Interface Identifiers Definition (接口标识定义参照)。我敢肯定即使你认识所有的单词,也不一定明白它们合在一块的意思。别急,休息一下,去抽只烟再说。


欢迎回来,我一直在考虑,要不要再介绍一下COM,我现在决定还是讲一下使用COM必须的GUID,但如果你急于求成的话,就跳过下面红色的部分。

简单地,你可以把COM理解为电脑芯片,目前大多数人对硬件要比软件熟悉的多,因为它们喜欢看的见的实在的东西。COM就象是软件里的集成电路,你不必管它里面是什么,只要把它插上,它就能工作——起码,大多数情况是这样。这就象你要造一块主板,但这块主板如果没有CPU的话是不可能工作的。在设计主板之前你就得考虑,你想用什么样的CPU。如果是奔腾4的话,你就得使用一个Soket468的接口,以便把CPU插上去。基本上你不必关心到底使用P4 1.6G还是P4 2G。使用COM也很相似,COM是由别人写好的程序,比如DirectX就是微软写的COM。你要做的就是通过一个接口将COM插上去。但在软件的世界里你根本看不到这个所谓的接口,它一般是由GUID来描述这个接口。

GUID(globally unique identifier),意及全球唯一标识符,全世界唯一的标识符???

这么说吧,如果让全世界所有人都用电脑来随意填写一个128位的字符串,那么会有多少人写出来的完全一样?我想华罗庚曾经研究这个问题研究到吐血。但不必担心,COM已经假定了没有任何人会写出同样的字符串,所有它们用此来作为程序的接口标识符(GUID)。如果你要去买一个有16位号码的彩票,那么你中奖的几率是多大呢?所有的COM程序员都会告诉你,死了这条心吧,这是不可能的。因为他们总是用16(128位)个16进制编码来做为他们程序的COM接口的标识符。程序每次运行时就去DLL库文件中查找这个标识符然后调用相应的东西。IID_DirectDraw7 就是这么一个标识符,它表示了DirectX运行库的接口。Windows正是通过它,把DirectX的动态运行库同你的程序连接起来,并给你你想要的DirectDraw7 对象。所以,REFIID就是申明这么一个接口标识符,你正是通过它来调用DirectDraw7对象的。一旦微软升级了DirectX 的版本,那么你无须修改你的程序,因为程序每次运行都会通过这个接口去调用最新的DirectDraw7,这就象把P4 1.6G 升级为P4 2G一样。你可能正纳闷这个标识符究竟是什么样的。就是这样:{15E65EC0-3B9C-11D2-B92F-00609797EA5B}。这就是DirectDraw7的全球唯一标识符,你的电脑上也有,如果装了DirectDraw7 的话。每个写COM的程序员都会为自己的COM定义这么一个标识符。一般用微软提供的guidgen.exe程序来得到。它在Visual Studio的Tools目录下。微软保证每次产生的标识符都是唯一的不可能重复。但这总让我担心,也许我天生爱杞人忧天。

你也许对此感到很满意,也许同我一样担心——如果这个标识符因为某种原因改变了的话,你的程序就几乎不能运行。这不是没有道理。如果你喜欢捣鼓WINDOWS的注册表的话,你一定见过很多类似的全球唯一标识符。现在你知道它们有什么用了吧。我给你出个馊主意,去改几个来试试,一般这些标识符其实只会影响程序的安装和卸载,但是——上帝保佑你!


还是接着来看 DirectDrawCreateEx 函数,我们又遇到了那个张牙舞爪的 IUnknown 类型。这次我得停下来看看她到底是什么了。

IUnknown 类型从来不受程序员的欢迎,但又总是诱惑着我们。她脾气古怪,即温柔,又暴躁——就象人面蛇身的美杜沙。总之,目前还有很多程序员不愿意谈论这个类型。也许它们不喜欢“我不知道”这个名字,我这次要想简单的讲一下,即使我很可能因此而倒霉。IUnknown 类型很简单,就是“我不知道”类型。基本上它只用来定义三个函数IUnknown: QueryInterface, AddRef, 和 Release。所有的COM程序都必须有这三个函数。QueryInterface 用来请求一个对象,AddRef 用来记录这个对象被请求的次数,Release 用来释放你上次请求的对象,同时递减 AddRef 的计数器。听起来仿佛很简单,但实际上远不是这么回事。你所请求的东西可能具有不同的内型,可能存在,可能不存在。也就是说可能成功,也可能失败,这可能无关紧要,也可能让你的程序崩溃。总之,它具有太多的未知因素,所以叫做“我不知道”类型。

但不好意思,这完全是我个人的俏皮话,大多数时候,你都可以放心地用 QueryInterface 来请求你想要的对象。不过我并未说谎,使用这个函数时,你的确得小心,而且 IUnknown 类型的确是“我不知道”类型,它仅仅是用来申明一个无法确定类型的变量而已。其实这一切都与我们正在谈论的 DirectDrawCreateEx 函数毫无关系,不过,你已经知道了什么是 IUnknown 类型,现在你就明白了IUnknown FAR *pUnkOuter 只是微软为将来留的一条后路,我不知道他们会不会在 DirectX 10.0时放点什么东西在里面,但也许他们早就将这个参数给忘得干干净净。微软总是在不停地谈论我们梦幻般的未来,而IUknown仿佛就是连接这个未来的时空隧道。但到底怎样穿过它,也许连微软自己都不知道。

该死,我又把话题扯远了。但我还不想闭嘴,为了表示我对你的仰慕,我再告诉你一个创建DirectDraw7的方法:

LPDIRECTDRAW lpdd=NULL; //申明一个基本的DirectDraw变量。

LPDIRECTDRAW7 lpdd7=NULL; //申明一个DirectDraw7变量。

if(DirectDrawCreate(NULL, &lpdd, NULL)!=DD_OK) //注意,这里用旧的函数

  return 0;

if(lpdd->QueryInterface(IID_IDirectDraw7, (void **)&lpdd7)!=DD_OK)

  return 0;

else

{

  lpdd->Release();

  lpdd=NULL;

}

现在,我们就用了那个“我不知道”类型的QueryInterface函数来创建了一个DirectDraw7对象。函数的参数你应该非常清楚了。它同刚才的DirectDrawCreateEx函数的区别是,QueryInterface用来申请任何可能的DirectDraw对象,如果你愿意你可以去请求一个DirectDraw4对象。但DirectDrawCreateEx函数确做不到,因为DirectDrawCreateEx是专门为DirectDraw7量身定做的只有在DirectX7.0中才有这个函数。

你可能注意到了,用QueryInterface函数时,必须先创建一个基本的DirectDraw对象,然后再删除它。你也许会觉得很麻烦,但这么做的好处是一旦创建DirectDraw7失败,你仍然可以继续使用基本的DirectDraw对象。不过,除非你在整个程序里都不用DirectDraw7的特征,否则,1.0 的对象很难坚持到程序结束!但这样,你又何必去请求一个7.0的对象呢?

我从没有花如此多的精力去谈论一个函数,这都是COM惹的祸,我发誓,对于COM,我不再谈论一个字了。

现在来设定窗口模式:

if(lpdd7->SetCooperativeLevel(GetActiveWindow(), DDSCL_FULLSCREEN | DDSCL_EXCLUSIVE)!=DD_OK)

  return 0;

这个函数几乎没有改变,用lpdd7替换掉原来的对象就行了。

然后是显示模式:

if(lpdd7->SetDisplayMode(800, 600, 32, 0, 0)!=DD_OK)

   return 0;

这个函数是新的,所以来看看原型:

HRESULT SetDisplayMode( DWORD dwWidth, DWORD dwHeight, DWORD dwBPP, DWORD dwRefreshRate, DWORD dwFlags );

前面三个参数都一样,增加的是dwRefreshRate,这是用来设定屏幕的刷新频率的,用0代表默认。当然,你也可以把它加到100HZ,我个人对这个参数非常厌恶,因为我根本就不知道用户的显示器能支持多少!还有dwFlags,也是个愚蠢的标志,因为它目前只它只有一个有效的值DDSDM_STANDARDVGAMODE,如果你把它放进去,你就会看到一个你非常熟悉,而且使你垂头丧气的屏幕,那就是你丢失了显卡驱动程序后的标准VGA模式。我憎恨这个参数,如果它在我的代码里,我就坚决不会编译我的程序。所以基本上这个新函数只是让事情变得更麻烦而已。还有,尽量不要用24位颜色,因为很多显卡不支持24位。

还有件事情我上次没有提到,就是创建一个调色板。你应该无数次听到过调色板,也许你仍然不清楚它到底是什么。但我先问你个问题,你还愿意看256色的图片吗?如果是否定的话,我要恭喜你,你没有把时间浪费在过时的技术上。只有用256色的显示模式时才需要设定调色板,16 位色以后就全都用RGB模式了。过去,人们钟情于256色是因为它节约显存。每个像素只用一个字节表示。所以如果你用800*600的显示模式的话,只占用480K的显存!而32位是用4个字节来表示一个像素。所以32位色占用 800*600*4=1,920,000=1.83MB 的显存。这对于当时许多只有1MB或2MB 显存的显卡是不能容忍的。但现在这对于你起码在8MB以上的显卡根本不是问题。所以,你大可以肆无忌惮地使用32位色。忘了8位颜色吧,甚至16位和24位。16位的颜色很讨厌。它用两个字节来存储三个字节的RGB颜色,你不得不去对它进行一系列的转换,计算出最佳的颜色。24位很好,刚好三个字节,但寻址很慢,而且很多显卡都不喜欢它。你可能会担心32位颜色中多出的一个字节,没有关系,在接触3D游戏之前,你根本不用理会它。

现在我们要创建新的DirectDraw7主页面,并且,我还要加一个后备缓冲。

LPDIRECTDRAWSURFACE7 lpddsprimary=NULL; //用新的DirectDraw7页面定义一个主页面

LPDIRECTDRAWSURFACE7 lpddsback=NULL;  //用新的DirectDraw7页面定义一个后备缓冲页面

同样,ddsd结构也更新了。

DDSURFACEDESC2 ddsd;// 注意用新的 DDSURFACEDESCE2 创建 ddsd 对象。


我们要填充ddsd了,上次我只做了非常粗略的设置,因为我并不想让1.0做太多事情。(尊敬老人是中华民族的优良传统!)现在,我们有了7.0了,是时候让它展示一下力量了。

ddsd.dwSize=sizeof(ddsd); //填充对象字节数。

ddsd.dwFlags=DDSD_CAPS | DDSD_BACKBUFFERCOUNT; //打开后备缓冲标志

ddsd.dwBackBufferCount=1; //设定一个后备缓冲

ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE | DDSCAPS_COMPLEX | DDSCAPS_FLIP; //创建一个带后备缓冲的主页面。

if(lpdd7->CreateSurface(&ddsd, &lpddsprimary, NULL)!=DD_OK) //Create the primary surface

  return 0;

ddsd.ddsCaps.dwCaps=DDSCAPS_BACKBUFFER; //请求一个后备缓冲。

if(lpddsprimary->GetAttachedSurface(&ddsd.ddsCaps, &lpddsback)!=DD_OK) //创建后备缓冲,指定给主页面

  return 0;

我们又来到了最崎岖的道路上。我相信你已经对页面有了较清楚的概念。但什么是后备缓冲呢?其实也是一个页面。这个页面同主页面同样大小,拥有同样的属性。我们总是把游戏画到后备缓冲上,再通过翻页到主页面来实现流畅的动画。这就是大名顶顶的双缓冲技术。你也许会对“翻页”感到迷惑,那么我就称之为拷贝,你完全可以这样来理解,因为要搞清它的细节足可让你减肥100斤!但如果你总喜欢刨根问底而且有足够的耐心的话,我就让你知道我为何要哄骗你!

微软把它称之为后备缓冲就是个弥天大谎!这其实是两个页面通过翻页函数轮换着作为主页面。比如有两个页面S1和S2,S1为主页面,S2为后备页面。这时你看到的是S1,然后你在后备页面(S2)上画一个美女,但你什么也没看见,好,调用翻页函数。看见美女了(S2),这就是说S2成为了主页面。再在后备页面(S1)上画一个帅哥,这时你看到的还是美女(S2),翻页,看到帅哥了(S1)。仍然在后备页面(S2)上画一朵玫瑰,翻页,看到美女和玫瑰了(S2),再在后备一面(S1)上画一只青蛙,翻页,看到帅哥和青蛙了(S1)。这不是智力游戏,到很象绕口令。花点时间你就能讲得很遛。

明白了吧。哈,恭喜恭喜!但是,这只是噩梦的开始!

正如你所担心的,如果我要画一个动画,比如让青蛙跳到帅哥头上。假设,这个动画只有两帧,那么第一帧,让青蛙在帅哥脚下起跳,然后第二帧画青蛙在帅哥头上降落,好,画完了。等一下,青蛙怎么落到美女头上了?这是因为你的游戏使终在一个循环体里运行,每次循环你只能画一帧画面,而每次循环你都要调用一次翻页函数。当你画青蛙起跳时,帅哥是后备页面,当你画青蛙降落时,美女成为后备页面了!它妈的,我已经开始讨厌后备缓冲了。你肯定对此担心的要命,这也的确是个凶险的问题。但事实上,当你真正开始写这个游戏时,你根本不会遇到这个问题!因为帅哥和美女永远都在一起,在同一个页面里!(这就是现实,也是我会坐在这儿写游戏的原因)

所以,我们要创建一个真正的后备页面,对这两个主页面进行全屏拷贝。

现在我有了一个和主页面同样大小的普通页面(S3),每次我都把图形画到这个页面里。然后,拷贝到后备缓冲页面,然后翻页。我已经不想在讲什么绕口令了,给你10分钟,根据上面的例子仔细想想,我现在要去大便,希望我回来时,你已经明白为什么要这么做。

好,想通了吧,如果没有的话,你也去蹲着想想,直到想明白了再回来接着看。

终于,我们可以好好看看程序代码了。如果你理解了后备缓冲你应该很容易明白这段代码。对主页面的创建和原来相同。但我们申明了一个后备缓冲页面。在对ddsd结构的填充时打开了使用后备缓冲的开关。ddsd.dwFlags=DDSD_CAPS | DDSD_BACKBUFFERCOUNT;然后告诉Directdraw我要使用一个后备缓冲。ddsd.dwBackBufferCount=1;你可能已经想到了这里可以使用多个后备缓冲,比如用三个页来翻面,哦,你真是个天才,三缓冲技术是目前最先进游戏使用的。它会带来最好的性能。但如果你问为什么不用4个或5个缓冲,因为要占用显存,缓冲越多占用的显存也越多,如果你对此根本不在乎的话——我也不在乎你使用100个!

继续,ddsd.ddsCaps.dwCaps=DDSCAPS_PRIMARYSURFACE | DDSCAPS_COMPLEX | DDSCAPS_FLIP;就是向DirectDraw说明我要创建一个支持翻页的主页面。说明后,就可以创建主页面了,这个函数和原来没有变化。

现在把要创建的页面设定为后备缓冲页面:ddsd.ddsCaps.dwCaps=DDSCAPS_BACKBUFFER;

然后创建它:lpddsprimary->GetAttachedSurface(&ddsd.ddsCaps, &lpddsback)!=DD_OK)。这个函数请求了一个后备缓冲给主页面。参数简单明白,但你也许会问为何用&ddsd.ddsCaps,因为你可以在里面设置缓冲页面的各种属性,本例中我没有作任何设置。

我不再写出函数的原型了,如果你有兴趣,就打开你的电子词典和MSDN,然后仔细阅读。

好,我们有了主页面和后备缓冲页面,我们已经可以在上面画点东西了。但是,为了以防出现青蛙跳到美女头上的尴尬,我们还得创建一个真正的缓冲页面。

LPDIRECTDRAWSURFACE7 lpddsrealback=NULL;  //用新的DirectDraw7页面定义一个我们自己的缓冲页面

memset(&ddsd, 0, sizeof(ddsd)); //将ddsd结构清零

ddsd.dwSize=sizeof(ddsd);  //再设定ddsd结构的字节长度。

ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT;  //打开页面宽度和长度的保险。

ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN | DDSCAPS_VIDEOMEMORY;  //指定为不可见页面,并存在于显存中

ddsd.dwWidth = 800;  //设定页面宽度

ddsd.dwHeight = 600;  //设定页面高度

lpdd7->CreateSurface(&ddsd, &lpddsrealback, NULL);  //创建它

我相信,你能看明白大部分代码。需要注意的是,我加了一个memset(&ddsd, 0, sizeof(ddsd));语句,这是用来清除以前对ddsd结构的设置,因为我们是第二次使用它了,我不想创建出来的页面还带有上次的特性。memset函数是古老的C标准函数,它可能对Win95以上的系统不兼容,但起码我的Win98能用。如果你不放心可以另外找一个,当然如果你熟悉指针操作的话,你完全可以自己写一个。总之,把ddsd结构所占的内存全部写进0就行。这是很好的练习,如果你现在还对指针毫无概念的话,那么当我们开始写游戏引擎时,你就会遇到大麻烦。

其它的已经很清楚,如果你的英语很好,我就不需要作什么解释了,如果不明白,就去翻词典吧。(多利用身边的资源)

好了,我们已经做了太多的工作了,我们已经涉及了非常高级的技术,如果你现在还感觉良好的话,你就已经是半个游戏开发专家了,我这么说也许只是想让你轻松一点,因为到现在我们还没有在屏幕上看到任何东西,我知道——你太痛苦了!

好吧,我们来画一个红色的矩形。但是,在画图之前还有太多的东西需要学习,我知道你等不及了,你根本不会去理会那些代码,只要它能运行,看到点东西,你就会开啤酒庆祝。好吧,好吧,在这里下载一个,运行看看。

眼花了吧!不过却是一堆垃圾。不管怎样,这是我们自己写的。而且用的是DirectX!

现在你可以安心听我说了吧,基本上你只需再学习一个函数就能写出这个程序了。那就是IDirectDrawSurface7::Blt函数。

Blt函数是DirectDraw的精华,你要是问它有什么用,很简单,它用来在页面间拷贝位图。而且,而且,它使用显卡的硬件加速!

我相信目前所有的显卡都支持硬件加速(2D加速卡!),但如果你的显卡没有的话,Blt也选可以用软件仿真来加速。这一定让你欣喜若狂吧。但别高兴太早,这个函数也很复杂,在使用它之前我们起码要做一件事,定义一个DDBLTFX结构。相信我,DDBLTFX结构要比ddsd结构更加复杂,如果我把它全部写出来,你可能会看到吐血。但基本上,你只需要知道有这么结构,它是用来告诉Blt要怎样拷贝位图。我有很长时间因为讨厌这个结构而使用自己写的位图拷贝函数,这就同我不知道超市的东西要比杂货店的便宜一样的愚蠢。

DDBLTFX ddbltfx; //声明一个DDBLTFX变量

memset(&ddbltfx, 0, sizeof(ddbltfx));  //同样,将内存清0

ddbltfx.dwSize=sizeof(ddbltfx);  //同样,填入该结构的字节数

ddbltfx.dwFillColor=_RGB32BIT(255,0,0);

你会发现,用法同ddsd几乎一样,但ddbltfx.dwFillColor是什么呢?它就是指定一个RGB颜色来填充拷贝的位图块。这里有一个RGB32BIT的宏调用。用来将24位的RGB颜色转换成32位。这个宏不是C++自带的,你必须在你的程序里定义它:

#define _RGB32BIT(r,g,b) ((b)+((g)<<8 )+((r)<<16))

这可能使你再次发狂,本文里可能有太多你还未接触过的技术。放松,我只是用了个位移操作来代替乘法而已。你只需要知道,任何数左移一位等于该数乘以2,右移一位则等于该数除以2。所谓左移就是在该数的二进制后面加0,右移就是去掉二进制数的最后一位。这不是什么复杂的数学原理,你高兴点,拿只笔在纸上算算,很块你就会感到满意。

但是,我为什么要使用位移呢?因为它快!快就是一切,如果你让我拥有速度,我甚至可以把烟戒掉!

当然,如果你不愿意戒烟的话,你可以继续使用上次的GDI函数:

ddbltfx.dwFillColor=RGB(255,0,0);

lpddsrealback->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx);

这个函数太重要了,所以仔细看看函数原型:

HRESULT Blt(

  LPRECT lpDestRect,

  LPDIRECTDRAWSURFACE7 lpDDSrcSurface,

  LPRECT lpSrcRect,

  DWORD dwFlags,

  LPDDBLTFX lpDDBltFx

)

lpDestRect:这是指定一个目标位图的地址,就是你要拷贝到页面的那个位置。如果为NULL则是整个页面。

lpDDSrcSurface:这是源页面的指针,就是从哪个页面拷贝。如果为NULL就不指定。

lpSrcRect:这是源页面的位图地址。就是拷贝页面的哪个部分。如果为NULL就是全部。

dwFlags:指定哪些DDBLTFX成员有效,本例中使用DDBLT_COLORFILL指定对位图进行色彩填充,DDBLT_WAIT用来强制Blt等待硬件刷新。

lpDDBltFx:ddbltfx结构的地址指针。

可能90%的人都不明白这个函数在做什么,如果你也不明白,那就是说你并不孤独。其实简单说就好比你在写字板里用鼠标Hightlight一段文章,然后将它拷贝到其它什么地方。这同后备缓冲技术一样,你完全可以就这么认为,然后放心地使用它。但如果你还要刨根问底,我也可以说其实完全不是那么回事。它其实是个硬件接口,它其实…… 我看我还是闭嘴吧。

但是,这个函数违背了我们的初衷,我们本来想画一个红色的矩形,但它却画了个红色的背景屏幕。怎么办呢?

可能你已经想到了,我们可以定义一个RECT结构来表示我们要拷贝的位图区。这很简单,就是定义一个矩形结构,然后指定左上角和右下角的坐标。

RECT rect;  //声明一个rect结构变量

rect.left=200;  //指定左上角X坐标

rect.top=200;  //指定左上角Y坐标

rect.right=400;  //指定右下角X坐标

rect.bottom=400;  //指定右下角Y坐标

把它的地址指定到Blt函数里就行了:

lpddsrealback->Blt(&rect, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &ddbltfx);

好了,结束了,完成了,你已经知道了所有,你可以在屏幕上画点东西了,这场漫长的马拉松就快到达终点了。虽然,你现在可能仍然感到一片茫然,但放心,我们就是茫然的一代,我们熟悉这种感觉,我们需要的只是主程序。

但是,不要急着去编译它,还有一些东西我要告诉你……

算了,还是留给你自己研究吧。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值