【Unity】現在値と目的値を補間してアニメーションする汎用クラス

f:id:halya_11:20180727015113g:plain

アニメーションさせたいけどイージングを使うほどでもない時など、
現在値と目的値を定数で毎フレーム線形補間するといい感じにアニメーションします。

本ブログでもよく使っているものの、毎回書くのが地味に面倒なので多少汎用的なクラスを作ってみました。

ソースコード

いきなりですがソースコードです。

using UnityEngine;

public abstract class Spring <T>
{
    /// <summary>
    /// 開始値
    /// </summary>
    public T StartValue { get { return _startValue; } set { _startValue = value; } }
    private T _startValue;
    /// <summary>
    /// 終了値
    /// </summary>
    public T EndValue { get { return _endValue; } set { _endValue = value; } }
    private T _endValue;
    /// <summary>
    /// 現在値
    /// </summary>
    public T CurrentValue { get { return _currentValue; } }
    private T _currentValue;
    /// <summary>
    /// ピンポンループするか
    /// </summary>
    public bool Pingpong { get { return _pingpong; } set { _pingpong = value; } }
    private bool _pingpong = true;
    /// <summary>
    /// ばねの強さ
    /// </summary>
    public float Strength { get { return _strength; } set { _strength = value; } }
    private float _strength = 3;
    /// <summary>
    /// 端に丸められる値の閾値
    /// </summary>
    public float EdgeRate { get { return _edgeRate; } set { _edgeRate = value; } }
    private float _edgeRate = 0.001f;

    /// <summary>
    /// Start~Endに対するCurrentの進捗率
    /// </summary>
    protected abstract float Progress { get; }

    private bool _inverse = false;
    private bool _isPaused = true;
    private event System.Action<T> _onUpdate;

    public Spring(T startValue, T endValue)
    {
        _startValue = startValue;
        _endValue = endValue;
        _currentValue = _startValue;
        _isPaused = true;
    }

    /// <summary>
    /// 再生する
    /// </summary>
    public void Play(System.Action<T> onUpdate)
    {
        _onUpdate = onUpdate;
        _isPaused = false;
    }
    
    /// <summary>
    /// 更新する
    /// </summary>
    public void Update(float deltaTime)
    {
        if (_isPaused) {
            return;
        }
        // 目標の値を取得する
        T target;
        if (_pingpong) {
            if (_inverse) {
                target = _startValue;
            }
            else {
                target = _endValue;
            }
        }
        else {
            target = _endValue;
        }
        // 現在の値を計算する
        var rate = Mathf.Min(1, _strength * deltaTime);
        _currentValue = Lerp(_currentValue, target, rate);

        // PingPongの場合は端に到達したら反転する
        if (_pingpong) {
            if (target.Equals(_endValue) && Progress > 1.0f - _edgeRate) {
                _inverse = true;
            }
            else if (target.Equals(_startValue) && Progress < _edgeRate) {
                _inverse = false;
            }
        }
        
        // コールバック
        if (_onUpdate != null) {
            _onUpdate(_currentValue);
        }
    }

    /// <summary>
    /// 一時停止から復帰する
    /// </summary>
    public void Resume()
    {
        _isPaused = false;
    }

    /// <summary>
    /// 一時停止する
    /// </summary>
    public void Pause()
    {
        _isPaused = true;
    }
    
    protected abstract T Lerp(T a, T b, float rate);
}

/// <summary>
/// Float用ばね制御クラス
/// </summary>
public class FloatSpring : Spring<float>
{
    public FloatSpring(float startValue, float endValue) : base(startValue, endValue)
    {
    }
        
    protected override float Progress {
        get { return (CurrentValue - StartValue) / (EndValue - StartValue); }
    }

    protected override float Lerp(float a, float b, float rate)
    {
        return Mathf.Lerp(a, b, rate);
    }
}

/// <summary>
/// Vector3用ばね制御クラス
/// </summary>
public class Vector3Spring : Spring<Vector3>
{
    public Vector3Spring(Vector3 startValue, Vector3 endValue) : base(startValue, endValue)
    {
    }
        
    protected override float Progress {
        get { return (CurrentValue - StartValue).magnitude / (EndValue - StartValue).magnitude; }
    }

    protected override Vector3 Lerp(Vector3 a, Vector3 b, float rate)
    {
        return Vector3.Lerp(a, b, rate);
    }
}

使いたい型に応じてSpringを派生します。
今回はとりあえずfloatとVector3に対応してみました。

使い方と結果

使う側はこんな感じです。

using UnityEngine;

public class Sample : MonoBehaviour {

    private Vector3Spring _spring = new Vector3Spring(Vector3.left, Vector3.right);

    private void Awake()
    {
        _spring.Play(value => transform.position = value);
    }

    private void Update ()
    {
        _spring.Update(Time.deltaTime);
    }
}

適当なGameObjectにアタッチして再生してみます。

f:id:halya_11:20180727014911g:plain

いい感じに動きました。

今度は少し設定を変えてみます。

using UnityEngine;

public class Sample : MonoBehaviour {

    [SerializeField]
    private Transform _traceTarget;

    private Vector3Spring _spring = new Vector3Spring(Vector3.left, Vector3.right);

    private void Awake()
    {
        _spring.Pingpong = false;
        _spring.Play(value => transform.position = value);
    }

    private void Update ()
    {
        _spring.StartValue = transform.position;
        _spring.EndValue = _traceTarget.position;
        _spring.Update(Time.deltaTime);
    }
}

これを再生すると、TraceTargetに追随するような動きを作れます。

f:id:halya_11:20180727015113g:plain