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 にアタッチ
- シーン上のプレイヤー(例:
Playerオブジェクト)を選択します。 SquashStretch.csをプロジェクトに追加し、Playerにアタッチします。- インスペクターで以下の値を調整します。
- 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 ロジックとは独立した一時的な演出を作ることができます。
このように、小さなコンポーネントに「伸縮演出」という責務だけを持たせておくと、プロジェクトが大きくなっても保守しやすく、演出の追加・変更も怖くなくなりますね。
