Windows游戏动画技术
在2D游戏中,播放动画的方式一般有两种,第一种是直接播放影片文件(比如AVI、RMVB等等影片格式〉,这常常用在游戏的开头动画、过场动画与结局动画中。另一种则是在游戏中利用连续贴图的方式,来达到动画显示的效果。其实,游戏程序本身几乎都是以无限循环的方式来不断地在游戏窗口中进行窗口画面重绘的操作,即使画面没有任何变化,这个重绘的操作依然在不断地进行,直到游戏程序结束运行为止。
本章我们将介绍制作游戏动画的两种技术。
他们分别是利用Windows本身的定时器来驱动连续动画的“定时器动画显示”技术,以及目前游戏程序中普遍使用的“游制循环”技术。
1.1 定时器动画显示
定时器是Windows 程序设计中比较重要的一块知识点, Windows程序设计相关教程往往都需要专门设章节花篇幅重点讲解定时器相关知识。而我们这本书的重点在于Windows游戏编程,完全没有全方位展开讲解定时器的必要。所以,这一节里只是给大家总结了一个通俗易用的“定时器使用三步曲”,让大家非常容易就能掌握游戏编程中定时器的使用方法。好,下面就开始吧。
在Windows 中,定时器是一种输入设备,它周期性地在每经过一个指定的时间间隔就通知应用程序一次。我们写的程序把时间间隔告诉Windows,比如说“每过10 秒钟通知我一声” ,然后Windows就会每过10秒钟周期性地给我们的程序发出WM_TIMER消息,表示时间到了。这种特性恰恰比较合适播放一系列连续静态的图片,产生出动画效果。
下面我们就来介绍如何建立与使用定时器。建立与使用定时器一般分为三步,我们把他们合称为“定时器使用三步曲”
- 第一步,创建定时器。
- 第二步,编写WM_TIMER消息响应代码.
- 第三步,删除定时器。
我们兵分三路,各个击破这三步。
1.1.1 创建定时器
WindowsAPI函数SetTimer()可为窗口建立一个定时器,并每隔一段时间就发出WM_TIMER消息。我们可以在MSDN 中查到这个函数的原型如下:
1. UINT\_PTR SetTimer(
2. \_\_in HWND hWnd, //接收定时器消息的窗口句柄
3. \_\_in UINT\_PTR nIDEvent, //定时器代号
4. \_\_in UINT uElapse, // 时间间隔
5. \_\_in TIMERPROC lpTimerFunc // 处理相应函数
6. );
- 第二个参数,UINT_PTR nIDEvent ,定时器的代号,这个代号在同一个窗口中必须是唯一的,且值不为0 。
- 第四个参数, TIMERPROC 类型的lpTimerFunc,用于指定一个由系统调用处理WM_TIMER消息的响应函数,如果我们不想通过指定响应函数来处理WM_TIMER 消息的话,这个参数就设为NULL 。
比如,想创建一个每隔0.1 秒发出WM_TIMER消息且不设定响应函数的定时器,那么代码就是这样写:
1. 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()函数中的部分代码:
1. //-----------------------------------【WndProc( )函数】--------------------------------------
2. // 描述:窗口过程函数WndProc,对窗口消息进行处理
3. //------------------------------------------------------------------------------------------------
4. LRESULT CALLBACK WndProc( HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam )
5. {
6. switch ( message ) //switch语句开始
7. {
8. case WM\_TIMER: //定时器消息
9. Game\_Paint(hwnd); //调用Game\_Paint()函数进行窗口绘图
10. break ;
//跳出该switch语句
11.
12. case WM\_KEYDOWN: // 若是键盘按下消息
13. if (wParam == VK\_ESCAPE) // 如果被按下的键是ESC
14. DestroyWindow(hwnd); // 销毁窗口, 并发送一条WM\_DESTROY消息
15. break ;
//跳出该switch语句
16.
17. case WM\_DESTROY: //若是窗口销毁消息
18. Game\_CleanUp(hwnd); //调用自定义的资源清理函数Game\_CleanUp()进行退出前的资源清理
19. PostQuitMessage( 0 ); //向系统表明有个线程有终止请求。用来响应WM\_DESTROY消息
20. break ;
//跳出该switch语句
21.
22. default :
//若上述case条件都不符合,则执行该default语句
23. return
DefWindowProc( hwnd, message, wParam, lParam );
//调用缺省的窗口过程
24. }
25.
26. return 0; //正常退出
27. }
我们在WM_TIMER消息后调用了自定义的绘图函数Game_Paint(),这样程序在每次收到WM_TIMER消息后,就会调用一次Game_Paint()对画面进行重新绘制,从而达到了动画显示的效果。
1.1.3 删除定时器
定时器建立后,就会一直自动地按照定义设定的时间间隔来发出WM_TIMER消息,如果要停用某个定时器,必须调用专门用于删除定时器的API函数——KillTimer。我们可以在MSDN 中查到这个函数的定义如下:
1. BOOL KillTimer(
2. \_\_in HWND hWnd,
3. \_\_in UINT\_PTR uIDEvent ///定时器代号
4. );
进阶理解一一定时器消息不是异步的!~
因为定时器使用硬件定时器中断,有时会让人产生误解,认为程序会异步地被中断来处理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消息的数目来计时。
1.1.4 示例程序GDldemo6 {style=“margin: 0px; padding: 0px;”}
这个程序中我们使用了定时器,来显示连续的静态图片,产生出动画效果。
素材方面, 是在网络上收集的一张gif 动图, 然后在Photoshop中一帧一帧导出为bmp 图片的,如下:
接下来我们看下代码。
程序代码片段一,全局变量声明:
1. //-----------------------------------【全局变量声明部分】-------------------------------------
2. // 描述:全局变量的声明
3. //------------------------------------------------------------------------------------------------
4. HDC g\_hdc=NULL,g\_mdc=NULL;
//全局设备环境句柄与全局内存DC句柄
5. HBITMAP g\_hSprite[12]; //声明位图数组用来储存各张人物位图
6. int g\_iNum=0;
//"g\_iNum"变量用来记录目前显示的图号
程序代码片段二,窗口过程函数WndProc():
1. //-----------------------------------【WndProc( )函数】--------------------------------------
2. // 描述:窗口过程函数WndProc,对窗口消息进行处理
3. //------------------------------------------------------------------------------------------------
4. LRESULT CALLBACK WndProc( HWND hwnd, UINT message,
WPARAM wParam, LPARAM lParam )
5. {
6. switch ( message ) //switch语句开始
7. {
8. case WM\_TIMER: //定时器消息
9. Game\_Paint(hwnd); //调用Game\_Paint()函数进行窗口绘图
10. break ;
//跳出该switch语句
11.
12. case WM\_KEYDOWN: // 若是键盘按下消息
13. if (wParam == VK\_ESCAPE) // 如果被按下的键是ESC
14. DestroyWindow(hwnd); // 销毁窗口, 并发送一条WM\_DESTROY消息
15. break ;
//跳出该switch语句
16.
17. case WM\_DESTROY: //若是窗口销毁消息
18. Game\_CleanUp(hwnd); //调用自定义的资源清理函数Game\_CleanUp()进行退出前的资源清理
19. PostQuitMessage( 0 ); //向系统表明有个线程有终止请求。用来响应WM\_DESTROY消息
20. break ;
//跳出该switch语句
21.
22. default :
//若上述case条件都不符合,则执行该default语句
23. return
DefWindowProc( hwnd, message, wParam, lParam );
//调用缺省的窗口过程
24. }
25.
26. return 0; //正常退出
27. }
需要注意的是,这里我们由于有了WM_TIMER来驱动绘制函数Game_Paint()的调用,所以就可以不用在窗口过程函数中
对WM_PAINT 消息进行响应了。
程序代码片段三, Game_lnit()函数:
1. //-----------------------------------【Game\_Init( )函数】--------------------------------------
2. // 描述:初始化函数,进行一些简单的初始化
3. //------------------------------------------------------------------------------------------------
4. BOOL Game\_Init( HWND hwnd )
5. {
6. g\_hdc = GetDC(hwnd); //获取设备环境句柄
7.
8. wchar\_t filename[20];
9. //载入各个萝莉位图
10. for (int i=0;i\<12;i )
11. {
12. memset(filename, 0, sizeof (filename)); //filename的初始化
13. swprintf\_s(filename,L"%d.bmp"
,i); //调用swprintf\_s函数,“组装”出对应的图片文件名称
14. g\_hSprite[i] = (HBITMAP
)LoadImage(NULL,filename,IMAGE\_BITMAP,WINDOW\_WIDTH,WINDOW\_HEIGHT,LR\_LOADFROMFILE);
15. }
16. //-----【位图绘制四步曲之二:建立兼容DC】-----
17. g\_mdc = CreateCompatibleDC(g\_hdc); //建立兼容设备环境的内存DC
18.
19. g\_iNum = 0; //设置初始的显示图号为"0"
20. SetTimer(hwnd,1,90,NULL); //建立定时器,间隔0.09秒发出消息
21.
22. Game\_Paint(hwnd);
23. return TRUE;
24. }
这里出现了两个新的函数memset 和swprintf_s,我们不妨来讲一下:
1. void \*memset(
2. void \*dest,
3. int c,
4. size\_t count
5. );
6. wchar\_t \*wmemset(
7. wchar\_t \*dest,
8. wchar\_t c,
9. size\_t count
10. );
wmemset 和memset就是参数类型和返回值不一样罢了,大体用法还是一致的,我们就看memset怎么用就可以了。
memset 函数就是将第一个参数dest 的前count〈即第三个参数)个字节用第二个参数c替换,并返回第一个参数被替换后的值。
看一下我们在这里用到memset 函数的这句代码:
1. 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语言中都用烂了,它也就是把格式化的数据写到一个流中。看一下我们这里用到这几句代码:
1. for( int i=0;i\<12;i )
2.
3. memset(filename, 0, sizeof (filename)); //filename的初始化
4. swprintf\_s(filename,L"%d.bmp"
,i); //调用swprintf\_s函数,“组装”出对应的图片文件名称
5. 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()函数:
1. //-----------------------------------【Game\_Paint( )函数】--------------------------------------
2. // 描述:绘制函数,在此函数中进行绘制操作
3. //--------------------------------------------------------------------------------------------------
4. VOID Game\_Paint( HWND hwnd )
5. {
6. //处理图号
7. if (g\_iNum == 11)
//判断是否超过最大图号,若超过最大图号“12”,则将显示图号重设为"0"。
8. g\_iNum = 0;
9.
10. //依据图号来贴图
11. SelectObject(g\_mdc,g\_hSprite[g\_iNum]);//根据图号选入对应的位图
12. BitBlt(g\_hdc,0,0,WINDOW\_WIDTH,WINDOW\_HEIGHT,g\_mdc,0,0,SRCCOPY); //以目前图号进行窗口贴图
13.
14. //图号自增
15. g\_iNum
; //将“g\_iNum”值加1,为下一次要显示的图号
16. }
由于Game_ CleanUp()函数需要调用一下KillTimer删除掉定时器,所以这回粉墨登场了。
程序代码片段五, Game_CleanUp()函数:
1. //-----------------------------------【Game\_CleanUp( )函数】--------------------------------
2. // 描述:资源清理函数,在此函数中进行程序退出前资源的清理工作
3. //---------------------------------------------------------------------------------------------------
4. BOOL Game\_CleanUp( HWND hwnd )
5. {
6. KillTimer(hwnd,1); //删除所建立的定时器
7. //释放资源对象
8. for (int i=0;i\<12;i )
9. DeleteObject(g\_hSprite[i]);
10. DeleteDC(g\_mdc);
11. ReleaseDC(hwnd,g\_hdc); //释放设备环境
12. return TRUE;
13. }
KillTimer(hwnd, 1)表示删除了hwnd 窗口中编号为1 的定时器。
1.2 游戏循环动画显示
本节我们讲的“游戏循环”技术,是目前Windows游戏中普遍采用的动画显示技术。
上一节中我们讲解了用定时器来产生动画的效果。定时器的使用固然简单方便,但是事实上这样的方法仅适合用在显示简易的动画以及小型的游戏程序中。因为一般而言,游戏本身需要显示顺畅的游戏画面,使玩家感觉不到延迟的状态。基本游戏画面必须在一秒钟之内更新至少25次以上,这一秒钟内程序还必须进行消息的处理和大量数学运算甚至音效的输出等操作。而使用定时器的消息来驱动这些操作,往往是达不到所要求的标准, 差强人意的。
这里我们要提出一种“游戏循环”的概念,”游戏循环“是将我们之前程序中的消息循环加以修改,方法是判断其中的内容目前是否有要处理的消息,如果有则进行处理,否则按照设定的时间间隔来重绘画面。采用“游戏循环”的游戏程序的执行效率足以秒杀采用定时器的游戏程序,所以目前市面上的大型游戏程序基本上都是利用游戏循环来驱动游戏画面的更新的。
下面就是一段对普通消息循环修改而成的“游戏循环”的核心代码,理解了这段代码,
就理解了“游戏循环”的理念。
1. //【5】消息循环过程
2. MSG msg = { 0 }; //定义并初始化msg
3. while
( msg.message != WM\_QUIT ) //使用while循环,如果消息不是WM\_QUIT消息,就继续循环
4. {
5. if
( PeekMessage( &msg, 0, 0, 0, PM\_REMOVE ) ) //查看应用程序消息队列,有消息时将队列中的消息派发出去。
6. {
7. TranslateMessage( &msg ); //将虚拟键消息转换为字符消息
8. DispatchMessage( &msg ); //分发一个消息给窗口程序。
9. }
10. else
11. {
12. g\_tNow = GetTickCount(); //获取当前系统时间
13. if
(g\_tNow-g\_tPre \>= 100) //当此次循环运行与上次绘图时间相差0.1秒时再进行重绘操作
14. Game\_Paint(hwnd);
15. }
16. }
我们调用的GetTickCount()函数会取得系统开始运行到目前所经过的时间,单位是毫秒(milliseconds ),也就是千分之一秒。
由于循环的运行速度远比定时器发出时间信号来得快,因此使用游戏循环可以更精准地控制程序运行速度并提高每秒钟画面重绘的次数。目前市面上的游戏,基本上都是采用的游戏循环的显示方式。
了解了游戏循环使用的基本概念之后,接下来依旧是通过一个程序实例示例程序GDIdemo7,来将本节知识融会贯通:\
这个示例程序中,我们以游戏循环的方法驱动Game_Paint函数,进行窗口的连续贴图,更精确地制作游戏动画效果。
使用的素材如下,非常酷的动漫人物舞刀的连续动画:
接下来看看程序代码。
程序代码片段一, 全局变量声明:
1. //-----------------------------------【全局变量声明部分】-------------------------------------
2. // 描述:全局变量的声明
3. //------------------------------------------------------------------------------------------------
4. HDC g\_hdc=NULL,g\_mdc=NULL;
//全局设备环境句柄与全局内存DC句柄
5. HBITMAP g\_hSprite[12];
//声明位图数组用来储存各张人物位图
6. DWORD g\_tPre=0,g\_tNow=0;
//声明l两个变量来记录时间,g\_tPre记录上一次绘图的时间,g\_tNow记录此次准备绘图的时间
7. int g\_iNum=0;
//g\_iNum变量用来记录目前显示的图号
程序代码片段二, Game_Init()函数:
1. //-----------------------------------【Game\_Init( )函数】--------------------------------------
2. // 描述:初始化函数,进行一些简单的初始化
3. //------------------------------------------------------------------------------------------------
4. BOOL Game\_Init( HWND hwnd )
5. {
6. g\_hdc = GetDC(hwnd); //获取设备环境句柄
7.
8. wchar\_t filename[20];
9.
10. //载入各个人物位图
11. for (int i=0;i\<12;i )
12. {
13. memset(filename, 0, sizeof (filename)); //filename的初始化
14. swprintf\_s(filename,L"%d.bmp"
,i); //调用swprintf\_s函数,“组装”出对应的图片文件名称
15. g\_hSprite[i] = (HBITMAP
)LoadImage(NULL,filename,IMAGE\_BITMAP,800,600,LR\_LOADFROMFILE);
16. }
17.
18. //-----【位图绘制四步曲之二:建立兼容DC】-----
19. g\_mdc = CreateCompatibleDC(g\_hdc); //建立兼容设备环境的内存DC
20.
21. Game\_Paint(hwnd);
22. return TRUE;
23. }
程序代码片段三, Game\_Paint() 函数:
1. //-----------------------------------【Game\_Paint( )函数】--------------------------------------
2. // 描述:绘制函数,在此函数中进行绘制操作
3. //--------------------------------------------------------------------------------------------------
4. VOID Game\_Paint( HWND hwnd )
5. {
6. if
(g\_iNum == 11) //判断是否超过最大图号,若超过最大图号“10”,则将显示图号重设为"0"。
7. g\_iNum = 0;
8.
9. SelectObject(g\_mdc,g\_hSprite[g\_iNum]);
10. BitBlt(g\_hdc,0,0,800,600,g\_mdc,0,0,SRCCOPY); //以目前图号进行窗口贴图
11.
12. g\_tPre = GetTickCount(); //记录此次绘图时间,供下次游戏循环中判断是否已经达到画面更新操作设定的时间间隔。
13. g\_iNum
; //将“g\_iNum”值加1,为下一次要显示的图号
14.
15. }
我们可以发现,定时器与我们这节讲的游戏循环其实差不多,就是想办法来找到一个不断调用绘制函数Game_Paint()的方式而已。
1.3 透明动画
透明动画是GDI游戏开发中经常会用到的基本技巧,它通过图案的连续显示及图案本身背景的透明化处理,在背景图上产生出栩栩如生的动画效果。
素材选择方面,我们使用的是一张国产单机游戏第一品牌《仙剑奇侠传》中第一代仙剑的主角“李逍遥”的侧面行走图:
我们知道,透明动画制作的前提是,必须在一个暂存的内存DC上完成每一张跑动图的透明,然后再贴到窗口上,这样画面更新时才不会出现在透明贴图过程中产生的闪烁现象。
实现这个程序的关键点,当然是绘制函数的写法。在我们的程序中,绘制函数为自定义的Game_Paint()函数。而在Game_Paint()函数里面我们主要实现了两个功能:
- 李逍遥行走图案的透明背景化。
- 更新贴图的坐标,实现动画效果。
下面我们先给出Game_ Paint()函数的写法:
首先,是Game_Paint()函数中多次使用的宏定义,这样的对窗口和宽度定义的宏从某种意义上减少了硬编码:
1. //-----------------------------------【宏定义部分】--------------------------------------------
2. // 描述:定义一些辅助宏
3. //------------------------------------------------------------------------------------------------
4. \#define WINDOW\_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度
5. \#define WINDOW\_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度
然后接着就是Game_ Paint() 函数的写法:
1. //-----------------------------------【Game\_Paint( )函数】--------------------------------------
2. // 描述:绘制函数,在此函数中进行绘制操作
3. //--------------------------------------------------------------------------------------------------
4. VOID Game\_Paint( HWND hwnd )
5. {
6. if (g\_iNum == 8)
//判断是否超过最大图号,若超过最大图号“7”,则将显示图号重设为"0"。
7. g\_iNum = 0;
8.
9. //在mdc中贴上背景图
10. SelectObject(g\_bufdc,g\_hBackGround);
11. BitBlt(g\_mdc,0,0,WINDOW\_WIDTH,WINDOW\_HEIGHT,g\_bufdc,0,0,SRCCOPY);
12.
13. //在mdc中进行透明处理
14. SelectObject(g\_bufdc,g\_hSprite);
15. TransparentBlt(g\_mdc,g\_iX,g\_iY,60,108,g\_bufdc,g\_iNum\*60,0,60,108,RGB(255,0,0));//采用TransparentBlt透明色彩法
16.
17.
18. //将最后的画面显示在窗口中
19. BitBlt(g\_hdc,0,0,WINDOW\_WIDTH,WINDOW\_HEIGHT,g\_mdc,0,0,SRCCOPY);
20.
21. g\_tPre = GetTickCount(); //记录此次绘图时间,供下次游戏循环中判断是否已经达到画面更新操作设定的时间间隔。
22. g\_iNum
; //将“g\_iNum”值加1,为下一次要显示的图号
23.
24. g\_iX =10; //计算下次贴图的坐标
25. //如果贴图坐标超出窗口边缘,将其坐标置为-60
26. if (g\_iX\>=WINDOW\_WIDTH)
27. g\_iX =-60;
28.
29. }
在这个示例程序中,我们用游戏循环配合透明色彩法, 来实现透明动画。
程序代码片段一, 全局变量声明:
1. //-----------------------------------【全局变量声明部分】-------------------------------------
2. // 描述:全局变量的声明
3. //------------------------------------------------------------------------------------------------
4. HDC g\_hdc=NULL,g\_mdc=NULL,g\_bufdc=NULL;
//全局设备环境句柄,两个全局内存DC句柄
5. HBITMAP
g\_hSprite=NULL,g\_hBackGround=NULL;
//定义两个位图句柄,一个用于存储人物图,一个用于存储背景图
6. DWORD g\_tPre=0,g\_tNow=0;
//声明l两个函数来记录时间,g\_tPre记录上一次绘图的时间,g\_tNow记录此次准备绘图的时间
7. int g\_iNum=0,g\_iX=0,g\_iY=0;
//g\_iNum用来记录图号,g\_iX,g\_iY分别记录贴图的横纵坐标
程序代码片段二, Game_lnit()函数:
1. //-----------------------------------【Game\_Init( )函数】--------------------------------------
2. // 描述:初始化函数,进行一些简单的初始化
3. //------------------------------------------------------------------------------------------------
4. BOOL Game\_Init( HWND hwnd )
5. {
6. HBITMAP bmp;
7.
8. g\_hdc = GetDC(hwnd);
9. g\_mdc = CreateCompatibleDC(g\_hdc); //创建一个和hdc兼容的内存DC
10. g\_bufdc = CreateCompatibleDC(g\_hdc);//再创建一个和hdc兼容的缓冲DC
11. bmp = CreateCompatibleBitmap(g\_hdc,WINDOW\_WIDTH,WINDOW\_HEIGHT); //建一个和窗口兼容的空的位图对象
12.
13. SelectObject(g\_mdc,bmp);//将空位图对象放到mdc中
14. g\_hSprite = (HBITMAP )LoadImage(NULL,L"goright.bmp"
,IMAGE\_BITMAP,480,108,LR\_LOADFROMFILE);
15. g\_hBackGround= (HBITMAP )LoadImage(NULL,L"bg.bmp"
,IMAGE\_BITMAP,WINDOW\_WIDTH,WINDOW\_HEIGHT,LR\_LOADFROMFILE);
16.
17. g\_iNum = 0;
18. g\_iX =0; //贴图起始X坐标
19. g\_iY= 350; //贴图起始Y坐标
20.
21. Game\_Paint(hwnd);
22. return TRUE;
23. }
第8-10 行代码精析:三个设备环境的初始化,为三缓冲动画绘图做准备。
第11-13 行代码精析: 因为后面我们是在g_mdc上进行透明处理的,所以需要建一个空的位图对象把他放到g_mdc 中。
1.4 排序贴图
“排序贴图”是源自于物体远近呈现的一种贴图概念。
为了避免这种因为贴图顺序固定而产生的错误画面,必须在每一次窗口重新显示时动态地重新决定画面上每一个物体的贴图顺序。
假设现在有10 只要进行贴图的小牛图案,先把它存在一个数组之中,从2D平面的远近角度来看,Y 轴坐标比较小的是比较远的物体。如果我们以小牛的Y轴坐标(要排序的值被我们称作键值)来对小牛数组由小到大进行排序,最后会使得Y轴坐标小的小牛排在数组的前面,而进行画面贴图时则由数组由小到大一个个进行处理,这样便可实现“远的物体先贴图“的目的了。
这里我们使用C++教程中不少运用的气泡排序( Bubble Sort)作为我们的排序法。
下面我们贴出以C/C 写出的气泡排序法的代码,对“pop[ ]”数组的各数据成员的Y值为键值来排序,输出的参数为“n”表示要排序的数组大小。
1. //-----------------------------------【BubSort( )函数】--------------------------------------
2. // 描述:进行气泡法排序
3. //------------------------------------------------------------------------------------------------
4. VOID BubSort(int n)
5. {
6. int i,j;
7. bool f;
8. Sprites tmp;
9.
10. for (i=0;i\<n-1;i )
11. {
12. f = false ;
13. for (j=0;j\<n-i-1;j )
14. {
15. if (Sprite[j 1].y \< Sprite[j].y)
16. {
17. tmp = Sprite[j 1];
18. Sprite[j 1] = Sprite[j];
19. Sprite[j] = tmp;
20. f = true ;
21. }
22. }
23. if (!f)
24. break ;
25. }
26. }
这个示例比较有趣,会产生多只小精灵随机跑动,每次进行画面贴图前先完成排序操作,呈现出比较顺畅真实的动画来。
先看一下人物素材图:
然后我们来看程序代码:
程序代码片段一,宏、全局结构体和全局变量声明:
1. //-----------------------------------【宏定义部分】--------------------------------------------
2. // 描述:定义一些辅助宏
3. //------------------------------------------------------------------------------------------------
4. \#define WINDOW\_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度
5. \#define WINDOW\_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度
6. \#define SPRITE\_NUMBER 30 //定义宏SPRITE\_NUMBER,表示画面上要出现的人物数目,在此设定为30个
7. //-----------------------------------【全局结构体定义部分】-------------------------------------
8. // 描述:全局结构体定义
9. //------------------------------------------------------------------------------------------------
10. struct
Sprites //定义sprite结构,代表画面上的人物,其结构成员x和y为贴图坐标,direction为目前人物的移动方向
11. {
12. int x,y; //x和y为贴图坐标
13. int direction; // direction为目前人物的移动方向
14. };
15.
16. //-----------------------------------【全局变量声明部分】-------------------------------------
17. // 描述:全局变量的声明
18. //------------------------------------------------------------------------------------------------
19. HDC g\_hdc=NULL,g\_mdc=NULL,g\_bufdc=NULL;
//全局设备环境句柄,两个全局内存DC句柄
20. HBITMAP
g\_hSprite[4],g\_hBackGround;
//声明位图数组用来储存各张人物位图
21. DWORD g\_tPre=0,g\_tNow=0;
//声明l两个函数来记录时间,g\_tPre记录上一次绘图的时间,g\_tNow记录此次准备绘图的时间
22. int g\_iPicNum=0,g\_iX=0,g\_iY=0;
//g\_iPicNum用来记录图号,g\_iX,g\_iY分别记录贴图的横纵坐标
23. Sprites Sprite[SPRITE\_NUMBER]; //按照SPRITE\_NUMBER的值建立数组Sprite[]
程序代码片段二, Game_lnit() 函数:
1. //-----------------------------------【Game\_Init( )函数】--------------------------------------
2. // 描述:初始化函数,进行一些简单的初始化
3. //------------------------------------------------------------------------------------------------
4. BOOL Game\_Init( HWND hwnd )
5. {
6. srand((unsigned)time(NULL)); //用系统时间初始化随机种子
7. HBITMAP bmp;
8.
9. g\_hdc = GetDC(hwnd);
10. g\_mdc = CreateCompatibleDC(g\_hdc); //创建一个和hdc兼容的内存DC
11. g\_bufdc = CreateCompatibleDC(g\_hdc);//再创建一个和hdc兼容的缓冲DC
12. bmp = CreateCompatibleBitmap(g\_hdc,WINDOW\_WIDTH,WINDOW\_HEIGHT); //建一个和窗口兼容的空的位图对象
13.
14. SelectObject(g\_mdc,bmp);//将空位图对象放到mdc中
15.
16. //加载各张跑动图及背景图
17. g\_hBackGround= (HBITMAP )LoadImage(NULL,L"bg.bmp"
,IMAGE\_BITMAP,WINDOW\_WIDTH,WINDOW\_HEIGHT,LR\_LOADFROMFILE);
18. g\_hSprite[0] = (HBITMAP )LoadImage(NULL,L"11.bmp"
,IMAGE\_BITMAP,384,96,LR\_LOADFROMFILE);
19. g\_hSprite[1] = (HBITMAP )LoadImage(NULL,L"22.bmp"
,IMAGE\_BITMAP,384,96,LR\_LOADFROMFILE);
20. g\_hSprite[2] = (HBITMAP )LoadImage(NULL,L"33.bmp"
,IMAGE\_BITMAP,384,96,LR\_LOADFROMFILE);
21. g\_hSprite[3] = (HBITMAP )LoadImage(NULL,L"44.bmp"
,IMAGE\_BITMAP,384,96,LR\_LOADFROMFILE);
22.
23.
24. //设定初始的贴图坐标都为窗口内的任意坐标,初始的移动方向都为向左。
25. for (int i=0;i\<SPRITE\_NUMBER;i )
26. {
27. Sprite[i].direction = 3; //起始方向
28. Sprite[i].x = rand()%WINDOW\_WIDTH; //贴图的起始X坐标
29. Sprite[i].y = rand()%WINDOW\_HEIGHT; //贴图的起始Y坐标
30. }
31.
32. Game\_Paint(hwnd);
33. return TRUE;
34. }
程序代码片段三, Game_paint()函数:
1. //-----------------------------------【Game\_Paint( )函数】--------------------------------------
2. // 描述:绘制函数,在此函数中进行绘制操作
3. //--------------------------------------------------------------------------------------------------
4. VOID Game\_Paint( HWND hwnd )
5. {
6. if
(g\_iPicNum == 4) //判断是否超过最大图号,若超过最大图号“3”,则将显示图号重设为"0"。
7. g\_iPicNum = 0;
8.
9. //在mdc中贴上背景图
10. SelectObject(g\_bufdc,g\_hBackGround);
11. BitBlt(g\_mdc,0,0,WINDOW\_WIDTH,WINDOW\_HEIGHT,g\_bufdc,0,0,SRCCOPY);
12.
13. BubSort(SPRITE\_NUMBER); //贴上人物图之前调用BubSort()函数进行排序
14.
15. for (int i=0;i\<SPRITE\_NUMBER;i )
16. {
17. SelectObject(g\_bufdc,g\_hSprite[Sprite[i].direction]);
18. TransparentBlt(g\_mdc,Sprite[i].x,Sprite[i].y,96,96,g\_bufdc,g\_iPicNum\*96,0,96,96,RGB(0,0,0));//采用TransparentBlt透明色彩法
19. }
20.
21. //将最后的画面显示在窗口中
22. BitBlt(g\_hdc,0,0,WINDOW\_WIDTH,WINDOW\_HEIGHT,g\_mdc,0,0,SRCCOPY);
23.
24.
25. g\_tPre = GetTickCount(); //记录此次绘图时间
26. g\_iPicNum ;
27.
28.
29. //下面这个for循环,决定每一只精灵下一次的移动方向及贴图坐标
30. for (int i=0;i\<SPRITE\_NUMBER;i )
31. {
32. switch
(rand()%4) //随机数除以4的余数来决定下次移动方向,余数0,1,2,3分别代表上,下,左,右
33. {
34. case 0: //上
35. Sprite[i].y -= 20;
36. //在计算出新的贴图坐标之后,还需判断此新的坐标会不会使得人物贴图超出窗口边界,若超出,则将该方向上的坐标设定为刚好等于临界值
37. if (Sprite[i].y \< 0)
38. Sprite[i].y = 0;
39. Sprite[i].direction = 0;
40. break ;
41. //其他方向按照和上面相同的方法计算
42. case 1: //下
43. Sprite[i].y = 20;
44. if (Sprite[i].y \> WINDOW\_HEIGHT-100)
45. Sprite[i].y = WINDOW\_HEIGHT-100;
46. Sprite[i].direction = 1;
47. break ;
48. case 2: //左
49. Sprite[i].x-= 20;
50. if (Sprite[i].x \< 0)
51. Sprite[i].x = 0;
52. Sprite[i].direction = 2;
53. break ;
54. case 3: //右
55. Sprite[i].x = 20;
56.
57. if (Sprite[i].x \>WINDOW\_WIDTH-100)
58. Sprite[i].x = WINDOW\_WIDTH-100;
59. Sprite[i].direction = 3;
60. break ;
61. }
62. }
63. }
接下来看一下程序运行截图:
1.5 章节小憩
我们在本章中重点学了一些游戏动画技术。动画显示技术一般有两种:定时器和游戏循环。我们现在己经基本上都使用游戏循环来显示动画和制作游戏了。透明动画相当于动画显示技术和透明贴图的一个结合。而排序贴图使我们做出来的游戏更加逼真。