UnityのUI実装でありがちなのが、ダメージ処理・HP計算・アニメーション・SE再生などを、全部ひとつの Update 関数に書いてしまうパターンです。
最初は動いて見えるのですが、だんだん「どこを触ればいいのか分からない」「他の画面でも同じ揺れを使いたいのにコピペ地獄」という状態になりがちですね。

この記事では、「UIが揺れる」という機能だけに責務を絞ったコンポーネント ShakeUI を作っていきます。
HPバー、ダメージアイコン、警告マークなどにポン付けするだけで、きれいに「ガタガタ揺れる」演出を追加できるようにして、UIロジックと演出ロジックを分離していきましょう。

【Unity】UIを簡単にガタガタ揺らす!「ShakeUI」コンポーネント

以下が、UIを一定時間揺らすための ShakeUI コンポーネントの完全版コードです。
Unity6(Unity 6000系)+新しいInput Systemなどの環境でもそのまま使えます。


using UnityEngine;
using System.Collections;

/// <summary>
/// UI用のシンプルな「揺れ」コンポーネント。
/// RectTransform を揺らして、ダメージや警告などの演出に使う。
/// 
/// ・Shake() を呼ぶだけで揺れが開始
/// ・揺れ中に再度 Shake() を呼ぶと、設定に応じて上書き or 加算
/// ・UI以外(Transform)にも一応使える設計
/// </summary>
[DisallowMultipleComponent] // 同じオブジェクトに複数アタッチされるのを防ぐ
public class ShakeUI : MonoBehaviour
{
    // --- 参照関連 ---

    [Header("対象Transform(未指定なら自動取得)")]
    [Tooltip("揺らしたい RectTransform。未指定の場合は、このGameObjectの RectTransform を探します。なければ Transform を使います。")]
    [SerializeField] private RectTransform targetRectTransform;

    [Tooltip("RectTransform が無いオブジェクト向け。未指定なら自身の Transform。")]
    [SerializeField] private Transform targetTransform;

    // --- 基本設定 ---

    [Header("揺れの基本設定")]
    [Tooltip("1回のShakeの長さ(秒)")]
    [SerializeField] private float duration = 0.3f;

    [Tooltip("揺れの強さ(位置のランダム振れ幅)")]
    [SerializeField] private float positionStrength = 20f;

    [Tooltip("揺れの強さ(回転のランダム振れ幅・度数)")]
    [SerializeField] private float rotationStrength = 10f;

    [Tooltip("時間経過による減衰カーブ(0~1の時間に対する減衰係数)")]
    [SerializeField] private AnimationCurve attenuationCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);

    [Header("挙動オプション")]
    [Tooltip("揺れ中に再度 Shake() が呼ばれた時、残り時間を上書きするかどうか")]
    [SerializeField] private bool overrideWhileShaking = true;

    [Tooltip("Time.timeScale の影響を受けるか。false にするとポーズ中でも揺れる")]
    [SerializeField] private bool useUnscaledTime = true;

    [Tooltip("Play On Awake: シーン開始時に自動で揺らすかどうか(デバッグ用)")]
    [SerializeField] private bool playOnAwake = false;

    // --- 内部状態 ---

    // 元の位置・回転を保持しておく
    private Vector3 _defaultLocalPosition;
    private Quaternion _defaultLocalRotation;

    private Coroutine _shakeCoroutine;
    private bool _initialized;

    private void Awake()
    {
        InitializeTarget();

        // 元のTransform情報を記録
        CacheDefaultTransform();

        if (playOnAwake)
        {
            Shake();
        }
    }

    private void OnEnable()
    {
        // Disable / Enable で元に戻らない事故を防ぐため、再度キャッシュ
        CacheDefaultTransform();
    }

    private void OnDisable()
    {
        // 無効化されるときに、変な位置で止まらないように元に戻す
        ResetTransform();
    }

    /// <summary>
    /// 対象の RectTransform / Transform を初期化する
    /// </summary>
    private void InitializeTarget()
    {
        if (_initialized) return;

        // RectTransform が指定されていなければ、自身から探す
        if (targetRectTransform == null)
        {
            targetRectTransform = GetComponent<RectTransform>();
        }

        // Transform が指定されていなければ、自身の Transform を使う
        if (targetTransform == null)
        {
            targetTransform = transform;
        }

        _initialized = true;
    }

    /// <summary>
    /// 元のローカル座標・回転をキャッシュする
    /// </summary>
    private void CacheDefaultTransform()
    {
        InitializeTarget();

        if (targetRectTransform != null)
        {
            _defaultLocalPosition = targetRectTransform.anchoredPosition3D;
            _defaultLocalRotation = targetRectTransform.localRotation;
        }
        else
        {
            _defaultLocalPosition = targetTransform.localPosition;
            _defaultLocalRotation = targetTransform.localRotation;
        }
    }

    /// <summary>
    /// 揺れを開始するための公開メソッド。
    /// HPバーやアイコンのダメージイベントから呼び出します。
    /// </summary>
    public void Shake()
    {
        // duration が 0 以下なら何もしない
        if (duration <= 0f)
        {
            return;
        }

        InitializeTarget();
        CacheDefaultTransform();

        // すでに揺れている場合の挙動
        if (_shakeCoroutine != null)
        {
            if (overrideWhileShaking)
            {
                // 前の揺れをキャンセルして新しく開始
                StopCoroutine(_shakeCoroutine);
                ResetTransform();
            }
            else
            {
                // 加算揺れにしたい場合は、ここで return せずに
                // 「残り時間を延長する」などのロジックを入れてもよい
                // ひとまず何もしないで return
                return;
            }
        }

        _shakeCoroutine = StartCoroutine(ShakeRoutine());
    }

    /// <summary>
    /// 外部から明示的に揺れを止めたいとき用。
    /// (例えば、画面遷移時に強制リセットしたい場合など)
    /// </summary>
    public void StopShake()
    {
        if (_shakeCoroutine != null)
        {
            StopCoroutine(_shakeCoroutine);
            _shakeCoroutine = null;
        }

        ResetTransform();
    }

    /// <summary>
    /// 実際に揺れを制御するコルーチン本体
    /// </summary>
    private IEnumerator ShakeRoutine()
    {
        float elapsed = 0f;

        while (elapsed < duration)
        {
            // 0~1 の正規化時間
            float t = Mathf.Clamp01(elapsed / duration);

            // 減衰係数(カーブから取得)
            float attenuation = attenuationCurve.Evaluate(t);

            // ランダムなオフセット(-1~1 の範囲)
            float offsetX = (Random.value * 2f - 1f) * positionStrength * attenuation;
            float offsetY = (Random.value * 2f - 1f) * positionStrength * attenuation;

            // ランダムな回転(Z軸のみを回転させる 2D UI 想定)
            float angleZ = (Random.value * 2f - 1f) * rotationStrength * attenuation;

            ApplyShake(offsetX, offsetY, angleZ);

            // 次のフレームまで待機
            if (useUnscaledTime)
            {
                elapsed += Time.unscaledDeltaTime;
            }
            else
            {
                elapsed += Time.deltaTime;
            }

            yield return null;
        }

        // 終了時に元の位置・回転に戻す
        ResetTransform();

        _shakeCoroutine = null;
    }

    /// <summary>
    /// 実際に Transform / RectTransform に揺れを適用する処理
    /// </summary>
    private void ApplyShake(float offsetX, float offsetY, float angleZ)
    {
        if (targetRectTransform != null)
        {
            // anchoredPosition3D をベースに XY を揺らす
            Vector3 shakenPos = _defaultLocalPosition + new Vector3(offsetX, offsetY, 0f);
            targetRectTransform.anchoredPosition3D = shakenPos;

            // Z軸回転だけを変更
            Quaternion shakenRot = Quaternion.Euler(0f, 0f, angleZ) * _defaultLocalRotation;
            targetRectTransform.localRotation = shakenRot;
        }
        else
        {
            // 通常の Transform の場合
            Vector3 shakenPos = _defaultLocalPosition + new Vector3(offsetX, offsetY, 0f);
            targetTransform.localPosition = shakenPos;

            Quaternion shakenRot = Quaternion.Euler(0f, 0f, angleZ) * _defaultLocalRotation;
            targetTransform.localRotation = shakenRot;
        }
    }

    /// <summary>
    /// 位置と回転を「元の状態」にリセットする
    /// </summary>
    private void ResetTransform()
    {
        if (targetRectTransform != null)
        {
            targetRectTransform.anchoredPosition3D = _defaultLocalPosition;
            targetRectTransform.localRotation = _defaultLocalRotation;
        }
        else
        {
            targetTransform.localPosition = _defaultLocalPosition;
            targetTransform.localRotation = _defaultLocalRotation;
        }
    }

    // --- デバッグ用:インスペクターからテストしやすくする ---

#if UNITY_EDITOR
    [ContextMenu("Test Shake")]
    private void ContextMenuTestShake()
    {
        Shake();
    }
#endif
}

使い方の手順

ここでは、HPバーダメージアイコン を例に、ShakeUI の使い方を見ていきます。

  1. ① UIオブジェクトを用意する
    • Canvas 配下に HPバー(Image)や キャラアイコン(Image)を作成します。
    • 例えば HPBar という名前の GameObject に Image コンポーネントが付いている状態を想定します。
  2. ② HPバーに「ShakeUI」コンポーネントを追加
    • Unity の Hierarchy で HPBar を選択。
    • Inspector の「Add Component」から ShakeUI を検索して追加します。
    • Target Rect Transform は空のままでOKです(自動で自分の RectTransform を使います)。
    • DurationPosition Strength をお好みで調整しましょう。
      • 例: Duration = 0.25, Position Strength = 15, Rotation Strength = 8
  3. ③ ダメージ処理から Shake() を呼ぶ
    たとえば、プレイヤーの HP を管理しているコンポーネントがあるとします。
    そこから「HPバーを揺らす」だけを分離して呼び出すと、責務がきれいに分かれます。
    
    using UnityEngine;
    
    /// <summary>
    /// 簡単なダメージテスト用コンポーネント。
    /// スペースキーを押すとダメージを受けた & HPバーを揺らす。
    /// (新InputSystemを使っていない、シンプルな例)
    /// </summary>
    public class PlayerDamageTester : MonoBehaviour
    {
        [SerializeField] private ShakeUI hpBarShake; // HPバーに付けた ShakeUI を参照
    
        [SerializeField] private int maxHp = 100;
        [SerializeField] private int currentHp = 100;
    
        private void Update()
        {
            // デバッグ用:スペースキーでダメージ
            if (Input.GetKeyDown(KeyCode.Space))
            {
                TakeDamage(10);
            }
        }
    
        private void TakeDamage(int amount)
        {
            currentHp = Mathf.Max(0, currentHp - amount);
    
            // HPが減ったタイミングで、HPバーを揺らす
            if (hpBarShake != null)
            {
                hpBarShake.Shake();
            }
    
            // ここでは「HPの数値計算」と「UIの揺れ」が完全に分離されている点がポイント
        }
    }
    
    • hpBarShake には、HPバーにアタッチした ShakeUI をドラッグ&ドロップで設定します。
    • ダメージ処理はあくまで HP の計算だけ。揺れ演出は ShakeUI に丸投げ、という構成ですね。
  4. ④ 他のUIにも流用する(敵HPバー・警告アイコンなど)
    • 敵キャラの頭上にある HPバー用の UI プレハブにも ShakeUI をアタッチ。
    • 敵のダメージ処理から enemyHpBarShake.Shake() を呼ぶだけで、同じ揺れ演出を再利用できます。
    • また、「画面中央の赤い警告アイコン」にも同じコンポーネントを付けて、
      • HPが一定以下になったら warningIconShake.Shake() を呼ぶ

      といった使い方もできます。

メリットと応用

ShakeUI を導入することで、次のようなメリットがあります。

  • 責務がはっきり分かれる
    ダメージ処理は「数値の更新」に集中し、演出は ShakeUI に任せられます。
    「HPが減ったのに揺れない」「揺れだけ変えたい」といった時に、どこを触ればいいかが明確になります。
  • プレハブ化・レベルデザインが楽になる
    HPバー付きの敵プレハブを作るとき、ShakeUI を組み込んだ UI プレハブをそのまま使い回せます。
    レベルデザイナーは「ダメージを受けたら Shake() を呼ぶ」という約束事だけ守れば、複雑なアニメーションコードを触らずに演出を追加できます。
  • 調整パラメータが1か所にまとまる
    Duration / PositionStrength / RotationStrength / 減衰カーブなどを、コンポーネント単位でインスペクターから調整可能です。
    「ボスのHPバーだけ揺れを強く」「UI全体の揺れ時間を短く」といった調整も、スクリプトを書き換えずに対応できます。
  • Godクラス化を防げる
    「UIManager」や「PlayerController」に演出コードを全部押し込むのではなく、
    「揺れ」「点滅」「フェード」などをコンポーネントとして小さく分割していくことで、長期的にメンテしやすいプロジェクトになります。

応用として、ダメージ量に応じて揺れの強さを変えるような拡張も簡単です。
例えば、以下のような関数を ShakeUI に追加しておくと、外部から強さを指定して揺らせます。


    /// <summary>
    /// 強さを指定して揺らすバージョン。
    /// 例:小ダメージ=0.5f、大ダメージ=1.5f など。
    /// </summary>
    public void ShakeWithIntensity(float intensity)
    {
        // 負の値は無効化
        if (intensity <= 0f) return;

        // 一時的に強さをスケールさせてから Shake を呼ぶ
        float originalPosStrength = positionStrength;
        float originalRotStrength = rotationStrength;

        positionStrength *= intensity;
        rotationStrength *= intensity;

        Shake();

        // 元の値に戻す
        positionStrength = originalPosStrength;
        rotationStrength = originalRotStrength;
    }

このように、「揺れ」という機能をコンポーネントとして切り出しておくと、
・プレイヤーHPバー
・敵HPバー
・スキルアイコン
・警告UI
など、さまざまなUIで同じ仕組みを再利用しつつ、演出だけを柔軟に変えていけます。
Updateにすべてを書き殴るスタイルから卒業して、コンポーネント思考でUI演出を組み立てていきましょう。