【Cocos Creator 3.8】WeatherRain の実装:アタッチするだけで「雨エフェクト+環境音」を実現する汎用スクリプト

この記事では、任意のノードにアタッチするだけで雨粒パーティクルを画面上部から降らせつつ、雨の環境音をループ再生できる汎用コンポーネント WeatherRain を実装します。

シーン内に特別な「マネージャーノード」や他のスクリプトを用意する必要はなく、この WeatherRain スクリプト単体で完結します。パーティクルの量、落下範囲、雨音の音量などはすべてインスペクタから調整できるように設計します。


コンポーネントの設計方針

1. 機能要件の整理

  • 画面上部から雨粒が落ちてくるようなエフェクトを再現する。
  • 雨が降っている間、雨の環境音(ループBGMのようなもの)を再生する。
  • 外部のカスタムスクリプトに依存せず、このコンポーネント単体で完結する。
  • インスペクタから以下のような項目を調整できるようにする:
    • 雨粒の発生範囲(X方向の幅、Y位置)
    • 雨粒の落下速度、寿命(消えるまでの時間)
    • 雨粒の生成頻度(1秒あたりの生成数)
    • 雨粒の見た目(Prefab 参照)
    • 雨音の AudioClip、音量、再生オン/オフ
  • 2Dゲーム/UIシーン双方で使いやすいように、ローカル座標ベースで雨粒を生成する。

2. 外部依存をなくすためのアプローチ

  • 雨粒は Prefab をインスペクタから指定してもらう。
  • 雨粒 Prefab には最低限 NodeUITransform さえあれば動作する設計にし、RigidBody 等は必須にしない。
  • 雨音再生には AudioSource コンポーネントを利用するが、自動でアタッチする:
    • このノードに AudioSource がなければ addComponent(AudioSource) で追加する。
    • すでに存在する場合はそれを使う。
  • 雨粒の動きは update() 内でシンプルな「下方向への移動+寿命時間で削除」を実装し、物理エンジンに依存しない。

3. インスペクタで設定可能なプロパティ設計

以下のようなプロパティを定義します。

  • 雨粒関連
    • rainDropPrefab (Prefab)
      • 1つの雨粒として生成する Prefab。
      • Sprite 付きのノードなどを指定することを想定。
    • spawnAreaWidth (number)
      • 雨粒を発生させる X 方向の幅(ローカル座標)。
      • ノード中心を 0 として、[-width/2, width/2] の範囲でランダムに生成。
    • spawnHeight (number)
      • 雨粒を生成する Y 座標(ローカル)。
      • 通常は画面上部より少し上に設定。
    • fallSpeedMin / fallSpeedMax (number)
      • 雨粒が落下する速度の最小値/最大値(単位:unit/秒)。
      • 各雨粒ごとにこの範囲でランダムに決定。
    • lifetimeMin / lifetimeMax (number)
      • 雨粒が削除されるまでの寿命(秒)の最小値/最大値。
      • 各雨粒ごとにこの範囲でランダムに決定。
    • spawnRate (number)
      • 1秒あたりに生成する雨粒の平均数。
      • 例:30 なら毎秒約30個。
    • maxRainDrops (number)
      • 同時に存在できる雨粒の上限。
      • 負荷対策用。上限を超えている間は新規生成を抑制。
  • 雨音関連
    • enableRainSound (boolean)
      • 雨音の再生を行うかどうか。
    • rainSound (AudioClip)
      • ループ再生する雨音のクリップ。
    • rainVolume (number)
      • 雨音の音量(0〜1)。
    • playOnStart (boolean)
      • コンポーネント有効化時に自動で雨音を再生するかどうか。
  • 動作制御
    • playOnEnable (boolean)
      • コンポーネントが有効化されたときに雨エフェクトを自動開始するか。
    • isPlaying (readonly / runtime state)
      • 内部状態として「雨が現在動作中か」を保持(インスペクタには出さない)。

TypeScriptコードの実装


import { _decorator, Component, Node, Prefab, instantiate, Vec3, math, AudioSource, AudioClip } from 'cc';
const { ccclass, property } = _decorator;

interface RainDropData {
    node: Node;
    speed: number;
    lifetime: number;
    age: number;
}

@ccclass('WeatherRain')
export class WeatherRain extends Component {

    // =========================
    // 雨粒設定
    // =========================
    @property({
        type: Prefab,
        tooltip: '1つの雨粒として生成する Prefab を指定します。\nSprite 付きのノードなどを想定しています。'
    })
    public rainDropPrefab: Prefab | null = null;

    @property({
        tooltip: '雨粒を発生させる X 方向の幅(ローカル座標)。\nノード中心から左右に半分ずつ広がります。'
    })
    public spawnAreaWidth: number = 800;

    @property({
        tooltip: '雨粒を生成する Y 座標(ローカル)。\n通常は画面の少し上あたりに設定します。'
    })
    public spawnHeight: number = 400;

    @property({
        tooltip: '雨粒の落下速度の最小値(単位:ユニット/秒)。'
    })
    public fallSpeedMin: number = 600;

    @property({
        tooltip: '雨粒の落下速度の最大値(単位:ユニット/秒)。'
    })
    public fallSpeedMax: number = 900;

    @property({
        tooltip: '雨粒の寿命(秒)の最小値。\nこの時間を過ぎると自動的に削除されます。'
    })
    public lifetimeMin: number = 0.8;

    @property({
        tooltip: '雨粒の寿命(秒)の最大値。\n各雨粒ごとにランダムに決定されます。'
    })
    public lifetimeMax: number = 1.4;

    @property({
        tooltip: '1秒あたりに生成する雨粒の平均数です。\n値を大きくすると雨の量が増えます。'
    })
    public spawnRate: number = 30;

    @property({
        tooltip: '同時に存在できる雨粒の最大数です。\n負荷が気になる場合は小さめに設定してください。\n0 以下の場合は上限なしとして扱います。'
    })
    public maxRainDrops: number = 200;


    // =========================
    // 雨音設定
    // =========================
    @property({
        tooltip: '雨音の再生を行うかどうか。'
    })
    public enableRainSound: boolean = true;

    @property({
        type: AudioClip,
        tooltip: 'ループ再生する雨音の AudioClip を指定します。'
    })
    public rainSound: AudioClip | null = null;

    @property({
        tooltip: '雨音の音量(0〜1)。'
    })
    public rainVolume: number = 0.5;

    @property({
        tooltip: 'コンポーネント有効化時に自動で雨音を再生するかどうか。'
    })
    public playSoundOnStart: boolean = true;


    // =========================
    // 動作制御
    // =========================
    @property({
        tooltip: 'コンポーネントが有効化されたときに雨エフェクトを自動開始するかどうか。'
    })
    public playOnEnable: boolean = true;


    // 内部状態
    private _isPlaying: boolean = false;
    private _spawnAccumulator: number = 0;
    private _rainDrops: RainDropData[] = [];
    private _audioSource: AudioSource | null = null;

    onLoad() {
        // AudioSource の取得または自動追加
        this._audioSource = this.getComponent(AudioSource);
        if (!this._audioSource) {
            this._audioSource = this.addComponent(AudioSource);
        }

        if (!this._audioSource) {
            console.error('[WeatherRain] AudioSource を取得・追加できませんでした。雨音は再生されません。');
        } else {
            this._audioSource.loop = true;
            this._audioSource.volume = this.rainVolume;
        }

        // パラメータの防御的補正
        if (this.fallSpeedMax < this.fallSpeedMin) {
            const tmp = this.fallSpeedMin;
            this.fallSpeedMin = this.fallSpeedMax;
            this.fallSpeedMax = tmp;
        }
        if (this.lifetimeMax < this.lifetimeMin) {
            const tmp = this.lifetimeMin;
            this.lifetimeMin = this.lifetimeMax;
            this.lifetimeMax = tmp;
        }
        if (this.spawnRate < 0) {
            this.spawnRate = 0;
        }
    }

    onEnable() {
        if (this.playOnEnable) {
            this.startRain();
        }
    }

    onDisable() {
        this.stopRain();
    }

    /**
     * 雨エフェクトと雨音を開始します。
     * 外部から手動で呼び出すこともできます。
     */
    public startRain() {
        this._isPlaying = true;
        this._spawnAccumulator = 0;

        if (this.enableRainSound && this._audioSource && this.rainSound) {
            this._audioSource.clip = this.rainSound;
            this._audioSource.volume = this.rainVolume;
            if (this.playSoundOnStart && !this._audioSource.playing) {
                this._audioSource.play();
            }
        }
    }

    /**
     * 雨エフェクトと雨音を停止します。
     * 既存の雨粒もすべて削除します。
     */
    public stopRain() {
        this._isPlaying = false;

        // 生成済みの雨粒を削除
        for (const drop of this._rainDrops) {
            if (drop.node && drop.node.isValid) {
                drop.node.destroy();
            }
        }
        this._rainDrops.length = 0;

        // 雨音停止
        if (this._audioSource && this._audioSource.playing) {
            this._audioSource.stop();
        }
    }

    update(deltaTime: number) {
        if (!this._isPlaying) {
            return;
        }

        // 雨粒の生成
        this.updateSpawn(deltaTime);

        // 雨粒の移動と寿命管理
        this.updateRainDrops(deltaTime);
    }

    private updateSpawn(deltaTime: number) {
        if (!this.rainDropPrefab) {
            // Prefab 未設定の場合は警告を出し、一度だけログを表示したい場合は工夫してもよい
            console.warn('[WeatherRain] rainDropPrefab が設定されていません。雨粒を生成できません。');
            return;
        }

        if (this.spawnRate <= 0) {
            return;
        }

        // 同時存在数の上限チェック
        if (this.maxRainDrops > 0 && this._rainDrops.length >= this.maxRainDrops) {
            return;
        }

        // spawnRate 個/秒 になるように時間を積算
        this._spawnAccumulator += deltaTime * this.spawnRate;

        // 1以上たまった分だけ雨粒を生成
        while (this._spawnAccumulator >= 1.0) {
            this._spawnAccumulator -= 1.0;
            this.spawnOneRainDrop();

            // 上限チェック(ループ内で増えすぎないように)
            if (this.maxRainDrops > 0 && this._rainDrops.length >= this.maxRainDrops) {
                break;
            }
        }
    }

    private spawnOneRainDrop() {
        if (!this.rainDropPrefab) {
            return;
        }

        const parent = this.node;
        if (!parent) {
            console.error('[WeatherRain] このコンポーネントがアタッチされているノードが無効です。');
            return;
        }

        const dropNode = instantiate(this.rainDropPrefab);
        parent.addChild(dropNode);

        // X 座標を -width/2 ~ width/2 の範囲でランダムに決定
        const halfWidth = this.spawnAreaWidth * 0.5;
        const x = math.randomRange(-halfWidth, halfWidth);
        const y = this.spawnHeight;
        const z = 0;

        dropNode.setPosition(new Vec3(x, y, z));

        // 落下速度と寿命をランダムに決定
        const speed = math.randomRange(this.fallSpeedMin, this.fallSpeedMax);
        const lifetime = math.randomRange(this.lifetimeMin, this.lifetimeMax);

        const data: RainDropData = {
            node: dropNode,
            speed,
            lifetime,
            age: 0
        };

        this._rainDrops.push(data);
    }

    private updateRainDrops(deltaTime: number) {
        // 後ろから前に向かって走査しながら削除する
        for (let i = this._rainDrops.length - 1; i >= 0; i--) {
            const drop = this._rainDrops[i];
            const node = drop.node;

            if (!node || !node.isValid) {
                this._rainDrops.splice(i, 1);
                continue;
            }

            // 下方向への移動
            const pos = node.position;
            pos.y -= drop.speed * deltaTime;
            node.setPosition(pos);

            // 寿命更新
            drop.age += deltaTime;
            if (drop.age >= drop.lifetime) {
                node.destroy();
                this._rainDrops.splice(i, 1);
            }
        }
    }
}

コードの要点解説

  • onLoad
    • AudioSource コンポーネントを取得し、なければ自動追加します。
    • 雨粒の速度や寿命の最小値/最大値が逆転している場合に入れ替えるなど、防御的な補正を行います。
  • onEnable / onDisable
    • playOnEnable が true の場合、自動で雨エフェクトを開始します。
    • 無効化時には雨エフェクトと雨音を停止し、既存の雨粒ノードも全て破棄します。
  • startRain / stopRain
    • 外部からも呼べる公開メソッドとして用意し、ゲーム内のイベントに応じて雨のオン/オフを切り替えられます。
    • startRain では雨音の AudioClip を設定し、必要であれば再生します。
  • update
    • _isPlaying が true のときだけ処理を行います。
    • updateSpawn で新しい雨粒の生成、updateRainDrops で既存雨粒の移動と寿命管理を行います。
  • updateSpawn
    • spawnRate(個/秒)に基づいて _spawnAccumulator に時間を蓄積し、1.0 を超えるたびに1個雨粒を生成します。
    • maxRainDrops による同時存在数の制限もここで行います。
    • rainDropPrefab が未設定の場合、console.warn で警告を出して処理をスキップします。
  • spawnOneRainDrop
    • instantiate で Prefab からノードを生成し、this.node の子として追加します。
    • ローカル X 座標は [-spawnAreaWidth/2, spawnAreaWidth/2] のランダム、Y は spawnHeight に固定します。
    • 落下速度と寿命をそれぞれ指定範囲内でランダムに決定し、_rainDrops 配列で管理します。
  • updateRainDrops
    • 雨粒ごとに「下方向へ移動」「寿命時間のカウント」を行い、寿命を超えたものは destroy() して配列から削除します。
    • 逆順ループで安全に削除しています。

使用手順と動作確認

1. スクリプトファイルの作成

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を WeatherRain.ts にします。
  3. 作成された WeatherRain.ts をダブルクリックして開き、中身をすべて削除してから、前述のコードを貼り付けて保存します。

2. 雨粒用 Prefab の準備

雨粒として表示するノードの Prefab を用意します。ここでは簡易的に Sprite を使った例を示します。

  1. Hierarchy パネルで右クリック → Create → UI → Sprite を選択し、ノード名を RainDrop などに変更します。
  2. Inspector で Sprite の画像に、細長い白い線や小さな点のようなテクスチャを設定します(なければ簡単なテクスチャを用意してください)。
  3. 必要に応じて以下を調整します:
    • Color:薄い青色や白に変更して雨っぽくする。
    • Content Size:幅 2〜4、高さ 8〜16 などの細長い形にする。
    • Opacity:80〜150 程度にして少し透明にする。
  4. この RainDrop ノードを Assets パネルへドラッグ&ドロップして Prefab 化します(例:Assets/Prefabs/RainDrop.prefab)。
  5. Prefab 化が完了したら、Hierarchy 側の元ノードは削除しても構いません。

3. 雨エフェクト用ノードの作成とコンポーネントのアタッチ

  1. Hierarchy パネルで右クリック → Create → Empty Node を選択し、ノード名を WeatherRainRoot などに変更します。
  2. このノードを画面中央あたりに配置しておきます(後で spawnHeight を調整するので、厳密な位置は気にしなくて構いません)。
  3. WeatherRainRoot ノードを選択した状態で、Inspector の Add Component ボタンをクリックします。
  4. Custom → WeatherRain を選択して、先ほど作成した WeatherRain コンポーネントをアタッチします。

4. Inspector でプロパティを設定

  1. Rain Drop Settings(雨粒設定)
    • Rain Drop Prefab
      • Assets パネルから、先ほど作成した RainDrop.prefab をドラッグしてこの欄にドロップします。
    • Spawn Area Width
      • 例:800(画面横幅に合わせて調整)。
    • Spawn Height
      • 例:400(カメラ中心が 0 の場合、画面上部より少し上)。
    • Fall Speed Min / Max
      • 例:600 / 900
      • 値を上げると雨が速く落ちるようになります。
    • Lifetime Min / Max
      • 例:0.8 / 1.4 秒。
      • 長くすると画面の下まで落ちてから消えるようになります。
    • Spawn Rate
      • 例:30(1秒あたり30個)。
      • 雨の量を増やしたい場合は 60〜100 程度まで上げてみてください。
    • Max Rain Drops
      • 例:200
      • パフォーマンスが厳しい場合は 100 くらいに下げるとよいです。
  2. Rain Sound Settings(雨音設定)
    • Enable Rain Sound
      • 雨音を鳴らしたい場合はチェックを ON にします。
    • Rain Sound
      • 雨の環境音の AudioClip(ループ再生に適したもの)を指定します。
    • Rain Volume
      • 例:0.5(0〜1)。
    • Play Sound On Start
      • シーン開始と同時に雨音を流したい場合は ON。
  3. Control(動作制御)
    • Play On Enable
      • シーン開始時から雨を降らせたい場合は ON。
      • ゲーム内イベントで手動制御したい場合は OFF にし、他のスクリプトから getComponent(WeatherRain)?.startRain() を呼び出します。

5. シーン再生での動作確認

  1. 上部の Play ボタンを押してシーンを再生します。
  2. WeatherRainRoot 周辺の上部から、雨粒 Prefab が連続して生成され、下方向へ落下していくことを確認します。
  3. 雨音を設定している場合、ループで雨の環境音が再生されていることを確認します。
  4. 雨の量が少ない/多すぎる場合は、再生を止めて Spawn RateMax Rain Drops を調整し、再度再生して確認します。

6. よりゲームに馴染ませるための調整例

  • カメラの表示範囲に合わせて Spawn Area WidthSpawn Height を調整する。
  • UI レイヤー上で雨を降らせたい場合は、Canvas 配下のノードに WeatherRain をアタッチする。
  • 濃い雨/弱い雨を表現するために、Spawn RateRain Volume をゲーム内イベントで変化させる。

まとめ

この WeatherRain コンポーネントは、

  • 任意のノードにアタッチするだけで「雨粒エフェクト+雨音」を実現できる。
  • 雨粒の見た目は Prefab で自由に差し替え可能。
  • 雨量、落下速度、寿命、発生範囲、同時存在数などをインスペクタから細かく調整できる。
  • 雨音の有無や音量も同じく Inspector ベースで制御できる。
  • 外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結する。

一度このコンポーネントをプロジェクトに組み込んでおけば、「雨を降らせたいシーンの任意のノードに WeatherRain をアタッチし、Prefab と AudioClip を設定するだけ」で、すぐに雨の演出を追加できます。天候システムの一部として、他の天候(雪、霧、砂嵐など)コンポーネントを同様の設計で増やしていけば、再利用性の高い演出ライブラリとしてプロジェクト全体の開発効率を大きく向上させられます。