UnityのSplineを設置するときのちょっとした便利ツールを作る

パスに沿ってオブジェクトを動かすという動作を実装することになった。
その際にSplineを利用するということが決まったがその設定がかなりしんどい...。

例えば下の画像のように4つの座標で任意の輪っかを設定するとなった際にはそれぞれのKnotのPositionを1つずつ設定する必要がある。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/4f31fd40-bbce-ab28-a3ca-e0c5dd42c22d.png)

これくらい簡単な図形ならまだよいが、例えば大きめの街に線路を引いて電車を走らせるみたいなことをする際にはきっと設定する座標も多く時間がかかってしまう...（超めんどくさかった！）


というわけで少し楽をできるようにスクリプトから生成するようにしたのがこちら。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/61181796-9db8-ae5c-5ad1-2645164ba4dc.png)

何らかのTransformを配置し、その位置をもとにSplineを生成するというもので、例で上げた画像はわかりやすいようにCubeを設置して、その位置をもとにSplineを生成している。

座標を設定するという点では同じだが、こちらはオブジェクトのTransformを指定しての設定なので3Dマップを作るときなど普段の操作と同様の感覚で設定できるので楽なはず！

使い方はInspectorのSerializeFieldにパスの座標にするTransformと、SplineContainerをアタッチしてボタンを押すだけ！
![デモ.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/3ee9af72-cebd-0656-0530-cfc16cd1747d.gif)

私は基準のSplineの外側にいくつか並走するラインを作りたかったので複数のSplineContainerを指定して設定幅分外側にもラインが引かれるような実装をしています。

（三本のラインが同心円上になるように設定してあるイメージ）
![スクリーンショット 2024-05-18 20.00.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/60e49b40-7d8e-97f8-43a4-14619b00c796.png)


以下はこの実装に利用したコードです。
```cs
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.Splines;

/// <summary>
/// オブジェクトの位置を元にスプラインを生成するクラス
/// </summary>
public class CourseSplineSetter : MonoBehaviour
{
#if UNITY_EDITOR
    [SerializeField] private Transform[] _splinePoints;
    [SerializeField] private SplineContainer[] _splineContainers;
    [SerializeField] private float width = 10.0f;
    [SerializeField] private bool _isClosed = true;
    [SerializeField] private TangentMode _tangentMode = TangentMode.AutoSmooth;
    
    public void CreateSpline()
    {
        for (int i = 0; i < _splineContainers.Length; i++)
        {
            float offset = i * width; // 各パスの幅を設定
            List<Vector3> offsetPath = CalculateOffsetPath(_splinePoints, offset);
            CreateSplineFromPath(_splineContainers[i].Spline, offsetPath);
        }
    }
    
    private List<Vector3> CalculateOffsetPath(Transform[] points, float offset)
    {
        List<Vector3> offsetPath = new List<Vector3>();
        int pointCount = points.Length;

        for (int i = 0; i < pointCount; i++)
        {
            Vector3 prevPoint = points[(i - 1 + pointCount) % pointCount].transform.position;
            Vector3 currentPoint = points[i].transform.position;
            Vector3 nextPoint = points[(i + 1) % pointCount].transform.position;
            
            var toCurrent = (currentPoint - prevPoint).normalized;
            var toNext = (nextPoint - currentPoint).normalized;
            offsetPath.Add(currentPoint + (toCurrent - toNext) * offset);
        }
        return offsetPath;
    }

    void CreateSplineFromPath(Spline spline, List<Vector3> pathPoints)
    {
        spline.Clear();
        foreach (var point in pathPoints)
        {
            spline.Add(new BezierKnot(point));
        }

        spline.Closed = _isClosed;
        spline.SetTangentMode(_tangentMode);
        
        SceneView.RepaintAll();
    }
#endif
}

#if UNITY_EDITOR
[CustomEditor(typeof(CourseSplineSetter))]
public class CourseSplineSetterWindow : Editor
{
    public override void OnInspectorGUI()
    {
        base.OnInspectorGUI();

        var setter = target as CourseSplineSetter;
        // Inspector上でボタンを表示
        if (!EditorApplication.isPlaying && GUILayout.Button("Create Spline"))
        {
            setter.CreateSpline();
        }
        
    }
}
#endif
```