1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Unity Lerpで目標値に到達しない問題と解決法

Posted at

課題

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が最もシンプルで確実 なので、迷ったらこれを使うことをおすすめします。

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?