从零开始重写KOK1(万王之王1) —— (4)遮挡、阻挡与寻路
2011-02-01 14:25
363 查看
0. 上篇文章,人物已经可以正确的朝向和移动了,这次我们要添加几个石头,并且达到以下效果
1) 人物和石头根据站位可以正确产生遮挡效果
2) 人物被石头阻挡住,即不能穿过石头
3) 当鼠标点击石头后面时,人物可以自动绕过石头走过去
效果截图:
可直接运行版本下载:>>点击进入下载页面<<
1. 物件管理器与遮挡
因为我们之前只有玩家一个物体,所以在画物体的时候直接调用player.draw()就可以了,但是当有很多物体的时候,我们就需要有一个管理器,来自动帮我们画当前窗口中的所有物体。为此我们可以利用C++的继承特性,给所有物体定义一个基类,这个基类负责定义统一的draw()方法,那么物件管理器就可以忽略这些物体的具体类型而统统调用draw()就行了。
下面的代码是所有物体的基类定义:
对于物件管理器,则非常的简单,就是把所有的物件的初始化、绘制、清理都在这个管理器里面去做,所以他的定义也是4个方法,另外,为了简单,我们没有采用从文件来读物件数据的方式,所以玩家和石头直接在物件管理器中硬编码了。下面是头文件:
其中ListObj存放的是基类型的指针,用来执行他们共有的Init、Logic、Draw、Release方法。在private中的东西就是上面我说的硬编码的物体。
下面我们来看看ObjManager的实现,非常简单:
我们一个一个函数来看,首先是ObjManager::Init(),他首先把所有的石头都加载到了统一的物件列表里,然后再初始化玩家并放入列表。这里有个小细节,就是先初始化石头,则地图上面很多地方就不可以放置玩家了,所以要先放石头再放玩家,否则可能玩家当期的坐标是在石头的内部,就会卡住了。具体如何进行判断落地点的,在后面会有介绍。
然后我们会发现,这里有一个int CompareGameObj(GameObj *obj1, GameObj *obj2)的函数,这个是来做什么的呢。不知道大家有没有听说过画家算法,原理很简单,就是先画背景,再画前景,这样就会产生层次效果。因为我们的游戏是斜45度,所以相当于在坐标Y大的(即屏幕靠下的)是离大家近的,Y小的就是离大家远的,所以我们可以根据Y的值来判断先画谁后画谁。这也就是遮挡的原理了。所以这个函数的作用就不言自明了,是提供给list的sort方法用的,让他们根据Y来排序。如图所示:
接下来的函数一看就知道了,除了Logic方法对ListObj排了一下序,其他的方法都是直接循环调用ListObj中GameObj的相关方法,没有什么好说的了。
2. 道路管理器WayManager与阻挡
说起寻路,估计大家都听过A*寻路算法,所以我第一感觉就是学习一下这个算法,再来看看我怎么去用。学习之后发现,果然可用,对于A*寻路算法我这里就不多讲了,随便搜搜就好多文章,这里简单说说他是干什么的,如下图:
A*寻路就是在干这么一件事:将地图分为一堆格子,那么我们已知起点格(玩家当前格)、终点格(鼠标点击格)和格子是否是障碍(地图信息),求从起点到终点,途经的格子队列,即路径。
所以在游戏中,我们相当于将一个大的网格附加到我们的地图上,形成一个WayTable数组,其实这个网格和地图没什么关系,有联系的只有他们所占的像素多少。然后我们会注意到一个问题,玩家所处的点,就是一个像素的坐标点,而一个格子占用了很多像素,这个怎么转换呢。我这里使用的是这种方式,因为将这个网格看成是一个数组WayTable,那么我就可以通过玩家的坐标点和每个网格的长宽占用多少像素为条件,求得当前玩家所处的WayTable的Index。反之,我可以通过一个WayTable
,来求得n所在的网格的像素中心点的坐标。由于数组是一维的,不方便只管的表示网格的位置,所以我经常把一个索引看成为这种形式:n = x + y*TableWidth,这里x是网格的列数,y是网格的行数,TableWidth是每行有多少个网格。而且一套X,Y正好可以看成是一个POINT类型。还有一个问题,就是当我们任意给定一个点,我们要找到距离这个点最近的不是障碍的网格,这样当玩家点到了石头上,仍然可以走到石头的附近去,而且前面讲的ObjManager的Init方法,要计算玩家的落地点,也是有这个需求。
综上所述,这个WayManager的定义如下:
其中有一个PathPoint的结构,现在不用管它,他是为了实现A*寻路算法而用的结构。大家翻阅A*寻路算法时自然能明白其用处。同样,在private中的变量也都是用来寻路的中间变量。
上面的几个转换的辅助函数很简单,实现如下:
A*寻路算法的实现代码也贴出来,里面有注释,我的这个函数将会返回计算出的路点的WayTable的一系列Index,直接调用后,就能拿这些Index来用了,很方便:
那么就剩一个小问题了,计算落地点,我画了个图,想法很简单:
即,假如搜索起点不可用,比如石头已经把这个路点(格子)占用了,那么我将在一个半径R内找一个可用的格子,我们通过图片可以发现一个特点,所有的红色实现长度L均满足 L = 2R。读者可以试试半径更大的情况,仍然如此。所以这个搜索函数实现如下:
至此,游戏已经达到文章开始时预期的效果。
其实还有很多方法可以在这个基础上进行优化,我能想到的就有不少:
1) 在A*寻路算法时维护开启列表的顺序,使其不用每次find时都需要再排序
2) 以后在加上小地图时,当玩家在小地图上点击一个位置时,可以用较大的网格来计算一个粗糙值,减少性能开销
3) 以后如果出现地图上有孤岛,导致要便利所有路点时,要把这些孤岛的索引放在一个列表里管理,防止大量遍历
4) 地图没有探出来之前,应该是无法寻路的,比如小地图是黑的,那么不应该点一下小地图就自动去寻找最佳路线,那样太假了。
5) 其实现在最大的问题在于,我们每个鼠标按下的的帧中,都会去算个路径,实际上没有必要,对于直线间没有障碍的情况,应该让他直接走过去而不去寻路,对于这点的发现,是我玩Diablo2的无意间,请看下面两张图:
在没有障碍物的情况下,人物会非常平滑的移动,而在有障碍物的情况下,人物即使绕过障碍物后走直线,仍然有所轻微抖动。
终于写完了,完整项目代码下载地址,和上一个文章一样,因为那时已经都写完了。
>>点击进入下载页<<
1) 人物和石头根据站位可以正确产生遮挡效果
2) 人物被石头阻挡住,即不能穿过石头
3) 当鼠标点击石头后面时,人物可以自动绕过石头走过去
效果截图:
可直接运行版本下载:>>点击进入下载页面<<
1. 物件管理器与遮挡
因为我们之前只有玩家一个物体,所以在画物体的时候直接调用player.draw()就可以了,但是当有很多物体的时候,我们就需要有一个管理器,来自动帮我们画当前窗口中的所有物体。为此我们可以利用C++的继承特性,给所有物体定义一个基类,这个基类负责定义统一的draw()方法,那么物件管理器就可以忽略这些物体的具体类型而统统调用draw()就行了。
下面的代码是所有物体的基类定义:
class GameObj { public: virtual int Init() = 0; virtual int Logic() = 0; virtual int Draw() = 0; virtual void Release() = 0; int MapX; int MapY; };
对于物件管理器,则非常的简单,就是把所有的物件的初始化、绘制、清理都在这个管理器里面去做,所以他的定义也是4个方法,另外,为了简单,我们没有采用从文件来读物件数据的方式,所以玩家和石头直接在物件管理器中硬编码了。下面是头文件:
class ObjManager { public: int Init(); int Logic(); int Draw(); void Release(); std::list<GameObj*> ListObj; private: std::list<Stone*> ListStone; Player player; };
其中ListObj存放的是基类型的指针,用来执行他们共有的Init、Logic、Draw、Release方法。在private中的东西就是上面我说的硬编码的物体。
下面我们来看看ObjManager的实现,非常简单:
#include "StdAfx.h" #include <list> #include "GameScreen.h" #include "GameObj.h" #include "Player.h" #include "Stone.h" #include "InputManager.h" #include "ObjManager.h" int ObjManager::Init() { // 先加载环境物体 for (int i = 0; i < 10; ++i) { Stone *stone = new Stone(); ListStone.push_back(stone); stone->Init(); stone->MapX += i * stone->Width; stone->SetBlock(); ListObj.push_back((GameObj*)stone); } // 最后加载玩家,因为要根据环境判断落地点是否需要修正 player.Init(); ListObj.push_back((GameObj*)&player); return 1; } int CompareGameObj(GameObj *obj1, GameObj *obj2) { if (obj1->MapY > obj2->MapY) { return 0; } else { if((obj1->MapY == obj2->MapY) && (obj1->MapX > obj2->MapX)) { return 0; } } return 1; } int ObjManager::Logic() { ListObj.sort(CompareGameObj); for(std::list<GameObj*>::iterator iter = ListObj.begin(); iter != ListObj.end(); ++iter) { (*iter)->Logic(); } return 1; } int ObjManager::Draw() { for(std::list<GameObj*>::iterator iter = ListObj.begin(); iter != ListObj.end(); ++iter) { (*iter)->Draw(); } return 1; } void ObjManager::Release() { for(std::list<GameObj*>::iterator iter = ListObj.begin(); iter != ListObj.end(); ++iter) { (*iter)->Release(); } for(std::list<Stone*>::iterator iter = ListStone.begin(); iter != ListStone.end(); ++iter) { delete *iter; } }
我们一个一个函数来看,首先是ObjManager::Init(),他首先把所有的石头都加载到了统一的物件列表里,然后再初始化玩家并放入列表。这里有个小细节,就是先初始化石头,则地图上面很多地方就不可以放置玩家了,所以要先放石头再放玩家,否则可能玩家当期的坐标是在石头的内部,就会卡住了。具体如何进行判断落地点的,在后面会有介绍。
然后我们会发现,这里有一个int CompareGameObj(GameObj *obj1, GameObj *obj2)的函数,这个是来做什么的呢。不知道大家有没有听说过画家算法,原理很简单,就是先画背景,再画前景,这样就会产生层次效果。因为我们的游戏是斜45度,所以相当于在坐标Y大的(即屏幕靠下的)是离大家近的,Y小的就是离大家远的,所以我们可以根据Y的值来判断先画谁后画谁。这也就是遮挡的原理了。所以这个函数的作用就不言自明了,是提供给list的sort方法用的,让他们根据Y来排序。如图所示:
接下来的函数一看就知道了,除了Logic方法对ListObj排了一下序,其他的方法都是直接循环调用ListObj中GameObj的相关方法,没有什么好说的了。
2. 道路管理器WayManager与阻挡
说起寻路,估计大家都听过A*寻路算法,所以我第一感觉就是学习一下这个算法,再来看看我怎么去用。学习之后发现,果然可用,对于A*寻路算法我这里就不多讲了,随便搜搜就好多文章,这里简单说说他是干什么的,如下图:
A*寻路就是在干这么一件事:将地图分为一堆格子,那么我们已知起点格(玩家当前格)、终点格(鼠标点击格)和格子是否是障碍(地图信息),求从起点到终点,途经的格子队列,即路径。
所以在游戏中,我们相当于将一个大的网格附加到我们的地图上,形成一个WayTable数组,其实这个网格和地图没什么关系,有联系的只有他们所占的像素多少。然后我们会注意到一个问题,玩家所处的点,就是一个像素的坐标点,而一个格子占用了很多像素,这个怎么转换呢。我这里使用的是这种方式,因为将这个网格看成是一个数组WayTable,那么我就可以通过玩家的坐标点和每个网格的长宽占用多少像素为条件,求得当前玩家所处的WayTable的Index。反之,我可以通过一个WayTable
,来求得n所在的网格的像素中心点的坐标。由于数组是一维的,不方便只管的表示网格的位置,所以我经常把一个索引看成为这种形式:n = x + y*TableWidth,这里x是网格的列数,y是网格的行数,TableWidth是每行有多少个网格。而且一套X,Y正好可以看成是一个POINT类型。还有一个问题,就是当我们任意给定一个点,我们要找到距离这个点最近的不是障碍的网格,这样当玩家点到了石头上,仍然可以走到石头的附近去,而且前面讲的ObjManager的Init方法,要计算玩家的落地点,也是有这个需求。
综上所述,这个WayManager的定义如下:
#pragma once #include <list> #include <stack> #include "PathPoint.h" class WayManager { public: int Init(); void Release(); int MapPointToWayTableIndex(POINT mapPoint); // 将地图坐标转换为所在路点的索引 POINT WayTableIndexToMapPoint(int tableIndex); // 求某路点的中心地图坐标 POINT WayTableIndexToPoint(int tableIndex); // 将路点表某索引转换为点的表示形式 std::stack<int> FindWay(int startIndex, int endIndex); // 寻路,将顺序的路点索引放入队列并返回,假定startIndex与endIndex都是可访问点 int GetNearestIndex(POINT mapPoint); // 对于地图上的某点,求最近的未被阻挡的路点索引 int UnitWidth; // 路点单元宽度 int UnitHeight; // 路点单元高度 int TableWidth; // 路点表横向有多少个单元 int TableHeight; // 路点表纵向有多少个单元 int *WayTable; // 路点表数组指针 int WayTableSize; // 路点表数组长度 private: int startIndex; // 寻路源 int endIndex; // 目的路点 std::list<PathPoint*> openlist; // 开启列表,寻路使用 std::list<PathPoint*> closelist; // 关闭列表,寻路使用 PathPoint* find(); // 寻路用的递归方法 };
其中有一个PathPoint的结构,现在不用管它,他是为了实现A*寻路算法而用的结构。大家翻阅A*寻路算法时自然能明白其用处。同样,在private中的变量也都是用来寻路的中间变量。
上面的几个转换的辅助函数很简单,实现如下:
int WayManager::MapPointToWayTableIndex(POINT mapPoint) { int ix, iy; ix = mapPoint.x / UnitWidth; iy = mapPoint.y / UnitHeight; if (ix < 0) ix = 0; if (ix >= TableWidth) ix = TableWidth - 1; if (iy < 0) iy = 0; if (iy >= TableHeight) iy = TableHeight - 1; return (ix + iy * TableWidth); } POINT WayManager::WayTableIndexToMapPoint(int tableIndex) { POINT tablePoint = WayTableIndexToPoint(tableIndex); POINT result; result.x = tablePoint.x * UnitWidth + UnitWidth / 2; result.y = tablePoint.y * UnitHeight + UnitHeight / 2; return result; } POINT WayManager::WayTableIndexToPoint(int tableIndex) { int ix, iy; iy = tableIndex / TableWidth; ix = tableIndex % TableWidth; POINT result; result.x = ix; result.y = iy; return result; }
A*寻路算法的实现代码也贴出来,里面有注释,我的这个函数将会返回计算出的路点的WayTable的一系列Index,直接调用后,就能拿这些Index来用了,很方便:
std::stack<int> WayManager::FindWay(int startIndex, int endIndex) { std::stack<int> result; // delete 过时路点 for (list<PathPoint*>::iterator iter = openlist.begin(); iter != openlist.end(); ++iter) { delete *iter; } for (list<PathPoint*>::iterator iter = closelist.begin(); iter != closelist.end(); ++iter) { delete *iter; } openlist.clear(); closelist.clear(); this->startIndex = startIndex; this->endIndex = endIndex; PathPoint *startPoint = new PathPoint(); startPoint->G = 0; startPoint->H = 0; startPoint->IsStart = true; startPoint->Parent = NULL; startPoint->Index = startIndex; openlist.push_back(startPoint); PathPoint *temp; // 循环临时变量,取每个路点检查的结果 while (true) { temp = find(); if (temp == NULL) { if (openlist.empty()) { return result; break; } } else { while (true) { if (temp->Index == startIndex) { break; } else { result.push(temp->Index); temp = temp->Parent; } } break; } } return result; } int ComparePathPoint(PathPoint *point1, PathPoint *point2) { int f1 = point1->G + point1->H; int f2 = point2->G + point2->H; if (f1 > f2) { return 1; } else { return 0; } } PathPoint* WayManager::find() { PathPoint *result; if (openlist.empty()) { return NULL; } else { openlist.sort(ComparePathPoint); PathPoint *point = openlist.back(); openlist.pop_back(); closelist.push_back(point); int sx, sy, ex, ey, dx, dy, dg, ti; POINT sPoint = WayTableIndexToPoint(point->Index); POINT ePoint = WayTableIndexToPoint(endIndex); sx = sPoint.x; sy = sPoint.y; ex = ePoint.x; ey = ePoint.y; PathPoint **roundPoints = new PathPoint*[8]; for (int i = 0; i < 8; ++i) { switch (i) { case 0: { dx = sx; dy = sy + 1; dg = 10; break; } case 1: { dx = sx - 1; dy = sy + 1; dg = 14; break; } case 2: { dx = sx - 1; dy = sy; dg = 10; break; } case 3: { dx = sx - 1; dy = sy - 1; dg = 14; break; } case 4: { dx = sx; dy = sy - 1; dg = 10; break; } case 5: { dx = sx + 1; dy = sy - 1; dg = 14; break; } case 6: { dx = sx + 1; dy = sy; dg = 10; break; } case 7: { dx = sx + 1; dy = sy + 1; dg = 14; break; } } ti = dx + TableWidth * (dy); // 是否在可用路点 if (dx >= 0 && dx < TableWidth && dy >= 0 && dy < TableHeight && WayTable[ti] == 0) { roundPoints[i] = new PathPoint(); roundPoints[i]->IsStart = false; roundPoints[i]->Parent = point; roundPoints[i]->Index = ti; roundPoints[i]->G = roundPoints[i]->Parent->G + dg; roundPoints[i]->H = (abs(ey - dy) + abs(ex - dx)) * 10; if (roundPoints[i]->Index == endIndex) { result = roundPoints[i]; delete [] roundPoints; return result; } // 是否已关闭 bool hasClose = false; for (std::list<PathPoint*>::iterator iter = closelist.begin(); iter != closelist.end(); ++iter) { if ((*iter)->Index == roundPoints[i]->Index) { hasClose = true; break; } } if (!hasClose) // 未关闭 { // 判断是否已打开 PathPoint *hasOpen = NULL; for (std::list<PathPoint*>::iterator iter = openlist.begin(); iter != openlist.end(); ++iter) { if ((*iter)->Index == roundPoints[i]->Index) { hasOpen = *iter; break; } } if (hasOpen != NULL) // 已打开 { // 谁的G小留谁 if ((hasOpen->G) < (roundPoints[i]-> G)) // 开启列表中的小,丢弃当前 { delete roundPoints[i]; } else // 当前小,替换开启列表中的项值,清除当前 { hasOpen->G = roundPoints[i]-> G; hasOpen->H = roundPoints[i]-> H; hasOpen->Parent = roundPoints[i]->Parent; delete roundPoints[i]; } } else // 未打开 { openlist.push_back(roundPoints[i]); } } else // 已关闭 { delete roundPoints[i]; } } } // end for delete [] roundPoints; } return NULL; }
那么就剩一个小问题了,计算落地点,我画了个图,想法很简单:
即,假如搜索起点不可用,比如石头已经把这个路点(格子)占用了,那么我将在一个半径R内找一个可用的格子,我们通过图片可以发现一个特点,所有的红色实现长度L均满足 L = 2R。读者可以试试半径更大的情况,仍然如此。所以这个搜索函数实现如下:
int WayManager::GetNearestIndex(POINT mapPoint) { int im = MapPointToWayTableIndex(mapPoint); if (WayTable[im] == 0) { return im; } POINT pi = WayTableIndexToPoint(im); int mx = pi.x; int my = pi.y; int ix, iy; for (int r = 1; r <= 5; ++r) // r是搜索半径 { ix = mx - r; iy = my + r; int i; for (i = 0; i < 2 * r; ++i) { --iy; if (ix >= 0 && iy >= 0 && ix < TableWidth && iy < TableHeight && WayTable[ix + iy * TableWidth] == 0) { return ix + iy * TableWidth; } } for (i = 0; i < 2 * r; ++i) { ++ix; if (ix >= 0 && iy >= 0 && ix < TableWidth && iy < TableHeight && WayTable[ix + iy * TableWidth] == 0) { return ix + iy * TableWidth; } } for (i = 0; i < 2 * r; ++i) { ++iy; if (ix >= 0 && iy >= 0 && ix < TableWidth && iy < TableHeight && WayTable[ix + iy * TableWidth] == 0) { return ix + iy * TableWidth; } } for (i = 0; i < 2 * r; ++i) { --ix; if (ix >= 0 && iy >= 0 && ix < TableWidth && iy < TableHeight && WayTable[ix + iy * TableWidth] == 0) { return ix + iy * TableWidth; } } } return -1; }
至此,游戏已经达到文章开始时预期的效果。
其实还有很多方法可以在这个基础上进行优化,我能想到的就有不少:
1) 在A*寻路算法时维护开启列表的顺序,使其不用每次find时都需要再排序
2) 以后在加上小地图时,当玩家在小地图上点击一个位置时,可以用较大的网格来计算一个粗糙值,减少性能开销
3) 以后如果出现地图上有孤岛,导致要便利所有路点时,要把这些孤岛的索引放在一个列表里管理,防止大量遍历
4) 地图没有探出来之前,应该是无法寻路的,比如小地图是黑的,那么不应该点一下小地图就自动去寻找最佳路线,那样太假了。
5) 其实现在最大的问题在于,我们每个鼠标按下的的帧中,都会去算个路径,实际上没有必要,对于直线间没有障碍的情况,应该让他直接走过去而不去寻路,对于这点的发现,是我玩Diablo2的无意间,请看下面两张图:
在没有障碍物的情况下,人物会非常平滑的移动,而在有障碍物的情况下,人物即使绕过障碍物后走直线,仍然有所轻微抖动。
终于写完了,完整项目代码下载地址,和上一个文章一样,因为那时已经都写完了。
>>点击进入下载页<<
相关文章推荐
- 从零开始重写KOK1(万王之王1) —— (2)优化地图加载
- 从零开始重写KOK1(万王之王1) —— (3)优化玩家移动与精确8方向朝向
- 从零开始重写KOK1(万王之王1) —— (1)让人物可在地图上使用鼠标跑动
- Cocos2d-x 重写draw方法绘制直线等图形时被遮挡覆盖问题的一种解决方案
- [转载]从零开始学习OpenGL ES之四补遗 – setupView重写
- 从零开始学习OpenGL ES之五补遗 – setupView重写
- 从零开始学习OpenGL ES之五补遗 – setupView重写
- [转载]从零开始学习OpenGL ES之四补遗 – setupView重写
- 从零开始学习OpenGL ES之四补遗 – setupView重写
- 从零开始学习OpenGL ES之四补遗 – setupView重写
- 从零开始学习OpenGL ES之五补遗 – setupView重写
- A*算法的寻路中的应用——无阻挡
- 从零开始学习OpenGL ES之四补遗 – setupView重写
- 从零开始学习OpenGL ES之四补遗 – setupView重写
- 从零开始学习OpenGL ES之五补遗 – setupView重写
- Cocos2d-x 重写draw方法绘制直线等图形时被遮挡覆盖问题的一种解决方案
- [转载]从零开始学习OpenGL ES之四补遗 – setupView重写
- URL重写小结
- 从零开始学C++之IO流类库(二):文件流(fstream, ifstream, ofstream)的打开关闭、流状态
- ViewGroup重写——滚动页面容器