[C++/UE5] 使用Spline進行AI巡邏路線開發的經驗分享

        最近在工作上接到一個需求,是要給策劃提供配置Npc的巡邏路線的流程。一開始因為策劃那邊並沒有甚麼要求,只要能在不同點之間巡邏就行,所以為了貪方便,就做了最簡單最經典的一版。就是在場景上事先放好N個點,然後Npc藍圖再接收一個Actor列表的變量,把這些點拖給這個變量;最後再讓Npc通過行為樹的執行,分別用順序模式(1->2->3->1->2->3)或YoYo模式(1->2->3->2->1)來遍歷移動到這些巡邏點Actor上。

        這個方法雖然用確實能用,跑起來也沒問題;但是有太多原因,以致於後面實際配起來還是各種不方便,比如由於UE在Outliner裡多選Actor是不會把他們的位置都顯示出來的;而且給Npc的列表變量賦值也不像Unity那樣,可以直接把多個對象拖到變量上,而是要一個一個拖,太難受了。

        而且這種方法也只支持點到點的直線移動,很難做到一種圓滑拐彎的感覺,所以後面Leader就讓我嘗試找一下像策劃配河流、彎路這種工具,看看能不能也應用在巡邏路線的配置上。

        結果還真給我找到了Spline這個組件。Spline的功能簡單來說就是給用戶在場景上標記多個點,然後它會根據每兩個點之間的tangent值,得出能連上兩點的線段。然後這些點可以選中單獨移動,按住Alt再拖動會新增一個新點,Spline類也提供了獲取這些點、根據起點開始距離獲取具體Location、根據Location獲取在線段上的距離等接口;最重要的一點是,它還會顯示出具體的路線!

        所以我建了一個Actor藍圖,加了Spline組,並隨便加了一些點,大概的效果就是這樣:

        然後我通過SplineComponent.GetLocationAtSplinePoint(Index, SpaceType)分別獲取到這些點的世界坐標,並應用到上一套的直線移動方案當中,就實現了基本的Spline巡邏路線配置

FVector AYLSplinePath::GetPatrolPointLocationByIndex(int Index, ESplineCoordinateSpace::Type SpaceType)
{
	ConfirmSplineComp();
	
	if(SplineComp)
	{
		int SplinePointCount = SplineComp->GetNumberOfSplinePoints();
		if(Index >= 0 && Index < SplinePointCount)
		{
			return SplineComp->GetLocationAtSplinePoint(Index, SpaceType);
		}
	}
	return FVector::ZeroVector;
}

        但是既然已經有具體的路線了,單純的直線移動肯定不滿足需求了。Spline雖然有給每個點的位置,但是卻沒有提供讓Actor沿著線段移動的接口,所以這一塊我們只能自己做。

        在網上搜了一圈,一般來說有兩種思路:

        第一種太複雜,還要依賴Timeline,而且由於我們是用來戰鬥Npc的巡邏功能,隨時會離開/返回巡邏路線,不是那種只需要一直循環播放就行的動畫,所以我選擇了第二種思路進行開發。

        在第二種思路的情況下,怎樣切分子線段,讓它們用一種怎樣的分布也是一個問題。如果使用平均分布:

        

	FVector FirstLocation = GetPatrolPointLocationByIndex(PrevIndex, SpaceType);
	FVector NextLocation = GetPatrolPointLocationByIndex(CurrIndex, SpaceType);
	
	float Distance1 = SplineComp->GetDistanceAlongSplineAtSplinePoint(PrevIndex);
	float Distance2 = SplineComp->GetDistanceAlongSplineAtSplinePoint(CurrIndex);
	float DistanceDiff = Distance2 - Distance1;
	float Step = DistanceDiff / SplitCount;
	
	TArray<FVector> Results;
	for(int i = 0; i < SplitCount; i++)
	{
	    FVector TargetPos = SplineComp->GetLocationAtDistanceAlongSpline(Distance1 + Step * (i + 1), SpaceType);
	    Results.Add(TargetPos);
	}

        在兩點之間的直線部分,是非常穩定的能沿著線段走,但是在線段起點/終點的轉彎部分,就會呈現直線移動的表現。兩點的距離越大,線段越長,分割數量越少,這個情況越嚴重,因為第一個子線段和最後一個子線段很可能沒有很好的覆蓋到起點和終點的彎曲部分。

        為了解決這個問題,我需要一種把子線段主要分布在起點和終點的分割方式,經過搜索,我找到了SmoothStep函數,是S型函數的一種。

        在我的情況下,x = i/SplitCount – 1,0 <= i <= SplitCount – 1。i = 0時, 子點位於起點上,i = SplitCount – 1時,子點位於終點上;在線段中間前的其他點,距離會越來越疏遠;而在中間後的點,距離又會越來越緊密。

        為了應用這套規則,我根據3x^2 – 2x^3獲取到具體某個子點在起點和終點間的插值(Alpha),然後通過FMath::Lerp(起點Distance,終點Distance,Alpha)獲取到該子點所在的Distance;最後通過SplineComp->GetLocationAtDistanceAlongSpline,根據距離獲取到具體的Location。

TArray<FVector> AYLSplinePath::SplitMidPointsBetweenPatrolPoints(int PrevIndex, int CurrIndex, int SplitCount, ESplineCoordinateSpace::Type SpaceType)
{
	ConfirmSplineComp();

	TArray<FVector> Result;
	
	if(SplineComp && SplitCount > 1)
	{
		int SplinePointCount = SplineComp->GetNumberOfSplinePoints();
		if(PrevIndex >= 0 && PrevIndex < SplinePointCount &&
			CurrIndex >= 0 && CurrIndex < SplinePointCount)
		{
			FVector FirstLocation = GetPatrolPointLocationByIndex(PrevIndex, SpaceType);
			FVector NextLocation = GetPatrolPointLocationByIndex(CurrIndex, SpaceType);
			
			float Distance1 = SplineComp->GetDistanceAlongSplineAtSplinePoint(PrevIndex);
			float Distance2 = SplineComp->GetDistanceAlongSplineAtSplinePoint(CurrIndex);
			UE_LOG(LogTemp, Log, TEXT("Distance1: %f, Distance2: %f"), Distance1, Distance2);

			TArray<float> NormalizedDistances;
			
			for(int i = 0; i < SplitCount; ++i)
			{
				float Alpha = static_cast<float>(i) / static_cast<float>(SplitCount - 1);
				Alpha = 3 * FMath::Pow(Alpha, 2) - 2 * FMath::Pow(Alpha, 3);
				NormalizedDistances.Add(Alpha);
				
				float Distance = FMath::Lerp(Distance1, Distance2, Alpha);
				FVector Location = SplineComp->GetLocationAtDistanceAlongSpline(Distance, SpaceType);
				UE_LOG(LogTemp, Log, TEXT("Alpha: %f, Location: %s, Distance: %f"), Alpha, *Location.ToString(), Distance);

				Result.Add(Location);
			}
			
			Result.Insert(FirstLocation, 0);
			Result.Add(NextLocation);
		}
	}

	return Result;
}

        最後,是SplitCount = 16的表現

Leave a Comment

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

這個網站採用 Akismet 服務減少垃圾留言。進一步了解 Akismet 如何處理網站訪客的留言資料