パスに沿ってなめらかに一定速度でオブジェクトを移動させる実装の解説

![my.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79902/bc3a1e57-1fa4-f1d2-e9f8-2cee9c51376e.gif)

# まえがき

## 問題

ゲームだと、パスに沿ってキャラを移動させたい時があります。
ただ、単純に実装すると速度がバラバラになってしまいます。

## 目的
今回は曲線でもだいたい一定の速度で動かせるようにする実装を解説します。
UnityならDoTweenのDoPathやCinemachineのCinemachinePathを使用すれば実装しなくても可能ですが、ベジェ曲線のみで、他の曲線を使うことはできません。
自分で実装すれば、好きな曲線を使えます。
ただ、今回は解説のため、ベジェ曲線を使います。

https://ja.wikipedia.org/wiki/ベジェ曲線

```c#
Vector3 Bezier3(float t, Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4)
{
    var d = 1 - t;
    return      d * d * d * p1 +
            3 * d * d * t * p2 +
            3 * d * t * t * p3 +
                t * t * t * p4;
}
```


## 参考

この記事はCinemachineのCinemachinePathBaseを参考に作っています。
[github](https://github.com/Unity-Technologies/upm-package-cinemachine/blob/a21def54acdce71e3b8885ae8444a04fca9ca7fa/Runtime/Core/CinemachinePathBase.cs)

# 解説
## 単純な実装
![first.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79902/9f60c00c-685f-33be-0248-85fc850c6a60.gif)


これはベジェ曲線の引数tに$Time.t$を渡すだけの実装です。
明らかに速度がおかしいです。
0-0.1と0.5-0.6の移動距離が違うのと、
スタート位置から1つ目のパスまで1秒、そこからゴールまで1秒かけているのが原因です。

```c#
void Update()

{
    var t = Time.time;
    var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t));
    var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1));

    if (indexA == indexB) return;

    transform.position = CalcPos(t);
}

Vector3 CalcPos(float t)
{
    var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t));
    var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1));
    return Bezier3(
        t-indexA,
        Paths[indexA].Pos,
        Paths[indexA].Pos + Paths[indexA].Tangent,
        Paths[indexB].Pos - Paths[indexB].Tangent,
        Paths[indexB].Pos);
}
```

## 改善した物
![my.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/79902/bc3a1e57-1fa4-f1d2-e9f8-2cee9c51376e.gif)


この問題はtを0-ベジェ曲線の長さで扱えれば速度をだいたい一定にすることができます。

やっていることは、tを少しずつ動かして、進んだ距離を測る。
進んだ距離からtを返すテーブルを作るという事です。

以上です。
こうすると、距離からtに変換する関数を作成できるので、だいたい一定の速度で移動できるようになります。

<details><summary>コード</summary>
<div>

```c#
[SerializeField] int Segment;
[SerializeField] PathContainer Paths;

float PathLength;

//セグメントの総数
int NumKeys;
float[] DistanceToTArray;
float DistanceStepSize;

void Start()
{
    Build();
}

void Build()
{
    PathLength = 0;
    NumKeys = (Paths.Length-1) * Segment+1;

    var tToDistance = CalcTToDistance();
    DistanceToTArray = CalcDistanceToT(tToDistance);
}

void Update()
{
    transform.position = CalcPos(DistanceToT(Time.time*PathLength/2));
}

//距離からtに変換
float DistanceToT(float distance)
{
    float d = distance / DistanceStepSize;
    int index = Mathf.FloorToInt(d);
    if(index>=DistanceToTArray.Length-1)return DistanceToTArray[DistanceToTArray.Length-1];
    float t = d - index;
    return Mathf.Lerp(DistanceToTArray[index], DistanceToTArray[index+1], t);
}

//tをSegmentに分割して進んだ距離を配列に入れて返す
float[] CalcTToDistance()
{
    var tToDistance = new float[NumKeys];
    
    var pp = Paths[0].Pos;
    float t = 0;
    for (int n = 1; n < NumKeys; n++)
    {
        t += 1f / Segment;
        Vector3 p = CalcPos(t);
        float d = Vector3.Distance(pp, p);
        PathLength += d;
        pp = p;
        tToDistance[n] = PathLength;
    }

    return tToDistance;
}

//距離をSegmentに分割してその位置のtを配列に入れて返す
float[] CalcDistanceToT(float[] tToDistance)
{
    var distanceToT = new float[NumKeys];
    distanceToT[0] = 0;
    DistanceStepSize = PathLength/(NumKeys-1);
    float distance = 0;
    int tIndex=1;
    for (int i = 1; i < NumKeys; i++)
    {
        distance += DistanceStepSize;
        var d = tToDistance[tIndex];
        while (d < distance && tIndex < NumKeys - 1)
        {
            tIndex++;
            d = tToDistance[tIndex];
        }

        var prevD = tToDistance[tIndex - 1];
        float delta = d - prevD;
        float t = (distance - prevD) / delta;
        distanceToT[i] = (1f/Segment)*(t + tIndex - 1);
    }

    return distanceToT;
}

Vector3 CalcPos(float t)
{
    var indexA = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, t));
    var indexB = Mathf.FloorToInt(Mathf.Min(Paths.Length - 1, indexA+1));
    return Bezier3(
        t-indexA,
        Paths[indexA].Pos,
        Paths[indexA].Pos + Paths[indexA].Tangent,
        Paths[indexB].Pos - Paths[indexB].Tangent,
        Paths[indexB].Pos);
}
```

</div>
</details>

# あとがき
以上です。
動作するコードはこちらです。
https://github.com/nakajimakotaro/PathSmoothMove