【Cocos Creator 3.8】GlowPulse の実装:アタッチするだけで WorldEnvironment の Glow をサイン波で明滅させる汎用スクリプト

このガイドでは、WorldEnvironment の Glow 強度をサイン波で揺らして、ネオン看板のような「ゆらぐ発光」演出を行う汎用コンポーネント GlowPulse を実装します。

シーン内の WorldEnvironment にこのコンポーネントをアタッチし、インスペクタからパラメータを調整するだけで、ゆっくり脈打つ光・激しく点滅するネオン・フェードインアウトする環境光など、さまざまな Glow アニメーションを簡単に実現できます。


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

要件整理

  • 対象: WorldEnvironment コンポーネントが付いたノード
  • 機能:
    • WorldEnvironment の Glow 強度(Intensity) をサイン波で周期的に変化させる。
    • 強度の 最小値・最大値周期(スピード)ランダム位相 などをインスペクタから調整可能にする。
    • 再生・停止を一時的に切り替えられるようにする。
  • 外部依存なし:
    • 他の GameManager やシングルトンへの依存は禁止。
    • 必要なものは すべてこのコンポーネントの @property から設定できるようにする。
  • 防御的実装:
    • アタッチされたノードに WorldEnvironment が無ければ警告を出し、処理を止める。
    • Glow が有効になっていない場合も、設定変更を試みる前にチェックする。

@property で調整可能にする項目

インスペクタで調整するプロパティを、次のように設計します。

  • enabledPulse: boolean
    • 説明: パルスアニメーションの有効/無効。
    • 用途: 一時的に GlowPulse の動作を止めたいときに使用。
  • minIntensity: number
    • 説明: Glow 強度の最小値。
    • 推奨範囲: 0.0 ~ 5.0 程度(プロジェクトの明るさに応じて調整)。
    • 注意: maxIntensity より大きい値を入れた場合は自動で入れ替える。
  • maxIntensity: number
    • 説明: Glow 強度の最大値。
    • 推奨範囲: 0.0 ~ 10.0 程度。
  • speed: number
    • 説明: サイン波の角速度(1秒あたりの位相の増加量)。
    • 効果: 値を大きくすると点滅が速くなる。
    • :
      • speed = 1.0: ゆっくり脈打つ光。
      • speed = 5.0: かなり速い点滅。
  • useUnscaledTime: boolean
    • 説明: director.getTotalTime() を使うか、dt ベースで進めるかの切り替え。
    • 用途: Time.timeScale のような概念を自前で入れている場合でも、影響を受けないアニメーションにしたいときに true にする。
  • randomizeStartPhase: boolean
    • 説明: 再生開始時にサイン波の開始位相をランダムにする。
    • 用途: 複数の GlowPulse をシーン内で同時に使うとき、すべて同じタイミングで明滅しないようにする。
  • startPhase: number
    • 説明: サイン波の初期位相(ラジアン)。
    • :
      • 0: サイン波の中心からスタート。
      • Math.PI / 2 相当: サイン波のピークからスタート。
    • 備考: randomizeStartPhasetrue の場合は、ここで指定した値は無視される。
  • applyOnLoad: boolean
    • 説明: onLoad 時に 現在の Glow 強度を基準値として保存するかどうか
    • 用途: 既にシーン上で設定している Glow 強度を「中心値」として扱いたい場合に便利。
  • debugLog: boolean
    • 説明: デバッグ用ログの ON/OFF。
    • 用途: 正しく WorldEnvironment が取得できているか、現在の強度がどう変化しているかを確認したいとき。

また、内部状態として以下を持ちます(インスペクタには表示しない):

  • _worldEnv: WorldEnvironment | null – 対象の WorldEnvironment コンポーネント。
  • _time: number – サイン波用の経過時間(位相計算用)。
  • _baseIntensity: number – 中心となる Glow 強度。
  • _amplitude: number – サイン波の振幅。

TypeScriptコードの実装


import { _decorator, Component, director, WorldEnvironment, CCFloat, CCBoolean, CCInteger, log, warn } from 'cc';
const { ccclass, property } = _decorator;

/**
 * GlowPulse
 * WorldEnvironment の Glow 強度をサイン波で周期的に変化させるコンポーネント。
 * このスクリプトを WorldEnvironment を持つノードにアタッチするだけで動作します。
 */
@ccclass('GlowPulse')
export class GlowPulse extends Component {

    @property({
        tooltip: 'Glow のパルスアニメーションを有効にするかどうか。\nチェックを外すと現在の強度のまま固定されます。',
    })
    public enabledPulse: boolean = true;

    @property({
        type: CCFloat,
        tooltip: 'Glow 強度の最小値。\nmaxIntensity より大きい値を指定した場合、自動的に入れ替えられます。',
        slide: true,
        range: [0, 10, 0.01],
    })
    public minIntensity: number = 0.5;

    @property({
        type: CCFloat,
        tooltip: 'Glow 強度の最大値。\nminIntensity より小さい値を指定した場合、自動的に入れ替えられます。',
        slide: true,
        range: [0, 10, 0.01],
    })
    public maxIntensity: number = 2.0;

    @property({
        type: CCFloat,
        tooltip: 'サイン波のスピード(角速度)。\n値を大きくすると点滅が速くなります。',
        slide: true,
        range: [0, 20, 0.01],
    })
    public speed: number = 2.0;

    @property({
        type: CCBoolean,
        tooltip: '経過時間の計算にスケールされていない時間(director.getTotalTime)を使うかどうか。\ntrue: フレームレートやタイムスケールの影響を受けにくい動きになります。\nfalse: 通常の update(dt) ベースで時間を進めます。',
    })
    public useUnscaledTime: boolean = true;

    @property({
        type: CCBoolean,
        tooltip: '開始時のサイン波の位相をランダムにするかどうか。\ntrue の場合、各 GlowPulse がバラバラなタイミングで明滅します。',
    })
    public randomizeStartPhase: boolean = true;

    @property({
        type: CCFloat,
        tooltip: 'サイン波の開始位相(ラジアン)。\nrandomizeStartPhase が true の場合、この値は無視されます。\n0: 中心からスタート, π/2: 最大値からスタート など。',
        slide: true,
        range: [0, Math.PI * 2, 0.01],
    })
    public startPhase: number = 0.0;

    @property({
        type: CCBoolean,
        tooltip: 'onLoad 時に現在の Glow 強度を基準値として使用するかどうか。\ntrue: シーンで設定した Glow 強度を中心値としてパルスさせます。\nfalse: minIntensity と maxIntensity の中間値を中心値として使用します。',
    })
    public applyOnLoad: boolean = true;

    @property({
        type: CCBoolean,
        tooltip: 'デバッグログを出力するかどうか。\nWorldEnvironment が見つからない場合などの情報をコンソールに表示します。',
    })
    public debugLog: boolean = false;

    // 内部状態
    private _worldEnv: WorldEnvironment | null = null;
    private _time: number = 0;             // サイン波用の時間(位相計算用)
    private _baseIntensity: number = 1.0;  // 中心となる強度
    private _amplitude: number = 0.5;      // 振幅
    private _lastUnscaledTime: number = 0; // useUnscaledTime 用の前フレーム時間

    onLoad() {
        // WorldEnvironment の取得
        this._worldEnv = this.getComponent(WorldEnvironment);
        if (!this._worldEnv) {
            warn('[GlowPulse] WorldEnvironment コンポーネントが見つかりません。このコンポーネントは WorldEnvironment を持つノードにアタッチしてください。');
            return;
        }

        // Glow が有効かどうかのチェック(環境によってプロパティ名が異なる可能性があるため try/catch)
        try {
            const glow = this._worldEnv.glow;
            if (!glow) {
                warn('[GlowPulse] WorldEnvironment.glow が設定されていません。Glow を有効にしてから使用してください。');
            }
        } catch (e) {
            warn('[GlowPulse] WorldEnvironment.glow プロパティにアクセスできません。このバージョンのエンジンで Glow が有効か確認してください。', e);
        }

        // min と max の関係を正規化
        this._normalizeMinMax();

        // 基準強度と振幅の計算
        if (this.applyOnLoad) {
            // 現在の Glow 強度を中心値として利用
            const currentIntensity = this._getCurrentGlowIntensity();
            if (currentIntensity != null) {
                this._baseIntensity = currentIntensity;
                const halfRange = (this.maxIntensity - this.minIntensity) * 0.5;
                this._amplitude = halfRange;
                if (this.debugLog) {
                    log('[GlowPulse] applyOnLoad: baseIntensity =', this._baseIntensity, 'amplitude =', this._amplitude);
                }
            } else {
                // 取得できなければ min/max の中間を利用
                this._setBaseAndAmplitudeFromMinMax();
            }
        } else {
            // min/max の中間を中心値として利用
            this._setBaseAndAmplitudeFromMinMax();
        }

        // 開始位相の設定
        if (this.randomizeStartPhase) {
            this._time = Math.random() * Math.PI * 2; // 0 ~ 2π
        } else {
            this._time = this.startPhase;
        }

        // unscaled time 用の初期値
        this._lastUnscaledTime = director.getTotalTime();

        if (this.debugLog) {
            log('[GlowPulse] onLoad 完了: min=', this.minIntensity, 'max=', this.maxIntensity,
                'base=', this._baseIntensity, 'amp=', this._amplitude, 'time=', this._time);
        }
    }

    start() {
        // 特に処理は不要だが、将来の拡張ポイントとして空実装。
    }

    update(dt: number) {
        // WorldEnvironment が取得できていない場合は何もしない
        if (!this._worldEnv) {
            return;
        }

        // パルスが無効なら何もしない
        if (!this.enabledPulse) {
            return;
        }

        // 経過時間の更新
        if (this.useUnscaledTime) {
            const now = director.getTotalTime();
            const unscaledDt = (now - this._lastUnscaledTime) / 1000.0; // ms → 秒
            this._lastUnscaledTime = now;
            this._time += unscaledDt * this.speed;
        } else {
            this._time += dt * this.speed;
        }

        // サイン波で -1 ~ 1 の値を生成
        const wave = Math.sin(this._time);

        // 強度を計算: baseIntensity ± amplitude
        const intensity = this._baseIntensity + this._amplitude * wave;

        // min/max でクランプしてから適用
        const clamped = this._clamp(intensity, this.minIntensity, this.maxIntensity);
        this._setGlowIntensity(clamped);

        if (this.debugLog) {
            // 頻繁に出すと重いので、必要に応じてコメントアウト推奨
            // log('[GlowPulse] intensity =', clamped);
        }
    }

    /**
     * minIntensity と maxIntensity の関係を正規化する。
     * min > max の場合は入れ替え、min == max の場合は max を少しだけ大きくする。
     */
    private _normalizeMinMax() {
        if (this.minIntensity > this.maxIntensity) {
            const tmp = this.minIntensity;
            this.minIntensity = this.maxIntensity;
            this.maxIntensity = tmp;
            warn('[GlowPulse] minIntensity が maxIntensity より大きかったため、値を入れ替えました。');
        } else if (this.minIntensity === this.maxIntensity) {
            this.maxIntensity = this.minIntensity + 0.01;
            warn('[GlowPulse] minIntensity と maxIntensity が同じだったため、maxIntensity を少しだけ大きくしました。');
        }
    }

    /**
     * min/max から baseIntensity と amplitude を計算して設定する。
     */
    private _setBaseAndAmplitudeFromMinMax() {
        this._baseIntensity = (this.minIntensity + this.maxIntensity) * 0.5;
        this._amplitude = (this.maxIntensity - this.minIntensity) * 0.5;
        if (this.debugLog) {
            log('[GlowPulse] _setBaseAndAmplitudeFromMinMax: base=', this._baseIntensity, 'amp=', this._amplitude);
        }
    }

    /**
     * 現在の Glow 強度を取得する。
     * 取得できない場合は null を返す。
     */
    private _getCurrentGlowIntensity(): number | null {
        try {
            const glow = this._worldEnv!.glow;
            if (!glow) {
                return null;
            }
            // Cocos 3.8 の WorldEnvironment.glow に intensity プロパティがある前提
            const intensity = (glow as any).intensity;
            if (typeof intensity === 'number') {
                return intensity;
            }
        } catch (e) {
            warn('[GlowPulse] 現在の Glow 強度の取得に失敗しました。', e);
        }
        return null;
    }

    /**
     * Glow 強度を設定する。
     */
    private _setGlowIntensity(value: number) {
        try {
            const glow = this._worldEnv!.glow;
            if (!glow) {
                return;
            }
            (glow as any).intensity = value;
        } catch (e) {
            warn('[GlowPulse] Glow 強度の設定に失敗しました。', e);
        }
    }

    /**
     * 数値を min/max でクランプする。
     */
    private _clamp(value: number, min: number, max: number): number {
        if (value < min) return min;
        if (value > max) return max;
        return value;
    }
}

コードのポイント解説

  • onLoad()
    • アタッチされたノードから WorldEnvironmentgetComponent で取得。
    • 見つからなければ warn を出して以降の処理をスキップ。
    • minIntensitymaxIntensity の関係を正規化。
    • applyOnLoad に応じて、現在の Glow 強度 または min/max の中間値を中心値として設定。
    • 開始位相(_time)を randomizeStartPhasestartPhase から決定。
  • update(dt)
    • enabledPulse が false の場合は何もしない。
    • useUnscaledTime が true の場合は director.getTotalTime() から経過時間を計算し、false の場合は通常の dt を使用。
    • this._timespeed を掛けて進め、Math.sin(this._time) で -1 ~ 1 の値を得る。
    • 中心値 _baseIntensity と振幅 _amplitude を使って強度を計算し、min/max でクランプしてから WorldEnvironment.glow.intensity に反映。
  • 防御的実装
    • WorldEnvironmentglow プロパティが存在しないケースを try/catchwarn でハンドリング。
    • min/max の逆転や同値のケースを自動修正。

使用手順と動作確認

1. TypeScript スクリプトの作成

  1. エディタ上部メニュー、または Assets パネルで作業用フォルダ(例: assets/scripts)を用意します。
  2. Assets パネルでスクリプトを作成したいフォルダを右クリックします。
  3. Create → TypeScript を選択します。
  4. 新しく作成されたスクリプトファイルの名前を GlowPulse.ts に変更します。
  5. ダブルクリックしてエディタ(VS Code など)で開き、中身をすべて削除してから 上記の TypeScript コードを貼り付けて保存します。

2. WorldEnvironment ノードの準備

GlowPulse は WorldEnvironment コンポーネントを持つノード にアタッチして使います。既にシーンに WorldEnvironment がある場合は、そのノードを使って構いません。

  1. Hierarchy パネルで、シーンルート(例: CanvasScene)の下に WorldEnvironment 用ノードを作ります。
    • 右クリック → Create → Empty Node を選択し、名前を WorldEnv などに変更します。
  2. WorldEnv ノードを選択し、InspectorAdd Component ボタンをクリックします。
  3. Rendering → WorldEnvironment を選択して追加します。
  4. WorldEnvironment コンポーネント内で Glow を有効化 します。
    • Glow セクションのチェックボックスをオンにし、最初の Intensity(強度) を 1.0 など適当な値にしておきます。

3. GlowPulse コンポーネントをアタッチ

  1. 再び WorldEnv ノードを選択し、InspectorAdd Component ボタンをクリックします。
  2. Custom → GlowPulse を選択して追加します。
    • Custom カテゴリに見つからない場合は、検索欄に GlowPulse と入力して探してください。

4. プロパティの設定例

Inspector で GlowPulse のプロパティを次のように設定して、どのような動きになるか試してみましょう。

ゆっくりと脈打つネオン風

  • enabledPulse: ON
  • minIntensity: 0.5
  • maxIntensity: 2.0
  • speed: 1.0
  • useUnscaledTime: ON
  • randomizeStartPhase: ON(単体ならどちらでも可)
  • startPhase: 0.0
  • applyOnLoad: ON(WorldEnvironment の現在の強度を中心値にしたい場合)
  • debugLog: OFF

激しく点滅する警告灯風

  • enabledPulse: ON
  • minIntensity: 0.0
  • maxIntensity: 5.0
  • speed: 8.0
  • useUnscaledTime: ON
  • randomizeStartPhase: OFF(複数を同期させたい場合)
  • startPhase: Math.PI / 2 相当(約 1.57)
  • applyOnLoad: OFF
  • debugLog: OFF

複数ノードで位相をずらしたネオン街

シーンに複数の WorldEnvironment 相当の演出ノードを作る場合(例えば、ポストエフェクトを複数レイヤーで切り替えるなどの特殊な構成):

  • それぞれのノードに GlowPulse をアタッチ。
  • randomizeStartPhaseON にしておくと、すべてが同じタイミングで明滅せず、自然なズレが生まれます。

5. 再生して動作確認

  1. エディタ右上の Play(▶)ボタン を押してゲームを再生します。
  2. ゲームビューで、シーン全体の Glow が 明るくなったり暗くなったりと脈打つように変化していれば成功です。
  3. 変化が分かりにくい場合は:
    • minIntensity を 0.0 に近づける。
    • maxIntensity を 3.0 以上に上げる。
    • speed を 3.0 以上に上げてみる。
  4. もし Glow が全く変化しない場合は:
    • WorldEnvironment コンポーネントで Glow のチェックボックスが ON になっているか確認。
    • コンソールに [GlowPulse] の警告が出ていないか確認。
    • WorldEnvironment の プロファイル(Environment Asset) 側で Glow が有効になっているか確認。

まとめ

GlowPulse コンポーネントは、

  • WorldEnvironment 1つにアタッチするだけで、シーン全体の Glow をサイン波で明滅させられる。
  • min/max 強度・スピード・位相・時間の種類などをすべてインスペクタから調整できる。
  • 外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結している。
  • 防御的なチェックにより、WorldEnvironment や Glow 設定の不足に対して警告を出してくれる。

このような「環境エフェクトをアタッチするだけで制御できる汎用コンポーネント」を用意しておくと、

  • シーンごとに簡単に 雰囲気の違うネオン演出を作れる。
  • レベルデザイナーやアーティストが コードに触れずにパラメータを調整できる。
  • Glow 以外のパラメータ(例えば Fog、Exposure など)にも同じパターンを応用しやすい。

プロジェクト内で「環境光のアニメーション」を増やしたくなったときは、この GlowPulse をベースに、他の WorldEnvironment プロパティをサイン波やイージングで動かすコンポーネントを追加していくと、統一された設計で管理しやすくなります。