Unity - 移動を簡単にするシステム"Splines"について

:::note warn
注意
ここで提供した情報は後で変更になる場合があります。その際は公式の情報を参考にしてください。
この記事で対象にしているのは、Ver.2.7です。(機能自体はあんまり変わらないかも)
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042593/eed4760a-c2ae-4d15-b405-f4f39f0a621f.png)[^1]
***
### Splinesについて雑談
`Splines`というのは、Unityの公式パッケージに入っている機能です。2022年1月から、リリースされ、記事を執筆した現在(2025年3月)では、Ver.2.8がリリースされています。
どうやら、標準機能として搭載してほしかったという要望が多かったらしいです。
>以前から標準機能として搭載して欲しいというリクエストが多く寄せられていたんですよね。これがようやく公式パッケージとして提供されることになった、という格好になっています。[^2]
### 本題のSplineを使ってみよう
まず、Splineをインストールします。Package Managerを開き、`Splines`と検索します。
そうすることで、以下のような画面になると思いますので、赤く囲っている`Install`ボタンをクリックしてください。(私はすでにSplinesを読み込んでいるので、表示が異なります。)
![スクリーンショット 2025-03-23 173833.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4042593/7cbde08a-d1fe-465e-9ec0-4f5ac3a21c24.png)
インポートできたら、ヒエラルキー上でマウス右クリックもしくは、`GameObject`ボタンから`Spline` 内の`New Spline`を選択し、シーン上でクリックしてSplineを書きます。`Enter`キーを押すことで終了できます。
あとの方法は[こちら](https://unity.com/ja/blog/engine-platform/building-better-paths-with-splines-in-2022-2 "Unity 2022.2 の Splines を使って、フロー状態でより美しいパスを描き出そう")を参照してください（他力本願ダナァー）

### Splineに沿ってオブジェクトを移動させよう
オブジェクトをSplineに沿って移動させるには、**移動させたいオブジェクト**の`Inspector`タブの`Add Component`をクリックし、`Spline Animate`と検索します。Splineの項目に、先程描いたSplineをアタッチしてください。実行するとオブジェクトがスプラインに沿って動くと思います。[^3]

### 自分でスクリプトを作りたい！
先程の項では、あらかじめプログラムされたスクリプトを使って動かしていましたが、どうです？**機能多すぎません？** 自分で使いこなせますか？ということで、ここからは、スプラインに沿ってオブジェクトを動かすスクリプトを作成しましょう！

### Splineをスクリプトから動かす[^4]
基本的な、スプラインを移動させるスクリプトを提示します。
```c#:SplineSystem.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class SplineSystem : MonoBehaviour
{
    [SerializeField] private SplineContainer splineContainer;
    [SerializeField] private Transform _followTarget; //この中にSplinesに追従させたいオブジェクト(正確には、オブジェクトの位置情報:transform)を入れる。
    [SerializeField] private float SplinesPercentage;
    [SerializeField] private Vector3 AddUpPosition;//おまけ

    void Update()
    {
        if (splineContainer == null || _followTarget == null)
            return;

        // 位置を更新
        Vector3 position = splineContainer.EvaluatePosition(SplinesPercentage);

        position += AddUpPosition;//指定されたベクトルを足す(数値がマイナスのときは、減算される)

        _followTarget.position = position;

        // 回転を更新
        Vector3 tangent = ((Vector3)splineContainer.EvaluateTangent(SplinesPercentage)).normalized;
        Vector3 up = ((Vector3)splineContainer.EvaluateUpVector(SplinesPercentage));
        _followTarget.rotation = Quaternion.LookRotation(tangent, up);
    }
}
```
:::note warn
注意
Splineをスクリプトから扱う際は、必ず
```c#
using UnityEngine.Splines;
```
と記入してください。
:::

「スクリプトにある`[SerializeField]`って何やねん」と思った方はこちらも参照してください。

https://qiita.com/Cotorain/items/2484b8c9d5e809c938a6

`SplinePosition`の値を変更すると、オブジェクトが移動できたかと思います。Splineの回転も適応されているかと思います。
肝心の仕組みは、ここで説明するのがめんどくさいので、こちらをご覧ください。

https://nekojara.city/unity-splines-script

### さっきのスクリプトを拡張しよう
ここでは、
1⃣Spline上の始点からの道のりを入力すると、そこに移動するスクリプト
2⃣速度(km/h)を入力すると、その速度でSpline上を移動することができるスクリプト
を紹介します。
### スクリプトを拡張しよう その1⃣
まず、先ほどのスクリプトでは、位置は割合で計算されています。
ということは、**スプラインの全体の長さが分かれば、始点からの道のりがわかる気がしませんか？**
そのスプラインの長さを求める関数は、
```c#
CalculateLength();
```
というものです。実際には、アタッチされたスプライン(最初のスクリプトでは`splineContainer`)の所属になるのですが。これでスプラインの長さを計算できます。
以下にサンプルスクリプトを示します。
```c#:SplineSystem.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class SplineSystem : MonoBehaviour
{
    [SerializeField] private SplineContainer splineContainer;
    [SerializeField] private Transform _followTarget;
    private float SplinesPercentage; //変更(完全にprivate化)
    [SerializeField] private float SplineLength_Position;//追加(スプラインの始点からの道のりを入力)
    /*[SerializeField]*/ private float SplineLength;//追加(指定されたSplineの長さを取得する変数)
    [SerializeField] private Vector3 AddUpPosition;
    [SerializeField] private float AddLength;//おまけで追加
    public TrainSystem trainSystem;

    void Start()
    {
        SplineLength = splineContainer.CalculateLength(); //追加(指定されたSplinesの長さを取得)
    }

    void Update()
    {
        if (splineContainer == null || _followTarget == null)
            return;
        
        SplinesPercentage = (SplineLength_Position + AddLength) / SplineLength; //追加(距離(Unity Unit)を割合に変換し、位置を決定する変数に代入)

        // 位置を更新
        Vector3 position = splineContainer.EvaluatePosition(SplinesPercentage);

        position += AddUpPosition;//指定されたベクトルを足す(数値がマイナスのときは、減算される)

        _followTarget.position = position;

        // 回転を更新
        Vector3 tangent = ((Vector3)splineContainer.EvaluateTangent(SplinesPercentage)).normalized;
        Vector3 up = ((Vector3)splineContainer.EvaluateUpVector(SplinesPercentage));
        _followTarget.rotation = Quaternion.LookRotation(tangent, up);
    }
}
```
メッセージで`//追加`と書かれているところが拡張したところです。
### スクリプトを拡張しよう その2⃣
:::note warn
注意
いまから提供するスクリプトは、
`1m = 1 Unity Unit`スケールになっていることを前提とします。
:::
今度は、時速(km/h)を指定してオブジェクトを動かしてみましょう！
ところで、時速(km/h)って、どうやって計算するんでしたっけ？秒に直して、Unity上で実行するとすると、答えは、
```
始点から進んだ距離 += (時速の値(=km/h) * (1000f/3600f)) * Time.deltaTime;
```
です。
1時間=3600秒、そして1km = 1000m = 1000Unity Unitなので、それらを割り、Time.deltaTimeでフレームレートが変わった分を補完してあげることで計算してくれます。
以下に、スクリプトを示します。
```c#:SplineSystem.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Splines;

public class SplineSystem : MonoBehaviour
{
    [SerializeField] private SplineContainer splineContainer;
    [SerializeField] private Transform _followTarget;
    private float SplinesPercentage;
    [SerializeField] private float SplineLength_Position;
    /*[SerializeField]*/ private float SplineLength;
    [SerializeField] private Vector3 AddUpPosition;
    [SerializeField] private float AddLength;
    [SerializeField] private float ObjectSpeed;//追加(この中に時速(km/h)を入れる)
    void Start()
    {
        SplineLength = splineContainer.CalculateLength();
    }

    void Update()
    {
        if (splineContainer == null || _followTarget == null)
            return;

        SplineLength_Position += (ObjectSpeed * (1000f / 3600f)) * Time.deltaTime;//追加(km/h単位をm/secに直して計算)
        
        SplinesPercentage = (SplineLength_Position + AddLength) / SplineLength;

        // 位置を更新
        Vector3 position = splineContainer.EvaluatePosition(SplinesPercentage);

        position += AddUpPosition;//指定されたベクトルを足す(数値がマイナスのときは、減算される)

        _followTarget.position = position;

        // 回転を更新
        Vector3 tangent = ((Vector3)splineContainer.EvaluateTangent(SplinesPercentage)).normalized;
        Vector3 up = ((Vector3)splineContainer.EvaluateUpVector(SplinesPercentage));
        _followTarget.rotation = Quaternion.LookRotation(tangent, up);
    }
}
```
先ほどと同じく、`//追加`とメッセージで書いてあるところが、追加したスクリプトです。
### また雑談
ここまで記事を読んでいただき、ありがとうございました！私事ですが、ようやく学校の修了式が終わってのんびりしながら書いていました。また、受験シーズンで忙しくなっていやいやと言っていますが(^^;)
そして、そんな受験を一緒に頑張る同志はおるか？小学生でも中学生でも高校生でも、受験を受ける人たちたちは一緒に頑張りましょう...

おわり！
[^1]:画像・参考サイト：[Unity 2022.2 の Splines を使って、フロー状態でより美しいパスを描き出そう](https://unity.com/ja/blog/engine-platform/building-better-paths-with-splines-in-2022-2 "Unity 2022.2 の Splines を使って、フロー状態でより美しいパスを描き出そう")
[^2]:引用：[Unity 2022 新機能！スプラインを使ってみよう！ - YouTube](https://www.youtube.com/watch?v=5IrKqVnvP6M)
[^3]:詳しい方法はこちらから：[【Unity2022】スプラインツールの導入方法と使い方｜ねこじゃらシティ](https://nekojara.city/unity-splines-intro)
[^4]:参考サイト：[【Unity2022】スプラインをスクリプトから扱う方法｜ねこじゃらシティ](https://nekojara.city/unity-splines-script "【Unity2022】スプラインをスクリプトから扱う方法｜ねこじゃらシティ")