陣列
DiscoveredTileIndexed 和 DiscoveredTileSortingCosts
這兩個陣列是用來儲存遍歷的方格的,DiscoveredTileSortingCosts儲存的是每個方格的消耗,DiscoveredTileIndexed儲存的是每個方格的位置即(x,y)。
DiscoveredTileSortingCosts中的消耗和DiscoveredTileIndexed位置是一一對應的,也即是DiscoveredTileIndexed中方格位置是按照消耗排序的
AnalysedTileIndex
每次從DiscoveredTileIndexed中取出第一個元素,即消耗最小的元素壓入到該陣列中,壓入後表示該點已經走過,後面的點如果周圍點出現在這個陣列中那麼就會跳過
PathFindingData
其為一個Map結構,儲存的就是最短路徑,由當前方格座標和方格引數組成,
CurrentNeighbors
用來儲存每次遍歷一個節點其的下一步可以到達的點
方格的結構體
和藍圖中一樣存放幾個必須的引數
USTRUCT(BlueprintType)
struct FPathFindingData : public FTableRowBase
{
GENERATED_USTRUCT_BODY()
FPathFindingData() {}
FPathFindingData(FIntPoint index, int costtoentertile, int costfromstart, int minimumcosttotarget, FIntPoint previousindex) :
Index(index), CostToEnterTile(costtoentertile), CostFromStart(costfromstart),MinimumCostToTarget(minimumcosttotarget),PreviousIndex(previousindex)
{}
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
FIntPoint Index = { -999, -999 };
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
int CostToEnterTile = 1;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
int CostFromStart = 999999;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
int MinimumCostToTarget = 999999;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Data")
FIntPoint PreviousIndex = { -999, -999 };
};
尋路函式
TArray<FIntPoint> AXGridPathFinding::FindPath(const FIntPoint& start, const FIntPoint& target, bool Diagonals)
返回PathFindingData中Key值組成的陣列。
首先向函式中傳入起點和終點以及是否考慮沿著對角線移動,然後清除上次尋路的結果,檢驗輸入座標是否合理
StartIndex = start;
TargetIndex = target;
bIncludeDiagonals = Diagonals;
//清除之前的路徑
ClearGenerateData();
//判斷輸入的合理性
if (!IsInputDataValid())
{
UE_LOG(LogTemp, Warning, TEXT("Input No Valid"));
return TArray<FIntPoint>();
}
計算起點的方格引數
int MinCostToEnd = GetMinimumCostBetweenTwoTiles(StartIndex, TargetIndex, bIncludeDiagonals);
FPathFindingData StartData;
StartData.Index = StartIndex;
StartData.CostToEnterTile = 1;
StartData.CostFromStart = 0;
StartData.MinimumCostToTarget = MinCostToEnd;
對起點進行計算得到初始的DiscoveredTileIndexed 和 DiscoveredTileSortingCosts並將起點壓入到PathFindingData中
DiscoverTile(StartData);
if(DiscoveredTileIndexed.Num() == 0) UE_LOG(LogTemp, Warning, TEXT("No DiscoveredTileIndexed"));
當起點的可移動點求出,遍歷DiscoveredTileIndexed陣列,對每個點進行分析,直到DiscoveredTileIndexed陣列長度為0,如果在分析中返回了true表示可以到終點,返回對應陣列
while (DiscoveredTileIndexed.Num() > 0)
{
//尋路
if (AnalyseNextDiscoveredTile())
{
UE_LOG(LogTemp, Warning, TEXT("Find Way"));
return GerneratePath();
}
}
return TArray<FIntPoint>();
計算當前點的可到達方格
void AXGridPathFinding::DiscoverTile(const FPathFindingData& TilePathData)
透過傳入當前節點的屬性,將當前節點壓入到PathFindingData中,然後從當前點計算更新DiscoveredTileIndexed 和 DiscoveredTileSortingCosts
void AXGridPathFinding::DiscoverTile(const FPathFindingData& TilePathData)
{
PathFindingData.Add(TilePathData.Index, TilePathData);
InsertTileInDiscoveredArray(TilePathData);
}
更新DiscoveredTileIndexed 和 DiscoveredTileSortingCosts
void AXGridPathFinding::InsertTileInDiscoveredArray(const FPathFindingData& TileData)
首先計算從起點經過該點TileData到終點所需要的路徑消耗SortingData,利用TileData結構體中的資料進行計算。
當DiscoveredTileIndexed陣列為空時,可以直接壓入節點座標,如果不為空,要比較DiscoveredTileSortingCosts中的最後一個元素和SortingData的大小,如果SortingData大,那麼也可以直接壓入,如果SortingData小,那麼需要從DiscoveredTileSortingCosts尋找到合理的位置進行插入,而DiscoveredTileSortingCosts與DiscoveredTileIndexed是一一對應的所以可以使用同一個下標進行插入
void AXGridPathFinding::InsertTileInDiscoveredArray(const FPathFindingData& TileData)
{
//計算當前點的消耗
int SortingData = TileData.CostFromStart + TileData.MinimumCostToTarget;
//如果排序陣列長度為0,或者當前點的SortingData比排序陣列中都大,那麼直接壓入
if (DiscoveredTileSortingCosts.Num() == 0)
{
//都是在對應的隊尾加入元素,保證順序
DiscoveredTileSortingCosts.Add(SortingData);
DiscoveredTileIndexed.Add(TileData.Index);
}
else
{
if (SortingData >= DiscoveredTileSortingCosts.Last())
{
DiscoveredTileSortingCosts.Add(SortingData);
DiscoveredTileIndexed.Add(TileData.Index);
}
else
{
for (int i = 0;i<DiscoveredTileSortingCosts.Num();i++)
{
if (SortingData < DiscoveredTileSortingCosts[i])
{
DiscoveredTileSortingCosts.Insert(SortingData, i);
DiscoveredTileIndexed.Insert(TileData.Index,i);
break;
}
}
}
}
}
對每個遍歷到的節點的處理
bool AXGridPathFinding::AnalyseNextDiscoveredTile()
該函式不僅涉及到對每個節點的處理,還需要對每個節點進行判斷是否為終點
每次處理的節點一定是當前消耗最小的節點,即首先需要從DiscoveredTileIndexed取出第一個元素,加入到AnalysedTileIndex陣列中,然後每次取出一個就需要將其從DiscoveredTileIndexed 和 DiscoveredTileSortingCosts移除。
然後根據下標從PathFindingData中獲取當前節點的結構體,然後計算當前節點下一步會到達哪些點,儲存在CurrentNeighbors陣列中,然後對陣列中每個元素進行遍歷,尋找其在DiscoveredTileIndexed 和 DiscoveredTileSortingCosts的合適位置進行插入,然後判斷是否為終點。
bool AXGridPathFinding::AnalyseNextDiscoveredTile()
{
//首先將消耗最小的節點取出,從儲存陣列和排序陣列中移除(一定是第一個元素),然後加入到分析陣列中
CurrentDiscoveredTile = PullCheapestTileOutOfDiscoveredList();
//獲取當前點的下一步可移動的點
CurrentNeighbors = GetValidTileNeighbor(CurrentDiscoveredTile.Index, bIncludeDiagonals);
if(CurrentNeighbors.Num() == 0) UE_LOG(LogTemp, Warning, TEXT("No CurrentNeighbors"));
while (CurrentNeighbors.Num() > 0)
{
if (DiscoverNextNeighbor())
{
return true;
}
}
return false;
}
獲取消耗最小點作為接下來的起始點
FPathFindingData AXGridPathFinding::PullCheapestTileOutOfDiscoveredList()
{
FIntPoint TileIndex = DiscoveredTileIndexed[0];
DiscoveredTileIndexed.RemoveAt(0);
DiscoveredTileSortingCosts.RemoveAt(0);
AnalysedTileIndex.Add(TileIndex);
//獲取存在於結果Map中的資料,如果沒有就返回空
FPathFindingData res = PathFindingData[TileIndex];
return res;
}
對當前點的下一步可到達點進行處理
bool AXGridPathFinding::DiscoverNextNeighbor()
每次獲取CurrentNeighbors中的第一個元素,然後移除它
首先對其下標進行判斷,判斷它是否之前出現在了AnalysedTileIndex陣列中,如果出現過,說明如果現在到該點會向回走,跳過對該點的處理
接下來計算從起點到下一步點的消耗CostFromStart,然後判斷下一步點是否之前存在DiscoveredTileIndexed中,如果存在那說明下一步點之前作為過其他點的下一步可移動點,但是並不是消耗最少的點。
那麼就需要從PathFindingData中取出下一步點的起點到該點的資料,來和CostFromStart比較。如果CostFromStart較大,說明下一步點不是最優點,可以直接返回了。
如果CostFromStart較小,那麼下一步點就可以作為下一步可到達點,首先從DiscoveredTileIndexed 和 DiscoveredTileSortingCosts移除它。
接下來的處理邏輯是公用的(CostFromStart較小或者該點之前不存在DiscoveredTileIndexed中)
首先就是計算下一步點的結構體資料,然後對下一步點進行壓入PathFindingData和更新DiscoveredTileIndexed 和 DiscoveredTileSortingCosts的操作。
接著判斷下一步點是否為終點,從而返回bool值
bool AXGridPathFinding::DiscoverNextNeighbor()
{
CurrentNeighbor = CurrentNeighbors[0];
CurrentNeighbors.RemoveAt(0);
//如果在分析陣列中已經存在說明之前遍歷過,即往回走了,直接返回false
if(AnalysedTileIndex.Contains(CurrentNeighbor.Index)) return false;
//計算起點到當前點的消耗,就是起點到前一個點的消耗 + 前一個點到目前點的消耗
int CostFromStart = CurrentDiscoveredTile.CostFromStart + CurrentNeighbor.CostToEnterTile;
//判斷該點是否存在於之前點的鄰接點但還未遍歷的陣列中
//如果在
int IndexInDiscovered = DiscoveredTileIndexed.Find(CurrentNeighbor.Index);
if (IndexInDiscovered != -1)
{
CurrentNeighbor = PathFindingData[CurrentNeighbor.Index];
if (CostFromStart >= CurrentNeighbor.CostFromStart)
{
return false;
}
else
{
DiscoveredTileIndexed.RemoveAt(IndexInDiscovered);
DiscoveredTileSortingCosts.RemoveAt(IndexInDiscovered);
}
}
//如果不在,那麼直接加入就行了
int MinimumCostToTarget = GetMinimumCostBetweenTwoTiles(CurrentNeighbor.Index, TargetIndex, bIncludeDiagonals);
UE_LOG(LogTemp, Warning, TEXT("CurrentNeighbor:%d,%d,CostFromStart:%d,MinimumCostToTarget:%d"), CurrentNeighbor.Index.X, CurrentNeighbor.Index.Y, CostFromStart, MinimumCostToTarget);
FPathFindingData temp(CurrentNeighbor.Index, CurrentNeighbor.CostToEnterTile, CostFromStart, MinimumCostToTarget, CurrentDiscoveredTile.Index);
DiscoverTile(temp);
if (CurrentNeighbor.Index == TargetIndex)
{
return true;
}
else return false;
}
生成路徑
對PathFindingData進行從後向前遍歷,透過存放在結構體中的前一個點座標這個成員進行迭代直到退回到起點,然後再翻轉陣列即可。
TArray<FIntPoint> AXGridPathFinding::GerneratePath()
{
FIntPoint Current = TargetIndex;
TArray<FIntPoint> InvertedPath, Res;
while (Current != StartIndex)
{
InvertedPath.Add(Current);
Current = PathFindingData[Current].PreviousIndex;
}
for (int i = InvertedPath.Num() - 1; i >= 0; i--)
{
//UE_LOG(LogTemp, Warning, TEXT("InvertedPath[i]:%d,%d"), InvertedPath[i].X, InvertedPath[i].Y);
Res.Add(InvertedPath[i]);
}
return Res;
}