☆ 颜色键
颜色键使一个位图被拷贝到另一个位图上时,不使所有的象素都显现。例如:当你把一个精灵(游戏中会动的对象一般都称作精灵)拷贝到地图上(背景上)时,这个精灵位图一般不会是一个精灵形状的位图,它通常都是一个矩形位图,位图里包含你所需要的精灵(除非你的精灵就是一个矩形机器人^_^),不使用颜色键拷贝的结果如图一:
【图一】
这并不是我们想在游戏中得到的效果。游戏中,这个精灵是不会有那个黑色的底框的。地图是先于精灵显示的,那么精灵走到树后时,还应有相应被遮挡的部分,这个先不讨论,下一节再说。现在,对我们更重要的是,如果不应用颜色键,这个精灵将永远带着这个黑色底框,这是绝对不能容忍的。
为了解决这个问题,我们使用源颜色键。这个源颜色键告诉你精灵矩形的哪些颜色将不被拷贝(当然我们是让黑色不被拷了^_^)。一个颜色键由两个值组成:一个低位颜色值,一个高位颜色值。当一个颜色键被申请使用时,在两个值之间的颜色,包括这两个值的颜色都将不会被拷贝。在DirectX中有一个结构用来处理它,叫作DDCOLORKEY,看看吧:
typedef struct _DDCOLORKEY{
DWORD dwColorSpaceLowValue;
DWORD dwColorSpaceHighValue;
} DDCOLORKEY, FAR* LPDDCOLORKEY;
很简单的结构,我就不解释了。我将展示给你使用了颜色键之后的效果。我使用颜色键的高位和低位两个值仅仅把黑色包括在它们之间。因此,黑色是唯一不会被拷贝的颜色。图二就是使用颜色键的结果:
【图二】
好多了,是不是?这就是我们想得到的结果!现在,在我告诉你怎样建立和使用颜色键之前,我还有说一说目标颜色键,尽管我们的确我们不常用到它(我们常用的是源颜色键)。鉴于源颜色键定义了哪些颜色键不能被拷贝,目标颜色键定义了哪些颜色不能被写入(覆盖)。听起来很怪异,是不是?我也有同感。^_^ 举个实例你就明白了。当你要把A位图拷贝到B位图的下面,意思就是把A位图作为背景,例如由于某种理由,需要把一个文本框拷贝到空的后缓冲区,然后再把背景画面拷贝到这个后缓冲区,但你又不能覆盖先前的文本框。因此,在后缓冲区里除了文本框的那些黑色的部分才能被写入象素。看看图三,真相大白:
【图三】
我也不清楚你什么时候需要处理这种情况,但是你的确可能用到(一旦你用到了,可千万要告诉我哦,我一直没有遇到这种情况呢^_^)。现在,你已经知道什么是颜色键了,让我们看看怎样使用它们吧!
☆ 设置颜色键
在DirectDraw中有两种方法使用颜色键。第一种,你可以链接一个颜色键(或者两个,如果你同时使用源和目标颜色键)到表面,然后在位块传输时定义DDBLT_KEYSRC,DDBLT_KEYDEST,DDBLTFAST_SRCCOLORKEY或DDBLTFAST_DESTCOLORKEY标志,具体使用哪个标志,取决于你使用哪个位块传输函数和使用哪种颜色键。第二种,你可以创建一个颜色键,然后通过DDBLTFX结构传送给位块传输操作。当你不断地需要使用颜色键时,我向你推荐第一种方法;反之,当你偶然要使用一次颜色键,就用第二种方法吧!
你可以把颜色键链接到已经建立好了的表面,也可以在建立表面的同时建立颜色键。两种方法我都将详细告诉你。假设你工作在16-bit显示模式下,是565象素格式,你要在后缓冲区使用一个仅包含黑色的源颜色键。如果你的后缓冲区已经建立好了,你就可以简单建立一个DDCOLORKEY结构,然后把它传递给IDirectDrawSurface7::SetColorKey()函数,如下所示:
HRESULT SetColorKey(
DWORD dwFlags,
LPDDCOLORKEY lpDDColorKey
);
记住要用FAILED()宏检测这个函数的返回值,保证一切都在计划之中。函数的参数很简单:
※ DWORD dwFlags:决定所使用颜色键类型的标志。以下三个是你将用到的:
◎ DDCKEY_COLORSPACE:该结构包含一个颜色范围,如果结构包含的是单独的颜色键则不作设置。
◎ DDCKEY_DESTBLT:该结构指定颜色键或者颜色范围作为用于位块传输操作的目标颜色键。
◎ DDCKEY_SRCBLT:该结构指定颜色键或者颜色范围作为用于覆盖操作的颜色键。
※ LPDDCOLORKEY lpDDColorKey:这是指向DDCOLORKEY结构的指针。
就这么多。你可以根据你所需要使用的颜色键适当地定义位块传输的标志。注意,一个颜色键链接到表面,并不意味着你每一次必须使用它。如果你只定义了DDBLT-WAIT或DDBLTFAST_WAIT标志,颜色键将被忽略。下面是设置颜色键的方法:
DDCOLORKEY ckey;
ckey.dwColorSpaceLowValue = RGB_16BIT565(0, 0, 0); // or we could just say '0'
ckey.dwColorSpaceHighValue = RGB_16BIT565(0, 0, 0);
if (FAILED(lpddsBack->SetColorKey(DDCKEY_SRCBLT, &ckey)))
{
// error-handling code here
}
如果你要为已经建立的颜色键链接一个表面,有几件事情你需要做。首先,当你定义DDSURFACEDESC2结构的有效成员时,你需要使dwFlags成员包含DDSD_CKSRCBLT或者DDSD_CKDESTBLT标志,具体使用哪个标志,取决于你要使用哪种颜色键。回头再看看DDSURFACEDESC2结构,它包含两种DDCOLORKEY结构。一种称为ddcdCKSrcBlt,另一种称为ddcdCKDestBlt,填写适当的结构来创建表面。你就需要干这么多!以下是关于640×480大小的离屏表面的实例代码:
// set up surface description structure
INIT_DXSTRUCT(ddsd);
ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT | DDSD_CKSRCBLT;
ddsd.dwWidth = 640; // width of the surface
ddsd.dwHeight = 480; // and its height
ddsd.ddckCKSrcBlt.dwColorSpaceLowValue = RGB_16BIT(0,0,0); // color key low value
ddsd.ddckCKSrcBlt.dwColorSpaceHighValue = RGB_16BIT(0,0,0); // color key high value
ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; // type of surface
// now create the surface
if (FAILED(lpdd7->CreateSurface(&ddsd, &lpddsBack, NULL)))
{
// error-handling code here
}
关于颜色键的部分到此结束。现在我们可以进行本章最后一项了——剪切。
☆ IDirectDrawClipper接口
假设你有一个图形,而你却只想把它的一部分显示在屏幕上。你应该怎样做呢?如果你曾经在DOS下编写过游戏,你可能对剪切望而生畏。Well,在DirectX下,这只是小菜一碟!首先的首先,这的确是很容易做到的,因为DirectX用矩形来做位块传输,改变矩形的坐标要比指定内存中的哪一部分图形被拷贝(就像在DOS下所做的)要容易的多。其次,DirectDraw还为此提供了一个接口——IDirectDrawClipper。
DirectDraw的剪切性能完全可以满足你的要求,你不但可以剪切矩形区域,你还可以剪切任意多边形区域!真的是很棒!如果你在屏幕上同时要显示一个主窗口,在屏幕的一边显示一个状态栏,在屏幕的底部显示一个文字提示栏,并且用黑色分隔开这些区域,你可以用DirectDraw的剪切功能完成它,非常容易的!
做这样的操作你需要分几步走。首先你得得到一个指向IDirectDrawClipper接口的指针。没什么难的,只需要调用IDirectDraw7::CreateClipper(),如下:
HRESULT CreateClipper(
DWORD dwFlags,
LPDIRECTDRAWCLIPPER FAR *lplpDDClipper,
IUnknown FAR *pUnkOuter
);
在你调用这个函数前,你应该首先声明一个LPDIRECTDRAWCLIPPER类型的指针,这样你才能把它的地址传递给这个函数。记着要检测函数的调用是否成功哦!以下是函数参数的解释:
※ DWORD dwFlags:简直就是幸福——这个参数还没有使用过,设置为0。^_^
※ LPDIRECTDRAWCLIPPER FAR *lplpDDClipper:把你的LPDIRECTDRAWCLIPPER指针的地址传递给它。
※ IUnknown FAR *pUnkOuter:你知道怎么做,设置为NULL。^_^
一旦你有了自己的接口指针,下一件事就是创建剪切列表(clip list)。多个剪切的矩形组成了剪切列表。需要使用到RGNDATA结构,它包含了足够的信息来定义一个任意的区域,看看它的原形吧:
typedef struct _RGNDATA {
RGNDATAHEADER rdh;
char Buffer[1];
} RGNDATA;
我需要详细解说一下它的参数。
※ RGNDATAHEADER rdh:它是RGNDATA结构中嵌套的一个结构。它包含了第二个参数——Buffer的所有信息。它定义了需要剪切区域里的矩形的数目,整个区域的形状等信息。我们过一会儿再具体讨论它。
※ char Buffer[1]:这并不意味着是只有一个值得数组;它将是在内存中任意大小的区域来控制着实际的剪切区域数据。同样的,对于RGNDATA结构,我们要声明一个指向该结构的指针,然后使用malloc()函数为RGNDATAHEADER设置足够的内存空间,也就是为剪切列表设置足够的空间。有一件事我要提醒你:剪切列表里的矩形按从上到下,然后从左到右排列,不能交迭。
我已经意识到你有些胡涂了,不要紧,继续学习,一切会好起来的。下面是RGNDATAHEADER结构的原形,它比较好理解:
typedef struct _RGNDATAHEADER {
DWORD dwSize;
DWORD iType;
DWORD nCount;
DWORD nRgnSize;
RECT rcBound;
} RGNDATAHEADER;
※ DWORD dwSize:结构的大小。简单的使用sizeof(RGNDATAHEADER)好了。
※ DWORD iType:它描述了每个区域的外形。它是另有玄机的,以后我们再把它扩展开来讨论。现在,你只要把它设置为RDH_RECTANGLES就好了,这也正是我们需要的。
※ DWORD nCount:这是组成该区域的矩形的数量。换句话说,就是你的剪切列表理的矩形数。
※ DWORD nRgnSize:为缓冲区的大小设置它,将得到自身的区域数据。由于我们使用n个矩形组成了剪切区域,所以它的大小应该是sizeof(RECT) *nCount。
※ DWORD rcBound:这是一个矩形类型,包含了剪切列表里的所有矩形。通常你把它设置成表面上需要剪切部分的尺寸。
现在,我们可以建立一个剪切列表了。首先我们声明一个LPRGNDATA的指针,分配给剪切列表足够的内存空间;然后根据上面我们所学的设置每个成员。让我们看看最简单的实例,你可能经常要用到它哦!它只有一个剪切区域,并且,就是整个屏幕,是640×480显示模式的。以下就是代码:
// first set up the pointer -- we allocate enough memory for the RGNDATAHEADER
// along with one RECT. If we were using multiple clipping area, we would have
// to allocate more memory.
LPRGNDATA lpClipList = (LPRGNDATA)malloc(sizeof(RGNDATAHEADER) + sizeof(RECT));
// this is the RECT we want to clip to: the whole display area
RECT rcClipRect = {0, 0, 640, 480};
// now fill out all the structure fields
memcpy(lpClipList->Buffer, &rcClipRect, sizeof(RECT)); // copy the actual clip region
lpClipList->rdh.dwSize = sizeof(RGNDATAHEADER); // size of header structure
lpClipList->rdh.iType = RDH_RECTANGLES; // type of clip region
lpClipList->rdh.nCount = 1; // number of clip regions
lpClipList->rdh.nRgnSize = sizeof(RECT); // size of lpClipList->Buffer
lpClipList->rdh.rcBound = rcClipRect; // the bounding RECT
一旦有了剪切列表,你需要把它作为你的剪裁板。你将调用IDirectDrawClipper接口的函数SetClipList()。就是下面这个东东:
HRESULT SetClipList(
LPRGNDATA lpClipList,
DWORD dwFlags
);
你所要做的就是把RGNDATA的指针传递给它。参数dwFlags没有用,设置为0。现在,剪切列表设置好了,还需要一步,就是把剪切列表链接到你所要控制的表面上,这需要调用SetClipper()函数,它将表面指针作为其唯一的参数:
HRESULT SetClipper(LPDIRECTDRAWCLIPPER lpDDClipper);
你知道应该怎样做:就是把你已经设置好的接口的指针传递给它。任何时候,你要位块传输一个有剪裁板相关联的表面,剪裁板将做所有的工作。所以如果你要在屏幕上显示一个贴片的一部分,例如传输一个矩形坐标为{-10,-10,6,6},或者类似的矩形贴图,都不会有麻烦的。很不错吧,嗯?
关于剪切的最后一件事情是必须要用free()函数释放你用malloc()设置的内存空间。还有,就是由于某种原因在调用SetClipList()或SetClipper()失败后,在返回错误代码前或你要根据失败的结果进行操作前,要释放内存空间。在你完成用LPRGNDATA的指针设置剪切列表后,这个指针就没有存在的意义了,所以它占用的内存空间将被立即释放。
☆ 总结
到此,关于DirectDraw的部分就讨论完了!你真的从这六章里学到了很多的知识,如果你坚持到现在,那么祝贺你,你真的已经走了很长一段路了。^_^ 对于这一章我们所学习的,我将把它们组合在一起,给你一个Demo程序,它将从资源调用位图,使用位块传输位图,颜色填充,放缩位图比例,使用剪切功能。这个就是源代码下载的地址:http://www.aeon-software.com/downloads/clipscale.zip 。
仍然有些东东我们应该讨论,例如页面的切换(flipping),双缓冲区的应用等,无论如何,我们还将继续,所以不必担心我们会遗漏重要的内容。^_^
现在,最初的原始积累已经结束,我们以后的焦点会从创建Windows程序转移到创建一个贴图基础的RPG游戏。以后的章节将包括用DirectInput建立一个好的输入机制,写一个基础的引擎脚本,播放背景音乐和配音,等等。下一章,我们将学习为贴图游戏制作一个简单的卷轴游戏引擎,很容易的,没有你想象的那么难哦!