《MFC游戏开发》笔记六 图像双缓冲技术:实现一个流畅的动画

来源:互联网 发布:网络办公室管理规定 编辑:程序博客网 时间:2024/05/24 05:46

本系列文章由七十一雾央编写,转载请注明出处。

 http://blog.csdn.net/u011371356/article/details/9334121

作者:七十一雾央 新浪微博:http://weibo.com/1689160943/profile?rightmod=1&wvr=5&mod=personinfo



       在前几节的笔记里,大家肯定会为一个问题感到心烦:画面怎么老是一闪一闪的啊,太难受了。确实是的,如果玩这样的游戏简直就是一种折磨。但是大家玩游戏的时候,从来没有遇到过这种情况吧?那么游戏开发者是怎么解决这个问题的呢?雾央在这一节笔记里给大家讲解一种简单通用的方法——图像双缓冲。

 

一、闪烁原因

       

       为了解决问题,我们得首先搞清楚闪烁的原因是什么,然后才能对症下药。能够导致游戏画面闪烁的原因非常多,但是对于我们做游戏开发的同学来说,最主要的就是一种:贴图贴的太频繁。

       如果大家是一个细心人的话,那么应该可以发现,在笔记三讲解贴图的时候,当我们贴出背景图的时候,是根本不会闪烁的,但是当我们贴出人物后,闪烁就出来了,而当我们移动人物的时候,闪烁的画面简直惨不忍睹啊。

       想弄清楚真正的原因就得要理解GDI绘图的原理:GDI绘图的时候是先绘制到显存里面,然后显存每隔一段时间就需要把里面的内容输出到屏幕上,这个时间就是刷新周期。在绘图的时候,系统会先用一种背景色擦除掉原来的图像,然后再绘制新的图像。如果这几次绘制不在同一个刷新周期中,那么我们看到的就是先看到背景色,再看到内容出来,就会有闪烁的感觉,而绘制的次数越多,看到这种现象的可能性就越大,就闪烁的越厉害。

 

二、图像双缓冲技术


       大家清楚了闪烁的原因后,再结合我们只贴出背景的时候并没有闪烁的事实,那么或许大家就可以想到一种解决方法了:我们事先将要画的所有东西画在一张图片上,然后将这张图直接贴出来,不就解决了吗?

       如果你想到这里,那么恭喜你,你已经想到了图像双缓冲技术。其实看起来很高端的这个名词其实非常简单。我们之前画图的时候都是直接画在窗口DC上,在之前我们可以自己先创建一个内存DC,然后把画图都画在内存DC中,最后再一次性的将内存DC输出到窗口DC中,就可以解决画面闪烁的问题了。

       下面我们讲述写代码的方法

       1.定义变量

       首先在CChildView.h中定义两个变量

[cpp] view plaincopy
  1. CDC m_cacheDC;   //缓冲DC  
  2. CBitmap m_cacheCBitmap;//缓冲位图  

       2.创建缓冲DC

       然后呢,在CChildView.cpp中OnPaint中创建缓冲DC

[cpp] view plaincopy
  1. //创建缓冲DC  
  2. m_cacheDC.CreateCompatibleDC(NULL);  
  3. m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height());  
  4. m_cacheDC.SelectObject(&m_cacheCBitmap);  

       3.在缓冲DC上绘图

       后面贴图都贴在缓冲DC上就可以了,如

[cpp] view plaincopy
  1. m_bg.Draw(m_cacheDC,m_client);  

      4.缓冲DC输出到窗口DC

       最后一次性的将缓冲DC中的内容输出到窗口DC中去,函数都是之前笔记二介绍过的,不熟悉的同学请阅读笔记二。 

[cpp] view plaincopy
  1. cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY);  

       此时OnPaint函数中的内容就如同下面这样

[cpp] view plaincopy
  1. void CChildView::OnPaint()   
  2. {  
  3.     //获取窗口DC指针  
  4.     CDC *cDC=this->GetDC();  
  5.     //获取窗口大小  
  6.     GetClientRect(&m_client);  
  7.     //创建缓冲DC  
  8.     m_cacheDC.CreateCompatibleDC(NULL);  
  9.     m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height());  
  10.     m_cacheDC.SelectObject(&m_cacheCBitmap);  
  11.       
  12.     //————————————————————开始绘制——————————————————————  
  13.     //贴背景,现在贴图就是贴在缓冲DC:m_cache中了  
  14.     m_bg.Draw(m_cacheDC,m_client);  
  15.     //贴英雄  
  16.     MyHero.hero.Draw(m_cacheDC,MyHero.x,MyHero.y,80,80,MyHero.frame*80,MyHero.direct*80,80,80);  
  17.     //最后将缓冲DC内容输出到窗口DC中  
  18.     cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY);  
  19.   
  20.     //————————————————————绘制结束—————————————————————  
  21.       
  22.     //在绘制完图后,使窗口区有效  
  23.     ValidateRect(&m_client);  
  24.     //释放缓冲DC  
  25.     m_cacheDC.DeleteDC();  
  26.     //释放对象  
  27.     m_cacheCBitmap.DeleteObject();  
  28.     //释放窗口DC  
  29.     ReleaseDC(cDC);  
  30. }  

 

三、实现一个真正意义上的动画demo


        雾央在这里实现的是一个骑着白马的少年在场景中闲逛的demo,按下WASD人物会向四个方向移动,移动的过程中动态更换图片。图片使用的是一张大图,然后每次去取其中一小块显示出来,当然大家也可以使用一张张分开好的图。具体的请看代码。

       先来几张截图看看效果,呵呵。





头文件

[cpp] view plaincopy
  1. // ChildView.h : CChildView 类的接口  
  2. //  
  3.   
  4.   
  5. #pragma once  
  6.   
  7.   
  8. // CChildView 窗口  
  9.   
  10. class CChildView : public CWnd  
  11. {  
  12. // 构造  
  13. public:  
  14.     CChildView();  
  15.   
  16. // 特性  
  17. public:  
  18.     struct shero  
  19.     {  
  20.         CImage hero;     //保存英雄的图像  
  21.         int x;             //保存英雄的位置  
  22.         int y;  
  23.         int direct;        //英雄的方向  
  24.         int frame;         //运动到第几张图片  
  25.     }MyHero;  
  26.   
  27.     CRect m_client;    //保存客户区大小  
  28.     CImage m_bg;      //背景图片  
  29.   
  30.     CDC m_cacheDC;   //缓冲DC  
  31.     CBitmap m_cacheCBitmap;//缓冲位图  
  32. // 操作  
  33. public:  
  34.   
  35. // 重写  
  36.     protected:  
  37.     virtual BOOL PreCreateWindow(CREATESTRUCT& cs);  
  38.   
  39. // 实现  
  40. public:  
  41.     virtual ~CChildView();  
  42.   
  43.     // 生成的消息映射函数  
  44. protected:  
  45.     afx_msg void OnPaint();  
  46.     DECLARE_MESSAGE_MAP()  
  47. public:  
  48.     afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);  
  49.     afx_msg void OnLButtonDown(UINT nFlags, CPoint point);  
  50.     afx_msg void OnTimer(UINT_PTR nIDEvent);  
  51.     afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);  
  52. };  

CPP文件

[cpp] view plaincopy
  1. // ChildView.cpp : CChildView 类的实现  
  2. //  
  3.   
  4. #include "stdafx.h"  
  5. #include "GameMFC.h"  
  6. #include "ChildView.h"  
  7.   
  8. #ifdef _DEBUG  
  9. #define new DEBUG_NEW  
  10. #endif  
  11.   
  12. //定时器的名称用宏比较清楚  
  13. #define TIMER_PAINT 1  
  14. #define TIMER_HEROMOVE 2  
  15. //四个方向  
  16. #define DOWN 0  
  17. #define LEFT 1  
  18. #define RIGHT 2  
  19. #define UP 3  
  20.   
  21. // CChildView  
  22.   
  23. CChildView::CChildView()  
  24. {  
  25. }  
  26.   
  27. CChildView::~CChildView()  
  28. {  
  29. }  
  30.   
  31.   
  32. BEGIN_MESSAGE_MAP(CChildView, CWnd)  
  33.     ON_WM_PAINT()  
  34.     ON_WM_KEYDOWN()  
  35.     ON_WM_LBUTTONDOWN()  
  36.     ON_WM_TIMER()  
  37.     ON_WM_CREATE()  
  38. END_MESSAGE_MAP()  
  39.   
  40.   
  41. //将png贴图透明  
  42. void TransparentPNG(CImage *png)  
  43. {  
  44.     for(int i = 0; i <png->GetWidth(); i++)  
  45.     {  
  46.         for(int j = 0; j <png->GetHeight(); j++)  
  47.         {  
  48.             unsigned char* pucColor = reinterpret_cast<unsigned char *>(png->GetPixelAddress(i , j));  
  49.             pucColor[0] = pucColor[0] * pucColor[3] / 255;  
  50.             pucColor[1] = pucColor[1] * pucColor[3] / 255;  
  51.             pucColor[2] = pucColor[2] * pucColor[3] / 255;  
  52.         }  
  53.     }  
  54. }  
  55.   
  56. // CChildView 消息处理程序  
  57.   
  58. BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs)   
  59. {  
  60.     if (!CWnd::PreCreateWindow(cs))  
  61.         return FALSE;  
  62.   
  63.     cs.dwExStyle |= WS_EX_CLIENTEDGE;  
  64.     cs.style &= ~WS_BORDER;  
  65.     cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS,   
  66.         ::LoadCursor(NULL, IDC_ARROW), reinterpret_cast<HBRUSH>(COLOR_WINDOW+1), NULL);  
  67.       
  68.     //-----------------------------------游戏数据初始化部分-------------------------  
  69.       
  70.     //加载背景  
  71.     m_bg.Load("bg.png");  
  72.     //加载英雄图片  
  73.     MyHero.hero.Load("heroMove.png");  
  74.     TransparentPNG(&MyHero.hero);  
  75.     //初始化英雄状态  
  76.     MyHero.direct=UP;  
  77.     MyHero.frame=0;  
  78.     //设置英雄初始位置  
  79.     MyHero.x=100;      
  80.     MyHero.y=400;  
  81.       
  82.     return TRUE;  
  83. }  
  84.   
  85. void CChildView::OnPaint()   
  86. {  
  87.     //获取窗口DC指针  
  88.     CDC *cDC=this->GetDC();  
  89.     //获取窗口大小  
  90.     GetClientRect(&m_client);  
  91.     //创建缓冲DC  
  92.     m_cacheDC.CreateCompatibleDC(NULL);  
  93.     m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height());  
  94.     m_cacheDC.SelectObject(&m_cacheCBitmap);  
  95.       
  96.     //————————————————————开始绘制——————————————————————  
  97.     //贴背景,现在贴图就是贴在缓冲DC:m_cache中了  
  98.     m_bg.Draw(m_cacheDC,m_client);  
  99.     //贴英雄  
  100.     MyHero.hero.Draw(m_cacheDC,MyHero.x,MyHero.y,80,80,MyHero.frame*80,MyHero.direct*80,80,80);  
  101.     //最后将缓冲DC内容输出到窗口DC中  
  102.     cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY);  
  103.   
  104.     //————————————————————绘制结束—————————————————————  
  105.       
  106.     //在绘制完图后,使窗口区有效  
  107.     ValidateRect(&m_client);  
  108.     //释放缓冲DC  
  109.     m_cacheDC.DeleteDC();  
  110.     //释放对象  
  111.     m_cacheCBitmap.DeleteObject();  
  112.     //释放窗口DC  
  113.     ReleaseDC(cDC);  
  114. }  
  115.   
  116. //按键响应函数  
  117. void CChildView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)  
  118. {  
  119.     //nChar表示按下的键值  
  120.     switch(nChar)  
  121.     {  
  122.     case 'd':         //游戏中按下的键当然应该不区分大小写了  
  123.     case 'D':  
  124.         MyHero.direct=RIGHT;  
  125.         MyHero.x+=5;  
  126.         break;  
  127.     case 'a':  
  128.     case 'A':  
  129.         MyHero.direct=LEFT;  
  130.         MyHero.x-=5;  
  131.         break;  
  132.     case 'w':  
  133.     case 'W':  
  134.         MyHero.direct=UP;  
  135.         MyHero.y-=5;  
  136.         break;  
  137.     case 's':  
  138.     case 'S':  
  139.         MyHero.direct=DOWN;  
  140.         MyHero.y+=5;  
  141.         break;  
  142.     }  
  143. }  
  144.   
  145. //鼠标左键单击响应函数  
  146. void CChildView::OnLButtonDown(UINT nFlags, CPoint point)  
  147. {  
  148.     char bufPos[50];  
  149.     sprintf(bufPos,"你单击了点X:%d,Y:%d",point.x,point.y);  
  150.     AfxMessageBox(bufPos);  
  151. }  
  152.   
  153. //定时器响应函数  
  154. void CChildView::OnTimer(UINT_PTR nIDEvent)  
  155. {  
  156.       
  157.     switch(nIDEvent)  
  158.     {  
  159.     case TIMER_PAINT:OnPaint();break;  //若是重绘定时器,就执行OnPaint函数  
  160.     case TIMER_HEROMOVE:               //控制人物移动的定时器  
  161.         {  
  162.             MyHero.frame++;              //每次到了间隔时间就将图片换为下一帧  
  163.             if(MyHero.frame==4)          //到最后了再重头开始  
  164.                 MyHero.frame=0;  
  165.         }  
  166.         break;  
  167.     }  
  168. }  
  169.   
  170.   
  171. int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)  
  172. {  
  173.     if (CWnd::OnCreate(lpCreateStruct) == -1)  
  174.         return -1;  
  175.   
  176.     // TODO:  在此添加您专用的创建代码  
  177.   
  178.     //创建一个10毫秒产生一次消息的定时器  
  179.     SetTimer(TIMER_PAINT,10,NULL);  
  180.     //创建人物行走动画定时器  
  181.     SetTimer(TIMER_HEROMOVE,100,NULL);  
  182.     return 0;  
  183. }  

       对于代码中一些地方,可能有一些同学有疑惑,雾央在这里作一下解释。

       首先是人物的移动动画,雾央使用了下面这张图作为素材

          

       这张图片是320x320,也就是说每个人物大小是80x80,第一行是人物向下移动,第二行是向左,第三行是向右,第四行是向下。

       我们在人物结构体中使用了一个变量direct记录人物的方向,并且宏定义了四个方向分别为0,1,2,3

       比如当前人物移动方向是右,即RIGHT,也就是2,那么我们就应该截取这张图片的第三行画出来,第三行y的起始坐标就是80*2,也就是80*direct

       我们还使用了一个变量frame记录当前方向上的帧数,即在x方向上的起始坐标,比如当前应该显示第二张图片,frame为1,那么就是80*1,即80*frame

       在Draw函数中最后四个参数的含义分别是源图片的x起始坐标,y起始坐标,宽度,高度,即画出源图片的从x,y开始的宽为width,高为height的部分。

现在大家应该清楚了这个过程吧?

         

       下面雾央再举一个例子来帮大家充分的理解这个过程。

       当前玩家按下了D键,人物要向右边行走,那么此时direct=RIGHT,RIGHT被我们宏定义为2

       开始frame=0;那么我们就画出原来图片中80*frame,80*direct开始高为80,宽为80的图片,即下图

                   

         在行走过程中frame++;接下来画的就是下面这个

          

         依次类推,当frame=4的时候,即画到最后一个的时候再从头开始,frame=0


        然后有同学提到在一直按着键的时候刚开始会感觉会停一下,雾央在这里说一下自己的想法。

        关于停一下,这个是由于第一次按下键后,人物移动很小,后来一直按着键,可以认为是以非常高的频率不断按键,人物移动的非常快,这之间一个对比就感觉停了一下,如果你把人物每次按键移动的距离增大很多,比如到50,就会觉得这种感觉小了很多。事实上,这样按键后人物突然增加一段位移是非常不科学的,用在游戏中也是非常不合适的,在后面的讲解中我会讲解一种新的人物移动方式,会很流畅,敬请关注。
       

         如果大家还有疑问,欢迎留言


      由于这一节笔记我觉得对于新手来说可能内容较多,所以我把源代码上传了,大家可以下载回去自己试试,本着分享的精神,当然是0积分下载了。

《MFC游戏开发》笔记六 源代码下载

 

       《MFC游戏开发》笔记六到这里就结束了,更多精彩请关注下一篇。如果您觉得文章对您有帮助的话,请留下您的评论,点个赞,能看到你们的留言是我最高兴的事情,因为这让我知道我正在帮助曾和我一样迷茫的少年,你们的支持就是我继续写下去的动力,愿我们一起学习,共同努力,复兴国产游戏。

        对于文章的疏漏或错误,欢迎大家的指出。

原创粉丝点击