真正的牛人不是要把什么都做好,而是想做好什么,就能做好什么。
接着上一节来实现FlappyBird。
我们来创建一个CGame类,把鸟类和柱子类都放进去,当然,一个场景中有很多柱子,我们用一个CPtrArray类来组织它,CPtrArray类是一个动态数组,也可以理解为链表,关于这部分内容有什么不了解的欢迎在下面和我讨论,之后我也会更新出一个关于数据结构的基础教程,请大家关注。在CGame类中再加入一些其它的必要属性,类就写好了:
#include "Bird.h"
#include "Column.h"
class CGame
{
public:
CGame();
virtual ~CGame();
private:
CBird m_bird;
CPtrArray m_columns;
CRect m_gameRect;
public:
void draw(CDC *pDC);
void move();
void onKey();
};
CGame::CGame()
{
m_gameRect = CRect(100,100,600,400);
m_bird.setPos(m_gameRect.left + m_gameRect.Width()/2 - 24, m_gameRect.top + m_gameRect.Height()/2 - 24);
}
void CGame::draw(CDC *pDC)
{
CPen *oldPen;
CPen brownPen(PS_SOLID,20,RGB(149,64,0));
CBrush *oldBrush;
CBrush blueBrush(RGB(168,202,215));
oldPen = pDC->SelectObject(&brownPen);
oldBrush = pDC->SelectObject(&blueBrush);
pDC->Rectangle(m_gameRect);
pDC->SelectObject(oldBrush);
pDC->SelectObject(oldPen);
m_bird.draw(pDC);
}
这里,构造函数中让小鸟的位置严格地在矩形中间,这里,位图宽度的一半24,我硬编码在代码里面了,这是非常不好的现象,这里可以获取图片信息的,但是再扩展的话这节的内容就太多了,暂时留个残缺吧。我们把矩形的画笔和画刷都变了,然后,我们在View类中去掉关于CBird和CColumn的东西,加上CGame的东西,运行试试?
但是这时候,我们发现屏幕闪得厉害,这是为什么呢?这是因为我们50ms就重绘一次窗口,每次重绘就把原来的擦除,然后再按照draw函数的流程来绘制,这一切都太快,所以我们看到了闪烁。如果你用的是XP或者一些版本的WIN7,你会发现它的记事本在拖动(改变大小)的时候也会有强烈的闪烁,这也是因为拖动会导致窗口重绘,和这里是一样的。既然微软的软件都有这个问题,那么是否就意味着我们的程序就这样呢?不,这样的程序给用户造成的感觉太不好了,我们要采用双缓冲技术来解决它。什么是双缓冲技术呢,简单说就是我们先把要画的东西画在一个DC里面,然后再用贴图的方式显示出来,而不是像这样直接在窗口里绘图。打个比方,现在的程序就好像你起床,然后在客人面前穿内裤,穿内衣,穿袜子……这个穿衣的过程都展示在客人面前,而双缓冲就好比你先穿好衣服,然后再给客人开门,客人直接就看到一个衣冠整齐的你。说了这么多,下面来看看代码实现吧:
void CGame::draw(CDC *pDC)
{
CRect clientRect;
clientRect = CGlobalParams::getInstance()->getClientRect();
CDC bufferDC;
bufferDC.CreateCompatibleDC(pDC);
CBitmap bufferBitmap;
bufferBitmap.CreateCompatibleBitmap(pDC,clientRect.Width(),clientRect.Height());
bufferDC.SelectObject(&bufferBitmap);
bufferDC.Rectangle(clientRect);
CPen *oldPen;
CPen brownPen(PS_SOLID,20,RGB(149,64,0));
CBrush *oldBrush;
CBrush blueBrush(RGB(168,202,215));
oldPen = bufferDC.SelectObject(&brownPen);
oldBrush = bufferDC.SelectObject(&blueBrush);
bufferDC.Rectangle(m_gameRect);
bufferDC.SelectObject(oldBrush);
bufferDC.SelectObject(oldPen);
m_bird.draw(&bufferDC);
pDC->BitBlt(0,0,clientRect.Width(),clientRect.Height(),&bufferDC,0,0,SRCCOPY);
}
在这里,我们定义了一个CDC bufferDC;,它就是缓冲DC,我们先把要画的东西在这个DC中画好,然后再用BitBlt贴到pDC中。但是即使这个bufferDC我们用了CreateCompatibleDC,还是无法直接绘图,因为它这时候还是一个空壳,回想一下我们贴图片的时候,是不是选了一张位图进去,这里也需要选一张位图进去,才能进行绘图,但是这里没有现成的位图,我们选一个多大的位图进去呢,很显然我们要把一个窗口大小的位图选进去,因为我们这个图是给pDC的。这里涉及到一个问题,就是怎么获得窗口大小,在CGame类中是没有GetClientRect方法的,我们可以在View类中获得这个窗口大小Rect,然后把它传进来,但是可能程序的其他很多地方都需要这个窗口大小,难道每次都用参数传进去么,这样将产生大量的冗余。也行有人会想到,可以把它放到一个全局变量里面,这样所有的类都能用到它,是的,这是一个好办法,但是在面向对象的程序中,全局变量在哪里定义呢?其实这里的一个较好的解决方案是放在一个单例类中,什么是单例类,这里涉及到一些设计模式的知识,关于这部分知识,我今后会更新出一份基础教程讨论,请大家关注,这里我就不展开了,有兴趣的同学可以研究我的源码。总之我们用单例类CGlobalParams得到了窗口大小,其实程序其他的一些全局变量都可以放到CGlobalParams这个单例类中。
之后,我们定义了一个位图,调用了CreateCompatibleBitmap指定位图的大小,并且让它与pDC兼容,啊哈,这里又涉及到兼容的问题了,之前我们是使用LoadBitmap从资源中加载的位图,所以没有兼容问题,LoadBitmap这个函数给我们做好了。这里我们自己定义的位图,故需要考虑兼容问题,道理还是一样的啦。然后我们把这张位图选进去,而位图创建之后是全黑的,我们为了让它有白色背景,调用了bufferDC.Rectangle(clientRect);,用系统默认的画笔和画刷先绘制一个窗口大小的矩形。然后就是我们自己要绘制的东西了,画好之后贴到pDC上面。
双缓冲终于完成了,我们来测试一下结果。!!!怎么还是这么闪,难道前面的功夫都白费了吗?
不要着急,当我们调用Invalidate的时候,MFC绘图的时候还会做一件事情,就是用背景色擦除窗口,MFC每次都擦一遍,所以我们还是看到了白色的闪烁,怎么让MFC不要擦除背景呢?我们可以在调用Invalidate的时候传递FALSE参数,这就是告诉MFC,不要擦除背景了,没必要!
还有一种方法就是找到WM_ERASEBKGND消息,为其添加响应函数,让它不要调用父类方法了,直接返回FALSE,也是同样的效果,而且这种方式更彻底一些。
再运行下试试看吧,是不是完全没有闪烁了呢?这效果才是高大上啊!
下面我们为CGame类添加按键响应函数和时间响应函数:
首先添加时间响应函数,也就是move函数,在这里面,要实现:鸟类的下降,柱子的移动,GameOver的检测等等功能,其中GameOver包括小鸟的位置跑到边框外,或者小鸟与柱子相撞,为了让外界知道是否GameOver,我们还需要添加一个函数:
void CGame::IsGameOver()
{
return m_bIsGameOver;
}
其中,m_bIsGameOver是一个初始为false的布尔变量。
我们先让move函数添加一点功能:
void CGame::move()
{
m_bird.drop();
if(m_columns.GetSize() == 0 || ((CColumn*)m_columns.GetAt(m_columns.GetUpperBound()))->getUpRect().left < m_gameRect.right - gap)
{
CColumn* pColumn = new CColumn();
pColumn->setHeight(m_gameRect.Height());
pColumn->setLength(rand() % 10 + 60);
pColumn->setStart(50 + rand() % (m_gameRect.Height() - 100 - pColumn->getLength()));
pColumn->setPos(m_gameRect.right,m_gameRect.top);
m_columns.Add(pColumn);
}
for(int i=0;imove();
}
}
首先小鸟下降:m_bird.drop();,然后我们添加柱子,什么时候添加呢?当没有柱子的时候要添加,也就是第一根柱子,还有就是当最后一根柱子移动到较远时,要添加一根新的柱子,这里m_columns.GetSize()返回数组大小,也就是数组中元素的个数,而这句话((CColumn*)m_columns.GetAt(m_columns.GetUpperBound()))->getUpRect().left,我要重点解释下:首先m_columns.GetUpperBound()返回数组中最大索引(从0开始),也就是数组中有3个元素的话,它返回2。m_columns.GetAt(m_columns.GetUpperBound())就是取得数组的最后一个元素咯,((CColumn*)m_columns.GetAt(m_columns.GetUpperBound()))我们知道数组中是CColumn*元素,所以我们强转一下,然后我们就可以调用CColumn里面的方法啦!如果你不会STL,CPtrArray还是很实用的,大家要掌握这种方法哦,这里的getUpRect是返回柱子上面的那一部分的矩形,在这里我根据需要在其他类中添加了相应方法,不一一叙述,有兴趣的同学请查看源码。
当然在OnDraw函数中我们也画出柱子:
for(int i=0;idraw(&bufferDC);
}
试一试啊
效果出来了,但是柱子已经伸到框框外面了,这里我们用一个技巧,再画完柱子之后再描一下边框,这个大家都知道怎么做吧,我就不赘述了,不知道的看源码吧,我们顺便调整一下参数,再看看效果:
这回的效果还行吧,但是柱子都出去了,怎么还在走呢,我们还需要将它销毁,赶紧加上这部分逻辑吧!
if(m_columns.GetSize() != 0 && ((CColumn*)m_columns.GetAt(0))->getUpRect().left < m_gameRect.left - 10)
{
CColumn* pColumn = (CColumn*)m_columns.GetAt(0);
delete pColumn;
m_columns.RemoveAt(0);
}
我们判断第一根柱子是否超出边界,若超出,则调用m_columns.RemoveAt(0)删除之。注意释放内存,不要造成内存泄露。再试试:
这时柱子已经行为正常了,我们还要添加GameOver的逻辑,快来加吧:
CRect interRect;
CRect smallGameRect = m_gameRect;
smallGameRect.DeflateRect(m_bird.getWidth(),m_bird.getHeight());
if(!interRect.IntersectRect(m_bird.getRect(),smallGameRect))
{
m_bIsGameOver = true;
}
for(i=0;igetUpRect(),m_bird.getRect());
BOOL b2 = interRect.IntersectRect(((CColumn*)m_columns.GetAt(i))->getDownRect(),m_bird.getRect());
if( b1 || b2 )
{
m_bIsGameOver = true;
break;
}
}
主要就是得到鸟类的Rect,判断它是否出格了,还有就是看看它有没有和那个柱子相交。这里有不懂的欢迎在评论和我讨论哈。这里吐槽一下MFC的CRect类设计,IntersectRect的作用是判断两个矩形是否相交,但是这个函数竟然没有设计为类静态函数,我们还要先定义一个CRect interRect;,再用interRect来调用IntersectRect,稍微有点不淡定啊!
然后我们把OnTimer改下:
if(!m_game.IsGameOver())
{
m_game.move();
Invalidate();
}
else
{
KillTimer(1);
MessageBox("Game Over!");
}
OnChar也改下:
if(!m_game.IsGameOver())
{
if(' ' == nChar)
{
m_game.onKey();
}
}
OnKey中让小鸟跳就好啦:
void CGame::onKey()
{
m_bird.jump();
}
OK,码得差不多了,来试下吧:
我太笨,为了多过几关,我把柱子的间距和间隔都调大了,FlappyBird的感觉还是有了,大家还可以在我的基础上完善这个游戏。
好了,玩也玩累了,又到了总结时间,这次我们学习了:
1、 学会了画刷和贴图,还知道怎么用透明画刷了
2、 搞定兼容DC和兼容位图
3、 学会了CPtrArray的使用
4、 超级给力的双缓冲
5、 CRect的一些高级函数
6、 还有就是最最重要的,面向对象编程思想
面向对象的思想不是一时半会就能掌握的,希望大家通过这个例子好好体会,平时注意这种思想的应用。期望大家在评论中和我讨论,并提出宝贵意见!
对了,这节的例子在http://download.csdn.net/detail/yeluoxiang/7158729,欢迎大家下载哦!