Unityでキャラクターのジャンプや着地演出を作るとき、つい1つの巨大なスクリプトの Update に「移動」「入力」「アニメーション」「エフェクト」など全部詰め込んでしまうことってありますよね。
最初はそれでも動きますが、だんだんと以下のような問題が出てきます。

  • ジャンプ処理に演出コード(scale変更)がベタ書きされていて、他のキャラで使い回せない
  • 「ジャンプの挙動だけ変えたいのに、演出コードまで巻き込んでバグる」
  • プレイヤーと敵で同じような演出をコピペしてしまい、修正コストが倍増する

こういうときは、「伸縮演出」だけを担当する小さなコンポーネントに分離しておくと、とても扱いやすくなります。
この記事では、ジャンプや着地の瞬間に親オブジェクトの localScale を変化させて、アニメーション的な柔らかさを演出する 「SquashStretch」コンポーネントを作っていきます。

【Unity】ジャンプにぷにっとした柔らかさを!「SquashStretch」コンポーネント

ここでは、「イベントを受けて伸縮するだけ」に責務を絞ったコンポーネントを作ります。
「ジャンプした」「着地した」といった判定は別コンポーネントに任せ、このコンポーネントは public メソッド経由で呼び出されるだけにする構成です。

フルコード:SquashStretch.cs


using UnityEngine;

/// <summary> 
/// ジャンプや着地など、イベントに応じてスケールを一時的に変化させるコンポーネント。
/// 「伸縮演出」だけを担当し、ジャンプ判定や物理挙動は別コンポーネントに任せる設計。
/// </summary>
[DisallowMultipleComponent]
public class SquashStretch : MonoBehaviour
{
    // 元のスケール(初期スケール)を保持
    Vector3 _defaultScale;

    // 現在の補間時間
    float _currentTime;

    // 演出中かどうか
    bool _isPlaying;

    // 今回の演出の開始スケールと終了スケール
    Vector3 _fromScale;
    Vector3 _toScale;

    // 1回の演出にかける時間
    [SerializeField] private float _duration = 0.15f;

    // 伸縮カーブ。0〜1の時間に対して、0〜1の補間係数を返す。
    // 例: 0→1→0と戻るバウンスカーブなどを設定すると雰囲気UP
    [SerializeField] private AnimationCurve _curve = AnimationCurve.EaseInOut(0, 0, 1, 1);

    // ジャンプ開始時に適用するスケール倍率
    [Header("Jump / Land scales")]
    [SerializeField] private Vector3 _jumpScaleMultiplier = new Vector3(0.9f, 1.1f, 1.0f);

    // 着地時に適用するスケール倍率
    [SerializeField] private Vector3 _landScaleMultiplier = new Vector3(1.1f, 0.8f, 1.0f);

    // 複数演出が連続したときに上書きするかどうか
    [Header("Behavior")]
    [SerializeField] private bool _overrideIfPlaying = true;

    // ワールドではなくローカルスケールを使う前提
    void Awake()
    {
        _defaultScale = transform.localScale;
    }

    void Update()
    {
        if (!_isPlaying) return;

        _currentTime += Time.deltaTime;
        float t = Mathf.Clamp01(_currentTime / Mathf.Max(_duration, 0.0001f));

        // カーブに沿って補間係数を取得
        float curveValue = _curve.Evaluate(t);

        // 開始スケールから終了スケールへ補間
        Vector3 targetScale = Vector3.LerpUnclamped(_fromScale, _toScale, curveValue);
        transform.localScale = targetScale;

        // 演出終了
        if (_currentTime >= _duration)
        {
            _isPlaying = false;
            transform.localScale = _defaultScale; // 最後は必ず元スケールに戻す
        }
    }

    /// <summary>
    /// ジャンプ開始時に呼び出すと、縦長に伸びる演出を再生する。
    /// 外部の「ジャンプ制御コンポーネント」から呼び出す想定。
    /// </summary>
    public void PlayJumpStretch()
    {
        PlayScaleAnimation(_jumpScaleMultiplier);
    }

    /// <summary>
    /// 着地時に呼び出すと、つぶれるような演出を再生する。
    /// 外部の「着地検出コンポーネント」から呼び出す想定。
    /// </summary>
    public void PlayLandSquash()
    {
        PlayScaleAnimation(_landScaleMultiplier);
    }

    /// <summary>
    /// 任意のスケール倍率を指定して演出を再生したい場合に使う汎用メソッド。
    /// 例: 攻撃時のノックバック演出などにも流用可能。
    /// </summary>
    public void PlayCustom(Vector3 scaleMultiplier)
    {
        PlayScaleAnimation(scaleMultiplier);
    }

    /// <summary>
    /// 演出を即座にキャンセルして元スケールに戻す。
    /// </summary>
    public void ResetScaleImmediate()
    {
        _isPlaying = false;
        _currentTime = 0f;
        transform.localScale = _defaultScale;
    }

    /// <summary>
    /// 内部用:指定の倍率でスケール演出を開始する。
    /// </summary>
    void PlayScaleAnimation(Vector3 multiplier)
    {
        // すでに再生中で、上書きしない設定なら無視
        if (_isPlaying && !_overrideIfPlaying)
        {
            return;
        }

        // 再生開始時点のスケールを起点にするか、
        // 常に _defaultScale を起点にするかは好みだが、
        // ここでは「現在のスケール」からの変化にしておく。
        _fromScale = transform.localScale;

        // 目標スケールは「元スケール × 倍率」で計算
        _toScale = new Vector3(
            _defaultScale.x * multiplier.x,
            _defaultScale.y * multiplier.y,
            _defaultScale.z * multiplier.z
        );

        _currentTime = 0f;
        _isPlaying = true;
    }

#if UNITY_EDITOR
    // エディタ上で値を変えたときにも _defaultScale を更新しておく
    void OnValidate()
    {
        if (!Application.isPlaying)
        {
            _defaultScale = transform.localScale;
        }

        if (_duration <= 0f)
        {
            _duration = 0.01f;
        }
    }
#endif
}

使い方の手順

ここでは例として、プレイヤーキャラクターがジャンプ&着地するタイミングで伸縮させるケースを想定します。

手順①:プレイヤーの GameObject にアタッチ

  1. シーン上のプレイヤー(例: Player オブジェクト)を選択します。
  2. SquashStretch.cs をプロジェクトに追加し、Player にアタッチします。
  3. インスペクターで以下の値を調整します。
    • Duration: 0.1〜0.2 くらいから試す
    • Curve: デフォルトの EaseInOut でもOK。バウンス系にするとよりアニメっぽくなります。
    • Jump Scale Multiplier: X:0.9, Y:1.1, Z:1.0 など
    • Land Scale Multiplier: X:1.1, Y:0.8, Z:1.0 など

手順②:ジャンプ制御側から呼び出す

次に、ジャンプや着地を管理しているコンポーネントから SquashStretch を呼び出します。
ここでは、非常にシンプルな例として「スペースキーでジャンプ」「地面に触れているかを簡易判定」するプレイヤー制御を書きます。


using UnityEngine;

/// <summary>
/// 非常にシンプルなジャンプ制御サンプル。
/// 実際のプロジェクトでは、よりしっかりした移動・接地判定を書くことを推奨。
/// </summary>
[RequireComponent(typeof(Rigidbody2D))]
[DisallowMultipleComponent]
public class SimplePlayerJump : MonoBehaviour
{
    [SerializeField] private float _jumpForce = 5f;
    [SerializeField] private LayerMask _groundLayer;
    [SerializeField] private float _groundCheckDistance = 0.1f;

    Rigidbody2D _rb;
    SquashStretch _squashStretch;

    void Awake()
    {
        _rb = GetComponent<Rigidbody2D>();
        _squashStretch = GetComponent<SquashStretch>();
    }

    void Update()
    {
        // 非常に単純な「スペースでジャンプ」入力
        if (Input.GetKeyDown(KeyCode.Space) && IsGrounded())
        {
            // 上方向に力を加えてジャンプ
            _rb.velocity = new Vector2(_rb.velocity.x, 0f);
            _rb.AddForce(Vector2.up * _jumpForce, ForceMode2D.Impulse);

            // ジャンプ伸び演出
            if (_squashStretch != null)
            {
                _squashStretch.PlayJumpStretch();
            }
        }
    }

    void FixedUpdate()
    {
        // 着地判定:地面に触れた瞬間に着地演出を再生
        if (IsGrounded() && Mathf.Abs(_rb.velocity.y) < 0.01f)
        {
            if (_squashStretch != null)
            {
                _squashStretch.PlayLandSquash();
            }
        }
    }

    bool IsGrounded()
    {
        // 足元に向かってレイを飛ばす簡易接地判定
        RaycastHit2D hit = Physics2D.Raycast(transform.position, Vector2.down, _groundCheckDistance, _groundLayer);
        return hit.collider != null;
    }

    void OnDrawGizmosSelected()
    {
        // 接地判定の可視化
        Gizmos.color = Color.yellow;
        Gizmos.DrawLine(transform.position, transform.position + Vector3.down * _groundCheckDistance);
    }
}

このように、ジャンプ制御(SimplePlayerJump)と伸縮演出(SquashStretch)を分離しておくことで、演出だけ別のキャラにも簡単に流用できます。

手順③:敵キャラや動く床にも使ってみる

  • 敵キャラの着地演出
    敵が高い場所から落ちてくるシーンで、着地時に PlayLandSquash() を呼ぶだけで、重量感のある演出が簡単に追加できます。
  • 動く床の上下移動
    動く床が一番下に到達した瞬間に PlayLandSquash()、一番上に到達した瞬間に PlayJumpStretch() を呼べば、床が「ぐにっ」とつぶれて戻るような表現もできます。

手順④:プレハブ化して量産

プレイヤーや敵、動く床など、「伸縮演出を入れたいオブジェクトには全部 SquashStretch をアタッチした状態でプレハブ化」しておくと、

  • プレハブ単位でスケールのカーブや倍率を調整できる
  • 共通の演出ロジックは 1 コンポーネントにまとまっているので、後から演出を変えたくなっても管理がラク

というメリットがあります。


メリットと応用

SquashStretch コンポーネントを使うことで、以下のようなメリットがあります。

  • 責務が明確:ジャンプ制御や物理挙動のコードから「演出ロジック」を切り離せる
  • プレハブ管理が楽:キャラごとにカーブや倍率を変えたいときは、このコンポーネントのインスペクターをいじるだけ
  • レベルデザインに優しい:レベルデザイナーが「この床だけちょっと柔らかく見せたい」といった調整を、スクリプトに触れずに行える
  • 演出の一元管理:後から「全キャラの伸縮演出を少し弱めたい」となったときは、このコンポーネントのデフォルト値を変えるだけでOK

また、PlayCustom メソッドを使えば、ジャンプ・着地以外にも応用できます。

  • 攻撃モーションの出始めで PlayCustom(new Vector3(1.2f, 0.8f, 1.0f)) を呼んで、攻撃の勢いを強調
  • ダメージを受けたときに、横にぺちゃんこになるような演出
  • アイテム取得時に、アイテムオブジェクトを一瞬だけ大きくしてから戻す

改造案:コルーチン版のワンショット演出

Update ベースではなく、「1回だけ演出して終わり」という用途が多い場合は、コルーチンで書くのも手です。
下記は、SquashStretch 内に追加できる簡易的なコルーチン版の演出メソッド例です。


    /// <summary>
    /// コルーチンを使ったワンショット演出例。
    /// 既存の PlayCustom と同じことをするが、呼び出し側で StartCoroutine する形。
    /// </summary>
    public System.Collections.IEnumerator PlayOnceCoroutine(Vector3 multiplier)
    {
        Vector3 from = transform.localScale;
        Vector3 to = new Vector3(
            _defaultScale.x * multiplier.x,
            _defaultScale.y * multiplier.y,
            _defaultScale.z * multiplier.z
        );

        float time = 0f;
        while (time < _duration)
        {
            time += Time.deltaTime;
            float t = Mathf.Clamp01(time / _duration);
            float curveValue = _curve.Evaluate(t);
            transform.localScale = Vector3.LerpUnclamped(from, to, curveValue);
            yield return null;
        }

        // 最後に元スケールへ
        transform.localScale = _defaultScale;
    }

例えば、攻撃時にだけ一度きりの演出をしたい場合は、


StartCoroutine(_squashStretch.PlayOnceCoroutine(new Vector3(1.2f, 0.8f, 1.0f)));

のように呼び出すことで、Update ロジックとは独立した一時的な演出を作ることができます。

このように、小さなコンポーネントに「伸縮演出」という責務だけを持たせておくと、プロジェクトが大きくなっても保守しやすく、演出の追加・変更も怖くなくなりますね。