对于一个游戏来说,画面的华丽程度在很大程度上决定了它的火热程度,记得以前初中时候我在网上找游戏玩时,首先看的就是画面是不是好看,技能是不是酷炫,呵呵。而精美游戏的实现就是通过贴图来实现啦,因此要想做出一个好游戏,光有Coder是不够的,必须要有给力的美工,当然还要有好的策划,好的数值设定什么的。不过大家自己学做游戏也不用担心素材的问题了,网上有很多,大家如果不是做商业游戏,用别人的是没什么问题的。
在这一节笔记里,我会讲解使用CBitmap和CImage两种贴图方式,并且会讲解一下透明贴图的实现。我先解释一下,所谓透明贴图,是指贴图只贴前景图,背景和大背景融合,比如大家贴了一张地图后,要在上面再贴一个小人,如果不采用透明贴图的话,那么小人图的背景也会被贴上。
一、一些基本的知识
创建一个窗口之后,显示的屏幕上便划分出三个区域,即屏幕区(Screen),窗口区(Window)与内部窗口区(Client)DeviceContext(设备内容)一般简称为DC,简单来说,DC就是程序可以进行绘图的地方。
若要取得窗口的DC,可以调用下面这个函数:
HDC GetDC(); //取得DC
若使用GetDC()函数取得窗口DC后,必须使用ReleaseDC()函数将DC释放。
Int ReleaseDC(HDC 要释放的DC名称);//释放DC,若运行成功,返回整数1,若失败返回0
我简单解释一下需要释放DC的原因:
每个进程的GDI句柄数是由上限的(在注册表中配置,一般值为10000)。如果不关闭,就会有句柄泄露(这些句柄会在进程结束时被关闭)。大家在编程要养成良好的习惯,就如同new的空间用完一定要delete一样,虽然可能表面上不会影响你的程序的运行,但是埋下了隐患。
在我们建立的MFC单文档程序中,大家在调整好窗口后,剩下的文件大家就只需要关注CChildView.h和CChildView.cpp这两个文件了,这个视图类会展示出程序的画面。
大家需要定义变量或者属性的时候可以在CChildView.h里面进行,就作为这个类的成员变量就好了。这些变量如果需要初始化,请放在CChildView.cpp中的BOOL CChildView::PreCreateWindow(CREATESTRUCT&cs) 函数里进行。在后面的教程中,如果未特别说明,就是按照这个规则来的。
在CChildView.h中有个函数void CChildView::OnPaint() ,这个就是绘图函数啦,我们需要绘制在屏幕上的所有东西就丢给它就好了。当产生WM_PAINT绘图消息时(比如别的窗口从我们的游戏窗口经过了等,就需要重新绘制一遍画面,windows就会产生一个WM_PAINT消息加入到程序的消息队列中去),这个OnPaint()函数就会被调用一遍。但是大家知道,游戏为了保持一个高的帧数(FPS),是需要不断的重绘画面的,因此我们就需要使得这个OnPaint()函数不断的被执行,这个可以采用定时器来做到,在后面我会进行讲解。
因此,在程序中绘图部分,我们首先要知道的是画图的地方,即获得DC,通过GetDC这个API可以实现,接着需要做的就是获得画图地方的大小,就是内部窗口区(Client),这个微软为我们提供了GetClientRect这个API,然后我们就可以画图啦,当然在最后需要释放DC哦。
讲了这么多,我们开始动手写代码吧。
大家在CChildView.h中加入变量定义
CRect m_client;
我解释一下,CRect 是一个矩形类,它由四个成员变量:top,bottom,left,right,可以用来记录一个矩形的左上角和右下角的坐标,一般我们都是用它来保存一个矩形物体的大小的。
接下来,在CChildView.cpp中的void CChildView::OnPaint()函数中写入下面的代码就好了
CDC *cDC=this->GetDC(); //获得当前窗口的DC
GetClientRect(&m_client); //获得窗口的尺寸
//加入我们要绘制的代码。。。。。。
ReleaseDC(cDC); //释放DC
到现在绘图的准备工作都做完了,接下来我们就开始绘图了。
二、使用CBitmap类进行绘图
大家看名字就可以知道,这个类是用来绘制位图的,即以“.bmp”为后缀的图片。一般游戏之中,需要使用的图片比较多,都会将图片先存为文件,然后从文件中读取,而从文件中读取图片的步骤有以下几步:
1.建立一个与窗口DC兼容的内存DC
我们加载的图片是需要先放在一个内存中的DC里,然后把需要显示的图片从内存DC中绘到窗口DC中即可,我们可以认为内存DC就是一个个备胎,每个DC都是一个屌丝男士,窗口DC就是女神啦,女神需要谁去帮忙做事的时候,那个屌丝男士就和女神在一起了,当然是暂时的,呵呵。
我们首先定义一个内存DC对象;
CDC m_bgcDC;
然后就开始创建一个DC了;
m_bgcDC.CreateCompatibleDC(NULL);
看名字我们就可以知道它的意思了,创建一个兼容的DC。它的参数是CDC*类型的,即一个DC的指针,函数会创建和参数DC兼容的DC。如果我们将参数填为NULL,函数会自动创建和当前程序DC兼容的内存DC,所以我们可以直接设置为NULL。
2.加载资源中的位图
首先将位图添加进资源中,方法是右键工程名->添加->资源,选择Bitmap,点击导入,然后找到你的位图即可。位图添加进来后会有一个ID,大家在资源视图下的Bitmap那一栏下可以看到,位图的ID可以修改,默认是第一章是IDB_BITMAP1,事实上它是一个整型数字。大家可以将其编号为连续的数字,方便在循环中加载位图,和使用它们来显示动画效果。
接下来,就是代码部分了,我们先定义一个位图对象;
CBitmap m_bgBitmap;
然后就可以从资源中加载了
m_bgBitmap.LoadBitmap(IDB_BITMAP1);
3.选用位图对象
加载完毕后,我们需要做的就是将这张图和这个DC关联起来,可以说是将这个图放到内存DC中去,这个就是设置备胎的属性了。
m_bgcDC.SelectObject(&m_bgBitmap);
参数为要选择的位图对象指针。
4.将内存DC的内容粘贴到窗口DC中,绘制出来
由于我们的窗口DC使用的是指针,因此使用方式为
cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_bgcDC,0,0,SRCCOPY);
BitBlt这个函数的原型如下:
BOOL BitBlt(
int nXDest, //目的DC的起始x
int nYDest, //起始y
int nWidth, //宽度
int nHeight, //高度
HDC hdcSrc, //源DC
int nXSrc, //源DC起始x
int nYSrc, //起始y
DWORD dwRop //贴图方式,大家设置为SRCCOPY就可以了
);
经过这四步后,我们已经可以显示出图片了
赶紧运行一下程序,大家看到图片了吗?
三、使用CImage类进行贴图
CImage是MFC中一种图片处理的类,其头文件为atlimage.h,如果是VS2010或者VS2012,就不需要包含头文件了。它对PNG格式的图片支持很好,对于其他格式的图片需要借助CBitmap类实现,由于比较麻烦且使用价值感觉不大,所以就略去了,我主要讲解一下对于PNG格式的处理方法。
CImage的使用非常的简单,它的使用分为三步:
1.定义一个CImage对象
CImage m_hero;
2.加载图片
m_hero.Load("hero_No.png");
Load函数的参数是图片的路径,这里使用的是相对路径
3.绘制图片
m_hero.Draw(*cDC,100,400,60,60);
Draw函数有多个重载类型,这里给出的是最常用的,参数分别表示要绘制的DC,起始x,起始y,宽度,高度。
简单的几步过后,我们看下效果:
PS:大家在尝试的时候会发现窗口在不断的闪动,这个问题大家可以先忽略,在后面我会讲解解决方法,这个要采用图像缓冲来处理。
四、透明特效
上面的那张截图,大家可以看到,绘制的人物的背景白色也贴出来了,这显示是我们不想要的,要去掉白色,主要有两种方法。
1.如果采用的不是CImage类贴图,那么可以采用TransparentBlt函数来代替BitBlt函数进行贴图,TransparentBlt原型如下:
BOOL TransparentBlt(
int nXOriginDest, //前面的参数是和BitBlt是一样的含义,就不多说了,最后一个参数和用法见下面
int nYOriginDest,
int nWidthDest,
int hHeightDest,
HDC hdcSrc,
int nXOriginSrc,
int nYOriginSrc,
int nWidthSrc,
int nHeightSrc,
UINT crTransparent
);
它的参数最后一个表示要透明的颜色,比如对于上图的人物图片来说,我们不希望白色被显示出来,那么我们就可以设置最后一个参数为RGB(255,255,255),RGB是一个宏,其三个参数分别表示红色,绿色,蓝色三原色的分量,范围在0~255。
采用这种方式进行透明贴图有一点需要注意,就是人物中不能含有要去掉的背景色,否则也会被去掉,因此在制作图片的时候需要注意背景色必须为纯色,且前景中不含有该颜色。
2.采用CImage类贴图
这个要进行透明贴图就简单多了,只需要将要贴的图片中不需要的部分弄成透明就可以了,贴出来就是透明的了。
对于有些图片中含有一些特定颜色的像素,有时候仍然不是透明的,这个是CImage实现的问题。大家可以采用的方法是每张图片加载后,都自己对它处理以下,我为大家封装了一个函数:
void TransparentPNG(CImage *png)
{
for(int i = 0; i <png->GetWidth(); i++)
{
for(int j = 0; j <png->GetHeight(); j++)
{
unsigned char* pucColor = reinterpret_cast<unsigned char*>(png->GetPixelAddress(i , j));
pucColor[0] = pucColor[0] *pucColor[3] / 255;
pucColor[1] = pucColor[1] *pucColor[3] / 255;
pucColor[2] = pucColor[2] *pucColor[3] / 255;
}
}
}
大家加载图片后,将CImage对象地址传递给它就好了,至于原理大家可以参考透明实现相关的知识。
就像下面这样
TransparentPNG(&m_hero);
透明后就是这样的了:
五、两种贴图方式的对比
采用CBitmap类进行贴图时,操作比较麻烦,而且BMP图片占空间还比较大;使用CImage图片操作简单,而且PNG格式小巧,占用空间小。另外,还有一点很重要的区别,使用CBitmap类时,图片被打包进了可执行程序中去,导致可执行程序比较大,但是exe程序可以到处运行,而不需要那些资源图片在旁边。使用CImage类时,PNG图片是从文件中读取的,所以要运行程序,PNG图片必须在exe程序旁边,当然,exe程序也就很小了。
由于CImage比较简单,在以后的教程中,我将都采用CImage贴图。
如果您正在跟着本教程学习,建议自己打一遍代码,多尝试,遇到困难自己想办法去解决,这样会有很大的提高。有需要代码的同学
事情,因为这让我知道我正在帮助曾和我一样迷茫的少年,你们的支持就是我继续写下去的动力,愿我们一起学习,共同努力,复兴国产游戏。