使用标准gdi实现游戏品质的动画系统.docx
《使用标准gdi实现游戏品质的动画系统.docx》由会员分享,可在线阅读,更多相关《使用标准gdi实现游戏品质的动画系统.docx(22页珍藏版)》请在冰豆网上搜索。
使用标准gdi实现游戏品质的动画系统
使用标准GDI实现游戏品质的动画系统
燕良2002年1月
前言
说到实现游戏品质的动画,很多人会立即想到DirectX,没错DirectDraw很强大,但是并不是必须用DirectDraw才行。
动画后面的理论和技巧都是一样的,这和末端使用什么API没有太大关系(假设那API不是太~~慢的话)。
就笔者实现的NewImageLib的测试结果,内部所有像素数据的存储和运算都纯软件实现,最后一步输出到屏幕使用GDI的性能比DirectDraw低不到10%,在Window9X系统上要低20%左右,这对很多软件来说是绝对可以承受的。
如今应用程序界面越做越华美,除了支持SKIN外,很多人都想在程序中参加一些例如sprite动画这种本来用在游戏上的技术,因为这原因引入DirectXAPI,显然是不值得的(况且DX版本晋级频繁,DX8中已经用DirectGraphic取代了DirectDraw)。
本文将以笔者使用标准GDI函数实现的商业游戏为例,带你进入高品质2D动画编程领域,并且保证其设备无关性。
本文假设读者有C/C++语言知识,Windows编程根底,GDI根本概念。
下面我将主要讲述我在过去工作中积累的经历和一些技巧,但是将不讲解以上根本概念。
读者最好有MFC根底,本文给出的代码将主要使用MFC,但是其中的道理却不限于MFC。
GDI根底
绘制一个位图(Bitmap)对象
GDI的所有操作都是在DC(devicecontext)上进展的,所以首先你应该有DC的概念,假设你对DC还不理解,如今就去翻一翻Windows编程的书吧。
首先我们要Load一个Bitmap对象,使用Win32API可以写成这样:
//从资源Load一个位图,假设从文件load的话,可以使用:
:
LoadImage()
HBITMAPhbmp=:
:
LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_MYBMP));
假设使用MFC可以这样写:
CBitmapbmp;
Bmp.LoadBitmap(IDB_MYBMP);
想把这个位图对象绘制到窗口上就要先得到窗口的DC,然后对这个DC操作。
请留意创立MemoryDC的代码,后面会用到。
Win32API的版本:
//假设位图大小为100*100像素
//假设hwnd是要绘制的窗口的HANDLE
HDChwnddc=:
:
GetDC(hwnd);
HDCmemdc=:
:
CreateCompatibleDC(hwnddc);
HBITMAPoldbmp=:
:
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派生类的成员函数中
CClientDCdc(this);
CDCmemdc;
memdc.CreateCompatibleDC(&dc);
CBitmap*oldbmp=memdc.SelectObject(&bmp);
dc.BitBlt(0,0,100,100,&memdc,0,0,SRCCOPY);
if(oldbmp)
memdc.SelectObject(oldbmp);
也可以这样:
CClientDCdc(this);
dc.DrawState(CPoint(0,0),CSize(100,100),&bmp,DST_BITMAP);
根本的代码就是这样,当然有更多的API可以用,这就要看你自己的了。
☺
常用像素格式
要进展图像编程的化对像素格式不理解似乎说不过去。
我想应该有较多的人并不太理解,所以这里简要的介绍一下。
1.8bit
也叫做256色形式。
每个像素占一个字节,使用调色板。
调色板实际上是一个颜色表,简单的讲就是,我们有256个油漆桶(因为像素的取值范围是0到255),每个油漆桶里面漆的颜色都由红,绿,蓝(RGB)三中根本的油漆按不同比例配置而成。
所以我们指定一个像素的颜色的时候只需要指定它用的第几号桶就好了。
这种形式造就了DOS时代的神奇形式—13H(320*200*256色),因为320*200*1Byte正好是16bit指针寻址才能的范围。
这种形式有2的18次方种颜色(通过改变调色板实现),可以同时显示256中颜色。
这形式刚刚推出的时候,有人惊呼这是人类智慧的结晶呢!
也是这种形式造就了1992年WestWood的<<卡兰蒂亚传奇>>和1995年大宇资讯的<<仙剑奇侠传>>这样的经典游戏。
在Windows下硬件调色板应该极少用到,但是你可以用软件调色板来压缩你的动画,这也是在2D游戏中常用的技巧。
2.16bit
这也是笔者最喜欢的形式。
它不使用调色板。
每个像素占两个字节,存储RGB值。
我觉得这种像素格式的效果(同时显示颜色数)和存储量(也影响速度)获得了比较好的统一。
但是假设你是写应用程序的话,我劝你不要用它。
因为它的RGB值都不是整个BYTE,例如565形式(16bit的一种形式),它的RGB所占用的bit就是这样的:
RRRRRGGGGGGBBBBB
3.24bit
每个像素有三个BYTE,分别存储RGB值,这对你来说是不是很方便?
是不是太好了?
可惜对我们可怜的计算机却不是,因为CPU访问奇数的地址会很费力,而且在硬件工艺上也有很多困难(详细我也不太清楚,请做过硬件的高手指点),所以你会发现你的显卡不支持这种形式,但是你可以在自己的软件中使用。
4.32bit
每个像素4个BYTE,分别存储RGBA,A值就是Alpha,也就是透明度,可以用像素混合算法实现多种效果,后面你就会看到。
Windows下的根本动画系统
动画驱动方式
先略说一下动画的根本原理,程序播放动画一般过程都是:
绘制—擦除—绘制,这样的重复过程,只要你重复的够快,至少每秒16次(被称作16FPS,FrameperSecond),我们可怜的眼睛就分辨不出单帧的图像了,看上去就是动画了。
在Windows环境下要驱动这样重复不停的操作有两种方法:
1.设置Timer
这很简单,只要设置一个足够短的Timer,然后响应WM_TIME(对应MFC中的OnTimer函数)就可以满足绝大部分应用程序的需要。
缺点是不够准确,而且Win2000和Win9x系统的准确性又有较大差异。
2.在消息循环中执行动画操作
这是在游戏中常用的方法,一般都会把WinMain中的消息循环写成这样:
while(TRUE)
{
//Lookformessages,ifnonearefoundthen
//updatethestateanddisplayit
if(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
{
if(0==GetMessage(&msg,NULL,0,0))
{
//WM_QUITwasposted,soexit
return(int)msg.wParam;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
if(g_bActive)//在主窗口不激活时不更新,以节省资源
{
//执行动画更新操作
}
//Makesurewegotosleepifwehavenothingelsetodo
elseWaitMessage();
}
}
假设你使用MFC,那么需要重载CWinAPP的Run虚函数,把上述消息循环交换进去。
播放动画
如今我们有了一个适当的时机执行更新操作了,如今就让我们试试动画吧。
下面的代码将不再提供Win32的版本。
为了表达方便,我需要一个播放动画的窗口,它必须是一个CWnd的派生类,假设这个类叫做CMyView,我们将在这个窗口中绘制动画。
首先我们为这个类添加一个成员函数〞voidCMyView:
:
RenderView()〞,你可以使用上面提到的方法调用这个函数。
如今准备工作都做好了,我们的动画该怎么存储呢?
别提动画GIF89a格式(假设你觉得只有GIF才有动画的话,那我劝你去做美术好了,别干程序了),假设你只想要个简单的动画播放当然可以,但是假设你想要做复杂点的,交互式动画,我劝你还是别用那东西。
假设我们有一个4帧的动画,怎么存储它呢?
我首先想到的就是存4个BMP文件,然后读入到一个CBitmap对象数组中,但是尊敬的大师ScottMeyers警告我们不要使用多态数组,因为编译器在某些情况下不能准确计算数组中对象的大小,所以下标运算符会产生可怕的效果。
然后我就想到了用CBitmap指针数组,这到是不错,不过管理起来稍嫌费事。
如今看看我最终的解决方法吧。
把一个帧序列安顺序拼接成一个文件,象这样:
然后用它创立一个CImageList对象,让我们仔细看一下创立的方法,使用
BOOLCImageList:
:
Create(intcx,intcy,UINTnFlags,intnInitial,intnGrow);
函数,前面两个参数用来指定我们一帧动画的尺寸。
这样就创立了一个空的ImageList,这样做的好处是可扩展行比较强。
下面我们需要把那个帧序列文件Load到一个CBitmap对象中,你可以存成JPG或者GIF文件来节省容量(后面将提到读取这些文件的简单方法,并且附一个实用类)。
当我们有了一个适宜的CBitmap对象后,可以把他添加到我们的ImageList中,使用:
BOOLCImageList:
:
intAdd(CBitmap*pbmImage,COLORREFcrMask);
一个实例:
constintSPRIRT_WIDTH=32;
constintSPIRIT_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帧动画,并且支持透明色(假设你不知道这个名字,稍后有讲解)哦!
voidCMyView:
:
RenderView()
{
CclientDCdc(this);
Staticintcurframe=0;
m_myimglist.Draw(&dc,curframe,Cpoint(0,0),ILD_TRANSPARENT);
curframe++;
If(curframe>m_myimglist.GetImageCount())
Curframe=0;
}
上面这个代码没有写擦除的操作,因为这根据详细需要有较大不同。
假设你只有一个精灵动画的话,你可以用一个Bitmap对象保存精灵所占矩形区域的图像。
你也可能需要有一个大的背景图每帧都要更新(这里我不讨论象dirtyrect这样的优化方法),所以你只要每次都画背景,然后画精灵就好了。
怎么样?
你已经实现了根本的动画系统,就是这么简单。
消除闪烁
假设你真正实现上面的代码的话,你会发现画面一闪一闪的,非常的不爽。
☹很多人都会怪到GDI头上,他们又会骂MS,说GDI太慢了。
其实非也(不是指MS不该骂,呵呵),任何直接写屏幕的操作都会产生闪烁,在DOS下直接写显存或者用DirectDrawAPI直接写PrimarySurface都会闪烁,因为你每个更新显示的操作都会被用户马上看到(因为垂直回扫的原因,或许会有延迟)。
消除闪烁最简单也是最经典的方法就是双缓冲(Doublebuffer)。
所谓的双缓冲其实道理非常简单,就是说我们在其它地方(简单的说就是不针对屏幕,不显示出来的地方)开拓一个存储空间,我们把所有的动画都要渲染到这个地方,而不是直接渲染到屏幕上(针对屏幕的存储区域)。
在GDI中,直接针对屏幕就是窗口DC,〞不可见的地方〞一般可以用MemoryDC。
在把所有动画渲染到后台缓冲之后,再一下次整体拷贝到屏幕缓冲区!
在纯软件2D图形引擎中,双缓冲一般意味着在内存中开拓一个区域用来存储像素数据。
而在DirectDraw中可以创立BackSurface,在把所有动画渲染到BackSuface上之后,然后使用Flip操作使其可见,Flip操作因为只是设置可见surface的地址,所以非常快速。
让我们重写一下voidCMyView:
:
RenderView()函数,来用GDI实现双缓冲:
voidCMyView:
:
RenderView()
{
CClientDCdc(this);
CRectrc;
GetClientRect(rc);
CDCmemdc;
memdc.CreateCompatibleDC(&dc);
CBitmapbmp;
Bmp.CreateCompatibleBitmap(&dc,rc.Width(),rc.Height());
CBitmap*oldbmp=memdc.SelectObject(&bmp);
Staticintcurframe=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对象,然后选入MemoryDC是必须的,因为CreateCompatibleDC所创立的DC里面只含有一个1*1像素的单色Bitmap对象,所以假设缺了这个步骤,任何在MemoryDC上的绘图操作都会没有效果。
延伸出一个问题,CreateCompatibleBitmap函数的第一个参数显然不可写成&memdc,假设那样的化,你就创立了一个单色的位图,我想你肯定不希望这样。
☺
重写后的函数看上去似乎多了很多无谓的操作,这是因为我们如今只有一个动画对象,假设我们有多个动画,而且还需要绘制动画的子窗口,那这样做的效果就会非常的好,不会有任何闪烁,而且向文章最后提到的图形MUD客户端,还能到达60FPS呢(在我家的赛阳433上)。
到此为止,我们的根本动画系统已经有了一个很好的根底了。
透明色(colorkey)处理
透明色就是指在绘制一张图片的时候,该颜色的像素不会被绘制上去,这通常用来做游戏的spirit动画,所以你可以看到各种形状不规那么的人物动画。
但是他们的数据都是一个矩形的像素区域,只是绘制的时候有些像素不被画上去罢了。
GDI提供一个TransparentBlt()函数来支持ColorKey,你可以在MSDN中查到该函数的说明。
但是我的代码中使用这个函数后,在Win9X系统下产生了严重的资源泄漏,但是在Win2000下却没事,所以假设你也发现这问题的话,我建议你使用下面的代码,来把一个CBitmap透明的绘制到DC上。
假设你有一个CBitmap的派生类CMyBitmap:
BOOLCMyBitmap:
:
DrawTransparentInPoint(CDC*pdc,intx,inty,COLORREFmask/*要过滤掉的颜色值*/)
{
//Quickreturn
if(pdc->GetSafeHdc()==NULL)
returnFALSE;
if(m_hObject==NULL)
returnFALSE;
CRectDRect;
DRect=Rect();
DRect.OffsetRect(x,y);
if(!
pdc->RectVisible(&DRect))
returnFALSE;
COLORREFcrOldBack=pdc->SetBkColor(RGB(255,255,255));
COLORREFcrOldText=pdc->SetTextColor(RGB(0,0,0));
CDCdcimg,dctrans;
if(dcimg.CreateCompatibleDC(pdc)!
=TRUE)
returnFALSE;
if(dctrans.CreateCompatibleDC(pdc)!
=TRUE)
returnFALSE;
CBitmap*oldbmpimg=dcimg.SelectObject(this);
CBitmapbmptrans;
if(bmptrans.CreateBitmap(Width(),Height(),1,1,NULL)!
=TRUE)
returnFALSE;
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);
returnTRUE;
}
Alpha混合
Alpha混合是一种像素混合的方法。
所谓的像素混合就是使用一定的算法把两个像素的值混合成一个新的像素值(倒,和没说一样),通常我们都把两个像素的值,分别叫做源(src)和目的(dst),然后把混合后的结果存入dst中:
dst=srcblenddst
假设源像素和目的像素都是RGBA格式,你可以使用每个像素的Alpha信息(或者叫做Alpha通道)组合出各种运算公式,例如
dst=src*src.alpha+dst*dst.alpha;
或者
dst=src*src.alpha+dst*(1-src.alpha)//这里我们假设alpha值是0~1的浮点数。
可惜标准GDI没有支持类似这种操作的函数(起码我没找到),它只支持另一种Alpha混合,我把它叫做constalphablend,也就是把两幅都不包含Alpha通道的图像的按照一个固定的Alpha值混合到一起,也就是每个像素都使用同一Alpha值。
GDI的支持这个操作的函数是:
AlphaBlend(
HDChdcDest,
intnXOriginDest,
intnYOriginDest,
intnWidthDest,
inthHeightDest,
HDChdcSrc,
intnXOriginSrc,
intnYOriginSrc,
intnWidthSrc,
intnHeightSrc,
BLENDFUNCTIONblendFunction
);
这个API的参数个数略多了一些,但是我想其中的位置参数你可以轻松搞定,还有就是源DC和目的DC,当然了,我们的GDI只能对DC操作,而不是对我们的像素数据,而我们只要把我的位图select到DC中就OK了,最后一个参数是一个构造,是用来指定Alpha的运算方式的,请看一个实际的例子:
BLENDFUNCTIONbf;
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);
也许你看过很多游戏,在弹出文字对话框的时候都是在游戏画面上蒙一层半透明的黑色,然后在这上面印字。
使用上述操作就可以到达此效果。
你可以先建立一个MemoryDC,然后把他填充为黑,然后把Alpha值设为128,然后混合到你要绘制的DC上(不一定是窗口DC哦,记得我们前面将的双缓冲吗?
)就OK了。
读取JPEG,GIF文件
JPEG压缩算法综合的信号学和视觉心理学,而GIF格式,特别是支持动画的GIF89a格式为了节约容量也做了很多种非常变态的优化,所以要写一个完全支持这些标准格式的解码器相当困难,也没有必要。
假设你需要进展JPEG文件的读写我推荐你使用IntelJpegLib,速度相当令人满意。
而GIF由于授权问题,没有任何官方组织提供的读写代码。
假设你只是需要读入JPEG和静态GIF(或者只一帧的动态GIF),我推荐你使用Windows提供的OleLoadPicture函数,下面这段代码可以把一个JPG,GIF,BMP读入到Bitmap对象中:
BOOLCIJLBitmap:
:
Load(LPCTSTRlpszPathName)
{
BOOLbSuccess=FALSE;
//Freeupanyresourcewemaycurrentlyhave
DeleteObject();
//openthefile
CFilef;
if(!
f.Open(lpszPathName,CFile:
:
modeRead))
{
TRACE(_T("Failedtoopenfile%s,Error:
%x\n"),lpszPathName,:
:
GetLastError());
returnFALSE;
}
//getthefilesize
DWORDdwFileSize=f.GetLength();
//Allocatememorybasedonfilesize
LPVOIDpvData=NULL;
HGLOBALhGlobal=GlobalAlloc(GMEM_MOVE