内容概要
我的第一份工作是开发图形化的MUD,这是当时工作的一个总结。顺便说一下,当时的产品叫做***笑傲江湖武侠社区***,是国内最早实现商业化运作的网络游戏,比《魔力宝贝》早半年左右。

【欢迎转载,请注明作者:房燕良,原文出处:游戏程序员的自我修养

前言


说到实现游戏品质的动画,很多人会立刻想到DirectX,没错DirectDraw很强大,但是并不是必须用DirectDraw才行。动画后面的理论和技巧都是一样的,这和末端使用什么API没有太大关系(如果那API不是太~~慢的话)。就笔者实现的NewImage Lib的测试结果,内部所有像素数据的存储和运算都纯软件实现,最后一步输出到屏幕使用GDI的性能比DirectDraw低不到10%,在Window9X系统上要低20%左右,这对很多软件来说是绝对可以接受的。

现在应用程序界面越做越华丽,除了支持SKIN外,很多人都想在程序中加入一些例如sprite动画这种原本用在游戏上的技术,因为这原因引入DirectX API,显然是不值得的(况且DX版本升级频繁,DX8中已经用DirectGraphic取代了DirectDraw)。本文将以笔者使用标准GDI函数实现的商业游戏为例,带你进入高品质2D动画编程领域,并且保证其设备无关性。

本文假设读者有C/C++语言知识,Windows编程基础,GDI基本概念。下面我将主要讲述我在过去工作中积累的经验和一些技巧,但是将不讲解以上基本概念。读者最好有MFC基础,本文给出的代码将主要使用MFC,但是其中的道理却不限于MFC。

GDI基础


绘制一个位图(Bitmap)对象

GDI的所有操作都是在DC(device context)上进行的,所以首先你应该有DC的概念,如果你对DC还不了解,现在就去翻一翻Windows编程的书吧。

首先我们要Load一个Bitmap对象,使用Win32 API可以写成这样:

//从资源Load一个位图,如果从文件load的话,可以使用::LoadImage()
HBITMAP hbmp=::LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_MYBMP));

如果使用MFC可以这样写:

    CBitmap bmp;
    Bmp.LoadBitmap(IDB_MYBMP);

想把这个位图对象绘制到窗口上就要先得到窗口的DC,然后对这个DC操作。请留意创建MemoryDC的代码,后面会用到。 Win32 API的版本:

    //假设位图大小为100*100像素
    //假设hwnd是要绘制的窗口的HANDLE
    HDC hwnddc=::GetDC(hwnd);
    HDC memdc=::CreateCompatibleDC(hwnddc);
    HBITMAP oldbmp=::SelectObject(memdc,hbmp);
    ::BitBlt(hwnddc,0,0,100,100,memdc,0,0,SRCCOPY);
    if(oldbmp)
        ::SelectObject(memdc,oldbmp);
    DeleteDC(memdc);
    ::ReleaseDC(hwnd,hwnddc);

MFC版本:

    //假设是在一个CWnd派生类的成员函数中
    CClientDC dc(this);
    CDC memdc;
    memdc.CreateCompatibleDC(&dc);
    CBitmap *oldbmp=memdc.SelectObject(&bmp);
    dc.BitBlt(0,0,100,100,&memdc,0,0,SRCCOPY);
    if(oldbmp)
        memdc.SelectObject(oldbmp);

也可以这样:

    CClientDC dc(this);
	dc.DrawState(CPoint(0,0),CSize(100,100),&bmp,DST_BITMAP);

基本的代码就是这样,当然有更多的API可以用,这就要看你自己的了。:smile:

常用像素格式

要进行图像编程的化对像素格式不了解似乎说不过去。我想应该有较多的人并不太了解,所以这里简要的介绍一下。

1、8bit

也叫做256色模式。每个像素占一个字节, 使用调色板。调色板实际上是一个颜色表,简单的讲就是,我们有256个油漆桶(因为像素的取值范围是0到255),每个油漆桶里面漆的颜色都由红,绿,蓝(RGB)三中基本的油漆按不同比例配置而成。所以我们指定一个像素的颜色的时候只需要指定它用的第几号桶就好了。

这种模式造就了DOS时代的神奇模式—13H(320200256色),因为3202001Byte正好是16bit指针寻址能力的范围。这种模式有2的18次方种颜色(通过改变调色板实现),可以同时显示256中颜色。这模式刚刚推出的时候,有人惊呼这是人类智慧的结晶呢!也是这种模式造就了1992年WestWood的«卡兰蒂亚传奇»和1995年大宇资讯的«仙剑奇侠传»这样的经典游戏。

在Windows下硬件调色板应该极少用到,但是你可以用软件调色板来压缩你的动画,这也是在2D游戏中常用的技巧。

2、16bit

这也是笔者最喜欢的模式。它不使用调色板。每个像素占两个字节,存储RGB值。我觉得这种像素格式的效果(同时显示颜色数)和存储量(也影响速度)取得了比较好的统一。但是如果你是写应用程序的话,我劝你不要用它。因为它的RGB值都不是整个BYTE,例如565模式(16bit的一种模式),它的RGB所占用的bit就是这样的:
RRRR RGGG GGGB BBBB

3、24bit

每个像素有三个BYTE,分别存储RGB值,这对你来说是不是很方便?是不是太好了?可惜对我们可怜的计算机却不是,因为CPU访问奇数的地址会很费劲,而且在硬件工艺上也有很多困难(具体我也不太清楚,请做过硬件的高手指点),所以你会发现你的显卡不支持这种模式,但是你可以在自己的软件中使用。

4、32bit
每个像素4个BYTE,分别存储RGBA,A值就是Alpha,也就是透明度,可以用像素混合算法实现多种效果,后面你就会看到。

Windows下的基本动画系统


动画驱动方式

先略说一下动画的基本原理,程序播放动画一般过程都是: 绘制—擦除—绘制,这样的重复过程,只要你重复的够快,至少每秒16次(被称作16FPS,Frame per Second),我们可怜的眼睛就分辨不出单帧的图像了,看上去就是动画了。

在Windows环境下要驱动这样重复不停的操作有两种方法:
1、设置Timer

这很简单,只要设置一个足够短的Timer,然后响应WM_TIME(对应MFC中的OnTimer函数)就可以满足绝大部分应用程序的需要。缺点是不够精确,而且Win2000和Win9x系统的精确性又有较大差异。

2、在消息循环中执行动画操作
这是在游戏中常用的方法,一般都会把WinMain中的消息循环写成这样:

    while( TRUE )
    {
        // Look for messages, if none are found then 
        // update the state and display it
        if( PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE ) )
        {
            if( 0 == GetMessage(&msg, NULL, 0, 0 ) )
            {
                // WM_QUIT was posted, so exit
                return (int)msg.wParam;
            }
           TranslateMessage( &msg ); 
           DispatchMessage( &msg );
        }
        else
        {
            if( g_bActive )//在主窗口不激活时不更新,以节省资源 
            {
	        //执行动画更新操作
            }
            // Make sure we go to sleep if we have nothing else to do
            else WaitMessage();
        }
	}

如果你使用MFC,则需要重载CWinAPP的Run虚函数,把上述消息循环替换进去。

播放动画

现在我们有了一个适当的时机执行更新操作了,现在就让我们试试动画吧。下面的代码将不再提供Win32的版本。 为了叙述方便,我需要一个播放动画的窗口,它必须是一个CWnd的派生类,假设这个类叫做CMyView,我们将在这个窗口中绘制动画。首先我们为这个类添加一个成员函数”void CMyView::RenderView()”,你可以使用上面提到的方法调用这个函数。

现在准备工作都做好了,我们的动画该怎么存储呢?别提动画GIF89a格式(如果你觉得只有GIF才有动画的话,那我劝你去做美术好了,别干程序了),如果你只想要个简单的动画播放当然可以,但是如果你想要做复杂点的,交互式动画,我劝你还是别用那东西。假设我们有一个4帧的动画,怎么存储它呢?我首先想到的就是存4个BMP文件,然后读入到一个CBitmap对象数组中,但是尊敬的大师Scott Meyers警告我们不要使用多态数组,因为编译器在某些情况下不能准确计算数组中对象的大小,所以下标运算符会产生可怕的效果。然后我就想到了用CBitmap指针数组,这到是不错,不过管理起来稍嫌麻烦。现在看看我最终的解决方法吧。把一个帧序列安顺序拼接成一个文件,象这样:

然后用它创建一个CImageList对象,让我们仔细看一下创建的方法,使用函数:

BOOL CImageList::Create( int cx int cy UINT nFlags int nInitial int nGrow );

前面两个参数用来指定我们一帧动画的尺寸。这样就创建了一个空的ImageList,这样做的好处是可扩展行比较强。下面我们需要把那个帧序列文件Load到一个CBitmap对象中,你可以存成JPG或者GIF文件来节省容量(后面将提到读取这些文件的简单方法,并且附一个实用类)。当我们有了一个合适的CBitmap对象后,可以把他添加到我们的ImageList中,使用:

BOOL CImageList::int Add( CBitmap* pbmImage COLORREF crMask );

一个实例:

const int SPRIRT_WIDTH=32;
const int SPIRIT_HEIGHT=32;
...
m_myimglist.Create(SPIRIT_WIDTH,SPIRIT_HIGHT,ILC_COLOR24|ILC_MASK,1,1);
if(bmp.Load(myani.bmp))
m_myimglist.Add(&bmp,RGB(152,152,152));

好了,现在我们已经准备好了这些数据,让我们来实作渲染函数吧,下面这端代码可以循环播放上面的4帧动画,并且支持透明色(如果你不知道这个名字,稍后有讲解)哦!

  void CMyView::RenderView()
  {
        CclientDC dc(this);
        Static int curframe=0;
         m_myimglist.Draw(&dc,curframe,Cpoint(0,0),ILD_TRANSPARENT);
        curframe++;
        If(curframe > m_myimglist.GetImageCount())
            Curframe=0;
}

上面这个代码没有写擦除的操作,因为这根据具体需要有较大不同。如果你只有一个精灵动画的话,你可以用一个Bitmap对象保存精灵所占矩形区域的图像。你也可能需要有一个大的背景图每帧都要更新(这里我不讨论象dirty rect这样的优化方法),所以你只要每次都画背景,然后画精灵就好了。

怎么样?你已经实现了基本的动画系统,就是这么简单。

消除闪烁

如果你真正实现上面的代码的话,你会发现画面一闪一闪的,十分的不爽。 很多人都会怪到GDI头上,他们又会骂MS,说GDI太慢了。其实非也(不是指MS不该骂,呵呵),任何直接写屏幕的操作都会产生闪烁,在DOS下直接写显存或者用DirectDraw API直接写Primary Surface都会闪烁,因为你每个更新显示的操作都会被用户马上看到(因为垂直回扫的原因, 或许会有延迟)。

消除闪烁最简单也是最经典的方法就是双缓冲(Double buffer)。所谓的双缓冲其实道理非常简单,就是说我们在其它地方(简单的说就是不针对屏幕,不显示出来的地方)开辟一个存储空间,我们把所有的动画都要渲染到这个地方,而不是直接渲染到屏幕上(针对屏幕的存储区域)。在GDI中,直接针对屏幕就是窗口DC,”不可见的地方”一般可以用Memory DC。在把所有动画渲染到后台缓冲之后,再一下次整体拷贝到屏幕缓冲区!

在纯软件2D图形引擎中,双缓冲一般意味着在内存中开辟一个区域用来存储像素数据。而在DirectDraw中可以创建Back Surface,在把所有动画渲染到Back Suface上之后,然后使用Flip操作使其可见,Flip操作因为只是设置可见surface的地址,所以非常快速。

让我们重写一下void CMyView::RenderView()函数,来用GDI实现双缓冲:

  void CMyView::RenderView()
  {
        CClientDC dc(this);
        CRect rc;
        GetClientRect(rc);
		CDC memdc;
   memdc.CreateCompatibleDC(&dc);
    CBitmap bmp;
        Bmp. CreateCompatibleBitmap (&dc,rc.Width(),rc.Height());
    CBitmap *oldbmp=memdc.SelectObject(&bmp);

        Static int curframe=0;
         m_myimglist.Draw(&memdc,curframe,Cpoint(0,0),ILD_TRANSPARENT);
        curframe++;
        If(curframe > m_myimglist.GetImageCount())
Curframe=0;
    if(oldbmp)
           memdc.SelectObject(oldbmp);
        dc.BitBlt(0,0,rc.Width(),rc.Height(),&memdc,0,0,SRCCOPY);
}

其中创建一个Bitmap对象,然后选入Memory DC是必须的,因为CreateCompatibleDC所创建的DC里面只含有一个1*1像素的单色Bitmap对象,所以如果缺了这个步骤,任何在MemoryDC上的绘图操作都会没有效果。延伸出一个问题, CreateCompatibleBitmap函数的第一个参数显然不可写成&memdc,如果那样的化,你就创建了一个单色的位图,我想你肯定不希望这样。:smile:

重写后的函数看上去似乎多了很多无谓的操作,这是因为我们现在只有一个动画对象,如果我们有多个动画,而且还需要绘制动画的子窗口,那这样做的效果就会非常的好,不会有任何闪烁,而且向文章最后提到的图形MUD客户端,还能达到60FPS呢(在我家的赛阳433上)。

到此为止,我们的基本动画系统已经有了一个很好的基础了。

透明色(color key)处理

透明色就是指在绘制一张图片的时候,该颜色的像素不会被绘制上去,这通常用来做游戏的spirit动画,所以你可以看到各种形状不规则的人物动画。但是他们的数据都是一个矩形的像素区域,只是绘制的时候有些像素不被画上去罢了。

GDI提供一个TransparentBlt()函数来支持Color Key,你可以在MSDN中查到该函数的说明。但是我的代码中使用这个函数后,在Win9X系统下产生了严重的资源泄漏,但是在Win2000下却没事,所以如果你也发现这问题的话,我建议你使用下面的代码,来把一个CBitmap透明的绘制到DC上。假设你有一个CBitmap的派生类CMyBitmap:

BOOL CMyBitmap::DrawTransparentInPoint(CDC *pdc, int x, int y, COLORREF mask/*要过滤掉的颜色值*/)
{
    //Quick return
    if(pdc->GetSafeHdc()==NULL)
        return FALSE;
    if (m_hObject == NULL)
        return FALSE;

    CRect DRect;
    DRect=Rect();
    DRect.OffsetRect(x,y);
    if(!pdc->RectVisible(&DRect))
        return FALSE;

    COLORREF crOldBack=pdc->SetBkColor(RGB(255,255,255));
    COLORREF crOldText=pdc->SetTextColor(RGB(0,0,0));

    CDC dcimg,dctrans;
    if(dcimg.CreateCompatibleDC(pdc)!=TRUE)
        return FALSE;
    if(dctrans.CreateCompatibleDC(pdc)!=TRUE)
        return FALSE;

    CBitmap *oldbmpimg=dcimg.SelectObject(this);

    CBitmap bmptrans;
    if(bmptrans.CreateBitmap(Width(),Height(),1,1,NULL)!=TRUE)
        return FALSE;

    CBitmap *oldbmptrans=dctrans.SelectObject(&bmptrans);

    dcimg.SetBkColor(mask);
    dctrans.BitBlt(0,0,Width(),Height(),&dcimg,0,0,SRCCOPY);

    pdc->BitBlt(x,y,Width(),Height(),&dcimg,0,0,SRCINVERT);
    pdc->BitBlt(x,y,Width(),Height(),&dctrans,0,0,SRCAND);
    pdc->BitBlt(x,y,Width(),Height(),&dcimg,0,0,SRCINVERT);

    if(oldbmpimg)
        dcimg.SelectObject(oldbmpimg);
    if(oldbmptrans)
        dctrans.SelectObject(oldbmptrans);
    pdc->SetBkColor(crOldBack);
    pdc->SetTextColor(crOldText);

    return TRUE;
}

Alpha混合

Alpha混合是一种像素混合的方法。所谓的像素混合就是使用一定的算法把两个像素的值混合成一个新的像素值(倒,和没说一样),通常我们都把两个像素的值,分别叫做源(src)和目的(dst),然后把混合后的结果存入dst中:
dst= src blend dst
如果源像素和目的像素都是RGBA格式,你可以使用每个像素的Alpha信息(或者叫做Alpha通道)组合出各种运算公式,例如
dst= srcsrc.alpha+dstdst.alpha;
或者
dst=srcsrc.alpha + dst(1-src.alpha) //这里我们假设alpha值是0~1的浮点数。

可惜标准GDI没有支持类似这种操作的函数(起码我没找到),它只支持另一种Alpha混合,我把它叫做const alpha blend,也就是把两幅都不包含Alpha通道的图像的按照一个固定的Alpha值混合到一起,也就是每个像素都使用同一Alpha值。GDI的支持这个操作的函数是:

AlphaBlend(
  HDC hdcDest,
  int nXOriginDest,
  int nYOriginDest,
  int nWidthDest,
  int hHeightDest,
  HDC hdcSrc,
  int nXOriginSrc,
  int nYOriginSrc,
  int nWidthSrc,
  int nHeightSrc,
  BLENDFUNCTION blendFunction
);

这个API的参数个数略多了一些,但是我想其中的位置参数你可以轻松搞定,还有就是源DC和目的DC,当然了,我们的GDI只能对DC操作,而不是对我们的像素数据,而我们只要把我的位图select到DC中就OK了,最后一个参数是一个结构,是用来指定Alpha的运算方式的,请看一个实际的例子:

    BLENDFUNCTION bf;
	bf.AlphaFormat=0;
	bf.BlendFlags=0;
	bf.BlendOp=AC_SRC_OVER;
	bf.SourceConstantAlpha=100;//指明透明度,取值范围是0~255
	
	AlphaBlend(pdc->GetSafeHdc(),rc.left,rc.top,rc.Width(),rc.Height(),
		memdc.GetSafeHdc(),0,0,rc.Width(),rc.Height(),bf);

也许你看过很多游戏,在弹出文字对话框的时候都是在游戏画面上蒙一层半透明的黑色,然后在这上面印字。使用上述操作就可以达到此效果。你可以先建立一个Memory DC,然后把他填充为黑,然后把Alpha值设为128,然后混合到你要绘制的DC上(不一定是窗口DC哦,记得我们前面将的双缓冲吗?)就OK了。

读取JPEG,GIF文件

JPEG压缩算法综合的信号学和视觉心理学,而GIF格式,特别是支持动画的GIF89a格式为了节约容量也做了很多种非常变态的优化,所以要写一个完全支持这些标准格式的解码器相当困难,也没有必要。

如果你需要进行JPEG文件的读写我推荐你使用Intel Jpeg Lib,速度相当令人满意。而GIF由于授权问题,没有任何官方组织提供的读写代码。

如果你只是需要读入JPEG和静态GIF(或者只一帧的动态GIF),我推荐你使用Windows提供的OleLoadPicture函数,下面这段代码可以把一个JPG,GIF,BMP读入到Bitmap对象中:

BOOL CIJLBitmap::Load(LPCTSTR lpszPathName)
{
    BOOL bSuccess = FALSE;
    
    //Free up any resource we may currently have
    DeleteObject();
    
    //open the file
    CFile f;
    if (!f.Open(lpszPathName, CFile::modeRead))
    {
        TRACE(_T("Failed to open file %s, Error:%x\n"), lpszPathName, ::GetLastError());
        return FALSE;
    }
    
    //get the file size
    DWORD dwFileSize = f.GetLength();
    
    //Allocate memory based on file size
    LPVOID pvData = NULL;
    HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE, dwFileSize);
    if (hGlobal == NULL)
    {
        TRACE(_T("Failed to allocate memory for file %s, Error:%x\n"), lpszPathName, ::GetLastError());
        return FALSE;
    }
    pvData = GlobalLock(hGlobal);
    //ASSERT(pvData);
    if(pvData==NULL)
    {
        TRACE(_T("Failed to lock memory\r\n"));
        return FALSE;
    }
    // read file and store in global memory
    if (f.Read(pvData, dwFileSize) != dwFileSize)
    {
        TRACE(_T("Failed to read in image date from file %s, Error:%x\n"), lpszPathName, ::GetLastError());
        GlobalUnlock(hGlobal);
        GlobalFree(hGlobal);
        return FALSE;
    }
    
    //Tidy up the memory and close the file handle
    GlobalUnlock(hGlobal);
    
    //create IStream* from global memory
    LPSTREAM pStream = NULL;
    if (FAILED(CreateStreamOnHGlobal(hGlobal, TRUE, &pStream)))
    {
        TRACE(_T("Failed to create IStream interface from file %s, Error:%x\n"), lpszPathName, ::GetLastError());
        GlobalFree(hGlobal);
        return FALSE;
    }
    
    // Create IPicture from image file
    if (SUCCEEDED(::OleLoadPicture(pStream, dwFileSize, FALSE, IID_IPicture, (LPVOID*)&m_pPicture)))
    {
        short nType = PICTYPE_UNINITIALIZED;
        if (SUCCEEDED(m_pPicture->get_Type(&nType)) && (nType == PICTYPE_BITMAP))
        {
            OLE_HANDLE hBitmap;
            OLE_HANDLE hPalette;
            if (SUCCEEDED(m_pPicture->get_Handle(&hBitmap)) &&
                SUCCEEDED(m_pPicture->get_hPal(&hPalette)))
            {
                Attach((HBITMAP) hBitmap);
                m_Palette.Attach((HPALETTE) hPalette);
                bSuccess = TRUE;
            }
        }
    }
    
    //Free up the IStream* interface
    pStream->Release();
    
    return bSuccess;
}

这个class的完整代码请看文章最后的参考。

子窗口管理

你也许注意过几乎所有游戏界面中的窗口都是使用动画的从屏幕外飞出(而且是半透明的,这你已经可以做到了)。游戏中一般都使用自己的UI系统。这里我们可以借助Windows对窗口的管理来轻松实现各种动画子窗口。

首先让我们从最简单的开始。假设在我们的动画窗口中需要一个漂亮的按钮怎么办,我劝你最好不要使用CBitmapButton,因为你已经上了每秒重画窗口16次以上这条贼船,我建议你在每次重画父窗口的时候重画所有子窗口,如此一来子窗口上如果要求有动画操作,也可以轻松实现了。既然做了,就把它做到最好。:smile:

那我们怎么定义一个button呢?你也许想到自己定义一个矩形区域,然后在父窗口的消息响应函数中检测是否是对此区域操作,这样在重画父窗口的时候特殊的画一次这个矩形区域就好了。这样是可以实现,但是显然不符合我们的OOP精神,界面元素一多,你很可能就会乱了阵脚。最后的解决方法当然是使用我们可爱的CWnd类,显然所有的界面元素都可以作为一个CWnd派生类的对象。不过我建议你不要从CButton派生,这带来的麻烦远多于它的价值。从CWnd派生一个类,然后在Create时注意使用WS_CHILD风格,并且指定父窗口为我们的动画窗口。

下面一个问题是如何调用这些子窗口重画操作呢?第一种较好的解决方法是先建立这样一个虚基类:

CmyAniWnd :public CWnd
{
	
	virtual void Render(CDC *pdc)=0;
	
} 

假设你有一个Button类和一个TextBox类:

CmyButton : public CmyAniWnd
CmyTextBox: public CmyAniWnd

这两个类都必须实现Render函数,这样在父窗口类中你可以保存一个指针数组,例如这样:

CPtrArray m_allchild;

在创建一个Button时这样写:

CmyButton *pbtn=new CmyButton;
m_allchild.Add(pbtn);
pbtn->Create();

然后在我们父窗口的RenderView函数(前面提到的,每次更新调用)中这样写即可:

CmyAniWnd *pchild=NULL;
for(int I;I<m_allchild.GetSize();I++)
{
	pchild=static_cast<CmyAniWnd*>(m_allchild.GetAt(i));
	ASSERT(::IsWindow(pchild->GetSafeHwnd());
	pchild->Render(&memdc);
}

这是一个典型的虚函数的应用,在调用这些子窗口的Render函数时,我们不需要知道它到底是Button还是TextBox,虚函数机制会自动帮我们找到该调用的函数。还有一点就是,请注意,一定要把子窗口渲染到我们的后台缓冲,也就是Memory DC中,否则还是会闪烁的。

上面这种方法适合于子窗口数目固定,更高级的界面会要求触发某个事件的时候产生一个子窗口,子窗口不断更新自己,并且在适当的时候把自己从UI系统中去除。让每个子窗口管理自己的生命期,是个不错的主意,不是吗?那你最好不要使用上面保存指针数组的方法,那样的话,子窗口在杀死自己的时候还要通知父窗口,以让父窗口把它的指针从数组中移除,这显然具有很高的偶合性,不是我们想要的。因为我们的所有子窗口都是标准的Windows对象,所以这使得我们有使用Windows消息的机会。我们首先要枚举所有子窗口,然后发一个自定义的更新消息给它,并把我们的MemoryDC的指针作为参数,具体例子代码如下:

void CMyView::RenderView()
{
	//其它更新操作
	::EnumChildWindows(GetSafeHwnd(),CMyView::UpdateChildWnd,LPARAM(&memdc));
	//其它更新操作
}

其中第二个参数是一个回调函数,你必须把它声明成全局函数,或者类的static成员函数,这里我们使用了后者。

BOOL CALLBACK CMyWnd::UpdateChildWnd(HWND hwnd, LPARAM lParam/*CDC* */)
{
	::SendMessage(hwnd,WM_COMMAND,CHILDCMD_RENDER,lParam);
	return TRUE;
}

这里我没有使用自定义消息,而是发送标准的WM_COMMAND,这样你可以给那个CmyAniWnd虚基类添加一个CWnd虚函数OnCommand(),然后在那里面检测如果wParam是CHILDCMD_RENDER的话,就调用纯虚函数Render(以lParam作为参数),子窗口派生类只要实现自己的Render函数就好,其它不用管了。

这里还有一个要注意的问题就是绘制的顺序问题,如果你想让子窗口盖住某些动画,就应该先渲染那些动画,然后渲染子窗口,反之亦反。

进阶技巧–使用DIB


像素操作

以上所有操作都局限于标准GDI函数,如果我们要实现更进一步的操作,例如当傍晚你希望把整个画面的颜色渲染能淡红色调,晚上的时候你要把整个画面变暗,早上再把它恢复到原来的亮度这些GDI都无法帮你做到。 如果想达到上述效果,就必须自己对像素的RGB值进行操作。

首先让我们要得到一个Bitmap对象中的像素数据。让我们看一下具体该怎么操作。假设我们有一个mybmp是一个CBitmap对象(或者其派生类对象),下面的代码把CBitmap中的像素取出:

    BITMAP bm;
    mybmp.GetBitmap(&bm);
    BITMAPINFO binfo;
	ZeroMemory(&binfo,sizeof(BITMAPINFO));
	binfo.bmiHeader.biBitCount=24;	//24bit像素格式
	binfo.bmiHeader.biCompression=0;
	binfo.bmiHeader.biHeight=-bm.bmHeight;
	binfo.bmiHeader.biPlanes=1;
	binfo.bmiHeader.biSizeImage=0;
	binfo.bmiHeader.biSize=sizeof(BITMAPINFOHEADER);
	binfo.bmiHeader.biWidth=bm.bmWidth;

    CClientDC dc(this);
	BYTE *pbuf;//用来存储像素数据
	int linebytes=(bm.bmWidth*3+3)&(~3);//4字节对齐
	int size=linebytes*bm.bmHeight;
	pbuf=new BYTE[size];
	::GetDIBits(dc,m_bmpSword,0,bm.bmHeight,pbuf,&binfo,DIB_RGB_COLORS);

上面代码执行后,我们的pbuf中就存储了从mybmp拷贝而来的像素数据,而且是24bit模式的,这样你就可以对所有这些像素进行你所需要的操作了,例如晚上了,你想把这个Bitmap变暗,我这里粗略的把每个像素的RGB值都降低一半,可以使用下面的循环:

	for(int I;I<size;I++)
		pbuf[i]=pbuf[i]/2;

得到了像素你就得到了一切,所有操作你都可以进行,例如上面提到的标准GDI不支持的Alpha通道。

呵呵,像素交给你了,这样我就放心了,那我走了…。。等等,你得到了这些像素,但是渲染时我们还是要使用标准GDI操作,所以好把这些像素设置回Bitmap对象中才行,好吧,这其实很简单,继续上面的代码:

SetDIBits(dc,mybmp,0,bm.bmHeight,pbuf,&binfo,DIB_RGB_COLORS);

最后别忘了:

delete[] pbuf;

RLE压缩

现在的个人电脑内存容量已经非常大了,但是对某些人来说还显得不够(或者他们不愿意浪费这些可怜的资源虽然它们可再生),例如在Diablo中一个骷髅兵从地上站起来的动画为96*96像素*100帧,所以你有很多这样的动画,最好压缩一下.

RLE是游戏常用的技巧,但是似乎已经超出了本文的范围。而且这方面的文章很多,我这里就不赘述了,留给你自己去进一步发掘.:smile:

参考


华山论键

2001年上半年,我为号称国内最大武侠社区的笑傲江湖.com实现的图形MUD客户端软件,基于上述技术。详情请见http://hslj.Xajh.com

其它类库

  • CIJLBitmap 一个CBitmap的派生类,可以Load BMP,JPG,GIF文件
  • NewImage Lib 纯软件2D图像引擎,支持RLE,Alpha通道等,与GDI和DX无关,所谓的Open-ending。