【Unity2022】スプラインをスクリプトから扱う方法

こじゃらこじゃら

Unity公式のスプラインツールで作成した曲線の情報をスクリプトからあれこれしたい場合はどうすればいいの?

このはこのは

Splinesパッケージの公式リファレンスを交えながら解説していくね。

Unity公式のビルトインスプラインツールSplinesパッケージ)で作成したスプラインをスクリプトから読み書きする方法の紹介です。

これらの情報は以下公式リファレンスにまとめられています。

参考: | Splines | 1.0.1

スプラインとスクリプトを連携させることで、例えばのようなことが可能になります。

スクリプト連携で出来ること
  • スプライン上の位置を取得する
  • 道のりを計算する
  • ある点に最も近いスプライン位置を計算する
  • スプラインをスクリプトから作成する

ここに挙げた以外にも、工夫次第で様々なことが実現可能です。

本記事では、このようにスプラインとスクリプトと相互運用する方法について解説していきます。

動作環境
  • Unity 2022.1.0f1
  • Splines 1.0.1

スポンサーリンク

前提条件

Unity 2022.1以降のUnityプロジェクトSplinesパッケージがインストールされているものとします。

ここまでの手順がわからない方は、以下記事で導入手順をまとめていますのでご覧ください。

スプライン情報へのアクセス方法

スプライン情報は、SplineContainerクラスのコンポーネントが管理しています。

参考:Class SplineContainer| Splines | 1.0.1

スクリプトからは、例えば次のようにアクセスして使います。

// SplineContainerを取得する
var splineContainer = GetComponent<SplineContainer>();

ここからは、SplineContainerを通じてスプライン情報を取得したり、編集したりする例をいくつか紹介します。

スプライン上の位置を取得する

スプラインには始点終点があり、両者を0~1の範囲で補間した位置を取得することができます。

取得には次のSplineContainer.EvaluatePositionメソッドを使います。

public float3 EvaluatePosition(float t)

引数には、0~1の範囲で割合をしていします。0は始点、1は終点を意味します。言い換えると、視点からの移動距離を0~1に正規化した値を指定します。

戻り値は、計算されたスプライン上のワールド空間の位置です。

参考:Class SplineContainer| Splines | 1.0.1

戻り値の型はfloat3となっていますが、これはMathematicsパッケージで定義された独自のベクトル型です。Vector3型に暗黙的に変換できます。

参考:Struct float3| Mathematics | 1.2.6

注意

ワールド空間の位置を取得する場合、SplineContainer.Spline.EvaluatePositionではなくSplineContainer.EvaluatePositionメソッドを使用してください。

SplineContainer splineContainer;
float percentage;

・・・(中略)・・・

// ワールド空間になる
var pos = splineContainer.EvaluatePosition(percentage);

以下のようにすると、ローカル空間の位置となります。

SplineContainer splineContainer;
float percentage;

・・・(中略)・・・

// ローカル空間になる
var pos = splineContainer.Spline.EvaluatePosition(percentage);

サンプルスクリプト

オブジェクトをスプライン上の位置に動かすサンプルスクリプトです。

SplineFollow.cs
using UnityEngine;
using UnityEngine.Splines;

public class SplineFollow : MonoBehaviour
{
    // スプライン
    [SerializeField] private SplineContainer _splineContainer;
    
    // スプラインに沿って移動させる対象
    [SerializeField] private Transform _followTarget;

    // 補間の割合
    [SerializeField, Range(0, 1)] private float _percentage;

    private void Update()
    {
        // 念のためNullチェック
        if (_splineContainer == null || _followTarget == null)
            return;

        // 計算した位置(ワールド座標)をターゲットに代入
        _followTarget.position = _splineContainer.EvaluatePosition(_percentage);
    }
}

上記スクリプトをSplineFollow.csという名前でUnityプロジェクトに保存し、Spline ContainerFollow Targetにそれぞれスプラインオブジェクト、スプラインに沿って移動させたいオブジェクトを指定します。

Percentageには0~1の範囲の割合を指定します。

実行結果

インスペクターからPercentageを変更すると、割合で補間されたスプライン位置にオブジェクトが動きます。

スプラインの場所によらず移動速度が一定になるように調整してくれます。

この辺の仕組みは、以下記事で解説しています。

スクリプトの解説

肝となる部分は以下コードです。

// 計算した位置(ワールド座標)をターゲットに代入
_followTarget.position = _splineContainer.EvaluatePosition(_percentage);

EvaluatePosition()メソッドで計算されたワールド位置をターゲットのワールド位置に指定しています。

これで、ターゲットがスプラインに沿って動くようになります。

スプライン全体の長さを計算する

始点から終点までの道のりは、以下メソッドで計算できます。

public float CalculateLength()

戻り値としてスプライン全体の長さが得られます。

参考:Class SplineContainer| Splines | 1.0.1

この全体の道のりを利用すると、割合tを用いて開始からの移動距離を求めたり、逆に移動距離から割合tを求めたりできます。

// スプライン
SplineContainer splineContainer;
// 割合t
float t= 0.5f;

・・・(中略)・・・

// スプライン全体の道のりを計算
var length = splineContainer.CalculateLength();

// 割合tに道のりを掛けると視点からの移動距離が計算できる
var distanceFromStart = t * length;
// スプライン
SplineContainer splineContainer;
// 始点から進む距離
float distanceFromStart = 5;

・・・(中略)・・・

// スプライン全体の道のりを計算
var length = splineContainer.CalculateLength();

// 進む距離と道のりから割合を算出
var t = distanceFromStart / length;

この始点からの移動距離と割合tの相互変換は、割合tに対してスプライン全体の道のりを掛けたり割ったりすることで求められるのは覚えておくと良いでしょう。

サンプルスクリプト

スプライン全体の道のりを利用して、始点から指定距離進んだ位置に移動するサンプルスクリプトです。

SplineLength.cs
using System;
using UnityEngine;
using UnityEngine.Splines;

public class SplineLength : MonoBehaviour
{
    // スプライン
    [SerializeField] private SplineContainer _splineContainer;

    // スプラインに沿って移動させる対象
    [SerializeField] private Transform _followTarget;

    // 始点から進む距離
    [SerializeField] private float _distanceFromStart;

    private void Update()
    {
        // 念のためNullチェック
        if (_splineContainer == null || _followTarget == null)
            return;

        // 全体の道のり計算
        var length = _splineContainer.CalculateLength();

        // 進む距離と道のりから割合を算出
        var percentage = _distanceFromStart / length;

        // 位置反映
        _followTarget.position = _splineContainer.EvaluatePosition(percentage);
    }
}

上記スクリプトをSplineLength.csとして保存し、インスペクターより各種設定を行うと機能します。

Distance From Startには、始点から進みたい距離を指定します。

実行結果

距離基準でオブジェクトが動くようになりました。

スクリプトの解説

以下コードで、スプラインの道のり距離から割合を算出しています。

// 全体の道のり計算
var length = _splineContainer.CalculateLength();

// 進む距離と道のりから割合を算出
var percentage = _distanceFromStart / length;

終点が1になるようにするには、進む距離を全体距離で割れば良いです。

そして、計算した割合をもとにスプライン上の位置を求めています。

// 位置反映
_followTarget.position = _splineContainer.EvaluatePosition(percentage);
こじゃらこじゃら

コース上を移動するオブジェクトを作りたい場合に使えそうだね!

直近のスプライン位置を計算する

指定された位置に最も近いスプライン上の位置を計算するAPIも用意されています。

SplineUtilityクラスの以下メソッドを使います。

public static float GetNearestPoint<T>(
    T spline,
    float3 point,
    out float3 nearest,
    out float t,
    int resolution = null,
    int iterations = 2
) where T : ISpline

第1引数splineには、スプライン情報を指定します。SplineContainerではないことに注意する必要があります。(後述)

第2引数pointには、入力位置を指定します。

第3引数nearesetには、計算された直近のスプライン位置を受け取る変数を指定します。

第4引数tには、計算された割合を受け取る変数を指定します。

第5引数resolutionには、計算時の精度(セグメントの分割数)を指定します。この値が大きくなるほど計算精度が上がりますが、処理負荷も増大します。

第6引数iterationsには、計算の反復数を指定します。これも値が大きくなるほど計算精度処理負荷が上がります。

参考:Class SplineUtility| Splines | 1.0.1

GetNearestPointメソッドには、もう一つオーバーロードされたメソッドが存在します。

public static float GetNearestPoint<T>(
    T spline,
    Ray ray,
    out float3 nearest,
    out float t,
    int resolution = null,
    int iterations = 2
) where T : ISpline

違いは第2引数rayのみで、レイによる位置判定となっています。点ではなく線で計算したい場合はこちらが適しています。

サンプルスクリプト

直近の位置を視覚化するためのサンプルスクリプトです。

NearestPoint.cs
using UnityEngine;
using UnityEngine.Splines;

public class NearestPoint : MonoBehaviour
{
    // スプライン
    [SerializeField] private SplineContainer _splineContainer;

    // 入力
    [SerializeField] private Transform _input;

    // 出力
    [SerializeField] private Transform _result;

    private void Update()
    {
        // 念のためNullチェック
        if (_splineContainer == null || _input == null || _result == null)
            return;

        // ワールド空間のスプライン情報
        using var nativeSpline = new NativeSpline(
            _splineContainer.Spline,
            _splineContainer.transform.localToWorldMatrix
        );

        // 直近の位置計算
        SplineUtility.GetNearestPoint(
            nativeSpline,
            _input.position,
            out var nearestPos,
            out var percent
        );

        // 位置を反映
        _result.position = nearestPos;
    }
}

上記スクリプトをNearestPoint.csという名前で保存し、適当なオブジェクトにアタッチし、各種パラメータの設定を行うと機能します。

InputにはGetNearestPoint()メソッドに入力する位置を示すオブジェクトResultには、GetNearestPoint()メソッドの出力結果の位置を反映するオブジェクトを指定します。

実行結果

Inputオブジェクトを動かすと、それに追従するようにResultオブジェクトが直近のスプライン位置に移動します。

こじゃらこじゃら

レースゲームで、コース上の車の位置や順位を計算したい場合に使えそうだね!

このはこのは

そうね、色々と応用できそうな機能だわ!

スクリプトの解説

今回はワールド空間での位置計算を行いたいため、スプラインの座標系変換を行っています。

// ワールド空間のスプライン情報
using var nativeSpline = new NativeSpline(
    _splineContainer.Spline,
    _splineContainer.transform.localToWorldMatrix
);

生成しているNativeSplineインスタンスはSpline構造体の一種で、読み取り専用のスプライン情報を表します。内部的にはNativeArrayのテンポラリバッファを使っているため、パフォーマンスが最適化されています。

参考:Struct NativeSpline| Splines | 1.0.1

コンストラクタの第1引数スプライン情報を指定します。通常は、SplineContainerのSplineプロパティを指定すれば良いです。

第2引数ワールド空間への変換行列を指定します。

注意

スプラインのパスはローカル空間となっているためワールド空間への変換を行っています。座標空間が異なった状態で計算処理を進めると、出力結果の位置がずれるなど不具合が生じるためご注意ください。

そして、上で求めたワールド空間のスプライン情報をもとに、直近の位置計算を行っています。

// 直近の位置計算
SplineUtility.GetNearestPoint(
    nativeSpline,
    _input.position,
    out var nearestPos,
    out var percent
);

スクリプトからスプラインを編集する

SplineContainerコンポーネントのSplineプロパティにスプラインのパスなど各種情報が格納されています。ここに対してポイントを追加・削除・編集などの操作ができます。

参考:Class Spline| Splines | 1.0.1

サンプルスクリプト

スプラインをスクリプトから作成する例です。

SplineEdit.cs
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;

public class SplineEdit : MonoBehaviour
{
    // スプライン
    [SerializeField] private SplineContainer _splineContainer;

    private void Start()
    {
        // 念のためNullチェック
        if (_splineContainer == null)
            return;

        var spline = _splineContainer.Spline;

        // 全削除
        spline.Clear();

        // 点を追加(正方形のパスを作成)
        spline.Add(new BezierKnot(new float3(-10, 0, -10)));
        spline.Add(new BezierKnot(new float3(10, 0, -10)));
        spline.Add(new BezierKnot(new float3(10, 0, 10)));
        spline.Add(new BezierKnot(new float3(-10, 0, 10)));

        // パスを一部編集(勾配をつける)
        spline[0] = new BezierKnot(new float3(-10, 5, -10));
        spline[1] = new BezierKnot(new float3(10, 5, -10));
        
        // 閉じたパスにする
        spline.Closed = true;
    }
}

スクリプトをSplineEdit.csという名前で保存し、適当なオブジェクトにアタッチし、Spline Containerにスプラインをアタッチすると機能します。

どのような形状のパスを作成しているかは、コメントをご覧ください。

実行結果

確かにコメント通りのパスがスクリプトから作成されていることが確認できました。

さいごに

Unity公式のスプラインとスクリプトを連携する方法について解説しました。

スプライン情報へのアクセスは、SplineContainerコンポーネントを通じて行うことができます。

スクリプトと連携できれば、スプライン情報をもとにキャラクターを動かしたり、線を描画したりといった具合に、様々な応用が期待できるでしょう。

関連記事

参考サイト

スポンサーリンク