游戏编程之十七 生成简单的动画

DirectDraw 游戏编程基础(4)


游戏使计算机的发展超越了晶体管时代
       
      
生成简单的动画
以上我们已经讨论过的所有的例程,都简要描述了如何在台缓冲区绘画,以及如何把后台缓冲区弹出到主表面(Surface)的简单的实现方法。然而,这些例程都是以极其缓慢的速度在运行。下边的例程,DDEX4和DDEX5以实时运行它们的函数,更象一个实际的程序。
DDEX4显示了如何为表面(Surface)设置一个颜色码,并且演示了如何使用IDirectDrawSurface方法将 隐屏表面(Surface)的某些部分复制到后台缓冲区来生成动画。DDEX4 显示了如何检察IDirectDrawSurface::BltFAst的返回值,并得知按位隔行拷贝是否成功。
DDEX5扩展了DDEX4的功能。它能在动画运行过程中,通过读调色板,来修正调色板。
颜色码和位图动画
DDEX3 的例程显示了一种将位图载入一个隐屏缓冲区的比较原始的形式。下边的 DDEX4的例程,使用我前边已经描述过的技术,将一个背景和一系列的 sprites载入一个隐屏表面(Surface)。使用IDirectDrawSurface::BltFast方法,将隐屏表面(Surface)的部分复制到后台缓冲区,这样,就生成了简单的位图动画。
All.bmp是被DDEX4使用的位图文件,它包括了背景和六十张动画片 ,并在黑色的背景下有一个旋转的红色的donut。DDEX4的例程包含了了几个新的函数,它们可以为循环的donut sprits 设置颜色码,然后把适当的sprites从隐屏表面(Surface)中复制到后台缓冲区中。
我很想指出,在DDEX4中,"donut"被看作为一个"torus"。每当我听见的复数的“torus(tori)”,我就禁不住想起年轻的好莱坞女影星,因此,在这里,我把它们叫做donuts  。我希望读者不要介意。
设置颜色码
除了在前边的例程中,dolnit函数中所用到的功能外,DDEX4例程还包含有为sprites设置颜色码的代码。颜色码是用来设置颜色值的,而且后者具有透明性。当使用一个硬件按位隔行拷贝器时,除了被设置为颜色码的值外,一个长方形的所有的象素都被按位隔行拷贝了,这样在一个表面(Surface)上就生成了非长方形的象素。在DDEX4中设置颜色码的代码如下:


            // Set the color key for this bitmap (black).
            // NOTE this bitmap has black as entry 255 in the colortable
          //  ddck.dwColorSpaceLowValue = 0xff;
          //  ddck.dwColorSpaceHighValue = 0xff;
          //  lpDDSone->SetColorKey( DDCKEY-SRCBLT,&ddck);
          
            // IF we do not want to hard code the palette index(0xff),
            // we can also set the color key like so...
            DDSetColorKEY(lpDDSone,RGB(0,0,0));
    
            return TRUE;
这一例程显示了设置颜色码的两种不同的方法。第一种方法,也即程序的前三行,把包含在DDCOLORKEY结构中的颜色码范围设置成16进制数FF或十进制数255。之后调用IDirectDrawSurface::SetcolorKey方法把颜色码设置为黑色码。当然,这样做的前提是假定黑色在颜色码表中的索引入口是0,要完成这项简单的工作,你可调用IDirectDrawSurface::SetcolorKey来改变相应的数据。
  如果你的位图没有将入口255设置为黑色会怎么样呢?你也可以通过在调用DDSetColorKey函数中设置你需要的颜色的RGB值方法选择颜色:黑色的RGB值为(0,0,0)。Ddutil.cpp文件中函数DDsetColorKey调用该文件中的另一个函数DDColorMatch。DDColorMatch函数存储了象素的当前颜色值,该象素要位于lpDDSone表面(Surface)的位图的(0,0)位置上,之后取出你已经提供的RGB值,并在(0,0)位置设置该象素的颜色。该函数然后用每象素的位数来屏蔽颜色值。一旦这样做了之后,原来的颜色就被放回到(0,0)位置,并且,该调用将实际的颜色码值返回给DDSetColorKey。当上诉过程完成之后,颜色码值就被放在结构DDCOLORKEY的dwCOLorSpaceLOwValue成员之中。同时,该码值也被复制到dwColorSpaceLowValue成员中。然后,再调用IDirectDrawSurface::SetColorKey设置颜色码。
你可能已经注意到了位于DDSetColorKey和DDColorMatch中的CLR_INVALID参数。在DDEX4的DDSetColorKey调用中,如果该参数的值被你传递作为颜色码,那么位图左上角的象素就将被用作颜色码。DDEX4位图的传送并不意味什么,因为,在(0,0)位的象素的颜色是一个灰色的阴影。然而,你若想看见如何将(0,0)位的象素用作DDEX4例程中的颜色码的话,你可以通过将位图文件(All.bmp)载入你的绘画应用程序中,然后,将(0,0)位的象素改变为黑色来完成上述功能。切记:必须将此改变存盘。然后改变DDEX4中的程序代码行:DDSetColorKey(lpDDsone,CLR_INvALID)来调用DDSetColorKey。
重新编辑DDEX4例程,要保证资源定义文件也被重新编辑过,以便将新的位图被包括进去。(为了保证资源定义文件被重新编辑,你需要对Ddex4.rc文件进行适当的删补工作,以使资源定义文件编辑的进行有足够的空间)。此时,DDEX4例程将把(0,0)位的象素用作颜色码。
DDEX4中的动画
     DDEX4例程使用updateFrame函数来生成一个简单动画(其使用包含在All.bmp文件的红色donuts中)。动画由位于一个三角形内的三个红色donuts组成。这个例程将Win32的GetTickCount和毫秒的数目相比较,这是因为对GetTickCount的最后定义决定了是否重新绘制任何sprites,然后再使用方法IDirectDrawSurface::BltFast。首先,把背景从隐屏表面(Surface)按位隔行拷贝到缓冲区中,然后将sprites按位隔行拷贝后台缓冲区(使用先前你已经设置好的颜色码来决定哪些象素是透明的)。在sprites被按位隔行拷贝到后台缓冲区后,DDEX4将调用方法IDirectDrawSurface::Flip以弹出后台缓冲区和主表面(Surface)。  
注意:当IDirectDrawSurface::BltFast被用来从隐屏表面(Surface)按位隔行拷贝到背景时,传值参数dwTruns的参数值就被设置为DDBLTFAST_NOCOLORKEY。这就表明:一个正常的按位隔行拷贝将在没有透明位的情形下发生。之后,当红色donuts被按位隔行拷贝到后台缓冲区,dwTrans参数的值就被设置为DDBLTFAST_SRCCOLORKEY。
在这个例程中,整个背景每次都将通过调用updateFrame函数重新绘制一次。优化这一例程的一种方法是:仅将改变当前循环的红色donuts背景的部分再重新画一遍。因为组成donut sprits的长方形的位置和大小始终不变。因此,你就可以很容易地利用这种优化方法去修正DDEX4例程。     
动态修正调色板
例程DDEX5是在例程DEEX4的基础上进行了适当的修正而得到的,该例程演示了当一个应用程序在其运行过程中,如何动态地改变调色板的人口方式。尽管这样或许并没有在程序中被普遍地应用,但DDEX5例程将给你一个手工操纵DirectDraw 调色板的尝试。  
    以下的一段程序代码摘自例程DDEX5,它是利用文件ALL.BMP下半部分(这部分包含了一系列红色的donuts)的值来调用调色板进入方式。
//First, set all colors as unused.
   For(I=0;I<256;I++) 
   {
       torusColors[I]=0;
   }
  //Lock the surface and scan the lower part (the torus area)
  //and remember all the indexex we find.
   Ddsd.dwSize=sizeof(ddsd);
   while (lpDDSOne->Lock(NULL,&ddsd,0,NULL)==DDERR_WASSTILLDRAWING);
  //Now serach through the torus frames and mark used colors.
   For (y=480;y<480+384;y++)
   {
        for(x=0;x<640;x++)
        {
            torusColors[((BYTE*)ddsd.lpSurface)[y*ddsd.lPitch+x]]=1;
        }
   }
   lpDDSOne->Unlock(NULL);
程序中,torusColors数组变量是一个被用在文件ALL.BMP下半部分的调色板色彩索引指针。在使用他以前,所有的torusColors数组变量都将被置为零。然后,隐屏缓冲区将被锁定,以准备决定是否有某个色彩索引值被用户使用。
torusColors数组变量被置于从480行、0列开始的位图中。数组中的色彩索引值将由位图表面(Surface)所占用的内存中的数据字节决定。数据在内存中的位置由结构DDSURFACEDESC中的成员IpSurface决定,它指向了位图(y*Ipitch+x)中480行、0列在内存中的相关位置。然后,该指针所指向的特定的色彩索引值将被置为1。行数变量y的值将随结构DDSURFACEDESC的成员Ipitch的值的变化而变化,以获得该象素在线性内存中的实际位置。
置于数组变量torusColors中的色彩索引值在以后将被用来决定究竟哪种色彩将在调色板中循环。由于没有通用的色彩用在背景和红色的donuts之间,所以,只有那些与红色的donuts相关的色彩将在调色板中循环。如果你想要检查它们的真假,只需从数组变量中移去成员Ipitch即可。然后,你需要重新编译、运行该程序,这样你就将见到所期望的结果。(如果没有乘上ylpitch,那么红色的dontus将不可能达到。仅仅只能看到背景中的色彩被索引,然后循环)。
常规显存宽度和扩展内存宽度的关系
    在我们继续谈论调色板循环以前,我想暂时离开我们原来的话题一会儿,来谈谈显存宽度和扩展宽度之间的关系。


 
    如果你的应用程序是为显存而编写的,那么扩展宽度就需要被赋予不同于显存宽度的值。因为并不是所有的显存都被安排在同一个线性块中。例如:在内存块中显存的扩展宽度将包括位图的显存宽度加上高速缓存的一部分。下图2说明了在内存块中显存宽度和扩展宽度。




图2:矩形内存常规内存和扩展内参的关系。


    说明:在上图中,前置缓冲区和后台缓冲区都是640*480 8位,而高速缓冲区均为384*480 8位。为了获得写入缓冲区的下一地址,你需要使用640字节的显存以及384字节(即1M)的存储空间,这正是下一行的开始。
    一般说来,当我们直接进入表面(Surface)所占内存空间时,总是利用方法IDirectDrawSurface::Lock来返回显存扩展宽度的大小(或者,也可以通过方法IDirectDrawSurface::GetDC实现)。请不要怀疑扩展宽度单单是建立在显示模式的基础上的。如果你的应用程序是工作在显示适配器上的,然而却看上去和其它的混淆不清,这可能是你自己的原因造成的。
调色板的循环
    函数updataFrame在例程DDEX5中的工作方式与其在例程DDEX4中的工作方式相同。它首先将背景按位隔行拷贝进后台缓冲区,然后将三个donuts  按位隔行拷贝进前景中。尽管在它表面(Surface)弹出表面(Surface)集以前,函数dolnit生成了调色板索引,而函数updataFrame正是通过它改变了调色板的主表面(Surface)。以上的功能由如下的一段程序代码实现:
    //Change the palette
    if(lpDDPal->GetEntries(0,0,256,pe)!=DD_OK
    {
        return;
    }
    for(I=1;I<256;I++)
    {
        if(!torusColors[I])
        {
            continue;
        }
        pe[I].peRed=(pr[I].peRed+2)%256;
        pe[I].peGreen=(pr[I].peGreen+1)%256;
        pe[I].peBlue=(pr[I].peBlue+3)%256;
    }
    if(lpDDPal->SetEntries(0,0,256,pe)!=DD_OK}return;}
上面的程序代码中,第一行的方法IDirectDrawPalette::GetEntries从对象 IDirectDrawPalette中获得了调色板变量值。由于数组变量pe指向调色板人口变量值将被视为无效,因此,该方法将返回数值DD_OK,然后继续。随后将进行数组变量torusColors的检查。这个检查将决定在初始化过程中颜色索引值是否被置了1。如果颜色索引值被置为了1,那么,在调色板人口由数组变量pe决定的红、绿、蓝三基色值将被循环。
    当所有被标记的调色板人口均被循环时,方法IDirectDrawPalette::GetEntries被称为真正改变了DirectDrawPalette的人口。如果你正在使用被置于主表面(Surface)的调色板时,这种变化将即刻发生。上述变化完成后,表面(Surface)就被表面(Surface)弹出。
检验Duel例子
以上5个基本的DirectDraw例程(DDEXx)给你示范了使用DirectDraw的大部分初步方式。如果,你还想在你的程序中,通过使用DirectDraw以绘制出更为精确的图片,DirectX 3 SDK还包含了一个被称为Duel的简单程序。Duel包含了一些迄今为止我们所讨论的相同的函数,但是,它们将被合并为程序代码,从而更加类似将在你的程序中使用的DirectDraw。
这里,大多数我们已经检验的DirectDraw方法都处于Duel应用程序中。所以,我不打算在次复习它们。读者可以自己花费一定的时间查阅一下Duel应用程序代码,看看DirectDraw方法如何真正地应用在程序中。
关于最优化(Optimizations)和规范化(Customizations)
由DirectX 3 SDK提供所有的DirectDraw例程都比较简单,并假定它们一些在许多系统事件上。在这部分中,我们将检查一些允许你的程序代码在真实世界情况下正常工作的最优化和规范化。
获得表面(Surface)弹出和按位隔行拷贝状态
当我们调用方法IDirectDrawSurface::Flip,开始表面(Surface)和后台缓冲区就发生交换。尽管,这种交换可能并非立即出现。例如:先前的表面(Surface)弹出并没有结束,或者没有成功,则方法IDirectDrawSurface::Flip 将返回DDERR_WASSTILLDRAWING。在由 DirectX 3 SDK 提供的例程中,方法 IDirectDrawSurface::Flip 调用并没有立即完成。下一次,系统将出现一个垂直的blank以安排一个表面(Surface)弹出。
  仅仅等待DDERR_WASSTILLDRAWING的值的返回并不是十分有效。相反,你可以建立一个函数在后台缓冲区中调用方法IDirectDrawSurface::GetFlipStatus,从而决定上次的表面(Surface)弹出是否已经完成。
如果上次的表面(Surface)弹出并没有完成,并且在状态调用返回值DERR_WASSTILLDRAWING时,你可以用这段空闲时间执行一些其它程序,然后再次检查状态变量。如果这时上次的表面(Surface)弹出已经完成,你就可以执行下一个表面(Surface)弹出。下面的例程说明了如何实现上述的检查过程: 
    while(lpDDSBack->GetFluipStatus(DDGFS_ISFLIPDONE)==
    DDERR_WASSTILLDRAWING);
        //Wait for flip to finish - do real game stuff here
        //rather than just burn CPU.
    Ddrval = lpDDSPrimary -> Flip(NULL,0);
    同理,你也可以使用方法IDirectDrawSurface::GetBltStatus确定一个按位隔行拷贝是否完成。由于IDirectDrawSurface::GetFlipStatus的值和IDirectDrawSurface::GetBltStatus的值立即返回,所以,你可以周期性地在你的出现中使用它们,其代价是降低了一些系统的执行速度。
利用按位隔行拷贝进行颜色填充
对于决大多数你想要显示的普通颜色填充,你都可以利用方法IDirectDrawSurface::Blt实现。例如:你最常用的颜色是蓝色(Blue),你可以利用带有DDBLT_COLORFILL标志的方法IDirectDrawSurface::Blt,首先将表面(Surface)填充为蓝色,然后就可以在其上写入任何你想要写的东西。这样就允许你非常迅速地填充大多数普通颜色,接着,你只能在表面(Surface)上写入最小数量的色彩。
    下面的例程给出了一种执行色彩填充的方法:


    DDBLTFX  ddbltfx;
    ddbltfx.dwsize = sizeof( ddbltfx );
    ddbltfx.dwFillColor = 0;
    ddrval = lpDDSPrimary -> Blt(
        NULL,        //destination
        NULL,NULL    //Surce rectangle
        DDBLT_COLORFILL,&ddbltfx);
    switch( ddrval )
    {
        case DDERR_WASSTILLDRAWING:
            .
            .
            .
        Case DDERR_SURFACELOST:
            .
            .
            .
        Case DD_OK:
            .
            .
            .
        Default:
    }
    


决定显示硬件的显示能力
DirectDraw使用硬件模拟去执行末端用户硬件不能支持的DirectDraw函数。为了加快你所编制的应用程序的执行速度,当生成一个DirectDraw对象时,你应该确定末端用户显示硬件的能力。DirectDraw将充分利用末端用户显示硬件的能力。 DirectDraw将充分利用末端用户系统上的任一可用的显示加速硬件。注意:如果末端用户系统上的显示适配器不具备你的应用程序所需要的显示加速硬件,那么,你的应用程序必须通过列表方式提供所需的模拟硬件。
    使用方法IDirectDrawSurface::GetCaps去弥补显示硬件的显示能力时,结构DDCAPS的成员dwCaps的值将由Directdraw硬件设备驱动器填充。通过这些值,我们可以区分系统中各个显示加速硬件的显示能力。结构DDCAPS提供了应用程序进行硬件模拟所需要的结构DDCAPS的地址。倘若,在你的显示适配器中有任一或者所有的硬件DirectDraw无法使用时,你就应该运用硬件模拟的方法。但是,在此之前你必须提供你的应用程序所需要的结构DDCAPS的硬件模拟参数。
    当然,在你开发游戏程序时,你有权决定末端用户系统的显示适配器是否具有返回值,这将有助于你成为具有丰富经验的游戏专家。当然,你也可以让它们没有返回值,而是做点什么别的事情。


显存中的位图存储
从显存到显存的按位隔行拷贝通常要比从系统内存到显存的按位隔行拷贝有效得多。正因为如此,你应该尽可能多的将你所编制的应用程序存放在显存中。
    大多数显示适配器拥有足够多的扩展内存,这样就足以供你用来存储单纯的主表面(Surface)和后台缓冲区。要想获得你的显示适配器中可供用来存储位图的内存的大小,你可以利用结构DDCAPS中的成员dwVidMemFree和dwVidMemTotal了实现(如果你使用方法IDirectDrawSurface::GetCaps获得末端用户显示适配器的显示能力)。如果你想进一步了解它们是如何工作的,可利用DirectX 3 SDK提供的DirectX Viewer应用程序,并且在DirectDraw Devices中选择文件夹“PrimaryDisplay Drivers”,然后再选择文件夹“General”,这样,你就知道了最小主表面(Surface)所占显存的总数以及自由空间还有多少。每当我们在对象DirectDraw中增加一个表面(Surface)时,相应的显存中的自由空间数也就随之减少。
三重缓冲技术
在一定的情况下,利用三重缓冲技术可以加速你的应用程序的显示过程。三重缓冲技术使用了包括一个主表面(Surface)和两个后台缓冲区。下面的一段程序代码说明了如何对三重缓冲进度表进行初始化:
    //Create the primary surface with 2 back buffers.
    Ddsd.dwSize=sizeof(ddsd);
    ddsd.dwFlags=DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
    ddsd.ddsCaps.dwCaps=DDSD_PRIMARYSURFACE |
                        DDSDCAPS_FLIP |
                        DDSCAPS_COMPLEX;
    ddsd.dwBackBufferCount = 2;
    ddrval = lpDD->CreateSurface(&ddsd,&lpDDSPrimary,NULL);
    if ( ddrval == DD_OK )
    {
        // Get apointer to the first back buffer.
        Ddscaps.dwCaps = DDSCAPS_BACKBUFFER;
        ddrval = lpDDPrimary->GetAttachedSurface(&ddscaps,&lpDDSBackOne);
        if ( ddrval != DD_OK )
        // error message here
        // Get a pionter to the second back buffer.
        ddsCaps.dwCaps=DDSCAPS_BACKBUFFER;
        ddrval = lpDDPrimary->GetAttachedSurface(&ddscaps,&lpDDSBackTwo);
    }


    三重缓冲技术允许你的应用程序连续地按位隔行拷贝到后台缓冲区中,甚至可以在第一个后台缓冲区的按位隔行拷贝已经结束而表面(Surface)弹出尚未完成的情况下连续进行。表面(Surface)弹出的执行并不是同步事件,其中一个表面(Surface)弹出可能比另外一个花费更长的时间。正因为如此如果你的应用程序只使用了一个后台缓冲区,那么,你必须花费更多的时间去等待方法IDirectDrawSurface::Flip返回DD_OK值。
    


下一步你应该干什么


    到此为止,你已经相当清楚地理解了如何在简单程序中使用DirectDraw了。当然,这点知识对于DirectDraw程序设计来说,就如同针尖对冰山那样显得微不足道。如果你想得到更多更好的方法,你就应该注册以获得其它的DirectX3 SDK样例,它们包括:
    Stretch。该样例演示了如何在窗口中生成非排它模式。该样例具有剪切按位隔行拷贝和拉伸剪切按位隔行拷贝的功能。
    Donut。  该样例演示了多重排它模式应用程序和非排它模式应用程序之间的交      互测试。
    Wormhole。该样例演示了调色板动化。
    Dxview。 该样例演示了如何检索显示硬件的显示能力。
    其他还有一些样例可以检测包括Iklowns,Foxbear,Palette,以及Flip2d在内的DirectDraw代码。但我仍然坚持要说的是,千万别小看他们!
    祝你好运,编程愉快!

你可能感兴趣的:(操作系统,计算机,人工智能,3D,游戏编程)