はじめに
前回の以下の記事で Splinesのサンプルプロジェクトをいろいろ確認したり、Editor拡張を作ったりしました。
今回は、これらから実際に Splineを使用したゲームを作っていきたいと思います。
デモ
開発環境
- Unity 2022.2.0f1
- Splines v2.1.0
Splinesとは
以下のようなサンプルとしてあるように曲線とパスを使用することでいままで標準ではできなかった軌跡を作ることが可能です。詳しくは、公式ドキュメントからの引用をご覧ください
Unity公式 Splines 2.1.0より (deeplにより日本語訳)
曲線とパスで作業する スプラインパッケージを使うと、パスに沿ってオブジェクトや動作を生成したり、軌跡を作成したり、図形を描いたりすることができます。
Splines パッケージには以下が含まれます。
Splines 2.0.0以上に含まれている機能
今回の実装でも用いている v1には含まれていない v2に含まれている Splinesの機能は以下になります
- SplinePath、SplineSlice、SplineRange タイプを追加しました。これらのタイプは、離散スプラインの部分的または完全なセクションの補間と評価を可能にします。
その他の変更点 → com.unity.splines changelog
実装方法
今回は Splinesの Sampleシーンを参考にして実装を行っています。
準備
1 . Unityに Splinesを導入する
2 . Sampleの追加
SplinesのMeshの更新
今回 Splinesに追加の点を追加した際のMesh周りの処理は、サンプルシーンにすでにあるもの以下のスクリプトを改変します。
Assets/Samples/Splines/2.1.0/Spline Examples (requires Shader Graph package)/Runtime/MultipleRoadBehaviour.cs
新たに MultipleRoad.cs
を作成して、以下を追加します。
using System.Collections.Generic; using Unity.Mathematics; using UnityEngine; using UnityEngine.Serialization; using UnityEngine.Splines; namespace _SplineTrain { [DisallowMultipleComponent] [RequireComponent(typeof(SplineContainer), typeof(MeshRenderer), typeof(MeshFilter))] public class MultipleRoad : MonoBehaviour { [FormerlySerializedAs("m_Spline")] [SerializeField] private SplineContainer mSpline; [FormerlySerializedAs("m_SegmentsPerMeter")] [SerializeField] private int mSegmentsPerMeter = 1; [FormerlySerializedAs("m_Mesh")] [SerializeField] private Mesh mMesh; [FormerlySerializedAs("m_TextureScale")] [SerializeField] private float mTextureScale = 1f; private IEnumerable<Spline> RoadSplines { get { if (mSpline == null) mSpline = GetComponent<SplineContainer>(); if (mSpline == null) return null; return mSpline.Splines; } } private Mesh RoadsMesh { get { if (mMesh != null) return mMesh; mMesh = new Mesh(); GetComponent<MeshRenderer>().sharedMaterial = Resources.Load<Material>("Road"); return mMesh; } } private int SegmentsPerMeter => Mathf.Min(10, Mathf.Max(1, mSegmentsPerMeter)); private readonly List<Vector3> _mPositions = new List<Vector3>(); private readonly List<Vector3> _mNormals = new List<Vector3>(); private readonly List<Vector2> _mTextures = new List<Vector2>(); private readonly List<int> _mIndices = new List<int>(); public void OnEnable() { //Avoid to point to an existing instance when duplicating the GameObject if (mMesh != null) mMesh = null; CreateRoads(); } public void OnDisable() { if (mMesh != null) #if UNITY_EDITOR DestroyImmediate(mMesh); #else Destroy(mMesh); #endif } public void CreateRoads() { RoadsMesh.Clear(); _mPositions.Clear(); _mNormals.Clear(); _mTextures.Clear(); _mIndices.Clear(); foreach (var spl in RoadSplines) { CreateRoad(spl); } RoadsMesh.SetVertices(_mPositions); RoadsMesh.SetNormals(_mNormals); RoadsMesh.SetUVs(0, _mTextures); RoadsMesh.subMeshCount = 1; RoadsMesh.SetIndices(_mIndices, MeshTopology.Triangles, 0); RoadsMesh.UploadMeshData(false); GetComponent<MeshFilter>().sharedMesh = mMesh; } private void CreateRoad(Spline roadSpline) { if (roadSpline == null || roadSpline.Count < 2) return; var length = roadSpline.GetLength(); if (length < 1) return; var segments = (int)(SegmentsPerMeter * length); int vertexCount = segments * 2, triangleCount = (roadSpline.Closed ? segments : segments - 1) * 6; var prevVertexCount = _mPositions.Count; _mPositions.Capacity += vertexCount; _mNormals.Capacity += vertexCount; _mTextures.Capacity += vertexCount; _mIndices.Capacity += triangleCount; for (var i = 0; i < segments; i++) { var index = i / (segments - 1f); var control = SplineUtility.EvaluatePosition(roadSpline, index); var dir = SplineUtility.EvaluateTangent(roadSpline, index); var up = SplineUtility.EvaluateUpVector(roadSpline, index); var scale = transform.lossyScale; //var tangent = math.normalize((float3)math.mul(math.cross(up, dir), new float3(1f / scale.x, 1f / scale.y, 1f / scale.z))); var tangent = math.normalize(math.cross(up, dir)) * new float3(1f / scale.x, 1f / scale.y, 1f / scale.z); var w = 1f; _mPositions.Add(control - (tangent * w)); _mPositions.Add(control + (tangent * w)); _mNormals.Add(Vector3.up); _mNormals.Add(Vector3.up); _mTextures.Add(new Vector2(0f, index * mTextureScale)); _mTextures.Add(new Vector2(1f, index * mTextureScale)); } for (int i = 0, n = prevVertexCount; i < triangleCount; i += 6, n += 2) { _mIndices.Add((n + 2) % (prevVertexCount + vertexCount)); _mIndices.Add((n + 1) % (prevVertexCount + vertexCount)); _mIndices.Add((n + 0) % (prevVertexCount + vertexCount)); _mIndices.Add((n + 2) % (prevVertexCount + vertexCount)); _mIndices.Add((n + 3) % (prevVertexCount + vertexCount)); _mIndices.Add((n + 1) % (prevVertexCount + vertexCount)); } } } }
サンプルスクリプトをそのまま使う際は、[ExecuteInEditMode]
が付いていることに注意してください。
ExecuteInEditMode
は以下のように 呼ばれる際に制限がかかっている状態になっています。
- Updateは、シーンに何か変化があったときだけ呼ばれます。
- OnGUIは、Game ViewがEventを受け取ったときに呼び出されます。
- OnRenderObject と他のレンダリングコールバック関数は、シーンビューやゲームビューが再描画されるたびに呼び出されます。
詳細は以下のドキュメントをご覧ください。 docs.unity3d.com
選択したSplinesの取得
ここで Splnies v2から新しく追加された SplineSlice
と SplineRange
を使用します。
使用している SplineContainerから任意の長さのSplineのポイントを取得して返します。
public SplinePath GetNextPath() { return new SplinePath(new[] { new SplineSlice<Spline>(CurrentSpline, new SplineRange(0, CurrentSplineCount), _containerTransform), }); }
Splineが更新されたときに SplineのMeshの更新
今回のプロジェクトでは、動的にSplineを増やしているためSplineにポイントを追加するたびにMeshを更新する必要があります。
private void Start() { Spline.Changed += OnSplineOnChanged; } private void OnSplineOnChanged(Spline spline, int i, SplineModification splineModification) { if (spline == _editRoadModel.CurrentSpline) { _multipleRoad.CreateRoads(); } }
サンプルコードから引用してきたコードの中にある CreateRoads
を Splineが更新されるたびに呼び出します。
Spline
には以下の Actionsが存在しているので、今回はこちらを使用します。今回は上記のコードにもあるように Start関数内でeventを発火して追加されるごとにMeshを更新しています。
public static event Action<Spline, int, SplineModification> Changed;
Simple Sample Demo
サンプルコードとして、Splinesを使用したMeshが一定時間ごとに増えていくSimple Sample ProjectのRepositoryも公開しています。