Unityで加速と減速をエミュレートしようとしています。
Unityでトラックを生成し、時間に基づいてトラック上の特定の場所にオブジェクトを配置するコードを記述しました。結果は少しこのようになります。
私が現在抱えている問題は、スプラインの各セクションの長さが異なり、立方体が異なるが均一な速度で各セクションを移動することです。これにより、セクション間を移行するときに、立方体の速度の変化が突然ジャンプします。
この問題を解決するために、GetTime(Vector3 p0, Vector3 p1, float alpha)
メソッドで Robert Pennerのイージング方程式 を使用しようとしました。しかし、これはある程度は役に立ちましたが、十分ではありませんでした。トランジションの間にまだスピードのジャンプがありました。
トラックのセグメント間で速度を大幅に上げることなく、立方体の位置を動的に緩和して加速および減速しているように見せるためのアイデアはありますか?
コードの簡単な実装を示すスクリプトを作成しました。どのゲームオブジェクトにも取り付けることができます。コードの実行時に何が起こっているかを簡単に確認するには、立方体や球などにアタッチします。
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class InterpolationExample : MonoBehaviour {
[Header("Time")]
[SerializeField]
private float currentTime;
private float lastTime = 0;
[SerializeField]
private float timeModifier = 1;
[SerializeField]
private bool running = true;
private bool runningBuffer = true;
[Header("Track Settings")]
[SerializeField]
[Range(0, 1)]
private float catmullRomAlpha = 0.5f;
[SerializeField]
private List<SimpleWayPoint> wayPoints = new List<SimpleWayPoint>
{
new SimpleWayPoint() {pos = new Vector3(-4.07f, 0, 6.5f), time = 0},
new SimpleWayPoint() {pos = new Vector3(-2.13f, 3.18f, 6.39f), time = 1},
new SimpleWayPoint() {pos = new Vector3(-1.14f, 0, 4.55f), time = 6},
new SimpleWayPoint() {pos = new Vector3(0.07f, -1.45f, 6.5f), time = 7},
new SimpleWayPoint() {pos = new Vector3(1.55f, 0, 3.86f), time = 7.2f},
new SimpleWayPoint() {pos = new Vector3(4.94f, 2.03f, 6.5f), time = 10}
};
[Header("Debug")]
[Header("WayPoints")]
[SerializeField]
private bool debugWayPoints = true;
[SerializeField]
private WayPointDebugType debugWayPointType = WayPointDebugType.SOLID;
[SerializeField]
private float debugWayPointSize = 0.2f;
[SerializeField]
private Color debugWayPointColour = Color.green;
[Header("Track")]
[SerializeField]
private bool debugTrack = true;
[SerializeField]
[Range(0, 1)]
private float debugTrackResolution = 0.04f;
[SerializeField]
private Color debugTrackColour = Color.red;
[System.Serializable]
private class SimpleWayPoint
{
public Vector3 pos;
public float time;
}
[System.Serializable]
private enum WayPointDebugType
{
SOLID,
WIRE
}
private void Start()
{
wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
wayPoints.Insert(0, wayPoints[0]);
wayPoints.Add(wayPoints[wayPoints.Count - 1]);
}
private void LateUpdate()
{
//This means that if currentTime is paused, then resumed, there is not a big jump in time
if(runningBuffer != running)
{
runningBuffer = running;
lastTime = Time.time;
}
if(running)
{
currentTime += (Time.time - lastTime) * timeModifier;
lastTime = Time.time;
if(currentTime > wayPoints[wayPoints.Count - 1].time)
{
currentTime = 0;
}
}
transform.position = GetPosition(currentTime);
}
#region Catmull-Rom Math
public Vector3 GetPosition(float time)
{
//Check if before first waypoint
if(time <= wayPoints[0].time)
{
return wayPoints[0].pos;
}
//Check if after last waypoint
else if(time >= wayPoints[wayPoints.Count - 1].time)
{
return wayPoints[wayPoints.Count - 1].pos;
}
//Check time boundaries - Find the nearest WayPoint your object has passed
float minTime = -1;
float maxTime = -1;
int minIndex = -1;
for(int i = 1; i < wayPoints.Count; i++)
{
if(time > wayPoints[i - 1].time && time <= wayPoints[i].time)
{
maxTime = wayPoints[i].time;
int index = i - 1;
minTime = wayPoints[index].time;
minIndex = index;
}
}
float timeDiff = maxTime - minTime;
float percentageThroughSegment = 1 - ((maxTime - time) / timeDiff);
//Define the 4 points required to make a Catmull-Rom spline
Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
Vector3 p1 = wayPoints[minIndex].pos;
Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;
return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
}
//Prevent Index Out of Array Bounds
private int ClampListPos(int pos)
{
if(pos < 0)
{
pos = wayPoints.Count - 1;
}
if(pos > wayPoints.Count)
{
pos = 1;
}
else if(pos > wayPoints.Count - 1)
{
pos = 0;
}
return pos;
}
//Math behind the Catmull-Rom curve. See here for a good explanation of how it works. https://stackoverflow.com/a/23980479/4601149
private Vector3 GetCatmullRomPosition(float t, Vector3 p0, Vector3 p1, Vector3 p2, Vector3 p3, float alpha)
{
float dt0 = GetTime(p0, p1, alpha);
float dt1 = GetTime(p1, p2, alpha);
float dt2 = GetTime(p2, p3, alpha);
Vector3 t1 = ((p1 - p0) / dt0) - ((p2 - p0) / (dt0 + dt1)) + ((p2 - p1) / dt1);
Vector3 t2 = ((p2 - p1) / dt1) - ((p3 - p1) / (dt1 + dt2)) + ((p3 - p2) / dt2);
t1 *= dt1;
t2 *= dt1;
Vector3 c0 = p1;
Vector3 c1 = t1;
Vector3 c2 = (3 * p2) - (3 * p1) - (2 * t1) - t2;
Vector3 c3 = (2 * p1) - (2 * p2) + t1 + t2;
Vector3 pos = CalculatePosition(t, c0, c1, c2, c3);
return pos;
}
private float GetTime(Vector3 p0, Vector3 p1, float alpha)
{
if(p0 == p1)
return 1;
return Mathf.Pow((p1 - p0).sqrMagnitude, 0.5f * alpha);
}
private Vector3 CalculatePosition(float t, Vector3 c0, Vector3 c1, Vector3 c2, Vector3 c3)
{
float t2 = t * t;
float t3 = t2 * t;
return c0 + c1 * t + c2 * t2 + c3 * t3;
}
//Utility method for drawing the track
private void DisplayCatmullRomSpline(int pos, float resolution)
{
Vector3 p0 = wayPoints[ClampListPos(pos - 1)].pos;
Vector3 p1 = wayPoints[pos].pos;
Vector3 p2 = wayPoints[ClampListPos(pos + 1)].pos;
Vector3 p3 = wayPoints[ClampListPos(pos + 2)].pos;
Vector3 lastPos = p1;
int maxLoopCount = Mathf.FloorToInt(1f / resolution);
for(int i = 1; i <= maxLoopCount; i++)
{
float t = i * resolution;
Vector3 newPos = GetCatmullRomPosition(t, p0, p1, p2, p3, catmullRomAlpha);
Gizmos.DrawLine(lastPos, newPos);
lastPos = newPos;
}
}
#endregion
private void OnDrawGizmos()
{
#if UNITY_EDITOR
if(EditorApplication.isPlaying)
{
if(debugWayPoints)
{
Gizmos.color = debugWayPointColour;
foreach(SimpleWayPoint s in wayPoints)
{
if(debugWayPointType == WayPointDebugType.SOLID)
{
Gizmos.DrawSphere(s.pos, debugWayPointSize);
}
else if(debugWayPointType == WayPointDebugType.WIRE)
{
Gizmos.DrawWireSphere(s.pos, debugWayPointSize);
}
}
}
if(debugTrack)
{
Gizmos.color = debugTrackColour;
if(wayPoints.Count >= 2)
{
for(int i = 0; i < wayPoints.Count; i++)
{
if(i == 0 || i == wayPoints.Count - 2 || i == wayPoints.Count - 1)
{
continue;
}
DisplayCatmullRomSpline(i, debugTrackResolution);
}
}
}
}
#endif
}
}
さて、これにいくつかのmathを入れましょう。
私は常にgamedevでの数学の重要性と有用性を提唱してきましたが、この答えについてはあまりにも深く掘り下げているかもしれませんが、あなたの質問はコーディングではなく、代数問題のモデリングと解決に関するものだと思います。 。とにかく、行きましょう。
大学の学位を持っている場合は、関数-パラメーターを受け取って結果を生成する操作-およびグラフについて何か覚えているかもしれません。 -関数とそのパラメーターの進化のグラフィック表現(またはプロット)。 f(x)
は何かを思い出させるかもしれません:それはf
という名前の関数がパラメータx
に依存していることを示しています。したがって、「to parameterize 」は、大まかに言って、1つ以上のパラメーターの観点からシステムを表現することを意味します。
あなたは用語に精通していないかもしれませんが、あなたはいつもそれをします。たとえば、Track
は、f(x,y,z)
という3つのパラメータを持つシステムです。
パラメータ化の興味深い点の1つは、システムを取得して、他のパラメータの観点から説明できることです。繰り返しますが、あなたはすでにそれをやっています。時間の経過に伴うトラックの進化を説明すると、各座標は時間の関数f(x,y,z) = f(x(t),y(t),z(t)) = f(t)
であると言えます。つまり、時間を使用して各座標を計算し、その座標を使用して、その特定の時間の空間にオブジェクトを配置できます。
最後に、私はあなたの質問に答え始めます。必要なトラックシステムを完全に説明するには、次の2つが必要です。
あなたはすでにこの部分を実質的に解決しました。シーン空間にいくつかのポイントを設定し、Catmull–Romスプラインを使用してポイントを補間し、パスを生成します。それは賢いことであり、それについてすることはあまり残っていません。
また、各ポイントにフィールドtime
を追加したので、移動するオブジェクトがこの正確な時間にこのチェックを通過することを確認します。これについては後でまた説明します。
パスソリューションの興味深い点の1つは、パス計算をpercentageThroughSegment
パラメーター(セグメント内の相対位置を表す0から1の範囲の値)でパラメーター化したことです。コードでは、固定のタイムステップで反復し、percentageThroughSegment
は、費やされた時間とセグメントの合計期間の比率になります。各セグメントには特定の期間があるため、多くの一定速度をエミュレートします。
これはかなり標準的ですが、微妙な点が1つあります。あなたは動きを説明する上で非常に重要な部分を無視しています:移動距離。
別のアプローチをお勧めします。移動距離を使用して、パスをパラメータ化します。次に、オブジェクトの動きは、時間に関してパラメータ化された移動距離になります。このようにして、2つの独立した一貫したシステムが得られます。働く手!
これからは、わかりやすくするためにすべてを2Dにしますが、後で3Dに変更するのは簡単です。
次のパスを検討してください。
ここで、i
はセグメントのインデックス、d
は移動距離、x, y
は平面内の座標です。これは、あなたのようなスプラインによって作成されたパス、またはベジェ曲線などで作成されたパスである可能性があります。
現在のソリューションでオブジェクトによって開発された動きは、次のようにdistance traveled on the path
とtime
のグラフとして説明できます。
表のt
はオブジェクトがチェックに到達する必要がある時間、d
はこの位置まで移動した距離、v
は速度、a
は加速度です。
上の図は、オブジェクトが時間とともにどのように進むかを示しています。横軸は時間、縦軸は移動距離です。縦軸は平らな線で「広げられた」経路であると想像できます。下のグラフは、時間の経過に伴う速度の変化です。
この時点でいくつかの物理学を思い出す必要があり、各セグメントで、距離のグラフは直線であり、加速のない一定速度での動きに対応していることに注意する必要があります。このようなシステムは、次の方程式で表されます。d = do + v*t
オブジェクトがチェックポイントに到達するたびに、その速度値が突然変化し(グラフに連続性がないため)、シーンに奇妙な影響を及ぼします。はい、あなたはすでにそれを知っています、そしてそれがあなたが質問を投稿した理由です。
さて、どうすればそれを改善できますか?うーん...速度グラフが連続している場合、それほど厄介な速度ジャンプではないでしょう。このような動きの最も簡単な説明は、均一に加速される可能性があります。このようなシステムは、次の式で表されます:d = do + vo*t + a*t^2/2
。また、初速度を想定する必要があります。ここではゼロを選択します(静止から離れます)。
予想どおり、速度グラフは連続的であり、移動はパスを介して加速されます。これは、次のようにメチッドStart
とGetPosition
を変更するUnityにコーディングできます。
private List<float> lengths = new List<float>();
private List<float> speeds = new List<float>();
private List<float> accels = new List<float>();
public float spdInit = 0;
private void Start()
{
wayPoints.Sort((x, y) => x.time.CompareTo(y.time));
wayPoints.Insert(0, wayPoints[0]);
wayPoints.Add(wayPoints[wayPoints.Count - 1]);
for (int seg = 1; seg < wayPoints.Count - 2; seg++)
{
Vector3 p0 = wayPoints[seg - 1].pos;
Vector3 p1 = wayPoints[seg].pos;
Vector3 p2 = wayPoints[seg + 1].pos;
Vector3 p3 = wayPoints[seg + 2].pos;
float len = 0.0f;
Vector3 prevPos = GetCatmullRomPosition(0.0f, p0, p1, p2, p3, catmullRomAlpha);
for (int i = 1; i <= Mathf.FloorToInt(1f / debugTrackResolution); i++)
{
Vector3 pos = GetCatmullRomPosition(i * debugTrackResolution, p0, p1, p2, p3, catmullRomAlpha);
len += Vector3.Distance(pos, prevPos);
prevPos = pos;
}
float spd0 = seg == 1 ? spdInit : speeds[seg - 2];
float lapse = wayPoints[seg + 1].time - wayPoints[seg].time;
float acc = (len - spd0 * lapse) * 2 / lapse / lapse;
float speed = spd0 + acc * lapse;
lengths.Add(len);
speeds.Add(speed);
accels.Add(acc);
}
}
public Vector3 GetPosition(float time)
{
//Check if before first waypoint
if (time <= wayPoints[0].time)
{
return wayPoints[0].pos;
}
//Check if after last waypoint
else if (time >= wayPoints[wayPoints.Count - 1].time)
{
return wayPoints[wayPoints.Count - 1].pos;
}
//Check time boundaries - Find the nearest WayPoint your object has passed
float minTime = -1;
// float maxTime = -1;
int minIndex = -1;
for (int i = 1; i < wayPoints.Count; i++)
{
if (time > wayPoints[i - 1].time && time <= wayPoints[i].time)
{
// maxTime = wayPoints[i].time;
int index = i - 1;
minTime = wayPoints[index].time;
minIndex = index;
}
}
float spd0 = minIndex == 1 ? spdInit : speeds[minIndex - 2];
float len = lengths[minIndex - 1];
float acc = accels[minIndex - 1];
float t = time - minTime;
float posThroughSegment = spd0 * t + acc * t * t / 2;
float percentageThroughSegment = posThroughSegment / len;
//Define the 4 points required to make a Catmull-Rom spline
Vector3 p0 = wayPoints[ClampListPos(minIndex - 1)].pos;
Vector3 p1 = wayPoints[minIndex].pos;
Vector3 p2 = wayPoints[ClampListPos(minIndex + 1)].pos;
Vector3 p3 = wayPoints[ClampListPos(minIndex + 2)].pos;
return GetCatmullRomPosition(percentageThroughSegment, p0, p1, p2, p3, catmullRomAlpha);
}
さて、それがどうなるか見てみましょう...
えーと…うーん。ある時点で後方に移動してから再び前進することを除いて、ほとんど良さそうに見えました。実際、グラフを確認すると、そこに記載されています。 12〜16秒の間、速度は否定的です。なぜこれが起こるのですか?この動きの機能(一定の加速度)は単純ですが、いくつかの制限があります。速度が急激に変化する場合、そのような副作用がなくても、前提(正しい時間にチェックポイントを通過する)を保証できる一定の加速度値がない場合があります。
さて何をしようか?
あなたにはたくさんのオプションがあります:
AnimationCurve
フィールドを追加し、素晴らしい組み込みのドロワーを使用してエディターで移動グラフをカスタマイズします。 AddKey
メソッドを使用してコントロールポイントを簡単に追加し、Evaluate
メソッドを使用して一時的に位置をフェッチできます。コンポーネントクラスでOnValidate
メソッドを使用して、カーブで編集するときにシーン内のポイントを自動的に更新することもできます。その逆も可能です。止まらないで!パスのラインギズモにグラデーションを追加して、どこが速くなったり遅くなったりするのを簡単に確認したり、エディターモードでパスを操作するためのハンドルを追加したり...クリエイティブになりましょう!
私が知る限り、ソリューションのほとんどはすでに含まれていますが、正しく初期化されていません。
ローカル速度はスプラインの長さに依存するため、セグメントの長さの逆数で速度を調整する必要があります(これは数ステップで簡単に概算できます) 。
確かに、あなたの場合、速度を制御することはできず、入力時間だけを制御できるので、必要なのは、の順序と長さに応じて_SimpleWayPoint.time
_の値を適切に分散することです。フィールド宣言で手動で初期化する代わりに、前のスプラインセグメント。このようにして、percentageThroughSegment
を均等に分散させる必要があります。
コメントで述べたように、その数学のいくつかはLerp()
:)でより単純に見えるかもしれません
あなたは彼らが彼らのホイールシステムのために持っているwheelcolliderチュートリアルで仕事を試みることができます。
シミュレートされた運転を実現するために、リジッドボディ変数とともに調整できるいくつかの変数があります。
彼らが書いているように
1つの車両インスタンスに最大20個のホイールを配置でき、各ホイールはステアリング、モーター、またはブレーキトルクを適用します。
免責事項:WheelCollidersの使用経験はごくわずかです。しかし、彼らはあなたが私に探しているもののように見えます。
最初にいくつかの用語を定義しましょう:
t
:_0
_から_1
_の範囲の各スプラインの補間変数。s
:各スプラインの長さ。使用するスプラインのタイプ(catmull-rom、bezierなど)に応じて、推定全長を計算する式があります。dt
:フレームごとのt
の変化。あなたの場合、これがすべての異なるスプラインにわたって一定である場合、各スプラインの長さが異なるs
であるため、スプラインの終点で突然の速度変化が見られます。各ジョイントでの速度変更を容易にする最も簡単な方法は次のとおりです。
_void Update() {
float dt = 0.05f; //this is currently your "global" interpolation speed, for all splines
float v0 = s0/dt; //estimated linear speed in the first spline.
float v1 = s1/dt; //estimated linear speed in the second spline.
float dt0 = interpSpeed(t0, v0, v1) / s0; //t0 is the current interpolation variable where the object is at, in the first spline
transform.position = GetCatmullRomPosition(t0 + dt0*Time.deltaTime, ...); //update your new position in first spline
}
_
どこ:
_float interpSpeed(float t, float v0, float v1, float tEaseStart=0.5f) {
float u = (t - tEaseStart)/(1f - tEaseStart);
return Mathf.Lerp(v0, v1, u);
}
_
上記の直感は、最初のスプラインの終わりに到達すると、次のスプラインで予想される速度を予測し、現在の速度がそこに到達するのを容易にするというものです。
最後に、イージングの見栄えをさらに良くするために:
interpSpeed()
で非線形補間関数を使用することを検討してください。