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

代码里一个空格的偏差都让我觉得看起来不舒服。

最近flappy bird那是相当火啊,但是它的操作却非常之简单,今天我们来学习一下MFC的绘图知识,顺便来实现一个简易的flappybird。


新建一个MFC单文档工程,让窗口打开就最大化。

我们找到OnDraw函数,在里面添加如下代码:

CPen *oldPen;
	CPen redPen(PS_SOLID,2,RGB(255,0,0));
	oldPen = pDC->SelectObject(&redPen);
	pDC->Rectangle(10,10,110,110);
	pDC->SelectObject(oldPen);

运行结果如下:

当然了,这是上节的东西,只是这里没有用CPen的CreatePen函数,而是在构造函数中就指定了redPen的参数。

我们再来画一个矩形,在pDC->Rectangle(10,10,110,110);后面加上pDC->Rectangle(100,100,200,200);,想想会是什么结果呢?


有没有出乎你的意料,注意那个角上的东西好像被覆盖了,那是怎么回事呢?我想你应该还记得系统会有一个默认的画笔吧,其实它还有一个默认的画刷,画笔是用来绘制轮廓的,而画刷是用来填充里面的空间的。也就是说,我们画矩形的时候,系统用画笔画出矩形的边,用画刷填充矩形的里面,而系统默认画刷是白色的,所以我们的矩形一角被白色画刷的填充给覆盖了。

我们可以做一个实验,让系统用我们自己定义的画刷。将OnDraw函数内代码改下:

CPen *oldPen;
	CPen redPen(PS_SOLID,2,RGB(255,0,0));
	CBrush *oldBrush;
	CBrush greenBrush(RGB(0,255,0));
	oldPen = pDC->SelectObject(&redPen);
	oldBrush = pDC->SelectObject(&greenBrush);
	pDC->Rectangle(10,10,110,110);
	pDC->Rectangle(100,100,200,200);
	pDC->SelectObject(oldPen);
	pDC->SelectObject(oldBrush);

运行下,看看结果:


在这里,我们定义了一个绿色的画刷,greenBrush,所以画出了内部是绿色的矩形啦!可能有人会问,我不想让矩形相互遮挡怎么办,我们要用一个透明的画刷才行,可是透明是什么颜色呢?其实这里要用到系统给我们准备的一个透明画刷。把oldBrush = pDC->SelectObject(&greenBrush);改成oldBrush = (CBrush*)pDC->SelectObject((HBRUSH)GetStockObject(HOLLOW_BRUSH));,再运行试试看?


下面来解释一下oldBrush =(CBrush *)pDC->SelectObject((HBRUSH)GetStockObject(HOLLOW_BRUSH));,首先GetStockObject(HOLLOW_BRUSH)它返回一个透明画刷的句柄,HOBJECT,所谓的句柄(Handle),就是一个标识,表示这是一个Object的资源,我们知道这是一个Brush的资源,所以我们将它强制转换(HBRUSH)GetStockObject(HOLLOW_BRUSH),其实这里不强制转换也没有关系,但是强转一下代码意思清晰多了。那为什么GetStockObject不直接返回画刷句柄HBRUSH呢?这是因为GetStockObject不仅仅能够产生画刷,还能产生画笔等其他资源,所以它只能返回一个抽象的HOBJECT,我们程序员自己根据传递的参数来将其强制转换,这种思想在面向对象中经常用到的。好了,因为我们给pDC的SelectObject方法传递的是HBRUSH,查一下手册,它返回的是HGDIOBJ,当然我们可以定义一个句柄来接收它,但是为了代码统一性,我们直接用oldBrush来接收,将SelectObject的返回强转为(CBrush *)。注意,这里虽然定义了一个(CBrush*),但是它的值表示的是句柄,而刚好他们的底层都是四个字节(32位系统上),所以可以兼容。当然,在恢复画刷的时候也要相应地强转一下,pDC->SelectObject((HGDIOBJ)oldBrush);。关于这部分有疑惑的同学请在评论中和我讨论,不想深入研究的同学直接这样用也没有关系。

         好了,我们学会了画文字,画笔,画刷,已经什么都能画了,但是什么都画太麻烦,如果有一张现成的图片该多好,我们直接把整个图片贴上来不就好了?MFC也是这么想的,它给我们提供了贴图的API。

         选择Insert->Resource:


选择Bitmap,点击New



会出现一个画图的界面,我们可以在这里画图,还可以回车设置其属性


在这里我们可以设置大小和ID等参数,我们将其ID设置为IDB_BITMAP_BIRD,画一个小鸟


不要嘲笑我的画功啊!将其保存。图片准备好了,那我们怎么将其弄到我们的程序中呢?

         我们在OnDraw中再加入下面代码:

CBitmap bitmap;
	bitmap.LoadBitmap(IDB_BITMAP_BIRD);
CDC bitmapDC;
	bitmapDC.CreateCompatibleDC(pDC);
	bitmapDC.SelectObject(&bitmap);
	pDC->BitBlt(10,10,48,48,&bitmapDC,0,0,SRCCOPY);

先来看下效果吧:


下面我来解释下代码:

         首先,我们定义一个CBitmap的类,它代表一个位图,我们调用它的LoadBitmap函数加载刚刚画的位图,当然,参数就是我们刚刚改的ID。然后我们定义了一个CDC,这是什么呢?如果你细心的话,就会知道这就是画图助手啊,发现了吧?我们暂且叫它位图DC吧。然后,我们调用它的CreateCompatibleDC方法让它与我们的pDC兼容,这是什么意思呢?因为系统只需要一个画图助手,而我们自己定义的画图助手如果也想可以合法使用的话,就需要调用这个函数申明和pDC兼容,然后,我们的位图DC调用SelectObject将位图载入,最后,我们调用pDC的BitBlt方法将位图DC中的位图放到界面上。

         为什么这么麻烦呢?这是因为画图助手要画位图,只能用SelectObject将其载入,如果直接用pDC载入的话,那么之前的图形就都没有了,所以我们创建了一个位图DC,也可以说是临时DC,而BitBlt函数有7个参数,首先是pDC的x和y坐标,然后是宽度和高度,然后是位图DC的地址,然后是位图DC的x和y,最后是贴图方式。希望大家没有晕,首先大家要明白这里有两个DC,我们把位图DC从0,0开始的48X48像素的东西帖到pDC的10,10位置。贴图方式SRCCOPY表示拷贝,也就是原封不动地贴图。

         我们可以去掉CreateCompatibleDC这一行,看一下结果:


崩溃了,所以我们要记得申明DC的兼容性哈,初学者经常忘记这个。

         如果你仔细看图就会发现,第一个矩形貌似有一点点被盖住了,我们换回绿色,再看看:


哇,这已经不能看了,白色背景怎么也进来了。我们当然不希望白色背景也跟着进来,怎么办呢?别急,我们刚刚讲了贴图方式,查下MSDN,我们发现有一个方式叫做SRCAND,它是将贴图部分进行与操作,白色对应的是0xFFFFFF,和1进行与操作不会变,这样不就能把白色去掉了吗?

         我们改下代码:pDC->BitBlt(10,10,48,48,&bitmapDC,0,0,SRCAND);

         看下结果:


哈哈,正如我们所料,但是头部的黄色也变成绿色了,这是为什么呢,绿色是0x00FF00,黄色是0xFFFF00,它们进行与当然还是0x00FF00,所以是绿色啦,如果这对你来说影响很大的话,我们可以利用PS做一个透明背景的位图,再用SRCCOPY贴上,但是这里我们暂且这样吧。

         好了,绘图知识讲得差不多了,咱们来实现这个flappy bird吧。这里我要给大家说一说面向对象的思想,我们分析一下这个游戏,其实就是一只鸟,点击一下就往上蹦一下,不点击就下降,这里我们用空格代替点击,然后还有一些绿色的柱子,从右向左移动。所以我们先抽象出小鸟类和柱子类。

         切换到Class View,在工程名上点击右键,选择NewClass


Class Type选择Generic Class,Name写CBird,点击OK


我们在给自己的类取名字的时候,都在前面加上C的前缀,表示这是一个Class,系统在生成File的时候会自动去掉前面的C。切换回File View,我们多了两个文件Bird.h和Bird.cpp,鸟需要知道自己的位置,给它私有的x和y

private:
	int m_x;
	int m_y;

同时,鸟应该自己提供一个draw的方法,它应该自己绘制自己,这个方法是提供给别人使用的,故申明为public,在面向对象的设计中,要做到尽量吝啬,能申明为private的东西绝对不申明为public,这样才把一个类做到最高程度的封装。

public:
	void draw(CDC* pDC);

我们需要使用贴图来画鸟,故还要一个私有的CBitmap。CBitmap m_bitmap;

         好了,我们在构造函数中初始化并且加载位图。

CBird::CBird()
{
	m_x = 0;
	m_y = 0;
	m_bitmap.LoadBitmap(IDB_BITMAP_BIRD);
}

在draw函数中将其画出:

void CBird::draw(CDC* pDC)
{
	CDC bitmapDC;
	bitmapDC.CreateCompatibleDC(pDC);
	bitmapDC.SelectObject(&m_bitmap);
	pDC->BitBlt(m_x,m_y,48,48,&bitmapDC,0,0,SRCAND);
}

好了,我们来试下吧,在View中定义一个bird。

private:
	CBird m_bird;

确保包含了头文件哦:#include"Bird.h"

         在OnDraw函数中注释其他的,添加上m_bird.draw(pDC);,试下吧


鸟儿真的出来了,但是我们还要添加定时器,并且在里面改变它的位置,怎么办呢?这就需要我们在鸟类中添加一个改变位置的方法:

void CBird::setPos(int x,int y)
{
	m_x = x;
	m_y = y;
}

然后注册定时器(还记得OnInitialUpdate吧,在这个函数中注册),每个100ms调用一次OnTimer,在里面设置它的位置,但是怎么设置呢?我们需要鸟类原来的坐标,再添加两个成员函数:

int CBird::getX()
{
	return m_x;
}

int CBird::getY()
{
	return m_y;
}

也许有人会说,这么麻烦,直接把m_x和m_y申明为public不就方便了吗?在外面就能直接访问了?是的,这样的确方便,但是这样却破坏了鸟类的封装性。对其控制也减弱了,而我们将其设为private,然后提供一组访问函数(getter和setter),这样以后要对其进行控制就方便了,比如我们在setPos函数中,当x和y为负数时,明显不对,我们可以将m_x和m_y设置成0。也许有人会说,这个逻辑在外面也能实现啊?是的,但是要明确的是,这部分逻辑是属于鸟类的,鸟类自己的位置设置代码,凭什么写到外面去呢?自己的事情怎么让别人做呢?这就是面向对象的强大之处,它让代码有了自己的位置,我们今后调试扩展啥的都会方便不少,大家以后可以慢慢体会。

好了,现在可以玩了,OnTimer中加上:

m_bird.setPos(m_bird.getX(),m_bird.getY() + 5);
Invalidate();

运行看看吧,小鸟能向下飞了,但是不一会儿就飞出屏幕了,而且是匀速飞的,这个效果太差强人意了吧?我们首先为其添加重力效果,噢,那需要速度和加速度,当然,这些东西都应该变成鸟类的东西,不是吗?鸟类提供一个drop方法就行了。

         我们将OnTimer中的代码改下:

m_bird.drop();
Invalidate();

然后马上去鸟类添加drop方法,再此之前,我们添加两个成员变量,代表速度和加速度:

int m_v;
int m_a;

为其初始化:

m_v = 0;
m_a = 1;

         drop函数如下:

void CBird::drop()
{
	m_v = m_v + m_a;
	m_y = m_y + m_v;
}

试下吧,通过调试,我们把时间间隔改为50ms,把加速度m_a改为3,当然你也可以自己根据自己的喜欢来改。但是鸟一下子就掉下去了,我们还要为其添加一个jump方法:

void CBird::jump()
{
	m_v = -10;
}

先简单将其速度向上,然后在WM_CHAR消息的响应函数中添加代码:

if(' ' == nChar)
{
	m_bird.jump();
}

试下吧,当我们按空格的时候,小鸟向上飞一小段,如果你想让鸟向上飞,狂按空格啊,暂时先用这个效果吧,我们先添加柱子类。


用同样的方法添加CColumn类,我们观察flappy bird的柱子,每一列有一个缺口,想想该定义哪些属性呢?我定义的几个属性是:

private:
	int m_x;		//x位置
	int m_y;		//y位置
	int m_height;	//长度
	int m_width;	//宽度
	int m_start;	//缺口起始位置
	int m_length;	//缺口长度
	int m_speed;	//速度

同样给它一些方法:

CColumn::CColumn()
{
	m_speed = 2;
	m_width = 5;
}
void CColumn::setPos(int x,int y)
{
	m_x = x;
	m_y = y;
}

void CColumn::setHeight(int height)
{
	m_height = height;
}

void CColumn::setStart(int start)
{
	m_start = start;
}

void CColumn::setLength(int length)
{
	m_length = length;
}

void CColumn::move()
{
	m_x = m_x - m_speed;
}

void CColumn::draw(CDC *pDC)
{
	CPen* oldPen;
	CPen grayPen(PS_SOLID,1,RGB(68,87,28));
	CBrush* oldBrush;
	CBrush greenBrush(RGB(0,255,0));
	oldPen = pDC->SelectObject(&grayPen);
	oldBrush = pDC->SelectObject(&greenBrush);
	pDC->Rectangle(m_x,m_y,m_x+m_width,m_y+m_start);
	pDC->Rectangle(m_x,m_y+m_start+m_length,m_x+m_width,m_y+m_height);
	pDC->SelectObject(oldPen);
	pDC->SelectObject(oldBrush);
}

里面的代码大家应该能看懂,参数大家可以自己调啦,然后我们在View类中测试一下看看:

头文件:#include"Column.h"

添加定义:CColumn m_column;

初始化参数:

m_column.setPos(100,100);
m_column.setHeight(100);
m_column.setStart(50);
m_column.setLength(20);

在Timer中:m_column.move();

OnDraw中:m_column.draw(pDC);

         哈哈,一个绿色的柱子已经动起来了:



当然了,这里柱子的缺口太小,而且只有一根柱子,而且位置也不对,而且,而且。。。这一切的一切都说明我们要在添加一个类来管理我们的鸟类和柱子,我们叫它CGame类吧。

         在CGame类中,我们定义游戏区域大小,定义鸟的初始位置,柱子的运动等等,是不是已经迫不及待了呢?

         但是这节的内容已经很多了,参考网友的建议,我还是把之后的部分放到下一节吧,让大家有时间喘口气,下节见!


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值