作者:七十一雾央 新浪微博:http://weibo.com/1689160943/profile?rightmod=1&wvr=5&mod=personinfo
在这个教程中主要内容是2D游戏,关于3D游戏,雾央也还在努力学习之中,等以后有时间,一定会把自己学到的知识分享给大家,所以这一节中主要讲解的就是2D游戏中的障碍物判定了,45度地图以后要有时间雾央会讲。
上一节中讲解了近似的矩形判定,这是一种比较常用的判定方式,很适合于两个移动的物体之间,比如很多跳跃类游戏中移动的台阶和人物之间的碰撞,但是在地图障碍物判定上就显得有些力不从心了。这一节雾央将会给大家讲解几种新的碰撞检测方法,对于新手朋友们可能内容略多,不过只要认真阅读,多思考思考,应该是不难理解的,如果有什么问题,欢迎大家留言讨论。
在玩游戏的时候,有些河流断崖等地方是不能通过的,高处跳跃落下的时候是不会穿过地平线的,石头挡着的地方是绝对不能路过的…………这些检测都属于地图障碍物判定的范围。
大家在开发游戏的时候,地图的结构类型不同,采用的障碍物判定一般也就不同,地图类型和障碍物检测联系密切,因此雾央会介绍一下不同地图类型可以采取的方法。
在介绍之前,雾央必须要说明一个前提:在雾央的教程里,做碰撞检测时,人物都是使用最小外接矩形进行的。大家当然可以逐像素进行,即遍历整个人物矩形,当检测到人物图形和背景图片在同一位置都具有像素且是障碍物的时候,即发生碰撞。这样去掉了人物图片透明处带来的误差,但是太过于低效,雾央觉得大家初学游戏没有必要追求这么高的精确度,同时也可以大大简化问题。
一、TileMap
现在在家的同学请回头看一下背后,嗯,什么诡异的事情都没有发生,呵呵,开个玩笑了。我们要看的其实是地面,大家看到的是不是像下面这样?
这个地面是雾央在网上找的一张图,大家想象一下,如果将其中的某些块瓷砖画上石头,画上树木、房屋等,在把瓷砖之间的分界线给隐藏起来,比如下面这样,那么它像什么?
应该说它就是一张2D游戏中的地图!
这种类型的地图叫“TileMap”,Tile就是瓷砖的意思,每一块瓷砖我们可以认为是一种地图元素,使用几种不同的地图元素,我们就可以拼出不同的地图,制造地图的过程一般通过地图编辑器进行,这种地图的特征就是网格形。
不知道大家还记不记得小时候在小霸王学习机上玩过的坦克大战?它支持自定义地图,那个部分如果拆分出来就是一个地图编辑器,我们可以选择土墙、钢铁墙、草丛、河流等很多种元素然后自己确定摆放位置,制造出一张地图来进行游戏,这就是TileMap,PS:雾央记得那时候最喜欢用钢铁墙将路封死,只留一个入口,然后守在哪里,呵呵。
这种地图比较简单,程序实现障碍物判定也很容易。因为我们用地图编辑器生成地图的时候,可以保存一个二维数组,指明某个方块的信息。在程序中读取这个二维数组,显示地图,进行障碍物判定。
在TileMap中进行障碍物判定是非常容易的事情,人物每次移动时,都检测前方是否是障碍物即可。人物的移动并不一定要是按网格移动,仍然是可以按像素移动的,当然按网格移动是最简单的,比如推箱子这种游戏。
TileMap要求的就是障碍物比较规则,并且是网格大小的整数倍,否则就会出现比较假的情况。
下面给出一个TileMap的例子,看起来是不是很挫的感觉~~(╯﹏╰)b
下面的图像也是通过TileMap实现的,看起来是不是好了很多?
TileMap对于游戏在表现力上强烈的需求,灵活的操作性来说有点力不从心,制造出的地图很多看起来都有四四方方的感觉,因此最适合于简单休闲的小游戏,比如坦克大战,推箱子,泡泡堂等,在有些大型游戏中也可能会使用到。但是TileMap也有自己独特的优点,仅仅使用几种有限的图元就可以组合出无穷多的各异的地图,并且很省内存空间,毕竟只需要加载几种小图元即可,比如说在沙漠、草原等整个场景风格近似的地图等。某种程度上来说,TileMap是把像素放大了几十倍,相比于逐像素处理来说,现在只需要按网格处理了,因此更便于操作。
二、横版台阶地图
雾央也不知道那种普普通通常见的地图叫什么,就暂且叫做横版台阶地图好了。这里雾央说的是那种障碍物可以随意摆放的地图,并不要求处在特定网格中,这样在游戏中的灵活性也就高了许多。比如下面这种
在这样的地图中,要实现障碍判定,有多种方式。
一种可行的是记录下每个台阶所在的矩形区域坐标范围,然后在程序中判断,情况同TileMap差不了太多,都比较简单可行,但是可能需要遍历每个台阶,比较麻烦。
另一种通过蒙版图进行,这个要方便了很多,但是有一个很大的缺憾,要求障碍物固定,对于那些移动的台阶就无能为力,或者需要额外添加判定。
雾央首先介绍一下蒙版图的概念,举个例子大家就明白了。
比如下面是一张背景图片
下面这张图就是它的蒙版图
大家应该明白了吧?就是将图中的障碍物地方用黑色表示,可以通行的地方用白色表示。那么我们在程序中只需要判定人物要移动到的位置是不是黑色,如果是黑色就不可以通过,否则就可以经过了。事实上可以认为,它是一种特殊的TileMap,只不过每个Tile的大小是一个像素,每个Tile用1表示可以通过,0表示不可通行而已。
明白了这个原理后,雾央相信写成代码应该不是难事,大家可以自己尝试下,雾央待会会附上使用蒙版图判定的详细代码,在障碍物判定上,同样分水平方向和垂直方向,雾央只会以垂直方向示例,水平方向同理,留待大家自己完成。
对了,大家之前一直使用的是CImage贴图,这个类功能很强大,它的一个成员函数GetPixel(x,y)可以获取xy处的像素,返回值为COLORREF类型, 它是32-bit 整型数值,代表了一种颜色。你可以使用 RGB 宏生成颜色变量来和它进行比较,注意x的范围是0到width,y的范围是0到height,包括起始点,但不包括终点,超过范围是会弹出错误框框的哦。在雾央的程序里面,是没有对人物离开地图范围进行判定的,所以大家如果操纵人物离开了窗口范围,再进行上下移动是会看到错误框的,这个由大家自己加上,比较简单。
判断是否可以通行的代码可以如同下面这样:
//是否可以通行的判断
bool CChildView::CanPass()
{
//水平方向的雾央省略了,留给大家自己完成
if(MyHero.direct==LEFT || MyHero.direct==RIGHT )
return true;
for(int x=MyHero.x;x<MyHero.x+MyHero.width;x++) //检测的宽度是人物的宽度
{
//雾央在这里偷了个懒,只检测了人物下一时刻要到达的位置,即MyHero.y-5处
//万一障碍物很薄,只有2个像素宽之类的,就会失效
//主要是因为以后人物的移动方式不会是这种位移直接增加的方式,所以雾央在这里主要是介绍一下思想
//在流畅动画那一节中,雾央会重新讲解,以后会给出新的demo
if(MyHero.direct==UP) //方向向上时
{
if(m_bgblack.GetPixel(x,MyHero.y-5)==RGB(0,0,0))
return false;//遇到黑色像素返回false
}
else if(MyHero.direct==DOWN) //方向向下时
{
//向下时,记得加上人物的宽度,因为人物的xy位置是它的左上角坐标
if(m_bgblack.GetPixel(x,MyHero.y+MyHero.height+5)==RGB(0,0,0))
return false;//遇到黑色像素返回false
}
}
return true;
}
现在我们来看看运行的效果吧
向上走,走不了啊
那往下走吧,还是走不了。。。
呼,写到这里,雾央发现已经比以前写的内容似乎都要多,不知道大家觉得怎么样?雾央本来还打算讲解一下游戏中斜坡的情况,有时间还想讲解一下45度地图的情况,现在看来只好放到下一讲去了,那之前说好的流畅动画等又得往后推一推了,~~(╯﹏╰)b。
三、源代码
头文件
// ChildView.h : CChildView 类的接口
//
#pragma once
#define SNOW_NUMBER 100 //雪花例子的数量
// CChildView 窗口
class CChildView : public CWnd
{
// 构造
public:
CChildView();
// 特性
public:
//人物结构体
struct charcter
{
CImage character; //保存人物的图像
int x; //保存人物的位置
int y;
int direct; //人物的方向
int frame; //运动到第几张图片
int width; //图片的宽度和高度,用于碰撞判定
int height;
}MyHero;
CRect m_client; //保存客户区大小
CImage m_bg; //背景图片
CImage m_bgblack; //背景蒙版图
CDC m_cacheDC; //缓冲DC
CBitmap m_cacheCBitmap;//缓冲位图
// 操作
public:
// 重写
protected:
virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
// 实现
public:
virtual ~CChildView();
// 生成的消息映射函数
protected:
afx_msg void OnPaint();
DECLARE_MESSAGE_MAP()
public:
bool CanPass();
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);
afx_msg void OnLButtonDown(UINT nFlags, CPoint point);
afx_msg void OnTimer(UINT_PTR nIDEvent);
afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
};
cpp文件
// ChildView.cpp : CChildView 类的实现
//
#include "stdafx.h"
#include "GameMFC.h"
#include "ChildView.h"
#include "mmsystem.h"
#pragma comment(lib,"winmm.lib")//导入声音头文件库
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
//定时器的名称用宏比较清楚
#define TIMER_PAINT 1
#define TIMER_HEROMOVE 2
//四个方向
#define DOWN 0
#define LEFT 1
#define RIGHT 2
#define UP 3
//窗口大小
#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
// CChildView
CChildView::CChildView()
{
}
CChildView::~CChildView()
{
mciSendString("stop bgMusic ",NULL,0,NULL);
}
BEGIN_MESSAGE_MAP(CChildView, CWnd)
ON_WM_PAINT()
ON_WM_KEYDOWN()
ON_WM_LBUTTONDOWN()
ON_WM_TIMER()
ON_WM_CREATE()
END_MESSAGE_MAP()
//将png贴图透明
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;
}
}
}
// CChildView 消息处理程序
BOOL CChildView::PreCreateWindow(CREATESTRUCT& cs)
{
if (!CWnd::PreCreateWindow(cs))
return FALSE;
cs.dwExStyle |= WS_EX_CLIENTEDGE;
cs.style &= ~WS_BORDER;
cs.lpszClass = AfxRegisterWndClass(CS_HREDRAW|CS_VREDRAW|CS_DBLCLKS,
::LoadCursor(NULL, IDC_ARROW), reinterpret_cast<HBRUSH>(COLOR_WINDOW+1), NULL);
//-----------------------------------游戏数据初始化部分-------------------------
//加载背景
m_bg.Load("bg.png");
m_bgblack.Load("bgblack.png");
//加载英雄图片
MyHero.character.Load("heroMove.png");
TransparentPNG(&MyHero.character);
MyHero.width=80;
MyHero.height=80;
//初始化英雄状态
MyHero.direct=UP;
MyHero.frame=0;
//设置英雄初始位置
MyHero.x=80;
MyHero.y=400;
//打开音乐文件
mciSendString("open background.mp3 alias bgMusic ", NULL, 0, NULL);
mciSendString("play bgMusic repeat", NULL, 0, NULL);
return TRUE;
}
void CChildView::OnPaint()
{
//获取窗口DC指针
CDC *cDC=this->GetDC();
//获取窗口大小
GetClientRect(&m_client);
//创建缓冲DC
m_cacheDC.CreateCompatibleDC(NULL);
m_cacheCBitmap.CreateCompatibleBitmap(cDC,m_client.Width(),m_client.Height());
m_cacheDC.SelectObject(&m_cacheCBitmap);
//————————————————————开始绘制——————————————————————
//贴背景,现在贴图就是贴在缓冲DC:m_cache中了
m_bg.Draw(m_cacheDC,0,0,WINDOW_WIDTH,WINDOW_HEIGHT,0,0,WINDOW_WIDTH,WINDOW_HEIGHT);
//贴英雄
MyHero.character.Draw(m_cacheDC,MyHero.x,MyHero.y,80,80,
MyHero.frame*80,MyHero.direct*80,80,80);
//最后将缓冲DC内容输出到窗口DC中
cDC->BitBlt(0,0,m_client.Width(),m_client.Height(),&m_cacheDC,0,0,SRCCOPY);
//————————————————————绘制结束—————————————————————
//在绘制完图后,使窗口区有效
ValidateRect(&m_client);
//释放缓冲DC
m_cacheDC.DeleteDC();
//释放对象
m_cacheCBitmap.DeleteObject();
//释放窗口DC
ReleaseDC(cDC);
}
//是否可以通行的判断
bool CChildView::CanPass()
{
//水平方向的雾央省略了,留给大家自己完成
if(MyHero.direct==LEFT || MyHero.direct==RIGHT )
return true;
for(int x=MyHero.x;x<MyHero.x+MyHero.width;x++) //检测的宽度是人物的宽度
{
//雾央在这里偷了个懒,只检测了人物下一时刻要到达的位置,即MyHero.y-5处
//万一障碍物很薄,只有2个像素宽之类的,就会失效
//主要是因为以后人物的移动方式不会是这种位移直接增加的方式,所以雾央在这里主要是介绍一下思想
//在流畅动画那一节中,雾央会重新讲解,以后会给出新的demo
if(MyHero.direct==UP) //方向向上时
{
if(m_bgblack.GetPixel(x,MyHero.y-5)==RGB(0,0,0))
return false;//遇到黑色像素返回false
}
else if(MyHero.direct==DOWN) //方向向下时
{
//向下时,记得加上人物的宽度,因为人物的xy位置是它的左上角坐标
if(m_bgblack.GetPixel(x,MyHero.y+MyHero.height+5)==RGB(0,0,0))
return false;//遇到黑色像素返回false
}
}
return true;
}
//按键响应函数
void CChildView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
//nChar表示按下的键值
switch(nChar)
{
case 'd': //游戏中按下的键当然应该不区分大小写了
case 'D':
MyHero.direct=RIGHT;
MyHero.x+=5;
break;
case 'a':
case 'A':
MyHero.direct=LEFT;
MyHero.x-=5;
break;
case 'w':
case 'W':
MyHero.direct=UP;
if(CanPass())
MyHero.y-=5;
break;
case 's':
case 'S':
MyHero.direct=DOWN;
if(CanPass())
MyHero.y+=5;
break;
}
}
//鼠标左键单击响应函数
void CChildView::OnLButtonDown(UINT nFlags, CPoint point)
{
char bufPos[50];
sprintf(bufPos,"你单击了点X:%d,Y:%d",point.x,point.y);
AfxMessageBox(bufPos);
}
//定时器响应函数
void CChildView::OnTimer(UINT_PTR nIDEvent)
{
switch(nIDEvent)
{
case TIMER_PAINT:OnPaint();break; //若是重绘定时器,就执行OnPaint函数
case TIMER_HEROMOVE: //控制人物移动的定时器
{
MyHero.frame++; //每次到了间隔时间就将图片换为下一帧
if(MyHero.frame==4) //到最后了再重头开始
MyHero.frame=0;
}
break;
}
}
int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
if (CWnd::OnCreate(lpCreateStruct) == -1)
return -1;
// TODO: 在此添加您专用的创建代码
//创建一个10毫秒产生一次消息的定时器
SetTimer(TIMER_PAINT,10,NULL);
//创建人物行走动画定时器
SetTimer(TIMER_HEROMOVE,100,NULL);
return 0;
}
雾央今天听到一首很好听的歌,吴琼的《故人叹》,就作为背景音乐了,希望大家喜欢咯。
《MFC游戏开发》笔记十到这里就结束了,更多精彩请关注下一篇。如果您觉得文章对您有帮助的话,请留下您的评论,点个赞,能看到你们的留言是我最高兴的事情,因为这让我知道我正在帮助曾和我一样迷茫的少年,你们的支持就是我继续写下去的动力,愿我们一起学习,共同努力,复兴国产游戏。
对于文章的疏漏或错误,欢迎大家的指出。