LineRenderer
は Positions
という配列から複数の点を指定しておくと、最初のものから順番に線を繋いで表示してくれるコンポーネントです。
一方、Vector3.Lerp
は2点間を線形補間する関数です。
https://docs.unity3d.com/ja/current/ScriptReference/Vector3.Lerp.html
LineRenderer でグラフをアニメーションさせたいとなった場合、考慮すべき点が2点であれば Lerp で Positions を弄ればアニメーションさせられるのですが、3点以上の場合それ用のコンポーネントを自作するのが良さそうだったので作ることにしました。
結果がこんな感じです。
複数の点を元にしたLerp
複数の点を横断できる Lerp らしきものを実装します。まず全点間の距離リストを作り、 0.0~1.0 の値を元にどの線分に点が属するかを求め、さらにその線分内で Lerp する実装にしました。
早速ですがコード全文です。
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
[ExecuteInEditMode]
[RequireComponent(typeof(LineRenderer))]
public class LineRendererProgressor : MonoBehaviour
{
private LineRenderer LineRenderer
{
get
{
if (lineRenderer) return lineRenderer;
return lineRenderer = GetComponent<LineRenderer>();
}
}
private LineRenderer lineRenderer;
[SerializeField] private Vector3[] positions;
public float Progress
{
get => progress;
set {
progress = value;
if (positions == null || positions.Length == 0) return;
UpdateLineRenderer();
}
}
[SerializeField] private float progress;
private void OnValidate()
{
if (positions == null || positions.Length == 0) return;
UpdateLineRenderer();
}
private void UpdateLineRenderer()
{
// 値の制限と終了判定
progress = Mathf.Clamp01(progress);
if (positions.Length == 1 || progress <= 0.0f)
{
LineRenderer.positionCount = 1;
LineRenderer.SetPosition(0, positions[0]);
return;
}
if (progress >= 1.0f)
{
LineRenderer.positionCount = positions.Length;
LineRenderer.SetPositions(positions);
return;
}
// 距離リストと合計
var distanceList = MakeDistanceList(positions);
var totalDistance = distanceList.Sum();
// 現在どの線分にあるか取得
var distanceCompleted = totalDistance * progress;
var currentIndex = GetIndexByDistanceCompleted(positions,
distanceList, distanceCompleted);
// 先端位置を計算
var distanceArrayToCurIdx = distanceList.Take(currentIndex);
var completedDistInCurSeg = distanceCompleted - distanceArrayToCurIdx.Sum();
var currentSegLength = distanceList[currentIndex];
var t = completedDistInCurSeg / currentSegLength;
var edgePos = Vector3.Lerp(positions[currentIndex], positions[currentIndex + 1], t);
// 更新
LineRenderer.positionCount = currentIndex + 2;
for (var i = 0; i < currentIndex + 2; ++i)
{
LineRenderer.SetPosition(i, i > currentIndex ? edgePos : positions[i]);
}
}
private IReadOnlyList<float> MakeDistanceList(Vector3[] points)
{
var distanceList = new List<float>();
for (var i = 0; i < points.Length - 1; ++i)
{
distanceList.Add(Vector3.Distance(points[i], points[i + 1]));
}
return distanceList;
}
private int GetIndexByDistanceCompleted(Vector3[] points, IReadOnlyList<float> distanceList, float distanceCompleted)
{
var distanceProcessed = 0f;
for (var i = 0; i < points.Length - 1; ++i)
{
if (distanceList[i] + distanceProcessed > distanceCompleted)
{
return i;
}
distanceProcessed += distanceList[i];
}
return points.Length - 1;
}
}
大体コメントにかかれていることをやっているだけでとてもシンプルです。数学力があるともう少し気の利いた実装ができるのかもしれませんが、ぼくには無いので諦めました…。
線分の長さは結構使い回すので List に保管しました。
あと、先端位置の計算が元々用意されている Lerp にあたり、そこだけを返す MultiLerp みたいな関数を定義した方が他のクラスでも使いまわせて良いかなーとも思いました。しかしその場合 MultiLerp で行うのと同じ計算を再度 UpdateLineRenderer でする必要があり微妙かもと思ったので、今回は UpdateLineRenderer 内に全て詰め込んでいます。