【Unity2022】スプラインの経路情報をスクリプトから扱う

こじゃらこじゃら

次のようなスプライン上の移動経路をスクリプトから管理したいの。

このはこのは

SplinePathクラスを使えば比較的楽に実現できるわ。

スプライン(Splinesパッケージ)の経路情報をスクリプトから管理する方法の解説記事です。

これは、SplinePathクラスを用いると比較的楽に実現できます。

// スプライン
SplineContainer splineContainer;

・・・(中略)・・・

// スプライン上の経路情報を作成する
SplinePath splinePath = new SplinePath(
    new[]
    {
        new SplineSlice<Spline>(splineContainer.Splines[0], new SplineRange(0, 2)),
        new SplineSlice<Spline>(splineContainer.Splines[2], new SplineRange(0, 5)),
    }
);

例えば、次のような経路情報を扱うことができます。

扱える経路の種類
  • スプラインの部分的な経路
  • 複数のスプラインに跨る経路
  • 分岐するスプライン上の経路

また、経路情報には区間移動方向のパラメータが保持されています。これにより、例えば道路を走る車を実装できます。

Splinesパッケージの公式サンプルにもSplinePathクラスを使ったデモシーンが用意されています。

本記事では、SplinePathクラスを用いてスプライン上の経路をスクリプトから管理する方法を解説していきます。

なお、SplinePathクラスはSplines 2.0.0以降でないと使用できないためご注意ください。

動作環境
  • Unity 2022.2.13f1
  • Splines 2.1.0

スポンサーリンク

前提条件

Unity 2022以降でUnityプロジェクトが開かれており、Splines 2.0.0以降がインストールされているものとします。

ここまでの手順が分からない方は、以下記事をご覧ください。

また、本記事の例では、Splinesパッケージ公式が提供しているサンプルシーンSplinePath Sampleを使うものとします。

SplinePathクラス

複数のスプラインにまたがる経路情報を扱うクラスです。SplinePathクラスのほか、ジェネリック型のSplinePath<T>クラスがあります。

public class SplinePath : SplinePath<SplineSlice<Spline>> { ・・・(中略)・・・ }
public class SplinePath<T> : ISpline, IHasEmptyCurves where T : ISpline { ・・・(中略)・・・ }

これらのクラスは、SplineSlice型のコレクションとして実装されています。

参考:Class SplinePath | Splines | 2.1.0

参考:Class SplinePath<T> | Splines | 2.1.0

SplineSlice構造体

単一のスプライン上の一部または全部の区間を表す構造体です。

次のようにジェネリック型として定義されます。

public struct SplineSlice<T> : ISpline where T : ISpline { ・・・(中略)・・・ }

内部的には次の情報を持っています。

SplineSlice構造体が保持する情報
  • スプラインオブジェクトへの参照(T型フィールド)
  • スプラインの範囲向き(SplineRange型フィールド)
  • 変換行列(float4x4型フィールド)

参考:Struct SplineSlice<T> | Splines | 2.1.0

単一のスプラインのある点からある点までの一方向の経路で良い場合は、SplineSlice構造体でも事足ります。

SplineRange構造体

スプライン上の範囲と向きの情報を持つ構造体です。

public struct SplineRange : IEnumerable<int> { ・・・(中略)・・・ }

範囲情報は、制御点単位で管理されます。 [1]

次のようなデータ表現になっています。

SplineRange構造体が保持する情報
  • 開始制御点のインデックス(int型フィールド)
  • インデックスを進める個数(int型フィールド)
  • 向き(SliceDirection型フィールド)

参考:Struct SplineRange | Splines | 2.1.0

経路に沿って移動するサンプル

SplinePathクラスを用いて経路に沿ってオブジェクトを移動させるサンプルスクリプトです。

PathMovementExample.cs
using System;
using System.Linq;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;

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

    // インスペクターから編集可能な経路情報
    [Serializable]
    private struct PathInfo
    {
        // SplineContainerのどのスプラインを使うかをインデックスで指定
        public int splineIndex;
        
        // 上記インデックスのスプラインにおける範囲情報
        public SplineRange range;
    }

    // 経路の作成情報
    [SerializeField] private PathInfo[] _path;

    // 経路上の位置(0~1の範囲で指定)
    [SerializeField, Range(0, 1)] private float _t;

    // 実際に使われるスプラインの経路情報
    private SplinePath _splinePath;

    // 初期化
    private void Start()
    {
        // SplinePathインスタンスを予め作成
        OnCreateSplinePath();
    }

    // フレーム更新
    private void Update()
    {
        // スプラインに沿って移動させる
        OnMove();
    }

#if UNITY_EDITOR

    // インスペクターから編集されたとき
    private void OnValidate()
    {
        // SplinePathインスタンスと移動処理を一緒に行う
        OnCreateSplinePath();
        OnMove();
    }

#endif

    // SplinePathインスタンスを作成する
    private void OnCreateSplinePath()
    {
        if (_splineContainer == null) return;

        // ワールド空間のスプラインとして扱うため、変換行列を指定する
        float4x4 matrix = _splineContainer.transform.localToWorldMatrix;

        // 経路の作成情報からSplinePathインスタンスを作成
        _splinePath = new SplinePath(
            // PathInfoからSplineSlice型のコレクションに変換
            _path.Select(x => new SplineSlice<Spline>(
                    _splineContainer[x.splineIndex],
                    x.range,
                    matrix
                )
            )
        );
    }

    // 予め作成されたSplinePathインスタンスの経路に沿って自身を移動させる
    private void OnMove()
    {
        if (_splinePath == null)
            return;

        // スプライン上の位置・向き・上ベクトルを取得
        if (!_splinePath.Evaluate(_t, out var position, out var tangent, out var upVector))
            return;

        // Transformに反映
        transform.SetPositionAndRotation(
            position,
            Quaternion.LookRotation(tangent, upVector)
        );
    }
}

上記をPathMovementExample.csという名前で保存し、スプラインに沿って移動させたいオブジェクトにアタッチし、インスペクターから経路情報の設定を行うと機能します。

Spline Container項目に移動対象のスプライン、Path項目に経路情報を入力してください。

例では、次のような経路情報としました。

実行結果

インスペクターより割合Tを変更すると、移動対象がその位置に移動します。向きもスプラインに沿って回転しています。

スクリプトの説明

シーン上などで定義されるスプラインの情報は、SplineContainerオブジェクトが管理しています。

これを参照する必要があるため、以下でインスペクターから指定するようにしています。

// スプライン
[SerializeField] private SplineContainer _splineContainer;

Splines 2.1.0現在、SplinePathクラスはインスペクターから編集できないため [2] 、以下で独自のパス構造体を定義して疑似的に編集可能にしています。

// インスペクターから編集可能な経路情報
[Serializable]
private struct PathInfo
{
    // SplineContainerのどのスプラインを使うかをインデックスで指定
    public int splineIndex;
    
    // 上記インデックスのスプラインにおける範囲情報
    public SplineRange range;
}

// 経路の作成情報
[SerializeField] private PathInfo[] _path;

上記の経路情報からSplinePathインスタンスを作成する処理は以下部分です。

// SplinePathインスタンスを作成する
private void OnCreateSplinePath()
{
    if (_splineContainer == null) return;

    // ワールド空間のスプラインとして扱うため、変換行列を指定する
    float4x4 matrix = _splineContainer.transform.localToWorldMatrix;

    // 経路の作成情報からSplinePathインスタンスを作成
    _splinePath = new SplinePath(
        // PathInfoからSplineSlice型のコレクションに変換
        _path.Select(x => new SplineSlice<Spline>(
                _splineContainer[x.splineIndex],
                x.range,
                matrix
            )
        )
    );
}

LinqSelectメソッドを使ってSplineSlice型のコレクションに変換しています。

SplinePathクラスコンストラクタとしてSplineSlice型コレクションを受け取ることで初期化できます。

参考:Struct SplineSlice<T> | Splines | 2.1.0

そして、予め作成されたSplinePathインスタンスから、割合tにおけるスプライン上の位置や向きなどを取得します。

取得から移動までの処理は以下で行っています。

// 予め作成されたSplinePathインスタンスの経路に沿って自身を移動させる
private void OnMove()
{
    if (_splinePath == null)
        return;

    // スプライン上の位置・向き・上ベクトルを取得
    if (!_splinePath.Evaluate(_t, out var position, out var tangent, out var upVector))
        return;

    // Transformに反映
    transform.SetPositionAndRotation(
        position,
        Quaternion.LookRotation(tangent, upVector)
    );
}

Evaluateメソッドから、経路における位置向き上向きベクトルを一気に取得できます。

なお、EvaluateメソッドはSplinePathクラスではなく、SplineUtilityクラスの拡張メソッドとして提供されているものです。

public static bool Evaluate<T>(
    this T spline,
    float t,
    out float3 position,
    out float3 tangent,
    out float3 upVector
) where T : ISpline;

TはISplineインタフェースを実装している型であれば良いため、SplinePath型以外にもSplineSlice型Spline型などに対しても使えます。

参考:Class SplineUtility | Splines | 2.1.0

なお、位置や向き、上向きベクトルを個別に取得したい場合は、以下メソッドが使えます。

位置の取得

public static float3 EvaluatePosition<T>(
    this T spline,
    float t
) where T : ISpline;

向きの取得

public static float3 EvaluateTangent<T>(
    this T spline,
    float t
) where T : ISpline;

上向きベクトルの取得

public static float3 EvaluateUpVector<T>(
    this T spline,
    float t
) where T : ISpline;

参考:Class SplineUtility | Splines | 2.1.0

実行時は、開始時にSplinePathインスタンスを作成し、フレーム毎に割合tにおける位置や向きなどを反映する移動処理を実施しています。

// 初期化
private void Start()
{
    // SplinePathインスタンスを予め作成
    OnCreateSplinePath();
}

// フレーム更新
private void Update()
{
    // スプラインに沿って移動させる
    OnMove();
}

インスペクターから値を編集するとオブジェクトが移動する仕組みは以下で実現しています。

#if UNITY_EDITOR

    // インスペクターから編集されたとき
    private void OnValidate()
    {
        // SplinePathインスタンスと移動処理を一緒に行う
        OnCreateSplinePath();
        OnMove();
    }

#endif

経路を編集するGUIツールについて

Splines 2.1.0現在では、インスペクターやシーンビュー上から経路を編集できるようなツールは存在しません。

そのため、現状ではスクリプトから動的にSplinePathクラスから経路情報を作成する必要があります。

今後のバージョンアップにより、将来的にはGUI経由で編集可能になるかもしれません。

さいごに

スプラインの経路情報はSplinePathクラスを使うと楽に管理できます。

補間の計算も経路の長さに基づいて行ってくれるため、移動処理などの実装が簡単になります。

これ以外にも、SplinePathオブジェクトの経路情報に基づいて線を描画したり、オブジェクトを配置したりなど、様々な応用が期待できるでしょう。

関連記事

参考サイト

スポンサーリンク