需求
之前做過一個無人車需要自主尋找最佳路徑,所以研究了相關的尋路算法,最終選擇A算法,因為其簡單易懂,是入門級的尋路算法。
但是在驗證的算法的時候,沒有直觀的感受,總是覺得會有什么問題,所以我就寫了一個可視化的A算法驗證,界面基於Qt開發。
項目說明
本項目主要分為2個部分,Qt繪制網格和A算法實現。下面可以看到,界面的實現和A算法的實現基本上是分離的。也就是說可以單獨使用,比如Qt網格繪制,可以用於掃雷游戲,A*算法的代碼可以直接拷貝,稍作修改就可以直接用於游戲,或者無人車。
效果
操作說明
鼠標右鍵:設置障礙物;鼠標左鍵:設置起點和終點。
項目文件截圖
項目實現
程序的實現流程:定義一個最小網格類->通過鼠標設置起點、終點和障礙物,並實時繪制->尋找路徑->繪制路徑。
定義最小網格類
首先,我們需要定義一個最小網格類,用於繪制網格地圖(GridMap)。
網格地圖是將二維場景中的地圖划分為一個個小網格,這個小網格是最小的空間單位,我們可以設置該網格為障礙物、起點或終點。
下面是最小網格類。
class Item
{
public:
Item();
Item(QPoint pos);
QPoint m_pos; //position
bool m_bIsObstacle; //whether is obstacle
int m_nObjectType; //the object type , Nothing,Robot or goal
};
可以看出最小網格對象很簡單,只需要一個位置信息,障礙物標志位,機器人或目標點標志。以上就是最小網格的全部屬性,當然如果想要實現更加復雜的功能,可以添加屬性。
初始化地圖
初始化地圖是指將2維地圖划分為網格的過程,其實就是new Item的過程。
void MainWindow::InitItems()
{
for(int i=0; i<m_nColumes; i++)
{
QVector<Item*> rowItems;
for(int j=0; j<m_nRows; j++)
{
QPoint pos = QPoint(i,j);
Item* pItem = new Item(pos);
rowItems.append(pItem);
}
m_items.append(rowItems);
}
}
其中m_nColumes和m_nRows是在初始化地圖之前設置的長寬。
設置障礙物
在尋路之前,我們要先設置好障礙物、起點和終點。設置障礙物用鼠標右鍵,單擊右鍵,設置小網格為障礙物,再次單擊取消障礙物;設置起點和終點用鼠標左鍵,單擊左鍵1次,設置小網格為起點,再次單擊為終點,再次單擊為空白,依次循環。
這里我們用鼠標事件實現,可以看到其實就是根據鼠標事件來設置相應位置Item對象的屬性。代碼很簡單。在設置起點和終點是需要特別標記,因為如果地圖中沒有標定起點和終點,就無法尋路了。
void MainWindow::mousePressEvent(QMouseEvent * e)
{
//得到鼠標處的格子坐標
QPoint pt;
pt.setX( (e->pos().x() - START_X ) / RECT_WIDTH);
pt.setY( (e->pos().y() - START_X ) / RECT_HEIGHT);
//wheather is the point in the Game area
if (!PointInGameArea(pt))
{
return;
}
//獲取所點擊矩形元素
Item* pItem = m_items[pt.x()][pt.y()];
//leftbutton set object tpye,rightbutton set obstacle
if(e->button()==Qt::LeftButton) //left button ,set Robot or goal
{
//
pItem->m_nObjectType++;
pItem->m_nObjectType%=3;
pItem->m_bIsObstacle=false; //clear obstacle
}
else if(e->button()==Qt::RightButton) //set obstacle or not obstacle
{
pItem->m_nObjectType=0; //clear object type
if (pItem->m_bIsObstacle)
{
pItem->m_bIsObstacle = false;
}
else
{
pItem->m_bIsObstacle = true;
}
}
}
鼠標事件會自動觸發paintEvent()函數,所以在點擊鼠標后,地圖會重繪。
新建地圖
新建地圖需要將上次的地圖清除,也就是ReleaseItems(),同時清除路徑ReleasePath(),然后再初始化地圖。
void MainWindow::NewGame()
{
resize(START_X*2 + m_nColumes*RECT_WIDTH ,START_Y*2 + m_nRows*RECT_HEIGHT);
ReleaseItems();
ReleasePath();
InitItems();
}
無論是初始化地圖還是新建地圖,都沒有提到地圖的繪制,那么地圖什么時候繪制呢?在NewGame()中有一個Resize()函數,也就是改變窗口的大小,該函數會觸發paintEvent()函數,所以當調用NewGame()函數后,就會繪制地圖了。
更新地圖
不說了,看代碼,很簡單,后面詳細介紹一下繪制地圖和繪制路徑。
void MainWindow::paintEvent(QPaintEvent *e)
{
DrawChessboard(); //繪制地圖背景
DrawItems(); //繪制地圖
DrawPath(); //繪制路徑
update();
}
繪制地圖
繪制地圖其實就是根據相應位置的Item對象的屬性,來繪制該網格。網格屬性在初始化和設置障礙物時,已經設置好了。代碼很簡單,就是判斷網格屬性是障礙物還是起點,然后直接繪制就好了。
void MainWindow::DrawItem(QPainter& painter,Item* pItem)
{
if(pItem->m_bIsObstacle) //show obstacle
{
QRect rcSrc(0,0,m_ObstacleImage.width(),m_ObstacleImage.height());
QRect rcTarget(START_X + pItem->m_pos.x()*RECT_WIDTH + 2,START_Y + pItem->m_pos.y()*RECT_HEIGHT + 2,RECT_WIDTH-4,RECT_HEIGHT-4);
painter.drawPixmap(rcTarget,m_ObstacleImage,rcSrc);
painter.setBrush(Qt::transparent);
painter.drawRect( START_X + pItem->m_pos.x()*RECT_WIDTH,START_Y + pItem->m_pos.y()*RECT_HEIGHT,RECT_WIDTH,RECT_HEIGHT);
return;
}
else if (pItem->m_nObjectType!=0) //show Robot item or goal item
{
if(pItem->m_nObjectType == 1) //show Robot
{
QRect rcSrc(0,0,m_RobotImage.width(),m_RobotImage.height());
QRect rcTarget(START_X + pItem->m_pos.x()*RECT_WIDTH + 2,START_Y + pItem->m_pos.y()*RECT_HEIGHT + 2,RECT_WIDTH-4,RECT_HEIGHT-4);
painter.drawPixmap(rcTarget,m_RobotImage,rcSrc);
painter.setBrush(Qt::transparent);
painter.drawRect( START_X + pItem->m_pos.x()*RECT_WIDTH,START_Y + pItem->m_pos.y()*RECT_HEIGHT,RECT_WIDTH,RECT_HEIGHT);
return ;
}
else
{
QRect rcSrc(0,0,m_GoalImage.width(),m_GoalImage.height());
QRect rcTarget(START_X + pItem->m_pos.x()*RECT_WIDTH + 2,START_Y + pItem->m_pos.y()*RECT_HEIGHT + 2,RECT_WIDTH-4,RECT_HEIGHT-4);
painter.drawPixmap(rcTarget,m_GoalImage,rcSrc);
painter.setBrush(Qt::transparent);
painter.drawRect( START_X + pItem->m_pos.x()*RECT_WIDTH,START_Y + pItem->m_pos.y()*RECT_HEIGHT,RECT_WIDTH,RECT_HEIGHT);
return;
}
}
else
{
painter.setBrush(Qt::green);
}
painter.drawRect( START_X + pItem->m_pos.x()*RECT_WIDTH,START_Y + pItem->m_pos.y()*RECT_HEIGHT,RECT_WIDTH,RECT_HEIGHT);
}
尋找路徑
地圖繪制好了以后,就可以尋路了。單擊搜索路徑后,就可以尋路了。尋路直線需要清除原來的路徑,避免上次的路徑影響。如果發現地圖中沒有起點和終點,就直接退出,避免造成尋路異常。
void MainWindow::OnSearchPath()
{
ReleasePath();
createMazeMap(); //create Maze map and start end point
if(!m_bIsHaveRobot || !m_bIsHaveGoal)
{
std::cout<<"have not roobot or goal"<<std::endl;
return;
}
astar.InitAstar(mazeMap);
path=astar.GetPath(start, end, false);
markPathInMazeMap();
printMazeMap();
printPath();
if(!path.empty())
path.pop_back();
if(!path.empty())
path.pop_front();
update();
}
代碼很簡單,關鍵就是path=astar.GetPath().下面重點分析一下尋路的過程。
尋路的代碼不是本人寫了,原作者已經不記得了,但是他的代碼有點問題,我調試后做了修改。下面是我參考的博客:
https://www.cnblogs.com/wlzy/p/7096114.html
博客上對A*的算法描述的非常清晰,簡單易懂,我就不重復了。但是我用他的代碼時,總是會出現路徑越界,調試之后發現是邊界檢測那塊有一點問題,也就是isCanreach()函數,讀者可以好好對比一下。同時我在源碼的基礎上添加了一些清除開路徑和閉路徑的操作,不知道為什么源碼沒有。如果沒有的話,會有問題的。
繪制路徑
尋路完成以后,就可以直接繪制路徑了。
void MainWindow::DrawPath()
{
QPainter painter(this);
painter.setBrush(Qt::yellow);
if(!path.empty())
for(auto &p:path)
{
painter.drawRect( START_X + p->x*RECT_WIDTH,START_Y + p->y*RECT_HEIGHT,RECT_WIDTH,RECT_HEIGHT);
}
}
總結
完成了基於Qt的A算法可視化分析后,我發現以后很多算法都可以可視化分析了,非常直觀,有助於理解算法。
基於Qt的A算法可視化分析
注:本文著作權歸作者,由demo大師代發,拒絕轉載,轉載需要作者授權