【Cocos Creator 3.8】VignetteDamage(瀕死視界)の実装:アタッチするだけで「HPに応じて画面四隅を赤黒く暗くする」ビネット演出を実現する汎用スクリプト

本記事では、1つのノードにアタッチするだけで、プレイヤーのHPが減るほど画面の縁が赤黒く暗くなる「瀕死視界」エフェクトを実現できる汎用コンポーネント VignetteDamage を実装します。

HPの値はインスペクタから直接設定・調整でき、他のGameManagerやHP管理スクリプトに一切依存しない設計です。UIキャンバス上にノードを1つ置いてアタッチするだけで、どんなゲームでもすぐに「ダメージ演出」を試せます。


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

機能要件の整理

  • HP(0〜最大値)の割合に応じて、画面の四隅を赤黒く暗くするビネット効果を表示する。
  • HPが高いときはほぼ見えない/完全に非表示、HPが低くなるほど強く表示される。
  • ビネットはフルスクリーンのUIノードとして表示し、Spriteでビネット用テクスチャを描画する。
  • HPの値はインスペクタから直接設定できる(他スクリプトからの参照不要)。
  • HP変化時の強度変化は、即時反映 or 補間(スムージング)を選択できる。
  • 色(赤系・黒系など)、最大強度(アルファ)、表示し始めるHP割合などをインスペクタから調整可能にする。
  • 標準コンポーネントへの依存は Sprite のみ。存在しない場合はログで警告し、動作を止める。

ノード構成の前提

  • Canvas配下にフルスクリーンのUIノード(例:VignetteLayer)を1つ作り、そのノードに本コンポーネントをアタッチします。
  • 同じノードに Sprite コンポーネントを追加し、ビネット用のPNG画像(四隅が暗く、中央が透明なような画像)を指定します。
  • Spriteの Type は SLICED ではなく SIMPLE を推奨(単純なフルスクリーン表示のため)。

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

VignetteDamage コンポーネントでは、以下のプロパティを用意します。

  • maxHp: number
    プレイヤーの最大HP。
    ・HPの割合(currentHp / maxHp)を計算するために使用。
    ・0以下を設定した場合は 1 に強制してエラーを防止。
  • currentHp: number
    現在のHP。
    ・0〜maxHp の範囲で指定。
    ・インスペクタ上で値を変えると、プレビュー上でビネット強度が変化する。
    ・ランタイム中に他スクリプトから getComponent(VignetteDamage) 経由で書き換えても良い(外部依存は必須ではない)。
  • minIntensityHpRatio: number
    「HPがこの割合より高い間は、ビネットをほぼ表示しない」ための閾値。
    ・0〜1の間で指定(例:0.7 なら HP70%以上でほぼ非表示)。
    ・この値より下回ったあたりから徐々に暗くなっていく。
  • maxIntensityHpRatio: number
    「HPがこの割合以下になると、ビネットが最大強度になる」ための閾値。
    ・0〜1の間で指定(例:0.2 なら HP20%以下で最大強度)。
    maxIntensityHpRatio <= minIntensityHpRatio の場合は、自動的に少しだけ小さく調整して破綻を防ぐ。
  • maxAlpha: number
    ビネットの最大アルファ値(0〜1)。
    ・1 に近いほど真っ黒/真っ赤に近くなる。
    ・0.6〜0.8 あたりが実用的。
  • tintColor: Color
    ビネットの色。
    ・デフォルトは暗い赤(例:RGB 180, 0, 0)。
    ・黒っぽい色を指定すれば「暗闇」、青っぽい色なら「寒気」などの演出も可能。
  • useSmoothTransition: boolean
    HP変化に対するビネット強度の変化を「なめらかに補間するかどうか」。
    ・オン:フワッと変化する。
    ・オフ:HPに応じて即座に変化。
  • lerpSpeed: number
    補間速度。
    useSmoothTransition が true のときのみ有効。
    ・大きいほど目標値に素早く追従(推奨値:5〜15)。
  • debugLog: boolean
    デバッグログの出力有無。
    ・true にすると、HPや強度の変化をコンソールに出力して挙動を確認できる。

これらのプロパティにより、HPのしきい値・色・強度・補間挙動をすべてインスペクタ上で完結して調整できます。


TypeScriptコードの実装

以下が、VignetteDamage.ts の完成コードです。


import { _decorator, Component, Sprite, Color, UITransform, math } from 'cc';
const { ccclass, property } = _decorator;

/**
 * VignetteDamage
 * HPの割合に応じて画面の四隅を赤黒く暗くするビネットエフェクトを制御するコンポーネント。
 * - 同じノードに Sprite コンポーネントが必要(ビネット用テクスチャを設定してください)。
 * - Canvas 配下のフルスクリーンUIノードにアタッチして使用します。
 */
@ccclass('VignetteDamage')
export class VignetteDamage extends Component {

    @property({
        tooltip: '最大HP。HP割合 = currentHp / maxHp で計算されます。\n0以下を設定した場合は自動的に1に補正されます。'
    })
    public maxHp: number = 100;

    @property({
        tooltip: '現在のHP。0〜maxHpの範囲で指定してください。\nインスペクタ上で変更するとビネット強度が更新されます。'
    })
    public currentHp: number = 100;

    @property({
        tooltip: 'このHP割合(0〜1)より高い間は、ビネットをほぼ表示しません。\n例: 0.7 → HP70%以上でほぼ非表示。'
    })
    public minIntensityHpRatio: number = 0.7;

    @property({
        tooltip: 'このHP割合(0〜1)以下になると、ビネットが最大強度になります。\n例: 0.2 → HP20%以下で最大強度。'
    })
    public maxIntensityHpRatio: number = 0.2;

    @property({
        tooltip: 'ビネットの最大アルファ(不透明度)。0〜1で指定します。\n1に近いほど強く暗く(赤く)なります。'
    })
    public maxAlpha: number = 0.75;

    @property({
        tooltip: 'ビネットの色。デフォルトは暗い赤系です。\n黒っぽい色で暗闇演出、青系で寒気演出などにも応用できます。'
    })
    public tintColor: Color = new Color(180, 0, 0, 255);

    @property({
        tooltip: 'HP変化に対してビネット強度をなめらかに補間するかどうか。\nオンにするとフワッと変化します。'
    })
    public useSmoothTransition: boolean = true;

    @property({
        tooltip: 'ビネット強度の補間速度。\nuseSmoothTransition が true のときのみ有効です。',
        visible: function (this: VignetteDamage) {
            return this.useSmoothTransition;
        }
    })
    public lerpSpeed: number = 10;

    @property({
        tooltip: 'デバッグログを有効にすると、HPや強度の変化がコンソールに出力されます。'
    })
    public debugLog: boolean = false;

    private _sprite: Sprite | null = null;
    private _currentIntensity: number = 0;  // 実際に表示している強度
    private _targetIntensity: number = 0;   // HPから計算された目標強度

    onLoad() {
        // 必要なコンポーネント(Sprite)を取得
        this._sprite = this.getComponent(Sprite);
        if (!this._sprite) {
            console.error('[VignetteDamage] このコンポーネントを使用するには、同じノードに Sprite コンポーネントが必要です。');
            return;
        }

        // UITransform が存在するか確認(フルスクリーン表示のため推奨)
        const uiTrans = this.getComponent(UITransform);
        if (!uiTrans) {
            console.warn('[VignetteDamage] UITransform コンポーネントが見つかりません。このノードがUIノードでない場合、画面全体を覆えない可能性があります。');
        }

        // パラメータの安全性を確保
        if (this.maxHp <= 0) {
            console.warn('[VignetteDamage] maxHp が 0 以下です。1 に補正します。');
            this.maxHp = 1;
        }

        // 閾値の整合性チェック
        if (this.maxIntensityHpRatio >= this.minIntensityHpRatio) {
            // 少しだけ小さくして破綻を防ぐ
            this.maxIntensityHpRatio = this.minIntensityHpRatio - 0.01;
            if (this.maxIntensityHpRatio < 0) {
                this.maxIntensityHpRatio = 0;
            }
            console.warn('[VignetteDamage] maxIntensityHpRatio が minIntensityHpRatio 以上だったため、自動的に補正しました。');
        }

        // 初期HPをクランプ
        this.currentHp = math.clamp(this.currentHp, 0, this.maxHp);

        // 初期強度を計算して反映
        this._targetIntensity = this._calculateIntensityFromHp();
        this._currentIntensity = this._targetIntensity;
        this._applyIntensityToSprite(this._currentIntensity);

        if (this.debugLog) {
            console.log(`[VignetteDamage] onLoad 完了。初期HP=${this.currentHp}, 初期強度=${this._currentIntensity.toFixed(3)}`);
        }
    }

    start() {
        // 特別な処理は不要だが、将来の拡張のためにメソッドを残しておく
    }

    update(deltaTime: number) {
        if (!this._sprite) {
            // Sprite がない場合は何もしない
            return;
        }

        // HPの変化に応じて目標強度を再計算
        const newTarget = this._calculateIntensityFromHp();
        if (Math.abs(newTarget - this._targetIntensity) > 0.0001) {
            this._targetIntensity = newTarget;
            if (this.debugLog) {
                console.log(`[VignetteDamage] HP変化検知: currentHp=${this.currentHp}, targetIntensity=${this._targetIntensity.toFixed(3)}`);
            }
        }

        // 実際に表示する強度を更新
        if (this.useSmoothTransition) {
            // 線形補間でなめらかに変化
            this._currentIntensity = math.lerp(this._currentIntensity, this._targetIntensity, math.clamp01(this.lerpSpeed * deltaTime));
        } else {
            // 即時反映
            this._currentIntensity = this._targetIntensity;
        }

        this._applyIntensityToSprite(this._currentIntensity);
    }

    /**
     * インスペクタや他スクリプトから安全にHPを設定するためのヘルパー。
     * 0〜maxHp の範囲にクランプされます。
     */
    public setHp(value: number) {
        const clamped = math.clamp(value, 0, this.maxHp);
        if (clamped !== this.currentHp) {
            this.currentHp = clamped;
            if (this.debugLog) {
                console.log(`[VignetteDamage] setHp: HP=${this.currentHp}`);
            }
        }
    }

    /**
     * 現在のHP割合(0〜1)を取得します。
     */
    public getHpRatio(): number {
        return this.currentHp / this.maxHp;
    }

    /**
     * HP割合に基づいてビネット強度(0〜1)を計算します。
     * - HP割合 >= minIntensityHpRatio → 0 に近い(ほぼ非表示)
     * - HP割合 <= maxIntensityHpRatio → 1(最大強度)
     * - その間 → 線形補間で 0〜1 の間の値
     */
    private _calculateIntensityFromHp(): number {
        const ratio = this.getHpRatio(); // 0〜1

        if (ratio >= this.minIntensityHpRatio) {
            return 0;
        }
        if (ratio <= this.maxIntensityHpRatio) {
            return 1;
        }

        // minIntensityHpRatio〜maxIntensityHpRatio の間を 0〜1 にマッピング
        const t = (this.minIntensityHpRatio - ratio) / (this.minIntensityHpRatio - this.maxIntensityHpRatio);
        return math.clamp01(t);
    }

    /**
     * 計算された強度(0〜1)を Sprite のカラーアルファに反映します。
     */
    private _applyIntensityToSprite(intensity: number) {
        if (!this._sprite) {
            return;
        }

        const alpha = math.clamp01(intensity) * math.clamp01(this.maxAlpha);

        // 基本色に強度を乗算して Color を作成
        const base = this.tintColor;
        const color = new Color(base.r, base.g, base.b, Math.round(255 * alpha));

        this._sprite.color = color;
    }
}

主要メソッドの解説

  • onLoad()
    ・同じノードに Sprite が付いているかをチェック。なければ console.error を出して処理を中断。
    maxHp や HP割合の閾値(minIntensityHpRatio, maxIntensityHpRatio)を安全な値に補正。
    currentHp を 0〜maxHp にクランプし、初期の強度を計算して Sprite に反映。
  • update(deltaTime)
    ・毎フレーム、currentHp から「目標強度(_targetIntensity)」を再計算。
    useSmoothTransition が true なら math.lerp を使って _currentIntensity をなめらかに補間。
    ・補間後の強度を _applyIntensityToSprite で Sprite の色・アルファに反映。
  • setHp(value)
    ・外部から HP を変更したい場合のための安全なヘルパーメソッド。
    ・0〜maxHp にクランプして currentHp に設定し、デバッグログを出力。
  • _calculateIntensityFromHp()
    ・HP割合(0〜1)からビネット強度(0〜1)を計算するコアロジック。
    ・HPが高いときは 0、低いときは 1、その間は線形で変化。
  • _applyIntensityToSprite(intensity)
    ・強度(0〜1) × maxAlpha から最終的なアルファ値を計算し、tintColor に乗せて Sprite の色として適用。

使用手順と動作確認

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

  1. Assets パネルで右クリック → Create → TypeScript を選択します。
  2. ファイル名を VignetteDamage.ts に変更します。
  3. 作成された VignetteDamage.ts をダブルクリックしてエディタ(VS Code など)で開き、
    既存コードをすべて削除して、前述の TypeScript コードを丸ごと貼り付けて保存します。

2. ビネット用ノードの作成

  1. Hierarchy パネルで Canvas ノードを確認します(ない場合は Create → UI → Canvas で作成)。
  2. Canvas を右クリック → Create → UI → Sprite を選択し、ビネット表示用のノードを作成します。
  3. 作成された Sprite ノードの名前を分かりやすく VignetteLayer などに変更します。
  4. VignetteLayer ノードを選択し、Inspector の UITransform で以下のように設定します:
    • Content Size: Width = 1920, Height = 1080(ゲーム解像度に合わせて調整)
    • Anchor: X = 0.5, Y = 0.5
    • Position: X = 0, Y = 0

    ゲーム解像度に合わせてフルスクリーンになるように調整してください。

3. Sprite コンポーネントの設定

  1. VignetteLayer ノードを選択し、Inspector の Sprite コンポーネントを確認します。
  2. Sprite の SpriteFrame に、ビネット用の画像(四隅が暗く、中央が透明なPNGなど)をドラッグ&ドロップで設定します。
  3. Sprite の TypeSIMPLE に設定します。
  4. 必要に応じて Blend ModeALPHA(デフォルト)にしておきます。

※ ビネット画像がない場合は、暫定的に黒の四角画像を設定しても動作確認は可能です(見た目はただの暗転になります)。

4. VignetteDamage コンポーネントのアタッチ

  1. Hierarchy で VignetteLayer ノードを選択します。
  2. Inspector 下部の Add Component ボタンをクリックします。
  3. Custom カテゴリから VignetteDamage を選択して追加します。

5. プロパティの設定例

VignetteLayer に追加された VignetteDamage のプロパティを、まずは次のように設定してみてください:

  • maxHp: 100
  • currentHp: 100(初期状態では全快)
  • minIntensityHpRatio: 0.7(HP70%以上でほぼ非表示)
  • maxIntensityHpRatio: 0.2(HP20%以下で最大強度)
  • maxAlpha: 0.75
  • tintColor: 暗い赤(例:R=180, G=0, B=0, A=255)
  • useSmoothTransition: true
  • lerpSpeed: 10
  • debugLog: false(挙動を見たい場合は true に)

6. エディタ上での動作確認(プレビュー)

  1. Scene ビューで VignetteLayer が画面全体を覆っていることを確認します。
  2. 再生ボタン(▶)を押してゲームをプレビューします。
  3. プレビュー中に VignetteLayer を選択し、Inspector の currentHp の値を変更してみます:
    • currentHp = 100 → ほぼビネットが見えない。
    • currentHp = 60 → ほんのり四隅が暗くなる。
    • currentHp = 30 → かなり赤黒く暗くなる。
    • currentHp = 10 → ほぼ最大強度で縁が真っ赤に近くなる。
  4. useSmoothTransition が true の場合、HPを急に変えてもビネットがフワッと追いついてくるのが確認できます。

7. ゲームロジックとの連携(任意)

本コンポーネントは他スクリプトに依存しませんが、必要であれば別スクリプトから HP を更新することもできます。

例:プレイヤーにダメージを与えたときに HP を減らす(任意のスクリプトで)


// 例: プレイヤーノードなどから VignetteLayer を取得して HP を更新
import { _decorator, Component, Node } from 'cc';
import { VignetteDamage } from './VignetteDamage';
const { ccclass, property } = _decorator;

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

    @property(Node)
    public vignetteNode: Node | null = null;

    private _vignette: VignetteDamage | null = null;

    start() {
        if (this.vignetteNode) {
            this._vignette = this.vignetteNode.getComponent(VignetteDamage);
        }
    }

    // どこかで呼ぶ
    public applyDamage(amount: number) {
        if (!this._vignette) return;
        const newHp = this._vignette.currentHp - amount;
        this._vignette.setHp(newHp);
    }
}

このように、VignetteDamage 自体は完全に独立しており、他スクリプトからの利用も任意です。


まとめ

  • VignetteDamage は、Canvas配下の1ノードにアタッチするだけで「HPに応じて画面の四隅を赤黒く暗くする瀕死視界エフェクト」を実現する汎用コンポーネントです。
  • HP値・しきい値・最大強度・色・補間挙動など、すべてインスペクタから調整可能なため、ゲームごとに細かいチューニングが簡単です。
  • 外部の GameManager や HP 管理クラスに依存せず、このスクリプト単体で完結しているため、既存プロジェクトへの後付けプロトタイピングにも適しています。
  • ビネット画像や tintColor を差し替えることで、「毒状態」「暗闇」「寒気」「狂気」など、さまざまな状態異常演出にも応用できます。

このコンポーネントをプロジェクトの「共通UIエフェクト」としてライブラリ化しておけば、新しいシーンやゲームでもノードを1つ追加してアタッチするだけで、すぐに瀕死演出を導入できます。ぜひ自分のタイトルに合わせて色やしきい値を調整してみてください。