代码里一个空格的偏差都让我觉得看起来不舒服。
最近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;
}
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类中,我们定义游戏区域大小,定义鸟的初始位置,柱子的运动等等,是不是已经迫不及待了呢?
但是这节的内容已经很多了,参考网友的建议,我还是把之后的部分放到下一节吧,让大家有时间喘口气,下节见!