第6章 光与影的交汇——Windows游戏动画技术

在2D 游戏中,播放动画的方式一般有两种,第一种是直接播放影片文件(比如AVI、RMVB 等等影片格式〉,这常常用在游戏的开头动画、过场动画与结局动画中。另一种则是在游戏中利用连续贴图的方式,来达到动画显示的效果。其实,游戏程序本身几乎都是以无限循环的方式来不断地在游戏窗口中进行窗口画面重绘的操作,即使画面没有任何变化,这个重绘的操作依然在不断地进行,直到游戏程序结束运行为止。
本章我们将介绍制作游戏动画的两种技术。

他们分别是利用Windows 本身的定时器来驱动连续动画的“定时器动画显示”技术,以及目前游戏程序中普遍使用的“游制循环”技术。

6.1 定时器动画显示

定时器是Windows 程序设计中比较重要的一块知识点, Windows 程序设计相关教程往往都需要专门设章节花篇幅重点讲解定时器相关知识。而我们这本书的重点在于Windows 游戏编程, 完全没有全方位展开讲解定时器的必要。所以,这一节里只是给大家总结了一个通俗易用的“定时器使用三步曲”,让大家非常容易就能掌握游戏编程中定时器的使用方法。好,下面就开始吧。
在Windows 中, 定时器是一种输入设备,它周期性地在每经过一个指定的时间间隔就通知应用程序一次。我们写的程序把时间间隔告诉Windows ,比如说“每过10 秒钟通知我一声” ,然后Windows 就会每过10秒钟周期性地给我们的程序发出WM_TIMER 消息,表示时间到了。这种特性恰恰比较合适播放一系列连续静态的图片,产生出动画效果。

下面我们就来介绍如何建立与使用定时器。建立与使用定时器一般分为三步,我们把他们合称为“定时器使用三步曲” :

  • 第一步,创建定时器。
  • 第二步,编写WM_TIMER消息响应代码.
  • 第三步,删除定时器。
我们兵分三路,各个击破这三步。

6.1.1 创建定时器

WindowsAPI 函数SetTimer()可为窗口建立一个定时器,并每隔一段时间就发出WM_TIMER 消息。我们可以在MSDN 中查到这个函数的原型如下:
UINT_PTR SetTimer(
  __in  HWND hWnd,           //接收定时器消息的窗口句柄
  __in  UINT_PTR nIDEvent,   //定时器代号
  __in  UINT uElapse,        // 时间间隔
  __in  TIMERPROC lpTimerFunc  // 处理相应函数
);
  • 第二个参数,UINT_PTR nIDEvent ,定时器的代号,这个代号在同一个窗口中必须是唯一的,且值不为0 。
  • 第四个参数, TIMERPROC 类型的lpTimerFunc ,用于指定一个由系统调用处理WM_TIMER消息的响应函数,如果我们不想通过指定响应函数来处理WM_TIMER 消息的话,这个参数就设为NULL 。
比如,想创建一个每隔0.1 秒发出WM_TIMER 消息且不设定响应函数的定时器,那么代码就是这样写:
SetTimer(hwnd,1,100,NULL);   //建立定时器,间隔0.1秒发出消息

6.1.2 WM_TIMER消息响应

响应定时器发出的WM_TIMER消息有两种方式,第一种是通过SetTimer 函数的第四个参数,指定出响应函数。第二种反射就是在消息处理函数中,通过对WM_TIMER消息添加相应响应代码。
通常我们会采取第二种方式,在消息处理函数中对WM_TIMER 消息添加相应响应代码。
首先,我们需要在调用SetTimer 函数时,把第四个参数取为NULL 。
SetTimer(hwnd, 1, 100 , NULL) ;
然后,在消息响应函数中添加相应的响应函数,下面是消息处理函数WndProc() 函数中的部分代码:
//-----------------------------------【WndProc( )函数】--------------------------------------
//	描述:窗口过程函数WndProc,对窗口消息进行处理
//------------------------------------------------------------------------------------------------
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )      
{
	switch( message )						//switch语句开始
	{
		case WM_TIMER:						//定时器消息
			Game_Paint(hwnd);                //调用Game_Paint()函数进行窗口绘图
			break;									//跳出该switch语句

		case WM_KEYDOWN:					// 若是键盘按下消息
			if (wParam == VK_ESCAPE)    // 如果被按下的键是ESC
				DestroyWindow(hwnd);		// 销毁窗口, 并发送一条WM_DESTROY消息
			break;									//跳出该switch语句

		case WM_DESTROY:					//若是窗口销毁消息
			Game_CleanUp(hwnd);			//调用自定义的资源清理函数Game_CleanUp()进行退出前的资源清理
			PostQuitMessage( 0 );			//向系统表明有个线程有终止请求。用来响应WM_DESTROY消息
			break;									//跳出该switch语句

		default:										//若上述case条件都不符合,则执行该default语句
			return DefWindowProc( hwnd, message, wParam, lParam );		//调用缺省的窗口过程
	}

	return 0;									//正常退出
}

我们在WM_TIMER 消息后调用了自定义的绘图函数Game_Paint(),这样程序在每次收到WM_TIMER 消息后,就会调用一次Game_Paint()对画面进行重新绘制,从而达到了动画显示的效果。

6.1.3 删除定时器

定时器建立后,就会一直自动地按照定义设定的时间间隔来发出WM_TIMER 消息,如果要停用某个定时器,必须调用专门用于删除定时器的API 函数——KillTimer。我们可以在MSDN 中查到这个函数的定义如下:

BOOL KillTimer(
  __in  HWND hWnd,
  __in  UINT_PTR uIDEvent ///定时器代号
);
进阶理解一一定时器消息不是异步的!~
因为定时器使用硬件定时器中断,有时会让人产生误解,认为程序会异步地被中断来处理WM_TIMER 消息。然而, WM_TIMER 消息并不是异步的。WM_TIMER 消息放在正常的消息队列之中,和其他消息排列在一起,因此,如果在SetTimer 呼叫中指定间隔为1000 毫秒,那么不能保证程序每1000 毫秒或者989 毫秒就会收到一个WM_TIMER 消息。如果其他程序的执行事件超过一秒,在此期间内,我们的程序将收不到任何WM_TIMER 消息。
事实上, Windows 对WM_TIMER消息的处理非常类似于对WM_PAINT 消息的处理,这两个消息都是低优先级的,程序只有在消息队列中没有其他消息时才接收它们。

WM_TIMER 还在另一方面和WM_PAINT 相似: Windows 不能持续向消息队列中放入多个WM_TIMER 消息,而是将多余的

WM_TIMER 消息组合成一个消息。因此,应用程序不会一次收到多个这样的消息,尽管可能在短时间内得到两个WM_TlMER 消息。应用程序不能确定这种处理方式所导致的WM_TIMER 消息遗漏的数目。

这样, WM_TIMER消息仅仅在需要更新时才提示程序,程序本身不能经由统计WM_TIMER消息的数目来计时。

6.1.4 示例程序GDldemo6

这个程序中我们使用了定时器,来显示连续的静态图片,产生出动画效果。
素材方面, 是在网络上收集的一张gif 动图, 然后在Photoshop 中一帧一帧导出为bmp 图片的,如下:


接下来我们看下代码。
程序代码片段一,全局变量声明:
//-----------------------------------【全局变量声明部分】-------------------------------------
//	描述:全局变量的声明
//------------------------------------------------------------------------------------------------
HDC				g_hdc=NULL,g_mdc=NULL;       //全局设备环境句柄与全局内存DC句柄
HBITMAP		g_hSprite[12];     //声明位图数组用来储存各张人物位图
int					g_iNum=0;         //"g_iNum"变量用来记录目前显示的图号

程序代码片段二,窗口过程函数WndProc():
//-----------------------------------【WndProc( )函数】--------------------------------------
//	描述:窗口过程函数WndProc,对窗口消息进行处理
//------------------------------------------------------------------------------------------------
LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam )      
{
	switch( message )						//switch语句开始
	{
		case WM_TIMER:						//定时器消息
			Game_Paint(hwnd);                //调用Game_Paint()函数进行窗口绘图
			break;									//跳出该switch语句

		case WM_KEYDOWN:					// 若是键盘按下消息
			if (wParam == VK_ESCAPE)    // 如果被按下的键是ESC
				DestroyWindow(hwnd);		// 销毁窗口, 并发送一条WM_DESTROY消息
			break;									//跳出该switch语句

		case WM_DESTROY:					//若是窗口销毁消息
			Game_CleanUp(hwnd);			//调用自定义的资源清理函数Game_CleanUp()进行退出前的资源清理
			PostQuitMessage( 0 );			//向系统表明有个线程有终止请求。用来响应WM_DESTROY消息
			break;									//跳出该switch语句

		default:										//若上述case条件都不符合,则执行该default语句
			return DefWindowProc( hwnd, message, wParam, lParam );		//调用缺省的窗口过程
	}

	return 0;									//正常退出
}

需要注意的是,这里我们由于有了WM_TIMER 来驱动绘制函数Game_Paint()的调用,所以就可以不用在窗口过程函数中
对WM_PAINT 消息进行响应了。
程序代码片段三, Game_lnit()函数:
//-----------------------------------【Game_Init( )函数】--------------------------------------
//	描述:初始化函数,进行一些简单的初始化
//------------------------------------------------------------------------------------------------
BOOL Game_Init( HWND hwnd )
{
	g_hdc = GetDC(hwnd);  //获取设备环境句柄
	
	wchar_t   filename[20];
	//载入各个萝莉位图
	for(int i=0;i<12;i++)      
	{
		memset(filename, 0, sizeof(filename));  //filename的初始化
		swprintf_s(filename,L"%d.bmp",i);  //调用swprintf_s函数,“组装”出对应的图片文件名称
		g_hSprite[i] = (HBITMAP)LoadImage(NULL,filename,IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);
	}
	//-----【位图绘制四步曲之二:建立兼容DC】-----
	g_mdc = CreateCompatibleDC(g_hdc);    //建立兼容设备环境的内存DC

	g_iNum = 0;                     //设置初始的显示图号为"0"
	SetTimer(hwnd,1,90,NULL);   //建立定时器,间隔0.09秒发出消息

	Game_Paint(hwnd);
	return TRUE;
}

这里出现了两个新的函数memset 和swprintf_s,我们不妨来讲一下:
void *memset(
   void *dest,
   int c,
   size_t count 
);
wchar_t *wmemset(
   wchar_t *dest,
   wchar_t c,
   size_t count
);

wmemset 和memset 就是参数类型和返回值不一样罢了,大体用法还是一致的,我们就看memset 怎么用就可以了。
memset 函数就是将第一个参数dest 的前count 〈即第三个参数)个字节用第二个参数c 替换,并返回第一个参数被替换后的值。
看一下我们在这里用到memset 函数的这句代码:
memset(filename, 0, sizeof(filename));  //filename的初始化
它就是把filename 数组的全部字节用0替换掉,也就是对filename进行初始化。

然后swprintf_s 函数,这个我们后面经常会用到,需要重点了解一下。首先swprintf_s 是swprintf函数的安全增强版本,其实他们用法什么的完全一致,只是加了_s 后缀的那个为安全版本的函数,这样VS2010 不会报警告。因为VS2010 编译器有新的标准,用swprintf 的话经常会报警告出来,所以我们用的时候习惯加上_s 后缀,成为swprintf 的安全增强版本swprintf_s 。
然后swprintf 又是sprintf 的宽字节版本。Sprintf 大家看起来应该都很亲切,我们在C 语言中都用烂了,它也就是把格式化的数据写到一个流中。看一下我们这里用到这几句代码:
        for(int i=0;i<12;i++)      
	{
		memset(filename, 0, sizeof(filename));  //filename的初始化
		swprintf_s(filename,L"%d.bmp",i);  //调用swprintf_s函数,“组装”出对应的图片文件名称
		g_hSprite[i] = (HBITMAP)LoadImage(NULL,filename,IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);
	}

就是在一个for 循环中,把0~11 的数字按10 进制格式化到“ %d.bmp ” 字符串中,得到1.bmp 、2.bmp 、3.bmp 之类的文件名,然后存到filename 中,接着就调用Loadlmage 函数把文件加载到位图句柄中。
其中的%d 就是代表10 进制转换的转换字符.
最后需要注意的一个点是使用swprintf 系列字符格式化函数要包含tchar.h 头文件。
另外需要注意的是,Game_lnit 函数中需要设置一个定时器,我们设置的是每隔0.09 秒发出消息。
我们接下来看看如何绘制,即Game_Paint()函数的实现代码。
程序代码片段四, Game_Paint()函数:
//-----------------------------------【Game_Paint( )函数】--------------------------------------
//	描述:绘制函数,在此函数中进行绘制操作
//--------------------------------------------------------------------------------------------------
VOID Game_Paint( HWND hwnd )
{
	//处理图号
	if(g_iNum == 11)               //判断是否超过最大图号,若超过最大图号“12”,则将显示图号重设为"0"。
		g_iNum = 0;               

	//依据图号来贴图
	SelectObject(g_mdc,g_hSprite[g_iNum]);//根据图号选入对应的位图
	BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY);         //以目前图号进行窗口贴图

	//图号自增
	g_iNum++;                    //将“g_iNum”值加1,为下一次要显示的图号
}

由于Game_ CleanUp()函数需要调用一下KillTimer 删除掉定时器,所以这回粉墨登场了。
程序代码片段五, Game_CleanUp()函数:
//-----------------------------------【Game_CleanUp( )函数】--------------------------------
//	描述:资源清理函数,在此函数中进行程序退出前资源的清理工作
//---------------------------------------------------------------------------------------------------
BOOL Game_CleanUp( HWND hwnd )
{
	KillTimer(hwnd,1);   //删除所建立的定时器  
	//释放资源对象
	for(int i=0;i<12;i++)
		DeleteObject(g_hSprite[i]);
	DeleteDC(g_mdc);
	ReleaseDC(hwnd,g_hdc);  //释放设备环境
	return TRUE;
}
KillTimer(hwnd, 1)表示删除了hwnd 窗口中编号为1 的定时器。

6.2 游戏循环动画显示

本节我们讲的“游戏循环”技术,是目前Windows游戏中普遍采用的动画显示技术。
上一节中我们讲解了用定时器来产生动画的效果。定时器的使用固然简单方便,但是事实上这样的方法仅适合用在显示简易的动画以及小型的游戏程序中。因为一般而言,游戏本身需要显示顺畅的游戏画面,使玩家感觉不到延迟的状态。基本游戏画面必须在一秒钟之内更新至少25 次以上,这一秒钟内程序还必须进行消息的处理和大量数学运算甚至音效的输出等操作。而使用定时器的消
息来驱动这些操作,往往是达不到所要求的标准, 差强人意的。
这里我们要提出一种“游戏循环”的概念,”游戏循环“是将我们之前程序中的消息循环加以修改,方法是判断其中的内容目前是否有要处理的消息,如果有则进行处理,否则按照设定的时间间隔来重绘画面。采用“游戏循环”的游戏程序的执行效率足以秒杀采用定时器的游戏程序,所以目前市面上的大型游戏程序基本上都是利用游戏循环来驱动游戏画面的更新的。

下面就是一段对普通消息循环修改而成的“游戏循环”的核心代码,理解了这段代码, 就理解了“游戏循环”的理念。

//【5】消息循环过程
	MSG msg = { 0 };				//定义并初始化msg
	while( msg.message != WM_QUIT )		//使用while循环,如果消息不是WM_QUIT消息,就继续循环
	{
		if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) )   //查看应用程序消息队列,有消息时将队列中的消息派发出去。
		{
			TranslateMessage( &msg );		//将虚拟键消息转换为字符消息
			DispatchMessage( &msg );			//分发一个消息给窗口程序。
		}
		else
		{
			g_tNow = GetTickCount();   //获取当前系统时间
			if(g_tNow-g_tPre >= 100)        //当此次循环运行与上次绘图时间相差0.1秒时再进行重绘操作
				Game_Paint(hwnd);
		}
	}

我们调用的GetTickCount()函数会取得系统开始运行到目前所经过的时间,单位是毫秒( milliseconds ),也就是千分之一秒。

由于循环的运行速度远比定时器发出时间信号来得快,因此使用游戏循环可以更精准地控制程序运行速度并提高每秒钟画面重绘的次数。目前市面上的游戏, 基本上都是采用的游戏循环的显示方式。
了解了游戏循环使用的基本概念之后,接下来依旧是通过一个程序实例示例程序GDIdemo7,来将本节知识融会贯通:
这个示例程序中,我们以游戏循环的方法驱动Game_Paint 函数,进行窗口的连续贴图,更精确地制作游戏动画效果。
使用的素材如下,非常酷的动漫人物舞刀的连续动画:

接下来看看程序代码。
程序代码片段一, 全局变量声明:

//-----------------------------------【全局变量声明部分】-------------------------------------
//	描述:全局变量的声明
//------------------------------------------------------------------------------------------------
HDC				g_hdc=NULL,g_mdc=NULL;     //全局设备环境句柄与全局内存DC句柄
HBITMAP		g_hSprite[12];						//声明位图数组用来储存各张人物位图
DWORD		g_tPre=0,g_tNow=0;          //声明l两个变量来记录时间,g_tPre记录上一次绘图的时间,g_tNow记录此次准备绘图的时间
int					g_iNum=0;					//g_iNum变量用来记录目前显示的图号

程序代码片段二, Game_Init()函数:

//-----------------------------------【Game_Init( )函数】--------------------------------------
//	描述:初始化函数,进行一些简单的初始化
//------------------------------------------------------------------------------------------------
BOOL Game_Init( HWND hwnd )
{
	g_hdc = GetDC(hwnd);  //获取设备环境句柄

	wchar_t   filename[20];

	//载入各个人物位图
	for(int i=0;i<12;i++)      
	{
		memset(filename, 0, sizeof(filename));  //filename的初始化
		swprintf_s(filename,L"%d.bmp",i);  //调用swprintf_s函数,“组装”出对应的图片文件名称
		g_hSprite[i] = (HBITMAP)LoadImage(NULL,filename,IMAGE_BITMAP,800,600,LR_LOADFROMFILE);
	}

	//-----【位图绘制四步曲之二:建立兼容DC】-----
	g_mdc = CreateCompatibleDC(g_hdc);    //建立兼容设备环境的内存DC

	Game_Paint(hwnd);
	return TRUE;
}
程序代码片段三, Game_Paint() 函数:

//-----------------------------------【Game_Paint( )函数】--------------------------------------
//	描述:绘制函数,在此函数中进行绘制操作
//--------------------------------------------------------------------------------------------------
VOID Game_Paint( HWND hwnd )
{
	if(g_iNum == 11)               //判断是否超过最大图号,若超过最大图号“10”,则将显示图号重设为"0"。
		g_iNum = 0;        

	SelectObject(g_mdc,g_hSprite[g_iNum]);
	BitBlt(g_hdc,0,0,800,600,g_mdc,0,0,SRCCOPY);         //以目前图号进行窗口贴图

	g_tPre = GetTickCount();     //记录此次绘图时间,供下次游戏循环中判断是否已经达到画面更新操作设定的时间间隔。
	g_iNum++;                    //将“g_iNum”值加1,为下一次要显示的图号

}

我们可以发现,定时器与我们这节讲的游戏循环其实差不多,就是想办法来找到一个不断调用绘制函数Game_Paint()的方式而已。

6.3 透明动画

透明动画是GDI 游戏开发中经常会用到的基本技巧,它通过图案的连续显示及图案本身背景的透明化处理,在背景图上产生出栩栩如生的动画效果。

素材选择方面,我们使用的是一张国产单机游戏第一品牌《仙剑奇侠传》中第一代仙剑的主角“李逍遥”的侧面行走图:


我们知道,透明动画制作的前提是,必须在一个暂存的内存DC 上完成每一张跑动图的透明,然后再贴到窗口上,这样画面更新时才不会出现在透明贴图过程中产生的闪烁现象。

实现这个程序的关键点, 当然是绘制函数的写法。在我们的程序中,绘制函数为自定义的Game_Paint() 函数。而在Game_Paint()函数里面我们主要实现了两个功能:

  • 李逍遥行走图案的透明背景化。
  • 更新贴图的坐标,实现动画效果。
下面我们先给出Game_ Paint()函数的写法:

首先,是Game_ Paint()函数中多次使用的宏定义,这样的对窗口和宽度定义的宏从某种意义上减少了硬编码:

//-----------------------------------【宏定义部分】--------------------------------------------
//	描述:定义一些辅助宏
//------------------------------------------------------------------------------------------------
#define WINDOW_WIDTH	800							//为窗口宽度定义的宏,以方便在此处修改窗口宽度
#define WINDOW_HEIGHT	600							//为窗口高度定义的宏,以方便在此处修改窗口高度

然后接着就是Game_ Paint() 函数的写法:

//-----------------------------------【Game_Paint( )函数】--------------------------------------
//	描述:绘制函数,在此函数中进行绘制操作
//--------------------------------------------------------------------------------------------------
VOID Game_Paint( HWND hwnd )
{
	if(g_iNum == 8)               //判断是否超过最大图号,若超过最大图号“7”,则将显示图号重设为"0"。
		g_iNum = 0;               

	//在mdc中贴上背景图
	SelectObject(g_bufdc,g_hBackGround);
	BitBlt(g_mdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_bufdc,0,0,SRCCOPY);

	//在mdc中进行透明处理
	SelectObject(g_bufdc,g_hSprite);
	TransparentBlt(g_mdc,g_iX,g_iY,60,108,g_bufdc,g_iNum*60,0,60,108,RGB(255,0,0));//采用TransparentBlt透明色彩法


	//将最后的画面显示在窗口中
	BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY);

	g_tPre = GetTickCount();     //记录此次绘图时间,供下次游戏循环中判断是否已经达到画面更新操作设定的时间间隔。
	g_iNum++;                    //将“g_iNum”值加1,为下一次要显示的图号

	g_iX+=10;					   //计算下次贴图的坐标
	//如果贴图坐标超出窗口边缘,将其坐标置为-60
	if(g_iX>=WINDOW_WIDTH)      
		g_iX =-60;

}

在这个示例程序中,我们用游戏循环配合透明色彩法, 来实现透明动画。
程序代码片段一, 全局变量声明:

//-----------------------------------【全局变量声明部分】-------------------------------------
//	描述:全局变量的声明
//------------------------------------------------------------------------------------------------
HDC				g_hdc=NULL,g_mdc=NULL,g_bufdc=NULL;      //全局设备环境句柄,两个全局内存DC句柄
HBITMAP		g_hSprite=NULL,g_hBackGround=NULL;								//定义两个位图句柄,一个用于存储人物图,一个用于存储背景图
DWORD		g_tPre=0,g_tNow=0;					//声明l两个函数来记录时间,g_tPre记录上一次绘图的时间,g_tNow记录此次准备绘图的时间
int					g_iNum=0,g_iX=0,g_iY=0;                //g_iNum用来记录图号,g_iX,g_iY分别记录贴图的横纵坐标

程序代码片段二, Game_lnit()函数:

//-----------------------------------【Game_Init( )函数】--------------------------------------
//	描述:初始化函数,进行一些简单的初始化
//------------------------------------------------------------------------------------------------
BOOL Game_Init( HWND hwnd )
{
	HBITMAP bmp;

	g_hdc = GetDC(hwnd);  
	g_mdc = CreateCompatibleDC(g_hdc);  //创建一个和hdc兼容的内存DC
	g_bufdc = CreateCompatibleDC(g_hdc);//再创建一个和hdc兼容的缓冲DC
	bmp = CreateCompatibleBitmap(g_hdc,WINDOW_WIDTH,WINDOW_HEIGHT); //建一个和窗口兼容的空的位图对象

	SelectObject(g_mdc,bmp);//将空位图对象放到mdc中
	g_hSprite = (HBITMAP)LoadImage(NULL,L"goright.bmp",IMAGE_BITMAP,480,108,LR_LOADFROMFILE);
	g_hBackGround= (HBITMAP)LoadImage(NULL,L"bg.bmp",IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);

	g_iNum = 0;
	g_iX =0;	//贴图起始X坐标
	g_iY= 350;    //贴图起始Y坐标

	Game_Paint(hwnd);
	return TRUE;
}

第8-10 行代码精析:三个设备环境的初始化,为三缓冲动画绘图做准备。
第11-13 行代码精析: 因为后面我们是在g_mdc 上进行透明处理的,所以需要建一个空的位图对象把他放到g_mdc 中。

6.4 排序贴图

“排序贴图”是源自于物体远近呈现的一种贴图概念。
为了避免这种因为贴图顺序固定而产生的错误画面,必须在每一次窗口重新显示时动态地重新决定画面上每一个物体的贴图顺序。
假设现在有10 只要进行贴图的小牛图案,先把它存在一个数组之中,从2D 平面的远近角度来看,Y 轴坐标比较小的是比较远的物体。如果我们以小牛的Y 轴坐标(要排序的值被我们称作键值)来对小牛数组由小到大进行排序,最后会使得Y 轴坐标小的小牛排在数组的前面,而进行画面贴图时则由数组由小到大一个个进行处理,这样便可实现“远的物体先贴图“的目的了。
这里我们使用C++教程中不少运用的气泡排序( Bubble Sort )作为我们的排序法。

下面我们贴出以C/C++写出的气泡排序法的代码,对“pop[ ]” 数组的各数据成员的Y值为键值来排序,输出的参数为“n” 表示要排序的数组大小。

//-----------------------------------【BubSort( )函数】--------------------------------------
//	描述:进行气泡法排序
//------------------------------------------------------------------------------------------------
VOID		BubSort(int n)
{
	int i,j;
	bool f;
	Sprites tmp;

	for(i=0;i<n-1;i++)
	{
		f = false;
		for(j=0;j<n-i-1;j++)
		{
			if(Sprite[j+1].y < Sprite[j].y)
			{
				tmp = Sprite[j+1];
				Sprite[j+1] = Sprite[j];
				Sprite[j] = tmp;
				f = true;
			}
		}
		if(!f)
			break;
	}
}

这个示例比较有趣, 会产生多只小精灵随机跑动,每次进行画面贴图前先完成排序操作, 呈现出比较顺畅真实的动画来。
先看一下人物素材图:


然后我们来看程序代码:
程序代码片段一,宏、全局结构体和全局变量声明:


//-----------------------------------【宏定义部分】--------------------------------------------
//	描述:定义一些辅助宏
//------------------------------------------------------------------------------------------------
#define WINDOW_WIDTH	800							//为窗口宽度定义的宏,以方便在此处修改窗口宽度
#define WINDOW_HEIGHT	600							//为窗口高度定义的宏,以方便在此处修改窗口高度
#define SPRITE_NUMBER 30  //定义宏SPRITE_NUMBER,表示画面上要出现的人物数目,在此设定为30个
//-----------------------------------【全局结构体定义部分】-------------------------------------
//	描述:全局结构体定义
//------------------------------------------------------------------------------------------------
struct Sprites        //定义sprite结构,代表画面上的人物,其结构成员x和y为贴图坐标,direction为目前人物的移动方向
{
	int x,y;			//x和y为贴图坐标
	int direction; // direction为目前人物的移动方向
}; 

//-----------------------------------【全局变量声明部分】-------------------------------------
//	描述:全局变量的声明
//------------------------------------------------------------------------------------------------
HDC				g_hdc=NULL,g_mdc=NULL,g_bufdc=NULL;      //全局设备环境句柄,两个全局内存DC句柄
HBITMAP		g_hSprite[4],g_hBackGround;								//声明位图数组用来储存各张人物位图
DWORD		g_tPre=0,g_tNow=0;					//声明l两个函数来记录时间,g_tPre记录上一次绘图的时间,g_tNow记录此次准备绘图的时间
int					g_iPicNum=0,g_iX=0,g_iY=0;                //g_iPicNum用来记录图号,g_iX,g_iY分别记录贴图的横纵坐标			
Sprites			Sprite[SPRITE_NUMBER];   //按照SPRITE_NUMBER的值建立数组Sprite[]

程序代码片段二, Game_lnit() 函数:

//-----------------------------------【Game_Init( )函数】--------------------------------------
//	描述:初始化函数,进行一些简单的初始化
//------------------------------------------------------------------------------------------------
BOOL Game_Init( HWND hwnd )
{
	srand((unsigned)time(NULL));      //用系统时间初始化随机种子
	HBITMAP bmp;

	g_hdc = GetDC(hwnd);  
	g_mdc = CreateCompatibleDC(g_hdc);  //创建一个和hdc兼容的内存DC
	g_bufdc = CreateCompatibleDC(g_hdc);//再创建一个和hdc兼容的缓冲DC
	bmp = CreateCompatibleBitmap(g_hdc,WINDOW_WIDTH,WINDOW_HEIGHT); //建一个和窗口兼容的空的位图对象

	SelectObject(g_mdc,bmp);//将空位图对象放到mdc中

	//加载各张跑动图及背景图
	g_hBackGround= (HBITMAP)LoadImage(NULL,L"bg.bmp",IMAGE_BITMAP,WINDOW_WIDTH,WINDOW_HEIGHT,LR_LOADFROMFILE);
	g_hSprite[0] = (HBITMAP)LoadImage(NULL,L"11.bmp",IMAGE_BITMAP,384,96,LR_LOADFROMFILE);
	g_hSprite[1] = (HBITMAP)LoadImage(NULL,L"22.bmp",IMAGE_BITMAP,384,96,LR_LOADFROMFILE);
	g_hSprite[2] = (HBITMAP)LoadImage(NULL,L"33.bmp",IMAGE_BITMAP,384,96,LR_LOADFROMFILE);
	g_hSprite[3] = (HBITMAP)LoadImage(NULL,L"44.bmp",IMAGE_BITMAP,384,96,LR_LOADFROMFILE);


	//设定初始的贴图坐标都为窗口内的任意坐标,初始的移动方向都为向左。
	for(int i=0;i<SPRITE_NUMBER;i++)
	{
		Sprite[i].direction = 3;    //起始方向
		Sprite[i].x = rand()%WINDOW_WIDTH;	   //贴图的起始X坐标
		Sprite[i].y = rand()%WINDOW_HEIGHT;    //贴图的起始Y坐标
	}

	Game_Paint(hwnd);
	return TRUE;
}


程序代码片段三, Game_paint()函数:

//-----------------------------------【Game_Paint( )函数】--------------------------------------
//	描述:绘制函数,在此函数中进行绘制操作
//--------------------------------------------------------------------------------------------------
VOID Game_Paint( HWND hwnd )
{
	if(g_iPicNum == 4)		//判断是否超过最大图号,若超过最大图号“3”,则将显示图号重设为"0"。
		g_iPicNum = 0;

	//在mdc中贴上背景图
	SelectObject(g_bufdc,g_hBackGround);
	BitBlt(g_mdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_bufdc,0,0,SRCCOPY);

	BubSort(SPRITE_NUMBER);    //贴上人物图之前调用BubSort()函数进行排序

	for(int i=0;i<SPRITE_NUMBER;i++)
	{
		SelectObject(g_bufdc,g_hSprite[Sprite[i].direction]);
		TransparentBlt(g_mdc,Sprite[i].x,Sprite[i].y,96,96,g_bufdc,g_iPicNum*96,0,96,96,RGB(0,0,0));//采用TransparentBlt透明色彩法
	}

	//将最后的画面显示在窗口中
	BitBlt(g_hdc,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,g_mdc,0,0,SRCCOPY);


	g_tPre = GetTickCount();     //记录此次绘图时间
	g_iPicNum++;


	//下面这个for循环,决定每一只精灵下一次的移动方向及贴图坐标
	for(int i=0;i<SPRITE_NUMBER;i++)
	{
		switch(rand()%4)          //随机数除以4的余数来决定下次移动方向,余数0,1,2,3分别代表上,下,左,右
		{
		case 0:					     //上
			Sprite[i].y -= 20;
			//在计算出新的贴图坐标之后,还需判断此新的坐标会不会使得人物贴图超出窗口边界,若超出,则将该方向上的坐标设定为刚好等于临界值
			if(Sprite[i].y < 0)
				Sprite[i].y = 0;
			Sprite[i].direction = 0;
			break;
			//其他方向按照和上面相同的方法计算
		case 1:		//下
			Sprite[i].y += 20;
			if(Sprite[i].y > WINDOW_HEIGHT-100)
				Sprite[i].y = WINDOW_HEIGHT-100;
			Sprite[i].direction = 1;
			break;
		case 2:				    	//左
			Sprite[i].x-= 20;
			if(Sprite[i].x < 0)
				Sprite[i].x = 0;
			Sprite[i].direction = 2;
			break;
		case 3:				    	//右
			Sprite[i].x+= 20;

			if(Sprite[i].x >WINDOW_WIDTH-100)
				Sprite[i].x = WINDOW_WIDTH-100;
			Sprite[i].direction = 3;
			break;
		}
	}
}
接下来看一下程序运行截图:




6.5 章节小憩

我们在本章中重点学了一些游戏动画技术。动画显示技术一般有两种:定时器和游戏循环。我们现在己经基本上都使用游戏循环来显示动画和制作游戏了。透明动画相当于动画显示技术和透明贴图的一个结合。而排序贴图使我们做出来的游戏更加逼真。


  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值