《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI

时间:2022-01-20 04:40:12

本系列文章由七十一雾央编写,转载请注明出处。

http://blog.csdn.net/u011371356/article/details/9374935

作者:七十一雾央 新浪微博:http://weibo.com/1689160943/profile?rightmod=1&wvr=5&mod=personinfo

在上一节笔记中,雾央预告说打算这一节讲流畅动画的改进以及重新封装代码的,但是看起来似乎没有多少同学感兴趣,所以雾央决定把他们往后挪一挪,先来讲解点有意思的东西。

在游戏之中,碰撞判定是必不可少的一块,在许多地方都需要用到。比如说在即时战斗游戏中,当人物碰到怪物的时候,人物是会扣血的;在回合制中,碰到怪物是要进入战斗场景的;游戏之中也还有一些荆棘之类的东西,还有非鼠标控制拾取物体等等,这些都涉及到了碰撞判定。

游戏之中,还有一块很重要的部分:障碍物判定,即玩家在运动的时候,需要判定哪些地方可以通过,哪些地方可以到达,这部分内容比较多,雾央将在下一节继续讲解,感兴趣的同学请保持关注。

在这一节笔记中,雾央要实现的demo是怪物追逐人物,当两者碰撞的时候给出提示信息。

一、简单怪物运动AI

相比于应用软件,游戏软件中包含的知识太多了,每一块拿出来都可以写一本厚厚的书,比如怪物AI这一部分。AI:Artificial Intelligence,即人工智能,人工智能研究如何用计算机去模拟、延伸和扩展人的智能;如何设计和制造更聪明的计算机以及智能水平更高的智能计算机等等。游戏人工智能博大精深,雾央也只是了解很少的一部分,本教程也只是面向游戏初学者,所以那些高深的基因算法、神经网络、蚂蚁算法等有待大家以后自己去探索。

本文将要实现的怪物运动AI是:怪物主动追逐人物。

首先,怪物追逐人物,在游戏中表现的效果就是两者距离不断靠近。归结到算法就是

if(Monster.x<Hero.x)
Monster.x++;
else Monster.x--;
if(Monster.y<Hero.y)
Monster.y++;
else Monster.y--;

当然大家还可以让怪物更智能,比如让怪物依据当前的血量和玩家的血量来决定是不是继续追击,在Dota AI地图中,敌方英雄AI在半血的时候一般就会回家补给状态去了,但是如果玩家的血量比电脑还要少,AI一般会选择奋不顾身的追杀。再比如大家可以给怪物加上活动范围,在玩家进入怪物的活动区间时才选择追逐玩家,当玩家与怪物之间的距离达到一定值时,放弃追击,这个都不不难实现的,只是在上面代码的基础上增加更多判断的代码而已。在后面雾央打算讲解一个完整的游戏demo,在里面将会继续完善怪物AI。

下面给大家看一下实现效果

一开始,玩家出现在地图左下角,怪物出现在地图右上角。

《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI

怪物向人物靠近

《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI

躲开怪物,呵呵

《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI

另外说一句,背景地图是古剑奇谭一的安陆县,这是雾央很喜欢玩的RPG游戏,画面和剧情真心很赞。一个月后即8.18日古剑奇谭二就要出了,古剑二由回合制改为即时战斗了,很是期待,呵呵。

二、碰撞判定

关于碰撞检测,雾央又得说这也是游戏中很复杂的一块了。越复杂的算法得到的精度当然也就越高,带来的就是运行速度的下降。在一般的游戏中,为了速度上的考虑,都会采用近似的算法。

精确的算法中比较简单的一种就是通过像素颜色进行,在下一节中雾央将会进行详解。这里先介绍一下近似算法。

近似算法一般都是根据物体的形状来进行,包括外接圆,外接矩形等判定方法。有些物体不太规则,可能还会对物体进行分段,每一段是一个小矩形,采用组合起来进行判定的方法。

对于我们来说,外接矩形方法就是一种非常简单,精度又不太差的方法,因为我们的人物形状很接近矩形。因此我们的人物与怪物碰撞检测就转化为了判断两个矩形碰撞的问题。

下面是两个矩形刚好碰撞的情况:

《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI

仔细观察,我们便可以得出碰撞时满足的条件:

《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI

假定黑色矩形固定,那么碰撞时,蓝色矩形的中心在红色方框内部。

那么,我们只需要时刻检查蓝色矩形的中心位置是否在红色方框内部即可。具体代码请见最后,雾央已经写好了注释。

在发生碰撞的时候在窗口左上角输出“发生碰撞”,否则输出“没有碰撞”,输出文字的函数是TextOut,大家看着代码应该可以直接明白参数的函数,即xy坐标和输出字符串。

运行效果如下:

《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI

《MFC游戏开发》笔记九 游戏中的碰撞判定初步&怪物运动简单AI

另外,雾央加入了背景音乐,使用的是媒体控制接口。大家要使用的话,记得添加头文件和导入库

#include "mmsystem.h"
#pragma comment(lib,"winmm.lib")//导入声音头文件库

至于打开,播放,暂停,关闭音乐等都可以通过mciSendString来实现,详细请参考代码或者百度,有不懂的也可以留言问雾央,这个函数甚至可以用来播放视频,很是强大,呵呵。当然大家也可以使用PlaySound这个API来播放音乐。

三、源代码欣赏



头文件

// 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;
int Xcenter;
int Ycenter;
}MyHero,Monster; CRect m_client; //保存客户区大小
CImage m_bg; //背景图片 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:
void GetMapStartX();
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文件

//-----------------------------------【程序说明】----------------------------------------------
// 【MFC游戏开发】笔记九 碰撞检测和运动型AI 配套源代码
// VS2010环境
// 更多内容请访问雾央CSDN博客 http://blog.csdn.net/u011371356/article/category/1497651
// 雾央的新浪微博: @七十一雾央
//------------------------------------------------------------------------------------------------ // 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");
//加载英雄图片
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; //加载怪物
Monster.character.Load("monster.png");
TransparentPNG(&Monster.character);
Monster.width=96;
Monster.height=96;
Monster.direct=LEFT;
Monster.frame=0;
Monster.x=700;
Monster.y=100; //打开音乐文件
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);
//贴怪物
Monster.character.Draw(m_cacheDC,Monster.x,Monster.y,96,96,
Monster.frame*96,Monster.direct*96,96,96);
//怪物状态更新
//水平方向上靠近
if(Monster.x<MyHero.x)
{
Monster.x++;
Monster.direct=RIGHT;
}
else if(Monster.x>MyHero.x)
{
Monster.x--;
Monster.direct=LEFT;
}
//竖直方向上靠近
if(Monster.y<MyHero.y)
Monster.y++;
else
Monster.y--; //判断是否碰撞
MyHero.Xcenter=MyHero.x+MyHero.width/2;
MyHero.Ycenter=MyHero.y+MyHero.height/2;
Monster.Xcenter=Monster.x+Monster.width/2;
Monster.Ycenter=Monster.y+Monster.height/2;
//设置文字背景透明
m_cacheDC.SetBkMode(TRANSPARENT);
//设置文字为红色
m_cacheDC.SetTextColor(RGB(255,0,0)); //假定我们将英雄作为图中的黑色矩形
if(Monster.Xcenter< MyHero.Xcenter+(MyHero.width/2+Monster.width/2) &&
Monster.Xcenter> MyHero.Xcenter-(MyHero.width/2+Monster.width/2) &&
Monster.Ycenter< MyHero.Ycenter+(MyHero.height/2+Monster.height/2) &&
Monster.Ycenter> MyHero.Ycenter-(MyHero.height/2+Monster.height/2) )
m_cacheDC.TextOut(0,0,"发生碰撞"); //在窗口左上角显示碰撞信息
else
m_cacheDC.TextOut(0,0,"没有碰撞"); //最后将缓冲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);
} //按键响应函数
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;
MyHero.y-=5;
break;
case 's':
case 'S':
MyHero.direct=DOWN;
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;
Monster.frame++;
if(Monster.frame==4) //以后雾央会对代码进行封装,就不会像这里这么重复了。
Monster.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;
}

 本节源代码请点击这里下载,0积分,必须的

《MFC游戏开发》笔记九到这里就结束了,更多精彩请关注下一篇。如果您觉得文章对您有帮助的话,请留下您的评论,点个赞,能看到你们的留言是我最高兴的事情,因为这让我知道我正在帮助曾和我一样迷茫的少年,你们的支持就是我继续写下去的动力,愿我们一起学习,共同努力,复兴国产游戏。

对于文章的疏漏或错误,欢迎大家的指出。