1. 程式人生 > >RPG遊戲製作-03-人物行走及A*尋路演算法

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

相關推薦

RPG遊戲製作-03-人物行走A*演算法

在遊戲中,可以控制人物的方法一般有:1.鍵盤 2.虛擬搖桿 3.滑鼠 4.手機觸碰。鍵盤一般是在PC端較為常用,如果在遊戲中使用wasd等操作人物的話,那麼在移植到安卓端時,就需要使用虛擬搖桿或虛擬按鈕來模擬鍵盤,以實現處理的統一性。滑鼠類似於手機的單點觸碰,而手機觸碰一般分

Html5遊戲框架Craftyjs入門簡單RPGA*

Crafty.js是一個比較簡單輕量的Html5遊戲框架,個人比較喜歡,是因為它足夠簡便(如果你只需要製作簡單的小遊戲,例如微信h5中的各種遊戲)。 遺憾的是,Crafty.js的社群活躍的人越來越少,文件和新的版本也對不上號,所以有的API只能是從原始碼中獲取使用方法了。

淺談遊戲中的A*演算法

本文為銳亞教育原創文章,轉載請註明轉載自銳亞教育 A*尋路 A*演算法基本原理 A*(唸作A星)演算法對初學者來說的確有些難度。本文並不試圖對這個話題作權威性的陳述,取而代之的是描述演算法的原理,使你可以在進一步的閱讀中理解其他相關的資

Unity A*演算法人物移動AI

最近閒來無事,從網上看了一些大神的心得,再融合自己的體會,寫了一個簡單點的尋路演算法,廢話不多說,直接上程式碼 <Grid> usingUnityEngine; usingSystem.Collections; usingS

理解A*演算法過程

   這兩天研究了下 A* 尋路演算法, 主要學習了這篇文章, 但這篇翻譯得不是很好, 我花了很久才看明白文章中的各種指代. 特寫此篇部落格用來總結, 並寫了尋路演算法的程式碼, 覺得有用的同學可以看看. 另外因為圖片製作起來比較麻煩, 所以我用的是原文裡的圖片. &n

A*演算法補充知識

A*演算法 輔助知識 unity視覺化輔助工具Gizmos Gizmos.color =Color.red;//設定畫筆的顏色 Gizmos.DrawWireCube(位置,Vector3(長寬高));//畫出一個範圍框: Gizmos.DrawCube(位置,三維屬性長寬高); /

A*演算法之解決目標點不可達問題

在遊戲世界的尋路中,通常會有這樣一種情況:在小地圖上點選目標點時,點選到了障礙物或者建築上,然後遊戲會提示我們目標地點無法到達。玩家必須非常小心的在小地圖上點選目標區域的空白部分,才能移動到目標地點。那麼,有沒有辦法來改進一下這種不友好的體驗呢? 下面給出兩種方法: 最近

A*演算法的優化與改進

提要通過對上一篇A*尋路演算法的學習,我們對A*尋路應該有一定的瞭解了,但實際應用中,需要對演算法進行一些改進和優化。 Iterative Deepening Depth-first search- 迭代深化深度優先搜尋在深度優先搜尋中一個比較坑爹情形就是在搜尋樹的一枝上沒有

第四章 Dijkstra和A*演算法

尋路 尋路希望ai中的角色能夠計算一條從當前位置到目標位置合適的路徑,並且這條路徑能夠儘可能的合理和短。 在我們的ai模型中,尋路在決策和移動之間。遊戲中大多數尋路的實現是基於a星演算法,但是它不能直接使用關卡資料工作,需要轉換成特別的資料結構:有向非負權重

unity中的一個簡單易用的A*演算法

以前專案中用到了尋路,就寫了個A*尋路的演算法類,感覺還不錯,這裡分享出來。A*演算法的原理網上有很多,這裡只簡單說一下我這個演算法的邏輯:*我的這個演算法裡面沒有關閉列表,因為我會根據地圖資料建立一個對應的節點資料的陣列,每個節點資料記錄自己當前的狀態,是開啟還是關閉的。節

一個高效的a *演算法(八方向)

http://blog.csdn.net/onafioo/article/details/41089579 這種寫法比較垃圾,表現在每次搜尋一個點要遍歷整個地圖那麼大的陣列,如果地圖為256 * 256,每次搜尋都要執行65535次,如果遍歷多個點就是n * 65

A* 演算法

寫在前面 再來填一個坑。之前一直說要寫的A* 終於有空補上了,本篇部落格首先會介紹最基礎的A* 實現,在簡單A* 的基礎上,嘗試實現稍複雜的A* 演算法(帶有高度資訊的地形,允許對角線尋路等)。 A*演算法簡介 本部落格不準備探討A* 演算法的原理,這

A*演算法基於C#實現

 玩過即時戰略,RPG等型別的遊戲的朋友一定會知道,當我們用滑鼠選取某些單位並命令其到達地圖上確定的位置時,這些單位總是可以自動的選擇最短的路徑到達。這個時候我們就會聯想到大名鼎鼎的A*尋路演算法,下文簡略介紹演算法實現原理,並附上C#實現方法。using System; u

A*演算法的C++簡單實現

2007/8/13 17:26:59 #include <iostream> #include <ctime> #include <list> #include <algorithm> #include <cassert&

A*演算法與它的速度

如果你是一個遊戲開發者,或者開發過一些關於人工智慧的遊戲,你一定知道A*演算法,如果沒有接觸過此類的東東,那麼看了這一篇文章,你會對A*演算法從不知道變得了解,從瞭解變得理解。我不是一個純粹的遊戲開發者,我只是因為喜歡而研究,因為興趣而開發,從一些很小的遊戲開始,直到接觸到了

Unity3d利用A*演算法實現模擬

這裡我先引用一篇詳細文章來介紹A*演算法文章內容如下簡易地圖 如圖所示簡易地圖, 其中綠色方塊的是起點 (用 A 表示), 中間藍色的是障礙物, 紅色的方塊 (用 B 表示) 是目的地. 為了可以用一個二維陣列來表示地圖, 我們將地圖劃分成一個個的小方塊.二維陣列在遊戲中的應

js實現A*演算法

js使用canvas繪製介面。 定義兩個類:Node儲存點及A*演算法中的F=G+H值;Point。 /** * 節點 * p : 節點對應的座標 * pn : 節點的父節點 * g : A*-g * h : A*-h */ function Node(p,

A*演算法

#include <stdio.h>#include <stdlib.h>#define hello printf("hello\n");#define hi printf("hi\n");typedef struct NODE node;typede

Unity3D A* 演算法

筆者介紹:姜雪偉,IT公司技術合夥人,IT高階講師,CSDN社群專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D遊戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。A*尋路演算法通常是應用於大型網路遊戲中,A*演算法通常應

Unity3d 實現 A * 演算法

原理A* 演算法是一種啟發式搜尋演算法,通過代價函式f = g+h,A*演算法每次查詢代價最低的點作為搜尋點,並更新搜尋節點列表。最終搜尋到目標位置。需要定義兩個列表 (Open和Closed列表):    private List<Tile> openList