課題
UnityでLerpを使って値を変化させる際、目標値にピッタリ到達せず、微妙に届かないという問題に遭遇することがありました。
具体例
// 1.0 → 1.5 に変化させたいが...
value = Mathf.Lerp(value, 1.5f, Time.deltaTime * speed);
// 結果: 1.49999999 (目標値に届かない!)
この問題により、以下のような不具合が発生します:
-
if (value >= targetValue)の条件が永遠に満たされない - アニメーションが完了しない
- それらが起因して状態遷移が正しく行われない
原因
1. Lerpの典型的な誤用パターン
// ❌ よくある間違い
currentValue = Mathf.Lerp(currentValue, targetValue, Time.deltaTime * speed);
このコードは毎フレーム現在値を起点に補間するため、指数関数的に減衰し、理論的には永遠に目標値に到達しません。
2. 浮動小数点演算の限界
浮動小数点数(float)は内部的に2進数で表現されるため、10進数の値を完全に正確に表現できないことがあります。
解決策
解決策1: Mathf.MoveTowards を使う(推奨)
MoveTowardsは目標値を超えないことが保証されており、ピタリと到達します。
// ✅ 推奨される方法
currentValue = Mathf.MoveTowards(currentValue, targetValue, speed * Time.deltaTime);
// 目標値に到達したか判定
if (currentValue >= targetValue)
{
// 確実にここに到達する
}
UniTaskと組み合わせた実装例
これは実際に書いたコードです。
当たり判定を指定の倍率拡大するもの。
今回はコチラを採用しました。
private async UniTask PerformCollisionExpansion()
{
_isColliding = true;
await UniTask.WaitUntil(() =>
{
CollisionExpansionRadiusFactor = Mathf.MoveTowards(
CollisionExpansionRadiusFactor,
_collisionExpansionRadius,
Time.deltaTime / _collisionExpansionDuration
);
return CollisionExpansionRadiusFactor >= _collisionExpansionRadius;
});
// ここに到達した時点で確実に目標値になっている
}
Vector3版も利用可能らしい
transform.position = Vector3.MoveTowards(
transform.position,
targetPosition,
speed * Time.deltaTime
);
解決策2: 閾値チェック + 強制設定
Lerpを使い続けたい場合は、目標値に十分近づいたら強制的に設定します。
currentValue = Mathf.Lerp(currentValue, targetValue, Time.deltaTime * speed);
// 閾値以下になったら目標値に設定
if (Mathf.Abs(currentValue - targetValue) < 0.001f)
{
currentValue = targetValue;
}
解決策3: 時間ベースのLerp(コルーチン)
開始値と終了値を固定し、経過時間で補間する方法です。
private IEnumerator LerpOverTime(float startValue, float endValue, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
currentValue = Mathf.Lerp(startValue, endValue, t);
yield return null;
}
// 最後に確実に目標値を設定
currentValue = endValue;
}
UniTaskで
private async UniTask LerpOverTime(float startValue, float endValue, float duration)
{
float elapsed = 0f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = Mathf.Clamp01(elapsed / duration);
currentValue = Mathf.Lerp(startValue, endValue, t);
await UniTask.Yield();
}
currentValue = endValue;
}
解決策4: Mathf.SmoothDamp を使う
より滑らかな加減速が必要な場合に適しています。
private float velocity = 0f;
void Update()
{
currentValue = Mathf.SmoothDamp(
currentValue,
targetValue,
ref velocity,
smoothTime
);
}
各手法の比較
| 手法 | メリット | デメリット | 用途 |
|---|---|---|---|
| MoveTowards | 確実に到達、シンプル | 等速移動(慣性なし) | 確実に目標値に到達したい場合 |
| 閾値チェック付きLerp | Lerpの動きを維持 | コードが冗長 | Lerpの減速を使いたいが到達も保証したい |
| 時間ベースLerp | アニメーション時間が正確 | コルーチン/UniTask必要 | 決まった時間でアニメーション |
| SmoothDamp | 最も滑らかな動き | 到達時間が不確定 | 物理的な追従動作 |
まとめ
-
単純に目標値まで移動:
MoveTowardsを使う -
滑らかな動きが必要:
SmoothDampを使う - 時間制御が必要: コルーチン/UniTask + 時間ベースLerp
- どうしてもLerpを使いたい: 閾値チェックを追加
基本的には MoveTowardsが最もシンプルで確実 なので、迷ったらこれを使うことをおすすめします。