在游戏中,可以控制人物的方法一般有:1.键盘 2.虚拟摇杆 3.鼠标 4.手机触碰。键盘一般是在PC端较为常用,如果在游戏中使用wasd等操作人物的话,那么在移植到安卓端时,就需要使用虚拟摇杆或虚拟按钮来模拟键盘,以实现处理的统一性。鼠标类似于手机的单点触碰,而手机触碰一般分为单点和多点触碰。这里使用触碰操作人物。
既然使用触碰进行操作人物,那么就需要一个从出发点到目的地的路径,这里选用的是A星算法。A星算法较为经典,也较为简单,对于一般的RPG游戏来说,典型的A星算法足以满足我们的需要,这里不赘述A星算法的原理,不了解的可以去看看这:
1.潘长安. 基于改进A星算法的城市交通寻径的研究[D].华侨大学,2015.
https://search.ehn3.com/doc_detail?dbcode=CMFD&filename=1015974456.NH
2.https://blog.csdn.net/mydo/article/details/49975873
第二篇是侯佩大大写的帖子,我就是模仿他的帖子实现的,并根据实际做了一些小小的改变。
先看代码吧。
ShortestPathStep.h
#ifndef __ShortestPathStep_H__
#define __ShortestPathStep_H__
#include<vector>
#include "SDL_Engine/SDL_Engine.h"
using namespace std;
using namespace SDL;
class ShortestPathStep : public Object
{
SDL_SYNTHESIZE(int, m_nGScore, GScore);
SDL_SYNTHESIZE(int, m_nHScore, HScore);
public:
static Point fromTile;
static Point toTile;
private:
ShortestPathStep* m_pParent;
Point m_tilePos;
public:
ShortestPathStep();
~ShortestPathStep();
static ShortestPathStep* create(const Point& tilePos);
bool init(const Point& tilePos);
bool equals(const ShortestPathStep& other) const;
Point& getTilePos() { return m_tilePos; }
void setTilePos(const Point& pos) { m_tilePos = pos;};
int getFScore() const;
void description();
ShortestPathStep* getParent() const;
void setParent(ShortestPathStep* other);
bool operator>(ShortestPathStep* other);
bool operator<(ShortestPathStep* other);
bool operator<=(ShortestPathStep* other);
bool operator>=(ShortestPathStep* other);
};
#endif
ShortestPathStep.cpp
#include "ShortestPathStep.h"
Point ShortestPathStep::fromTile = Point::ZERO;
Point ShortestPathStep::toTile = Point::ZERO;
ShortestPathStep::ShortestPathStep()
:m_nGScore(0)
,m_nHScore(0)
,m_pParent(nullptr)
{
}
ShortestPathStep::~ShortestPathStep()
{
}
ShortestPathStep* ShortestPathStep::create(const Point& tilePos)
{
auto step = new ShortestPathStep();
if (step && step->init(tilePos))
step->autorelease();
else
SDL_SAFE_DELETE(step);
return step;
}
bool ShortestPathStep::init(const Point& tilePos)
{
this->setTilePos(tilePos);
return true;
}
bool ShortestPathStep::equals(const ShortestPathStep& other) const
{
return m_tilePos.equals(other.m_tilePos);
}
int ShortestPathStep::getFScore() const
{
/* auto dx1 = m_tilePos.x - ShortestPathStep::toTile.x;
auto dy1 = m_tilePos.y - ShortestPathStep::toTile.y;
auto dx2 = fromTile.x - ShortestPathStep::toTile.x;
auto dy2 = fromTile.y - ShortestPathStep::toTile.y;
auto cross = abs(dx1 * dy2 - dx2 * dy1);
if (cross != 1 && cross != 2)
cross = 100;*/
return m_nGScore + m_nHScore /** (int)cross*/;
}
void ShortestPathStep::description()
{
printf("tile_pos:%.f,%.f gscore%d,hscore%d\n",m_tilePos.x,m_tilePos.y,m_nGScore,m_nHScore);
}
ShortestPathStep* ShortestPathStep::getParent() const
{
return m_pParent;
}
void ShortestPathStep::setParent(ShortestPathStep* other)
{
m_pParent = other;
}
bool ShortestPathStep::operator>(ShortestPathStep* other)
{
return this->getFScore() > other->getFScore();
}
bool ShortestPathStep::operator<(ShortestPathStep* other)
{
return this->getFScore() < other->getFScore();
}
bool ShortestPathStep::operator<=(ShortestPathStep* other)
{
return this->getFScore() <= other->getFScore();
}
bool ShortestPathStep::operator>=(ShortestPathStep* other)
{
return this->getFScore() >= other->getFScore();
}
ShortestPathStep类作为A星算法的“步”,即角色在根据步数组后到达目的地,它封装了Point,还添加了既定代价G和估算代价H。getFScore函数的数学公式是 F = G + cross *H,这个公式是我在第一篇论文中看到的优化A星算法中启发函数H的比重
(另外一个是使用最小堆来优化open表的排序,这个我尝试过,但是可能是我的最小堆实现有问题,其实际效率不如插入排序)。
经我个人验证,cross会导致求得的路径不是最优解,故目前暂不采用。
然后就是A星算法。
#ifndef __AStar_H__
#define __AStar_H__
#include <cmath>
#include <algorithm>
#include <functional>
#include "SDL_Engine/SDL_Engine.h"
using namespace std;
using namespace SDL;
enum class Direction;
class ShortestPathStep;
class AStar : public Object
{
SDL_SYNTHESIZE(Size,m_mapSize,MapSize);
public:
std::function<bool (const Point& tilePos)> isPassing;
std::function<bool (const Point& tilePos,Direction dir)> isPassing4;
private:
vector<ShortestPathStep*> m_openSteps;
vector<ShortestPathStep*> m_closeSteps;
public:
AStar();
~AStar();
CREATE_FUNC(AStar);
bool init();
//不检测toTile是否可通过
ShortestPathStep* parse(const Point& fromTile,const Point& toTile);
private:
void insertToOpenSteps(ShortestPathStep* step);
int computeHScoreFromCoord(const Point& fromTileCoord,const Point& toTileCoord);
//根据对应位置获取代价
int calculateCost(const Point& tilePos);
bool isValid(const Point& tilePos)const;
vector<ShortestPathStep*>::const_iterator containsTilePos(const vector<ShortestPathStep*>& vec,const Point& tilePos);
};
#endif
在本游戏中,由于角色只能上下左右四方向行走,故估算函数采用曼哈顿距离。这里的A星算法有isPassing函数和isPassing4函数,isPassing函数是为了判断某一步/点是否可以通过。而isPassing4则是判断某一点的上下左右四个方向是否能通过。如下图:
1,2,3都是特定方向不可通过的,如主角可以站立在1,2,3上,但是对于1,主角向右时无法通过,2,3同理。而对于4而言,本身就无法通过。即只有图块可以通过时,四方向通过才有意义。
其在tiled中的属性如下:
为3图块属性,表示下和右不可通过。为4的属性。
pass_%s 是该图块某方向是否能通过,priority优先级则是人物是否可通过,以及可通过的遮挡关系。
这个在我以前的帖子里(https://blog.csdn.net/bull521/article/details/78935142)有说明,不再赘述。
AStar.cpp
ShortestPathStep* AStar::parse(const Point& fromTile,const Point& toTile)
{
bool bPathFound = false;
ShortestPathStep* pTail = nullptr;
//设置开始和结束位置
ShortestPathStep::fromTile = fromTile;
ShortestPathStep::toTile = toTile;
//方向数组
vector<Direction> dirs;
dirs.push_back(Direction::Down);
dirs.push_back(Direction::Left);
dirs.push_back(Direction::Right);
dirs.push_back(Direction::Up);
//把开始位置插入到开始列表中
auto from = ShortestPathStep::create(fromTile);
m_openSteps.push_back(from);
do
{
ShortestPathStep* currentStep = m_openSteps.front();
m_openSteps.erase(m_openSteps.begin());
//添加到封闭列表
m_closeSteps.push_back(currentStep);
//如果当前路径就是结束路径
if (currentStep->getTilePos().equals(toTile))
{
bPathFound = true;
pTail = currentStep;
//清除开放列表
m_openSteps.clear();
m_closeSteps.clear();
break;
}
//对四方向进行遍历
for (const auto& dir : dirs)
{
Point tilePos;
Direction nextDir;
StaticData::getInstance()->direction(dir, nullptr, &tilePos, &nextDir);
tilePos += currentStep->getTilePos();
//在闭合列表已经存在该位置 直接跳过
if (containsTilePos(m_closeSteps,tilePos) != m_closeSteps.end())
{
continue;
}
int moveCost = calculateCost(tilePos);
//如果该位置不在开放列表中,添加
auto it = containsTilePos(m_openSteps, tilePos);
if (it == m_openSteps.end())
{
//目标合法才添加 默认toTile可通过
if (isValid(tilePos) && isPassing4(currentStep->getTilePos(),dir)
&& (tilePos == toTile || isPassing(tilePos)) && isPassing4(tilePos,nextDir))
{
ShortestPathStep* step = ShortestPathStep::create(tilePos);
step->setParent(currentStep);
step->setGScore(currentStep->getGScore() + moveCost);
step->setHScore(computeHScoreFromCoord(tilePos, toTile));
//插入到开放列表中
insertToOpenSteps(step);
}
}
else
{
auto step = (*it);
//当前花费小于原来的花费,覆盖其值
if (currentStep->getGScore() + moveCost < step->getGScore())
{
step->setGScore(currentStep->getGScore() + moveCost);
step->setParent(currentStep);
//移除后重新添加
m_openSteps.erase(it);
insertToOpenSteps(step);
}
}
}
}while( !m_openSteps.empty());
return pTail;
}
注意这里的toTile,在AStart中,对toTile是不进行检测的,这样处理是为了以后便于与NPC的交互,至于toTile是否可达应该交给上层进行判断。
AStar::parse就是获取路径,如果搜索失败,则返回空指针,表示目的地不可达。
流程图如下:
parse中获取相邻的节点后,先判断是否已经访问过了,即是否在close表中,如果不在则判断是否在open表中,如果在,则判断是否应该更新对应的F值,否则对该点的合法性(是否可通过,四方向通过)进行判断。
void AStar::insertToOpenSteps(ShortestPathStep* step)
{
int stepFScore = step->getFScore();
auto it = m_openSteps.begin();
//找打合适的插入位置
for (;it != m_openSteps.end();it++)
{
auto temp = *it;
if (stepFScore < temp->getFScore())
{
break;
}
}
//插入
m_openSteps.insert(it, step);
}
int AStar::computeHScoreFromCoord(const Point& fromTileCoord,const Point& toTileCoord)
{
return (int)abs(fromTileCoord.x - toTileCoord.x ) + (int)abs(fromTileCoord.y - toTileCoord.y);
}
int AStar::calculateCost(const Point& tilePos)
{
return 1;
}
bool AStar::isValid(const Point&tilePos) const
{
if (tilePos.x < 0 || tilePos.x > m_mapSize.width
|| tilePos.y < 0 || tilePos.y > m_mapSize.height)
return false;
return true;
}
vector<ShortestPathStep*>::const_iterator AStar::containsTilePos(const vector<ShortestPathStep*>& vec,const Point& tilePos)
{
auto it = find_if(vec.cbegin(), vec.cend(), [tilePos](ShortestPathStep* step)
{
return step->getTilePos().equals(tilePos);
});
return it;
}
open表的排序使用了插入排序。calculateCost是估算代价,这里统一为1,在以后的开发里如果添加地形可以在这里进行更改代价。至于computeHScoreFromCoord函数则是采用了曼哈顿距离计算H值。
然后就是StaticData增加了以下:
#ifndef __StaticData_H__
#define __StaticData_H__
#include <string>
#include <vector>
#include <functional>
#include "SDL_Engine/SDL_Engine.h"
using namespace std;
USING_NS_SDL;
//定义一些常用的宏
#define STATIC_DATA_PATH "data/static_data.plist"
/*简化使用*/
#define STATIC_DATA_STRING(key) (StaticData::getInstance()->getValueForKey(key)->asString())
#define STATIC_DATA_INT(key) (StaticData::getInstance()->getValueForKey(key)->asInt())
#define STATIC_DATA_FLOAT(key) (StaticData::getInstance()->getValueForKey(key)->asFloat())
#define STATIC_DATA_BOOLEAN(key) (StaticData::getInstance()->getValueForKey(key)->asBool())
#define STATIC_DATA_POINT(key) (StaticData::getInstance()->getPointForKey(key))
#define STATIC_DATA_ARRAY(key) (StaticData::getInstance()->getValueForKey(key)->asValueVector())
#define STATIC_DATA_TOSTRING(key) (StaticData::getInstance()->toString(key))
/*方向,跟贴图有关*/
enum class Direction
{
Down = 0,
Left,
Right,
Up,
};
class AStar;
class StaticData : public Object
{
private:
static StaticData* s_pInstance;
public:
static StaticData* getInstance();
static void purge();
private:
//键值对
ValueMap m_valueMap;
//角色键值对
ValueMap m_characterMap;
//A*寻路算法
AStar* m_pAStar;
private:
StaticData();
~StaticData();
bool init();
public:
/**
@brief 根据键获取值
@key 要查询的键
@return 返回的值,如果不存在对应的值,则返回空Value
*/
Value* getValueForKey(const string& key);
//加载角色数据以及加载所需要的图片并解析
bool loadCharacterFile(const string& filename);
//获取人物行走动画
Animation* getWalkingAnimation(const string& chartletName, Direction direction);
Animation* getWalkingAnimation(const string& filename, int index, Direction dir, float delay, int loops, bool restoreOriginalFrame);
//获取A星算法
AStar* getAStar() { return m_pAStar; }
bool direction(Direction dir,string* sDir,Point* delta,Direction* oppsite);
private:
//添加角色战斗图并生成16状态动画
bool addSVAnimation(const string& filename);
//添加角色升级文件
bool addLevelUpData(const string& filename);
/*在纹理指定区域rect按照宽度切割,并返回*/
vector<SpriteFrame*> splitTexture(Texture* texture, const Rect& rect ,float width);
};
#endif
class AStar;
class StaticData : public Object
{
private:
static StaticData* s_pInstance;
public:
static StaticData* getInstance();
static void purge();
private:
//键值对
ValueMap m_valueMap;
//角色键值对
ValueMap m_characterMap;
//A*寻路算法
AStar* m_pAStar;
private:
StaticData();
~StaticData();
bool init();
public:
/**
@brief 根据键获取值
@key 要查询的键
@return 返回的值,如果不存在对应的值,则返回空Value
*/
Value* getValueForKey(const string& key);
//加载角色数据以及加载所需要的图片并解析
bool loadCharacterFile(const string& filename);
//获取人物行走动画
Animation* getWalkingAnimation(const string& chartletName, Direction direction);
Animation* getWalkingAnimation(const string& filename, int index, Direction dir, float delay, int loops, bool restoreOriginalFrame);
//获取A星算法
AStar* getAStar() { return m_pAStar; }
bool direction(Direction dir,string* sDir,Point* delta,Direction* oppsite);
private:
//添加角色战斗图并生成16状态动画
bool addSVAnimation(const string& filename);
//添加角色升级文件
bool addLevelUpData(const string& filename);
/*在纹理指定区域rect按照宽度切割,并返回*/
vector<SpriteFrame*> splitTexture(Texture* texture, const Rect& rect ,float width);
};
#endif
新增的direction函数的功能是根据当前的方向获取对应的反方向,以及单位向量。
bool StaticData::init()
{
//读取文件并保存键值对
m_valueMap = FileUtils::getInstance()->getValueMapFromFile(STATIC_DATA_PATH);
m_pAStar = AStar::create();
SDL_SAFE_RETAIN(m_pAStar);
return true;
}
m_pAStar = AStar::create();
SDL_SAFE_RETAIN(m_pAStar);
return true;
}
bool StaticData::direction(Direction dir,string* sDir,Point* delta,Direction* oppsite)
{
if (sDir == nullptr && delta == nullptr && oppsite == nullptr)
return false;
Point temp;
Direction oppsiteDir = dir;
string text;
switch (dir)
{
case Direction::Down:
text = "down";
temp.y = 1.f;
oppsiteDir = Direction::Up;
break;
case Direction::Left:
text = "left";
temp.x = -1.f;
oppsiteDir = Direction::Right;
break;
case Direction::Right:
text = "right";
temp.x = 1.f;
oppsiteDir = Direction::Left;
break;
case Direction::Up:
text = "up";
temp.y = -1.f;
oppsiteDir = Direction::Down;
break;
default:
break;
}
if (sDir != nullptr)
*sDir = text;
if (delta != nullptr)
*delta = temp;
if (oppsite != nullptr)
*oppsite = oppsiteDir;
return true;
}
然后就是角色类的更新:
#ifndef __Character_H__
#define __Character_H__
#include <string>
#include "Entity.h"
using namespace std;
enum class State
{
None,
Idle,
Walking,
};
enum class Direction;
class ShortestPathStep;
class NonPlayerCharacter;
class Character : public Entity
{
SDL_SYNTHESIZE_READONLY(string, m_chartletName, ChartletName);//当前贴图名,也可以认为是人物名称,唯一
SDL_SYNTHESIZE(float, m_durationPerGrid, DurationPerGrid);//每格的行走时间
private:
Direction m_dir;
State m_state;
bool m_bDirty;
Character* m_pFollowCharacter;
//运动相关
vector<ShortestPathStep*> m_shortestPath;
unsigned int m_nStepIndex;
ShortestPathStep* m_lastStep;
Point m_pendingMove;
bool m_bHavePendingMove;
bool m_bMoving;
//NonPlayerCharacter* m_pTriggerNPC;在地6节添加
public:
Character();
~Character();
static Character* create(const string& chartletName);
bool init(const string& chartletName);
//跟随某角色
void follow(Character* character);
//设置npc
void setTriggerNPC(NonPlayerCharacter* npc);
//方向改变
Direction getDirection() const;
void setDirection(Direction direction);
bool isMoving() const { return m_bMoving; }
//运动 默认tilePos必能通过,由上层筛选
bool moveToward(const Point& tilePos);
//移动一步
bool moveOneStep(ShortestPathStep* step);
private:
//切换状态
void changeState(State state);
//构造路径并运行动画
void constructPathAndStartAnimation(ShortestPathStep* pHead);
void popStepAndAnimate();
//清除行走路径
void clearShortestPath();
Direction getDirection(const Point& delta) const;
};
#endif
每个角色类都有一个贴图(这点和以后的NPC相同,不过NPC允许没有贴图),贴图表示了当前角色的行走动画,所谓唯一指的是在character.plist文件中键唯一,即一对一映射。
bool Character::moveToward(const Point& tilePos)
{
//当前角色正在运动,则更改待到达目的地
if (m_bMoving)
{
m_bHavePendingMove = true;
m_pendingMove = tilePos;
return true;
}
auto fromTile = GameScene::getInstance()->getMapLayer()->convertToTileCoordinate(this->getPosition());
//A*算法解析路径
AStar* pAStar = StaticData::getInstance()->getAStar();
auto pTail = pAStar->parse(fromTile, tilePos);
//目标可达,做运动前准备
if (pTail != nullptr)
{
this->constructPathAndStartAnimation(pTail);
return true;
}
return false;
}
Character类中主要添加了一些要实现寻路而必须要有的变量。如m_bHavePendingMove和m_pendingMove是为了保证当前的角色正在运动时,为了保证角色的位置契合图块(所谓契合,表示角色在停止后一定是处理图块的正中间),同时为了响应终点的改变而做的滞后寻路。其他新增的几个私有函数则是为了配合moveToward。节点之间可以认为是链表,寻路完成返回的是pTail即为终点,反过来就是从开始到终点的完整路径。
void Character::constructPathAndStartAnimation(ShortestPathStep* pHead)
{
//此时的角色一定不在运动中
//构建运动列表
while (pHead != nullptr && pHead->getParent() != nullptr)
{
auto it = m_shortestPath.begin();
m_shortestPath.insert(it,pHead);
SDL_SAFE_RETAIN(pHead);
pHead = pHead->getParent();
}
//此位置为主角当前tile 位置
SDL_SAFE_RELEASE(m_lastStep);
m_lastStep = pHead;
SDL_SAFE_RETAIN(m_lastStep);
this->popStepAndAnimate();
}
角色的行走和动画全权交给了popStepAndAnimate进行处理。除了constructPathAndStartAnimation调用popStepAndAnimate函数外,它还会由“自己”调用(内部是MoveTo + Callback,在回调函数Callback中会再次回调popStepAndAnimate函数以保证行走的正确进行和结束)。
void Character::popStepAndAnimate()
{
m_bMoving = false;
//存在待到达目的点,转入
if (m_bHavePendingMove)
{
m_bHavePendingMove = false;
this->clearShortestPath();
//滞后改变
this->moveToward(m_pendingMove);
return ;
}//运动结束
else if (m_nStepIndex >= m_shortestPath.size())
{
this->clearShortestPath();
//站立动画
this->changeState(State::Idle);
return ;
}//点击了NPC,且将要到达
/* else if (m_pTriggerNPC != nullptr && m_nStepIndex == m_shortestPath.size() - 1)
{
auto delta = m_shortestPath.back()->getTilePos() - m_lastStep->getTilePos();
auto newDir = this->getDirection(delta);
//改变方向
if (newDir != m_dir)
{
m_bDirty = true;
m_dir = newDir;
}
this->clearShortestPath();
this->changeState(State::Idle);
m_pTriggerNPC->execute(this->getUniqueID());
m_pTriggerNPC = nullptr;
return ;
}*/
//存在跟随角色,设置跟随
if (m_pFollowCharacter != nullptr)
{
m_pFollowCharacter->moveOneStep(m_lastStep);
}
SDL_SAFE_RELEASE(m_lastStep);
m_lastStep = m_shortestPath.at(m_nStepIndex);
SDL_SAFE_RETAIN(m_lastStep);
auto tileSize = GameScene::getInstance()->getMapLayer()->getTiledMap()->getTileSize();
//开始新的运动
auto step = m_shortestPath.at(m_nStepIndex++);
auto tilePos = step->getTilePos();
Point pos = Point((tilePos.x + 0.5f) * tileSize.width,(tilePos.y + 0.5f) * tileSize.height);
//开始运动
MoveTo* move = MoveTo::create(m_durationPerGrid, pos);
CallFunc* moveCallback = CallFunc::create([this]()
{
//发送事件
_eventDispatcher->dispatchCustomEvent(GameScene::CHARACTER_MOVE_TO_TILE, this);
this->popStepAndAnimate();
});
//运行动作
auto seq = Sequence::createWithTwoActions(move,moveCallback);
this->runAction(seq);
//引擎原因,需要先调用一次
seq->step(1.f/60);
//是否改变方向
auto delta = pos - this->getPosition();
Direction newDir = this->getDirection(delta);
if (newDir != m_dir)
{
m_dir = newDir;
m_bDirty = true;
}
//改为运动状态
this->changeState(State::Walking);
m_bMoving = true;
}
popStepAndAnimate函数的功能就是处理行走和动画,以及跟随者的行走处理。需要注意的是,Callback内部还会分发 名为GameScene::CHARACTER_MOVE_TO_TILE的用户事件,该事件主要是为了方便以后触发NPC(如传送阵)而做的一点准备,目前暂时用不到。而在上面几个函数中,对m_lastStep进行了引用是为了重用,具体用在moveOneStep()函数中。
bool Character::moveOneStep(ShortestPathStep* step)
{
//当前角色正在运动.先停止运动
if (!m_shortestPath.empty())
{
this->clearShortestPath();
}
SDL_SAFE_RETAIN(step);
this->m_shortestPath.push_back(step);
this->popStepAndAnimate();
return true;
}
moveOneStep()主要用在跟随角色,其处理和moveToward大致相同。
还有就是该函数会根据接下来的位置和当前位置进行比较,来判断方向是否改变和是否应该更新行走动画。
void Character::clearShortestPath()
{
for (auto it = m_shortestPath.begin();it != m_shortestPath.end();)
{
auto step = *it;
SDL_SAFE_RELEASE_NULL(step);
it = m_shortestPath.erase(it);
}
m_nStepIndex = 0;
}
Direction Character::getDirection(const Point& delta) const
{
Direction nextDir = Direction::Down;
if (delta.x > 0.f)
{
nextDir = Direction::Right;
}
else if (delta.x < 0.f)
{
nextDir = Direction::Left;
}
else if (delta.y > 0)
{
nextDir = Direction::Down;
}
else if (delta.y < 0)
{
nextDir = Direction::Up;
}
return nextDir;
}
getDirection函数的功能是根据矢量值获取方向。
然后就是isPassing和isPassing4函数。这两个函数由MapLayer提供,并在GameScene中丰富其功能。
bool MapLayer::isPassing(int gid)
{
//获取图块优先级
//默认为人物优先级最高
int priority = 1;
//获取对应属性
ValueMap* properties = nullptr;
//获取失败
if ( !m_pTiledMap->getTilePropertiesForGID(gid, &properties))
return true;
//获取图块优先级
ValueMap::iterator it = properties->find("priority");
if (it != properties->end())
{
int value = it->second.asInt();
priority = value;
}
//优先级为0则不可通过
return priority != 0;
}
bool MapLayer::isPassing(int gid,Direction direction)
{
//获取对应属性
ValueMap* properties = nullptr;
if (!m_pTiledMap->getTilePropertiesForGID(gid, &properties))
return true;
//获取对应的键
string key;
switch (direction)
{
case Direction::Down: key = "pass_down"; break;
case Direction::Left: key = "pass_left"; break;
case Direction::Right: key = "pass_right"; break;
case Direction::Up: key = "pass_up"; break;
}
auto it = properties->find(key);
//获取对应值并返回
if (it != properties->end())
{
bool ret = it->second.asBool();
return ret;
}
else//默认为可通过
return true;
}
注意:getTilePropertiesForGID(int,ValueMap**)和cocos2d-x不同getTilePropertiesForGID(int, Value**);
这两个函数简单来说就是获取对应的图块,来检测其是否存在对应的属性。使用指针的原因在于指针的效率。
class GameScene : public Scene
{
private:
static GameScene* s_pInstance;
public:
static GameScene* getInstance();
static void purge();
private:
EventLayer* m_pEventLayer;
MapLayer* m_pMapLayer;
PlayerLayer* m_pPlayerLayer;
Character* m_pViewpointCharacter;
public:
static const int CHARACTER_LOCAL_Z_ORDER = 9999;//需要比tmx地图总图块大
static const string CHARACTER_MOVE_TO_TILE;
private:
GameScene();
~GameScene();
bool init();
void preloadResources();
bool initializeMapAndPlayers();
//重写MapLayer方法
bool isPassing(const Point& tilePos);
bool isPassing4(const Point& tilePos, Direction dir);
public:
void update(float dt);
//改变场景
void changeMap(const string& mapName, const Point& tileCoodinate);
//设置视图中心点
void setViewpointCenter(const Point& position, float duration = 0.f);
//设置视角跟随
void setViewpointFollow(Character* character);
public:
void clickPath(const Point& location);
MapLayer* getMapLayer() const { return m_pMapLayer; }
};
EventLayer* m_pEventLayer;
MapLayer* m_pMapLayer;
PlayerLayer* m_pPlayerLayer;
Character* m_pViewpointCharacter;
public:
static const int CHARACTER_LOCAL_Z_ORDER = 9999;//需要比tmx地图总图块大
static const string CHARACTER_MOVE_TO_TILE;
private:
GameScene();
~GameScene();
bool init();
void preloadResources();
bool initializeMapAndPlayers();
//重写MapLayer方法
bool isPassing(const Point& tilePos);
bool isPassing4(const Point& tilePos, Direction dir);
public:
void update(float dt);
//改变场景
void changeMap(const string& mapName, const Point& tileCoodinate);
//设置视图中心点
void setViewpointCenter(const Point& position, float duration = 0.f);
//设置视角跟随
void setViewpointFollow(Character* character);
public:
void clickPath(const Point& location);
MapLayer* getMapLayer() const { return m_pMapLayer; }
};
GameScene中新添加了一个事件层,主要是为了过滤以及分发触碰事件。
string const GameScene::CHARACTER_MOVE_TO_TILE = "character move to tile";
bool GameScene::init()
{
this->preloadResources();
m_pEventLayer = EventLayer::create();
this->addChild(m_pEventLayer);
//地图层
m_pMapLayer = MapLayer::create();
this->addChild(m_pMapLayer);
//角色层
m_pPlayerLayer = PlayerLayer::create();
this->addChild(m_pPlayerLayer);
//初始化地图和角色
this->initializeMapAndPlayers();
this->scheduleUpdate();
return true;
}
bool GameScene::initializeMapAndPlayers()
{
//设置A星算法
AStar* pAStar = StaticData::getInstance()->getAStar();
pAStar->isPassing = SDL_CALLBACK_1(GameScene::isPassing, this);
pAStar->isPassing4 = SDL_CALLBACK_2(GameScene::isPassing4, this);
//获取地图
auto dynamicData = DynamicData::getInstance();
//TODO:暂时使用存档1
dynamicData->initializeSaveData(1);
//获得存档玩家控制的主角队伍的数据
auto& valueMap = dynamicData->getTotalValueMapOfCharacter();
Character* last = nullptr;
//解析数据并生成角色
for (auto itMap = valueMap.begin();itMap != valueMap.end();itMap++)
{
auto chartletName = itMap->first;
auto& propertiesMap = itMap->second.asValueMap();
//创建角色
Character* player = Character::create(chartletName);
player->setDurationPerGrid(0.25f);
//传递给主角层
m_pPlayerLayer->addCharacter(player);
//TODO:设置属性
//DynamicData::getInstance()->
//设置跟随
if (last != nullptr)
player->follow(last);
else//设置视角跟随
{
this->setViewpointFollow(player);
}
last = player;
}
auto mapFilePath = dynamicData->getMapFilePath();
auto tileCooridinate = dynamicData->getTileCoordinateOfPlayer();
//改变地图
this->changeMap(mapFilePath, tileCooridinate);
return true;
}
//设置A星算法
AStar* pAStar = StaticData::getInstance()->getAStar();
pAStar->isPassing = SDL_CALLBACK_1(GameScene::isPassing, this);
pAStar->isPassing4 = SDL_CALLBACK_2(GameScene::isPassing4, this);
//获取地图
auto dynamicData = DynamicData::getInstance();
//TODO:暂时使用存档1
dynamicData->initializeSaveData(1);
//获得存档玩家控制的主角队伍的数据
auto& valueMap = dynamicData->getTotalValueMapOfCharacter();
Character* last = nullptr;
//解析数据并生成角色
for (auto itMap = valueMap.begin();itMap != valueMap.end();itMap++)
{
auto chartletName = itMap->first;
auto& propertiesMap = itMap->second.asValueMap();
//创建角色
Character* player = Character::create(chartletName);
player->setDurationPerGrid(0.25f);
//传递给主角层
m_pPlayerLayer->addCharacter(player);
//TODO:设置属性
//DynamicData::getInstance()->
//设置跟随
if (last != nullptr)
player->follow(last);
else//设置视角跟随
{
this->setViewpointFollow(player);
}
last = player;
}
auto mapFilePath = dynamicData->getMapFilePath();
auto tileCooridinate = dynamicData->getTileCoordinateOfPlayer();
//改变地图
this->changeMap(mapFilePath, tileCooridinate);
return true;
}
默认情况下,设置主角为视角中心点。
bool GameScene::isPassing(const Point& tilePos)
{
auto mapSize = m_pMapLayer->getTiledMap()->getMapSize();
//不可超出地图
if (tilePos.x < 0 || tilePos.x > (mapSize.width - 1)
|| tilePos.y > (mapSize.height - 1) || tilePos.y < 0)
{
return false;
}
auto layer = m_pMapLayer->getCollisionLayer();
auto gid = layer->getTileGIDAt(tilePos);
return m_pMapLayer->isPassing(gid);
}
bool GameScene::isPassing4(const Point& tilePos, Direction dir)
{
auto layer = m_pMapLayer->getCollisionLayer();
auto gid = layer->getTileGIDAt(tilePos);
return m_pMapLayer->isPassing(gid, dir);
}
MapLayer提供的函数是判断对应图块的gid的属性。GameScene则在此的基础上,先获取到对应位置的图块id,然后再进行判断。而使用isPassing和isPassing4的原因是,如果在GameScene都为isPassing的话,编译器会报错,编译器无法知道使用的到底是哪个函数。
void GameScene::update(float dt)
{
//视角跟随
if (m_pViewpointCharacter != nullptr
&& m_pViewpointCharacter->isMoving())
{
this->setViewpointCenter(m_pViewpointCharacter->getPosition());
}
}
void GameScene::setViewpointCenter(const Point& position, float duration)
{
Size visibleSize = Director::getInstance()->getVisibleSize();
const int tag = 10;
//地图跟随点移动
float x = (float)MAX(position.x, visibleSize.width / 2);
float y = (float)MAX(position.y, visibleSize.height / 2);
//获取地图层的地图
auto tiledMap = m_pMapLayer->getTiledMap();
auto tileSize = tiledMap->getTileSize();
auto mapSize = tiledMap->getMapSize();
auto mapSizePixel = Size(tileSize.width * mapSize.width, tileSize.height * mapSize.height);
//不让显示区域超过地图的边界
x = (float)MIN(x, (mapSizePixel.width - visibleSize.width / 2.f));
y = (float)MIN(y, (mapSizePixel.height - visibleSize.height / 2.f));
//实际移动的位置
Point actualPosition = Point(x, y);
//屏幕中心位置坐标
Point centerOfView = Point(visibleSize.width / 2, visibleSize.height / 2);
Point delta = centerOfView - actualPosition;
Action* action = nullptr;
//地图运动
if (duration < FLT_EPSILON)
{
action = Place::create(delta);
}
else
{
action = MoveTo::create(duration, delta);
}
action->setTag(tag);
if (tiledMap->getActionByTag(tag) != nullptr)
{
tiledMap->stopActionByTag(tag);
}
tiledMap->runAction(action);
}
void GameScene::setViewpointFollow(Character* character)
{
m_pViewpointCharacter = character;
}
这两个函数主要是为了实现任意角色的视角跟随,默认情况下是跟随主角。
void GameScene::clickPath(const Point& location)
{
auto nodePos = m_pMapLayer->getTiledMap()->convertToNodeSpace(location);
auto tilePos = m_pMapLayer->convertToTileCoordinate(nodePos);
//目标不可达
if (!this->isPassing(tilePos))
return false;
//主角运动
auto player = m_pPlayerLayer->getPlayer();
player->moveToward(tilePos);
return true;
}
在onTouchBegan函数中对触摸点进行判断,如果合法就传递给主角,使之运动。
#include "EventLayer.h"
#include "GameScene.h"
#include "StaticData.h"
EventLayer::EventLayer()
{
}
EventLayer::~EventLayer()
{
}
bool EventLayer::init()
{
auto listener = EventListenerTouchOneByOne::create();
listener->onTouchBegan = SDL_CALLBACK_2(EventLayer::onTouchBegan,this);
listener->onTouchMoved = SDL_CALLBACK_2(EventLayer::onTouchMoved,this);
listener->onTouchEnded = SDL_CALLBACK_2(EventLayer::onTouchEnded,this);
_eventDispatcher->addEventListener(listener,this);
return true;
}
bool EventLayer::onTouchBegan(Touch* touch,SDL_Event* event)
{
gameScene->clickPath(location);
return true;
}
void EventLayer::onTouchMoved(Touch* touch,SDL_Event* event)
{
}
void EventLayer::onTouchEnded(Touch* touch,SDL_Event* event)
{
}
目前的事件层主要起到了事件转发功能,以后会在此的基础上有选择的转发事件。
本节运行结果:
本节代码:链接:https://pan.baidu.com/s/1r8gcC7LX9EaeqDK_ukJuOg 密码:6mpf