[Unity] Dreamteck Splines實現沿路徑移動功能

千松發表於2024-07-14

Dreamteck Splines實現沿路徑移動功能

最近有一個“讓物體沿固定路徑移動”的需求,因此接觸到了Dreamteck Splines外掛。

Dreamteck Splines可以很方便地繪製各種插值曲線,但在實現物體移動的時候卻遇到了很多坑,因此在這裡記錄一下。

1. 繪製路徑線

首先,讓我們在場景上建立一個空物體,並新增SplineComputer元件。

img

由於我這是個2D專案,所以選擇在Z平面上繪製。

img

之後編輯器中就會顯示出跟隨滑鼠的網格線,點選左鍵就可以逐點繪製Spline了。

img

img

在右側選項中可以修改Spline的型別。預設型別是Catmull Rom,我們可以把它改成直線Linear

img

img

2. 獲取座標

SplineComputer類有兩類獲取座標點的方法:

  • GetPoint(int)
  • GetPoints()

這兩個方法用於獲取我們手動新增的座標點,也就是我們上圖中的那三個點。這明顯不符合我們“沿路徑移動”的需求。

而下面這三個方法才是返回Spline上的所有座標點

  • Evaluate(double)
  • EvaluatePosition(double)
  • EvaluatePositions()

其中,EvaluateEvaluatePosition的區別在於,Evaluate返回的是SplineSample物件,包括了座標、朝向、顏色、百分比等資訊,而EvaluatePosition則是簡單地返回一個Vector3的座標。在只需要座標的情況下,推薦使用更加輕量化的EvaluatePosition

3. 移動示例(踩坑)

EvaluatePosition(double)方法傳入一個0~1的值,就會返回Spline上對應的座標,因此我們可以用這個指令碼手動模擬物體的移動過程。

using Dreamteck.Splines;
using UnityEngine;

public class Move : MonoBehaviour
{
    [Range(0, 1)]
    public float Percent;
    public Transform Target;
    private SplineComputer spline;


    void Start()
    {
        spline = GetComponent<SplineComputer>();
    }

    void Update()
    {
        Target.position = spline.EvaluatePosition(Percent);
    }
}

將指令碼掛載到SplineComputer所在的物體上,拖動右側的滑動條即可移動目標物體。

img

一切看似十分正常,直到我們又新增了一條長度不同的線段。

img

可以明顯地看出,在後面這個較短的路徑中,物體的移動速度明顯變慢了。

當我們直接將進度設為0.5後,便能發現問題所在。

img

目標物體移動並沒有移動到Spline的終點,而是移動到了我們設定的第二個控制點上。

這個問題在官方文件3.3. Sample Mode中有對應的解答:

預設情況下,樣條曲線(Spline)在 [0-1] 的百分比範圍內進行計算(evaluated),涵蓋了所有座標點。

例如,一條由 3 個點組成的樣條曲線,計算百分比為 0.5 的座標點,將始終返回第二個點的位置,因為它位於中間。

img

然而,如果第一個點和第二個點非常接近,而第三個點距離它們很遠,計算百分比為 0.5 的座標點不會返回樣條曲線的中間位置,它仍將返回第二個點。因為某些區域的取樣點比其他區域更密集

img

為了說明這一點,以下是顯示了取樣點密度的樣條曲線:每條垂直線表示一個取樣點(spline sample)。在這種情況下,點 1 和點 2 之間有 10 個取樣點,但點 2 和點 3 之間也只有 10 個取樣點。

img

從中我們可以看出,問題的根源在於,Evaluate引數中的percent並不是指Spline長度的百分比,而是表示Spline取樣點的百分比。而取樣點的不均勻分佈,導致了取樣點百分比和長度百分比不一致的情況。

5 方法一:修改取樣模式(Sample Mode)

前面我們提到"取樣點的不均勻分佈,導致了取樣點百分比和長度百分比不一致的情況"。

反過來說,我們只需要讓取樣點能夠均勻分佈,就可以解決這一問題。

SplineComputer提供了三種取樣模式(Sample Mode):

  • Default(預設):兩點間的取樣點數量固定
    img
  • Uniform(均勻):根據Spline長度,均勻分佈取樣點。但在Spline較長時會有更大的效能開銷。
    img
  • Optimized(最佳化):與預設模式相同,但會執行最佳化操作刪除不必要的取樣點
    img

所以我們需要選擇Uniform模式,以實現均勻分佈取樣點的需求。

請注意,在Default和Optimized模式下,當移動控制點時,Spline僅更新受該點影響的區域中的取樣點。而在Uniform模式下,將重新計算整個Spline。在Optimized模式下,還提供了一個額外的滑塊來控制最佳化的角度閾值。

img

這樣一來就能正確地勻速移動目標了

img

但是!

除了效能開銷外,這個方法還會帶來一系列問題。

首先,它會導致線段脫離控制點:

img

其次,它還有個很致命的BUG

Uniform模式下,如果你用CalculateLength方法獲取Spline的長度,那麼初始狀態下將會始終返回0。此時必須對他"進行一些操作",比如移動控制點,修改其他引數等,讓他響應一次變化。之後CalculateLength才能正確返回數值。

img

6 方法二:使用Travel函式(推薦)

為了避免上述問題,我們可以使用Travel函式計算某個長度在Spline上對應的取樣點百分比。

它的使用方法在官方文件20.4. Converting World Units to Spline Percentages中有所提及。

假如我們要獲取Spline中心點的座標,只需要傳入Spline長度的一半,也就是spline.CalculateLength() / 2,然後Travel函式就會返回對應的percent。這時再呼叫EvaluatePosition(percent)即可得到中心點的座標。其他位置的座標也是同理,我們只需要給出對應的長度即可獲取座標。

這樣一來,我們就可以方便地實現沿路徑勻速移動的功能了。

using Dreamteck.Splines;
using UnityEngine;

public class Move : MonoBehaviour
{
    [Range(0, 1)]
    public double Percent;
    public float Speed;
    public Transform Target;
    private float distance;
    private SplineComputer spline;


    void Start()
    {
        spline = GetComponent<SplineComputer>();
        distance = 0;
    }

    void Update()
    {
        distance += Speed * Time.deltaTime;
        // 有需要的話可以用這個限制上限
        // distance = Math.Min(distance, spline.CalculateLength()); 
        Percent = spline.Travel(0, distance);
        Target.position = spline.EvaluatePosition(Percent);
        
        if (Percent == 1)
        {
            // do something
            Debug.Log("Done");
        }
    }
}

效果如下

img

img

參考資料

Dreamteck Splines – User Manual


本文釋出於2024年7月14日

最後編輯於2024年7月14日

相關文章