RPG游戏制作-03-人物行走及A*寻路算法

在游戏中,可以控制人物的方法一般有: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);

};
#endifclass 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

  • 3
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值