Unityを触り始めた頃って、つい Update() の中に「雨の制御」「BGMの再生」「プレイヤー操作」など、全部まとめて書いてしまいがちですよね。動くには動くのですが、数週間後に自分で見返しても「どこで何をしているのか分からない巨大スクリプト」になりがちです。

そうなると、ちょっとだけ雨の量を変えたい環境音を別のシーンでも使いたいといった時に、毎回大きなスクリプトを開いてゴリゴリ修正する羽目になります。

この記事では、「雨エフェクトだけ」を担当する小さなコンポーネントとして WeatherRain を用意し、

  • 画面上部から雨粒パーティクルを降らせる
  • 環境音(雨のサウンド)を再生・制御する

という役割にきっちり分割してみます。
シーンにポンと置くだけで「このエリアは雨が降っている」状態を作れるようにして、プレハブ化やレベルデザインを楽にしていきましょう。

【Unity】ワンクリックで雨空演出!「WeatherRain」コンポーネント

フルコード(C# / MonoBehaviour)


using UnityEngine;

/// <summary>
/**
 * シーン内に雨エフェクトと環境音をまとめて配置するコンポーネント。
 * - パーティクルで雨粒を降らせる
 * - AudioSource で雨の環境音をループ再生する
 * - 再生オン/オフ、強さ変更などを簡単に制御できる
 *
 * 想定する使い方:
 * - 空オブジェクトにこのコンポーネントを付けて、雨エリアの中心に配置
 * - 雨の範囲や量はインスペクターから調整
 * - 必要なら複数の WeatherRain をシーン内に置いて、エリアごとに雨設定を変える
 */
/// </summary>
[RequireComponent(typeof(ParticleSystem))]
[RequireComponent(typeof(AudioSource))]
public class WeatherRain : MonoBehaviour
{
    // ====== パーティクル関連設定 ======

    [Header("雨パーティクル設定")]
    [Tooltip("雨粒を生成する ParticleSystem。通常は自動でアタッチされます。")]
    [SerializeField] private ParticleSystem rainParticleSystem;

    [Tooltip("雨の強さ(パーティクル発生レートの倍率)。1.0 が基準。")]
    [SerializeField, Range(0f, 3f)] private float rainIntensity = 1.0f;

    [Tooltip("雨の降る範囲の幅(X軸)。パーティクルの Shape を Box にして使う想定です。")]
    [SerializeField] private float areaWidth = 20f;

    [Tooltip("雨の降る範囲の奥行き(Z軸)。")]
    [SerializeField] private float areaDepth = 20f;

    [Tooltip("カメラの上に雨を追従させる場合に参照するカメラ。null の場合は追従しません。")]
    [SerializeField] private Camera targetCamera;

    [Tooltip("カメラよりどれだけ上に雨の発生位置を置くか。")]
    [SerializeField] private float heightOffsetFromCamera = 10f;

    // ====== サウンド関連設定 ======

    [Header("雨サウンド設定")]
    [Tooltip("雨の環境音を再生する AudioSource。通常は自動でアタッチされます。")]
    [SerializeField] private AudioSource rainAudioSource;

    [Tooltip("雨サウンドの AudioClip。ループ再生されます。")]
    [SerializeField] private AudioClip rainLoopClip;

    [Tooltip("雨サウンドの音量。")]
    [SerializeField, Range(0f, 1f)] private float rainVolume = 0.5f;

    [Tooltip("雨の強さに応じて音量も変化させるかどうか。")]
    [SerializeField] private bool linkVolumeToIntensity = true;

    [Tooltip("雨の強さ 1.0 のときの標準音量。")]
    [SerializeField, Range(0f, 1f)] private float baseVolumeAtIntensity1 = 0.5f;

    // ====== 再生制御 ======

    [Header("再生制御")]
    [Tooltip("シーン開始時に自動的に雨を再生するかどうか。")]
    [SerializeField] private bool playOnStart = true;

    [Tooltip("雨を再生中かどうか(インスペクターからオンオフ可能)。")]
    [SerializeField] private bool isPlaying = true;

    // パーティクルの初期レートを保持しておくための変数
    private float _initialEmissionRate;

    // パーティクルの Shape 情報をキャッシュしておく
    private ParticleSystem.ShapeModule _shapeModule;

    // パーティクルの Emission 情報をキャッシュしておく
    private ParticleSystem.EmissionModule _emissionModule;

    private void Reset()
    {
        // コンポーネントを追加した時に自動で参照をセットする
        rainParticleSystem = GetComponent<ParticleSystem>();
        rainAudioSource = GetComponent<AudioSource>();

        // AudioSource の基本設定(環境音向け)
        if (rainAudioSource != null)
        {
            rainAudioSource.playOnAwake = false;
            rainAudioSource.loop = true;
            rainAudioSource.spatialBlend = 1.0f; // 3D サウンドにして空間的な広がりを出す
        }
    }

    private void Awake()
    {
        // 参照が空の場合は自動取得を試みる
        if (rainParticleSystem == null)
        {
            rainParticleSystem = GetComponent<ParticleSystem>();
        }

        if (rainAudioSource == null)
        {
            rainAudioSource = GetComponent<AudioSource>();
        }

        // パーティクルの各モジュールをキャッシュ
        if (rainParticleSystem != null)
        {
            _shapeModule = rainParticleSystem.shape;
            _emissionModule = rainParticleSystem.emission;

            // 初期の Emission レートを記録しておく
            var rateOverTime = _emissionModule.rateOverTime;
            _initialEmissionRate = rateOverTime.constant;
        }

        // AudioSource の初期設定
        if (rainAudioSource != null)
        {
            rainAudioSource.loop = true;
            rainAudioSource.playOnAwake = false;

            if (rainLoopClip != null)
            {
                rainAudioSource.clip = rainLoopClip;
            }
        }
    }

    private void Start()
    {
        // 開始時の雨の範囲と強さを反映
        ApplyAreaSize();
        ApplyRainIntensity();
        ApplyRainVolume();

        if (playOnStart)
        {
            Play();
        }
        else
        {
            Stop();
        }
    }

    private void LateUpdate()
    {
        // カメラ追従が有効なら、毎フレーム位置を更新
        FollowCameraIfNeeded();
    }

    /// <summary>
    /// 雨の再生を開始する。
    /// </summary>
    public void Play()
    {
        isPlaying = true;

        if (rainParticleSystem != null && !rainParticleSystem.isPlaying)
        {
            rainParticleSystem.Play();
        }

        if (rainAudioSource != null && rainLoopClip != null && !rainAudioSource.isPlaying)
        {
            rainAudioSource.Play();
        }
    }

    /// <summary>
    /// 雨の再生を停止する。
    /// </summary>
    public void Stop()
    {
        isPlaying = false;

        if (rainParticleSystem != null && rainParticleSystem.isPlaying)
        {
            rainParticleSystem.Stop();
        }

        if (rainAudioSource != null && rainAudioSource.isPlaying)
        {
            rainAudioSource.Stop();
        }
    }

    /// <summary>
    /// 雨の強さ(Intensity)を変更する。0 なら止んだ状態に近くなる。
    /// </summary>
    /// <param name="intensity">0〜3 くらいを想定。1 が標準。</param>
    public void SetIntensity(float intensity)
    {
        rainIntensity = Mathf.Max(0f, intensity);
        ApplyRainIntensity();
        ApplyRainVolume();
    }

    /// <summary>
    /// 雨の範囲(幅と奥行き)を変更する。
    /// </summary>
    public void SetAreaSize(float width, float depth)
    {
        areaWidth = Mathf.Max(0f, width);
        areaDepth = Mathf.Max(0f, depth);
        ApplyAreaSize();
    }

    /// <summary>
    /// カメラ追従のターゲットを設定する。
    /// null を渡すと追従を無効化。
    /// </summary>
    public void SetTargetCamera(Camera camera)
    {
        targetCamera = camera;
    }

    /// <summary>
    /// 雨サウンドのループクリップを変更する。
    /// 再生中に変更した場合は即座に切り替え。
    /// </summary>
    public void SetRainClip(AudioClip newClip)
    {
        rainLoopClip = newClip;

        if (rainAudioSource == null) return;

        bool wasPlaying = rainAudioSource.isPlaying;
        rainAudioSource.Stop();
        rainAudioSource.clip = newClip;

        if (wasPlaying && newClip != null)
        {
            rainAudioSource.Play();
        }
    }

    /// <summary>
    /// 雨の音量のベース値を設定する。
    /// </summary>
    public void SetBaseVolume(float volume)
    {
        baseVolumeAtIntensity1 = Mathf.Clamp01(volume);
        ApplyRainVolume();
    }

    // ====== 内部処理メソッド ======

    /// <summary>
    /// パーティクルの Shape を、指定した幅・奥行きに合わせて更新する。
    /// </summary>
    private void ApplyAreaSize()
    {
        if (rainParticleSystem == null) return;

        // Shape は Box を想定
        _shapeModule = rainParticleSystem.shape;
        _shapeModule.shapeType = ParticleSystemShapeType.Box;

        // Box のサイズを設定(Y は高さなので小さめでOK)
        _shapeModule.scale = new Vector3(areaWidth, 0.1f, areaDepth);
    }

    /// <summary>
    /// 雨の強さに応じてパーティクルの発生レートを変更する。
    /// </summary>
    private void ApplyRainIntensity()
    {
        if (rainParticleSystem == null) return;

        _emissionModule = rainParticleSystem.emission;

        // 初期レートに強さを掛け合わせる
        float newRate = _initialEmissionRate * rainIntensity;

        var rateOverTime = _emissionModule.rateOverTime;
        rateOverTime.constant = newRate;
        _emissionModule.rateOverTime = rateOverTime;
    }

    /// <summary>
    /// 雨の強さに応じて音量を調整する。
    /// </summary>
    private void ApplyRainVolume()
    {
        if (rainAudioSource == null) return;

        if (!linkVolumeToIntensity)
        {
            rainAudioSource.volume = rainVolume;
            return;
        }

        // 強さ 1.0 のとき baseVolumeAtIntensity1 になるように調整
        float targetVolume = baseVolumeAtIntensity1 * rainIntensity;

        // 音量は 0〜1 にクランプ
        targetVolume = Mathf.Clamp01(targetVolume);
        rainAudioSource.volume = targetVolume;
    }

    /// <summary>
    /// カメラ追従が有効な場合、雨の発生位置をカメラ上部に移動させる。
    /// </summary>
    private void FollowCameraIfNeeded()
    {
        if (targetCamera == null) return;

        Vector3 camPos = targetCamera.transform.position;
        transform.position = new Vector3(
            camPos.x,
            camPos.y + heightOffsetFromCamera,
            camPos.z
        );
    }

#if UNITY_EDITOR
    private void OnValidate()
    {
        // インスペクター上で値を変更したときにも即座に反映させる
        if (!Application.isPlaying)
        {
            // Editor 上では Awake がまだ呼ばれていない可能性があるのでケアする
            if (rainParticleSystem == null)
            {
                rainParticleSystem = GetComponent<ParticleSystem>();
            }
            if (rainParticleSystem != null)
            {
                _shapeModule = rainParticleSystem.shape;
                _emissionModule = rainParticleSystem.emission;

                var rateOverTime = _emissionModule.rateOverTime;
                _initialEmissionRate = rateOverTime.constant;
            }
        }

        ApplyAreaSize();
        ApplyRainIntensity();
        ApplyRainVolume();
    }
#endif
}

使い方の手順

  1. 雨用の GameObject を作成する
    • ヒエラルキーで 右クリック > Create Empty を選び、名前を WeatherRain に変更します。
    • このオブジェクトに WeatherRain スクリプトをアタッチします。
  2. ParticleSystem を設定する
    • 同じオブジェクトに Particle System コンポーネントを追加します(RequireComponent で自動追加されている場合もあります)。
    • インスペクターで以下のような設定にすると、シンプルな雨になります:
      • Duration: 5
      • Looping: チェック ON
      • Start Lifetime: 1〜2 くらい
      • Start Speed: 10〜30 くらい(落下速度)
      • Start Size: 0.05〜0.1 くらい(細い雨粒)
      • Emission: Rate over Time を 200〜500 程度
      • Shape: Box を選択(サイズはスクリプト側で上書きされます)
    • マテリアルは、ライン状のテクスチャやシンプルな白テクスチャを使うとそれっぽく見えます。
  3. 雨の環境音を設定する
    • 同じオブジェクトに AudioSource コンポーネントを追加します(こちらも自動追加されている可能性があります)。
    • WeatherRain コンポーネントの「雨サウンド設定」欄で、Rain Loop Clip に雨のループ音源をドラッグ&ドロップします。
    • Play On Start を ON にしておくと、シーン開始と同時に雨音が鳴ります。
    • 3D サウンドにしたい場合は AudioSource.spatialBlend = 1 に設定(Reset() で自動設定されます)。
  4. 具体的な使用例
    • プレイヤーが常に雨の中を進む 3D アクション
      • WeatherRainTarget Camera にメインカメラを指定します。
      • これで雨の発生位置がカメラの上に追従し、どこへ移動しても常に「画面上部から雨が降る」状態になります。
    • 特定エリアだけ雨が降るフィールド
      • 雨を降らせたいエリアの中心に WeatherRain オブジェクトを配置します。
      • Area Width / Area Depth を、そのエリアの大きさに合わせて調整します。
      • Target Camera は空のままにしておけば、エリア固定の雨になります。
    • 動く床や移動する敵の上だけ雨を降らせる
      • 動く床(または敵)の子オブジェクトとして WeatherRain を配置します。
      • 親オブジェクトが移動すると、子の雨エフェクトも一緒に移動するので、「この敵の周りだけ常に雨が降っている」といった表現ができます。

メリットと応用

WeatherRain を使うメリットは、「雨エフェクト+雨サウンド」を丸ごと 1 コンポーネントに閉じ込められることです。

  • プレイヤーの移動スクリプトやゲーム進行スクリプトから「雨の挙動」を切り離せる
  • 雨の強さ・範囲・音量を、プレハブのインスペクターからまとめて調整できる
  • シーン間で同じ雨設定を使いたいときは、WeatherRain プレハブをドラッグするだけで再利用可能
  • 後から「雷エフェクト」や「霧エフェクト」を追加したくなっても、別コンポーネントとして素直に拡張できる

レベルデザイナー視点で見ると、「このエリアは弱い雨」「このエリアは土砂降り」といった違いを、スクリプトを一切触らずにインスペクター上のスライダーだけで調整できるのがかなり便利です。
雨の有無も Play() / Stop() メソッドだけで切り替えられるので、トリガーゾーンと組み合わせて「雨エリアに入ると降り出す」といった演出も楽に作れます。

改造案:
たとえば、「一定時間をかけてだんだん雨を強くする(または弱くする)」といった演出を追加したい場合、以下のようなメソッドを WeatherRain に足してみるのもおすすめです。


/// <summary>
/// 指定した時間をかけて、現在の雨の強さから目標の強さへ徐々に変化させる。
/// コルーチンを使った簡易なフェード。
/// </summary>
public void FadeIntensity(float targetIntensity, float duration)
{
    // すでに実行中のフェードがあれば止める
    StopAllCoroutines();
    StartCoroutine(FadeIntensityRoutine(targetIntensity, duration));
}

private System.Collections.IEnumerator FadeIntensityRoutine(float targetIntensity, float duration)
{
    float startIntensity = rainIntensity;
    float time = 0f;

    while (time < duration)
    {
        time += Time.deltaTime;
        float t = duration > 0f ? time / duration : 1f;
        float newIntensity = Mathf.Lerp(startIntensity, targetIntensity, t);

        SetIntensity(newIntensity);
        yield return null;
    }

    // 最終値をしっかりセット
    SetIntensity(targetIntensity);
}

ゲーム内イベント(ボス戦突入時など)でこの FadeIntensity を呼び出せば、じわじわと雨が強くなっていくドラマチックな演出を簡単に作れます。
こうした「1つの責務に特化したコンポーネント」を積み重ねていくと、プロジェクト全体の見通しもぐっと良くなっていきますね。