动手学MFC之十——带上绘图搞定FlappyBird(下)

真正的牛人不是要把什么都做好,而是想做好什么,就能做好什么。

接着上一节来实现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;i<m_columns.GetSize();i++)
	{
		((CColumn*)m_columns.GetAt(i))->move();
	}
}

首先小鸟下降: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;i<m_columns.GetSize();i++)
	{
		((CColumn*)m_columns.GetAt(i))->draw(&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;i<m_columns.GetSize();i++)
	{
		BOOL b1 = interRect.IntersectRect(((CColumn*)m_columns.GetAt(i))->getUpRect(),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,欢迎大家下载哦!



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值