【Unity】SmoothDampで滑らかな追従を実装する

こじゃらこじゃら

オブジェクトを滑らかに追従させる方法ってないの~?

このはこのは

UnityのSmoothDampメソッドを使えば簡単だわ!

Unityには目標値に滑らかに向かう変化を実現するSmoothDamp系メソッドが用意されています。

SmoothDamp系メソッド一覧
  • Mathf.SmoothDamp
  • Vector2.SmoothDamp
  • Vector3.SmoothDamp
  • Mathf.SmoothDampAngle

これらのスムージングは、目標に近づくにつれて減速し、最終的に目標値になるような動きをします。

ほかによく用いられる方法としてLerpメソッドを使う方法がありますが、これと比較してSmoothDampメソッドには以下のメリットがあります。

SmoothDampのメリット
  • フレームレートに依存しない
  • 目標値に到達するまでのおおよその時間を指定できる
  • 最大速度を指定できる

上記を考慮したスムージングを実装しようとすると、少々複雑な処理を考えなければいけませんが、この問題もSmoothDampメソッドを用いれば解決できます。 [1]

本記事では、SmoothDampメソッドを使い、フレームレートに依存しない滑らかな追従を実現する方法について解説します。

この作品はユニティちゃんライセンス条項の元に提供されています

動作環境
  • Unity2021.1.11f1

スポンサーリンク

Mathf.SmoothDamp()メソッド

float型の値を滑らかに目標値に追従させるメソッドです。
このメソッドを毎フレーム実行することで滑らかな追従を実現します。

メソッド形式

メソッドの形式は以下の通りです。

public static float SmoothDamp(
    float current,
    float target,
    ref float currentVelocity,
    float smoothTime,
    float maxSpeed = Mathf.Infinity,
    float deltaTime = Time.deltaTime
);

第1引数currentには、現在値を指定します。
この値を基準に次の値が計算されます。

第2引数target目標値です。
最終的に行き着く先の値を指定します。

第3引数currentVelocityには、現在速度を格納する変数を指定します。
SmoothDamp関数内部で計算のために使われます。

先頭についているrefは参照渡しを示すC#のキーワードです。
変数の参照を渡すことで、その変数の値を呼び出されるメソッドから読み書きできるようになります。

第4引数smoothTimeには、目標値に到達するまでのおおよその時間を秒数で指定します。
おおよそなので、厳密にこの時間が掛かるという訳ではありません。

第5引数maxSpeedには、値変化するときの最高速度を指定します。
指定が省略された場合は、Mathf.Infinityすなわち正の無限大が指定されます。これにより、制限速度の上限を無くすことが可能です。

第6引数deltaTimeには、この関数が最後に呼び出されてからの経過時間を指定します。
指定が省略された場合は、Time.deltaTimeの値となります。

戻り値は、変化後の値です。
次フレームにMathf.SmoothDampメソッドを呼び出す時、第1引数currentに指定する値になります。

サンプルスクリプト

あるオブジェクトのx座標をターゲットに追従させるサンプルスクリプトです。

SmoothDampExample.cs
using UnityEngine;

/// <summary>
/// ターゲットのx座標に追従させるスクリプト
/// </summary>
public class SmoothDampExample : MonoBehaviour
{
    // ターゲット
    [SerializeField] private Transform _target;

    // 追従させるオブジェクト
    [SerializeField] private Transform _follower;

    // 目標値に到達するまでのおおよその時間[s]
    [SerializeField] private float _smoothTime = 0.3f;

    // 最高速度
    [SerializeField] private float _maxSpeed = float.PositiveInfinity;

    // 現在速度(SmoothDampの計算のために必要)
    private float _currentVelocity = 0;

    // x座標をターゲットのx座標に追従させる
    private void Update()
    {
        // 現在位置取得
        var currentPos = _follower.position;

        // 次フレームの位置を計算
        currentPos.x = Mathf.SmoothDamp(
            currentPos.x,
            _target.position.x,
            ref _currentVelocity,
            _smoothTime,
            _maxSpeed
        );

        // 現在位置のx座標を更新
        _follower.position = currentPos;
    }
}

上記スクリプトを適当なゲームオブジェクトにアタッチして、_targetに追従対象のターゲット、_followerに追従させるオブジェクトを指定してください。必要に応じて_smoothTimeと_maxSpeedも調整してください。

実行結果

ターゲットに向かってx座標が滑らかに遅れて追従していることが分かります。

Vector2.SmoothDamp()メソッド

Mathf.SmoothDamp()メソッドのVector2版です。
2次元座標単位で滑らかな追従を実装したい場合に重宝します。

メソッドの形式は以下の通りです。

public static Vector2 SmoothDamp(
    Vector2 current,
    Vector2 target,
    ref Vector2 currentVelocity,
    float smoothTime,
    float maxSpeed = Mathf.Infinity,
    float deltaTime = Time.deltaTime
);

Mathf.SmoothDamp()メソッドと比較して、第1~3引数がfloat型からVector2型に変わったところ以外は一緒です。

Vector3.SmoothDamp()メソッド

Mathf.SmoothDamp()メソッドのVector3版です。
メソッドの形式は以下の通りです。

public static Vector3 SmoothDamp(
    Vector3 current,
    Vector3 target,
    ref Vector3 currentVelocity,
    float smoothTime,
    float maxSpeed = Mathf.Infinity,
    float deltaTime = Time.deltaTime
);

こちらも、第1~3引数がVector3型になっている点以外はMathf.SmoothDamp()メソッドと一緒です。

Mathf.SmoothDampAngle()メソッド

Mathf.SmoothDamp()メソッドの角度対応版です。

向きなどの角度に対して滑らかな追従をしたい場合に役立ちます。
角度は内部的に正規化して計算されるため、360度以上やマイナスなどの角度でも問題なく計算できます。

メソッドの形式

メソッドの形式は以下の通りです。

public static float SmoothDampAngle(
    float current,
    float target,
    ref float currentVelocity,
    float smoothTime,
    float maxSpeed = Mathf.Infinity,
    float deltaTime = Time.deltaTime
);

引数の形式はMathf.SmoothDamp()メソッドと一緒です。
current、targetには度数法の角度を指定します。

戻り値には、度数法表記の正規化された角度が返されます。

サンプルスクリプト

指定されたターゲットの方向に滑らかに回転するスクリプトの例です。

SmoothDampAngleExample.cs
using UnityEngine;

/// <summary>
/// ターゲットに回転するスクリプト
/// </summary>
public class SmoothDampAngleExample : MonoBehaviour
{
    // ターゲット
    [SerializeField] private Transform _target;

    // 追従させるオブジェクト
    [SerializeField] private Transform _follower;

    // 目標値に到達するまでのおおよその時間[s]
    [SerializeField] private float _smoothTime = 0.3f;

    // 最高速度(角速度)
    [SerializeField] private float _maxSpeed = float.PositiveInfinity;

    // 現在の角速度(SmoothDampAngleの計算のために必要)
    private float _currentVelocity = 0;

    // ターゲットの方向に回転する
    private void Update()
    {
        // ターゲットへの角度計算
        var targetDir = _target.position - _follower.position;
        var targetAngle = Mathf.Atan2(targetDir.y, targetDir.x) * Mathf.Rad2Deg;

        // 自身の角度計算
        var currentDir = _follower.right;
        var currentAngle = Mathf.Atan2(currentDir.y, currentDir.x) * Mathf.Rad2Deg;

        // 次フレームの回転角度計算
        currentAngle = Mathf.SmoothDampAngle(
            currentAngle,
            targetAngle,
            ref _currentVelocity,
            _smoothTime,
            _maxSpeed
        );

        // 角度の反映
        _follower.rotation = Quaternion.Euler(0, 0, currentAngle);
    }
}

実行結果

Lerpと比較した場合のメリット

スムージングを実現するのによく見かける方法として、Lerpメソッドを用いる方法があります。しかし、この方法にはフレームレートによって動きが変わってしまう可能性のあるデメリットが存在します。ただ、実装次第である程度変化を抑えることは可能です。

SmoothDamp系メソッドは、フレームレートによらず動きがほぼ一定になるメリットがあります。

検証用スクリプト

異なるフレームレートの動作検証用として、Lerpを使う方法、SmoothDampを使う方法それぞれでフレームレートを変えられるスクリプトを用意しました。

Lerpの検証用スクリプト

TestLerp.cs
using System.Collections;
using UnityEngine;

/// <summary>
/// Lerpで指定周期更新
/// </summary>
public class TestLerp : MonoBehaviour
{
    [SerializeField] private Transform _target;
    [SerializeField] private float _deltaTime = 1;
    [SerializeField, Range(0, 1)] private float _smoothTime = 0.5f;

    private IEnumerator Start()
    {
        var tr = transform;

        while (true)
        {
            var pos = tr.position;

            pos.x = Mathf.Lerp(
                pos.x,
                _target.position.x,
                _smoothTime * _deltaTime
            );

            tr.position = pos;

            yield return new WaitForSeconds(_deltaTime);
        }
    }
}

SmoothDampの検証用スクリプト

TestSmoothDamp.cs
using System.Collections;
using UnityEngine;

/// <summary>
/// SmoothDampで指定周期更新
/// </summary>
public class TestSmoothDamp : MonoBehaviour
{
    [SerializeField] private Transform _target;
    [SerializeField] private float _deltaTime = 1;
    [SerializeField] private float _smoothTime = 1;

    private IEnumerator Start()
    {
        var tr = transform;
        var currentVelocity = 0f;

        while (true)
        {
            var pos = tr.position;
            
            pos.x = Mathf.SmoothDamp(
                pos.x,
                _target.position.x,
                ref currentVelocity,
                _smoothTime,
                Mathf.Infinity, _deltaTime
            );

            tr.position = pos;

            yield return new WaitForSeconds(_deltaTime);
        }
    }
}

実行結果

Lerp、SmoothDampそれぞれのスクリプトでの実行結果です。
更新周期を0.5秒、0.1秒で追従するオブジェクトを並べて比較してみました。

Lerpの実行結果

移動中の位置が周期で若干ずれていることが分かります。

SmoothDampの実行結果

移動中の位置が周期によらずほぼ一致する結果となりました。

さいごに

本記事では、スムージングの便利メソッドSmoothDampについて解説しました。

Lerpメソッドで実装するのが簡単ですが、実装によってはフレームレートによって挙動が変化してしまう問題があります。

SmoothDamp系メソッドは、この問題を複雑な実装を要求せずに解決してくれます。
Lerpメソッドでも工夫次第ではこの問題を解決できますが、特に拘りがない限りはSmoothDampメソッドを使うのが楽です。

様々な場面で活躍できるでしょう。

参考サイト

スポンサーリンク