【Cocos Creator 3.8】BloodSplatter の実装:アタッチするだけで「血飛沫パーティクル+壁・床への血痕デカール生成」を実現する汎用スクリプト

敵やプレイヤーがダメージを受けたとき、「血が飛び散って壁や床にベタッと付く」演出は、アクションゲームやホラーゲームでよく使われます。この記事では、どのノードにアタッチしても単体で完結する、汎用の血飛沫コンポーネント BloodSplatter を TypeScript で実装します。
被弾イベントなどから spawnBlood() を呼び出すだけで、パーティクル風の血飛沫 と、壁・床に残る血痕デカール を自動生成できるようにします。


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

1. 機能要件の整理

  • このコンポーネントをアタッチしたノードを「血の発生源」とする。
  • 任意のタイミング(被弾時など)で spawnBlood() を呼ぶと:
    • 発生源の位置・向きから複数の血飛沫スプライト(パーティクル風)を生成し、短時間で消える。
    • 床や壁に付着したように見える「血痕デカール」を生成し、一定時間後にフェードアウトする(または残しっぱなしも選択可)。
  • 外部の GameManager や他スクリプトに依存せず、インスペクタのプロパティだけで挙動を調整可能にする。
  • 必要なリソース(血飛沫用 Prefab / 血痕デカール用 Prefab)は @property で受け取り、未設定時はエラーログを出して安全に動作を止める。
  • 物理エンジンへの依存は避け、簡易なランダム方向・速度で飛ばす 2D パーティクル風処理をスクリプト内で完結させる。

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

以下のようなプロパティを用意し、「どれくらい飛ぶか」「どれくらい残るか」をゲームごとに調整できるようにします。

  • 血飛沫パーティクル関連
    • bloodParticlePrefab: Prefab
      血飛沫 1 粒を表す Prefab(例: 小さな赤い Sprite)。これを複数インスタンス化して飛ばします。
    • particleCount: number
      1 回の発生で生成する血飛沫の個数。例: 10〜50 個。
    • particleMinSpeed: number
      血飛沫の初速度(最小)。例: 100。
    • particleMaxSpeed: number
      血飛沫の初速度(最大)。例: 400。
      particleMinSpeedparticleMaxSpeed のランダム値)
    • particleGravity: number
      血飛沫にかかる簡易重力(Y 方向の加速度)。負の値で下方向。例: -800。
    • particleLifetime: number
      血飛沫が生存する時間(秒)。この時間が経つと自動で破棄されます。
    • particleSpreadAngle: number
      発生方向の開き角度(度)。例: 120 なら、基準方向 ±60 度に散らばる。
    • particleBaseAngle: number
      基準発射角度(度)。0 度を右、90 度を上とする 2D の角度。
      敵の向きに合わせて変えたい場合は、インスペクタで調整するか、スクリプトから書き換えます。
    • particleRandomScaleMin / Max: number
      粒のスケールをランダムにするための最小値・最大値。例: 0.5〜1.5。
  • 血痕デカール関連
    • decalPrefab: Prefab
      壁や床に貼り付ける血痕の Prefab(例: 血だまりや血しぶきの Sprite)。
    • spawnDecal: boolean
      血痕デカールを生成するかどうか。false にするとパーティクルのみ。
    • decalCount: number
      1 回の発生で生成するデカールの個数。例: 1〜5。
    • decalOffsetRadius: number
      発生源からどれくらいバラけた位置にデカールを置くか(半径)。例: 30。
    • decalRandomRotation: boolean
      デカールをランダム回転させるかどうか。
    • decalRandomScaleMin / Max: number
      デカールのスケールランダム範囲。例: 0.8〜1.4。
    • decalFadeOut: boolean
      デカールを時間経過でフェードアウトさせるかどうか。
    • decalLifetime: number
      フェードアウトを開始するまでの時間(秒)。0 ならすぐにフェード開始。
    • decalFadeDuration: number
      完全に消えるまでにかけるフェード時間(秒)。
  • 共通・ユーティリティ
    • autoPlayOnStart: boolean
      true の場合、start() 時に 1 回自動で spawnBlood() を呼びます。テスト用。
    • debugLog: boolean
      true の場合、各種ログを console.log に出力して挙動を確認しやすくします。

これらをすべて 1 つのコンポーネントにまとめ、他のスクリプトには一切依存しません。血飛沫用 Prefab と血痕用 Prefab だけをインスペクタから割り当てれば動作します。


TypeScriptコードの実装

以下が完成した BloodSplatter.ts の全コードです。


import { _decorator, Component, Node, Prefab, instantiate, Vec3, math, UITransform, Sprite, Color, tween, Tween, director } from 'cc';
const { ccclass, property } = _decorator;

/**
 * BloodSplatter
 * 被弾位置から血飛沫パーティクルを飛ばし、床や壁に血痕デカールを生成する汎用コンポーネント。
 * - 他スクリプトへの依存なし
 * - インスペクタで挙動を細かく調整可能
 * - spawnBlood() を呼ぶだけでエフェクトを再生
 */
@ccclass('BloodSplatter')
export class BloodSplatter extends Component {

    // === 血飛沫パーティクル関連 ===

    @property({
        type: Prefab,
        tooltip: '血飛沫1粒を表すPrefab。小さな赤いSpriteなどを設定してください。'
    })
    public bloodParticlePrefab: Prefab | null = null;

    @property({
        tooltip: '1回の発生で生成する血飛沫パーティクルの個数。'
    })
    public particleCount: number = 20;

    @property({
        tooltip: '血飛沫パーティクルの最小初速度。'
    })
    public particleMinSpeed: number = 150;

    @property({
        tooltip: '血飛沫パーティクルの最大初速度。'
    })
    public particleMaxSpeed: number = 400;

    @property({
        tooltip: '血飛沫パーティクルにかかる重力(Y方向の加速度)。負の値で下方向。'
    })
    public particleGravity: number = -800;

    @property({
        tooltip: '血飛沫パーティクルの生存時間(秒)。この時間を過ぎると自動で破棄されます。'
    })
    public particleLifetime: number = 0.6;

    @property({
        tooltip: '発射方向の基準角度(度)。0=右, 90=上。'
    })
    public particleBaseAngle: number = 90;

    @property({
        tooltip: '発射方向の開き角度(度)。基準角度 ± (この値/2) の範囲でランダムに飛びます。'
    })
    public particleSpreadAngle: number = 120;

    @property({
        tooltip: '血飛沫パーティクルのスケール最小値。'
    })
    public particleRandomScaleMin: number = 0.6;

    @property({
        tooltip: '血飛沫パーティクルのスケール最大値。'
    })
    public particleRandomScaleMax: number = 1.4;

    // === 血痕デカール関連 ===

    @property({
        type: Prefab,
        tooltip: '床や壁に貼り付ける血痕(デカール)のPrefab。血だまりや血しぶきのSpriteを設定してください。'
    })
    public decalPrefab: Prefab | null = null;

    @property({
        tooltip: '血痕デカールを生成するかどうか。falseの場合はパーティクルのみ。'
    })
    public spawnDecal: boolean = true;

    @property({
        tooltip: '1回の発生で生成する血痕デカールの個数。'
    })
    public decalCount: number = 2;

    @property({
        tooltip: '発生源からどれくらい離れた位置にデカールを配置するか(半径)。'
    })
    public decalOffsetRadius: number = 30;

    @property({
        tooltip: 'デカールをランダム回転させるかどうか。'
    })
    public decalRandomRotation: boolean = true;

    @property({
        tooltip: 'デカールのスケール最小値。'
    })
    public decalRandomScaleMin: number = 0.8;

    @property({
        tooltip: 'デカールのスケール最大値。'
    })
    public decalRandomScaleMax: number = 1.4;

    @property({
        tooltip: 'デカールを時間経過でフェードアウトさせるかどうか。'
    })
    public decalFadeOut: boolean = true;

    @property({
        tooltip: 'デカールが表示されている時間(秒)。この時間経過後にフェードアウトを開始します。'
    })
    public decalLifetime: number = 5.0;

    @property({
        tooltip: 'デカールが完全に消えるまでにかけるフェード時間(秒)。'
    })
    public decalFadeDuration: number = 1.5;

    // === 共通・ユーティリティ ===

    @property({
        tooltip: 'trueにすると、start()時に自動で1回血飛沫を発生させます(テスト用)。'
    })
    public autoPlayOnStart: boolean = false;

    @property({
        tooltip: 'trueにすると、コンソールにデバッグログを出力します。'
    })
    public debugLog: boolean = false;

    // 内部状態
    private _activeParticles: {
        node: Node;
        velocity: Vec3;
        lifetime: number;
    }[] = [];

    private _dtAccumulator: number = 0;

    onLoad() {
        // プロパティの防御的チェック
        if (!this.bloodParticlePrefab) {
            console.warn('[BloodSplatter] bloodParticlePrefab が設定されていません。血飛沫パーティクルは生成されません。', this.node.name);
        }
        if (this.spawnDecal && !this.decalPrefab) {
            console.warn('[BloodSplatter] spawnDecal が true ですが decalPrefab が設定されていません。血痕デカールは生成されません。', this.node.name);
        }

        // 数値の整合性を軽く補正
        if (this.particleMaxSpeed < this.particleMinSpeed) {
            const tmp = this.particleMaxSpeed;
            this.particleMaxSpeed = this.particleMinSpeed;
            this.particleMinSpeed = tmp;
        }
        if (this.decalRandomScaleMax < this.decalRandomScaleMin) {
            const tmp = this.decalRandomScaleMax;
            this.decalRandomScaleMax = this.decalRandomScaleMin;
            this.decalRandomScaleMin = tmp;
        }
        if (this.particleRandomScaleMax < this.particleRandomScaleMin) {
            const tmp = this.particleRandomScaleMax;
            this.particleRandomScaleMax = this.particleRandomScaleMin;
            this.particleRandomScaleMin = tmp;
        }

        if (this.debugLog) {
            console.log('[BloodSplatter] onLoad - initialized on node:', this.node.name);
        }
    }

    start() {
        if (this.autoPlayOnStart) {
            this.spawnBlood();
        }
    }

    update(deltaTime: number) {
        // 血飛沫パーティクルの簡易物理更新
        if (this._activeParticles.length === 0) {
            return;
        }

        this._dtAccumulator += deltaTime;

        // 物理更新(1フレームごとで十分だが、積算してもよい)
        const gravityVec = new Vec3(0, this.particleGravity, 0);

        for (let i = this._activeParticles.length - 1; i >= 0; i--) {
            const p = this._activeParticles[i];
            p.lifetime -= deltaTime;
            if (p.lifetime <= 0) {
                // 生存時間終了
                p.node.destroy();
                this._activeParticles.splice(i, 1);
                continue;
            }

            // v += g * dt
            p.velocity.add(Vec3.multiplyScalar(new Vec3(), gravityVec, deltaTime));

            // pos += v * dt
            const pos = p.node.position;
            const deltaPos = Vec3.multiplyScalar(new Vec3(), p.velocity, deltaTime);
            pos.add(deltaPos);
            p.node.setPosition(pos);
        }
    }

    /**
     * 外部から呼び出して血飛沫+血痕を生成するメインAPI。
     * 例: 被弾時に this.bloodSplatter.spawnBlood(); のように呼び出します。
     */
    public spawnBlood(): void {
        const parent = this.node.parent;
        if (!parent) {
            console.warn('[BloodSplatter] 親ノードが存在しないため、血飛沫を生成できません。', this.node.name);
            return;
        }

        if (this.debugLog) {
            console.log('[BloodSplatter] spawnBlood called on node:', this.node.name);
        }

        const originPos = this.node.worldPosition.clone();

        // 親の座標系に変換(新しいノードは親のローカル座標で配置されるため)
        const parentTransform = parent.getComponent(UITransform);
        // UITransform が無くても worldPosition をそのまま setWorldPosition すればOKなので、
        // ここでは worldPosition ベースで生成する。

        // --- 血飛沫パーティクル生成 ---
        if (this.bloodParticlePrefab && this.particleCount > 0) {
            for (let i = 0; i < this.particleCount; i++) {
                const particleNode = instantiate(this.bloodParticlePrefab);
                parent.addChild(particleNode);
                particleNode.setWorldPosition(originPos);

                // 角度をランダムに決定
                const halfSpread = this.particleSpreadAngle * 0.5;
                const angleDeg = this.particleBaseAngle + math.randomRange(-halfSpread, halfSpread);
                const angleRad = angleDeg * Math.PI / 180;

                // 速度をランダムに決定
                const speed = math.randomRange(this.particleMinSpeed, this.particleMaxSpeed);
                const vx = Math.cos(angleRad) * speed;
                const vy = Math.sin(angleRad) * speed;
                const velocity = new Vec3(vx, vy, 0);

                // スケールをランダムに決定
                const scale = math.randomRange(this.particleRandomScaleMin, this.particleRandomScaleMax);
                particleNode.setScale(scale, scale, 1);

                // ランダム回転を与えてもよい(見た目のバリエーション)
                const randomRot = math.randomRange(0, 360);
                particleNode.setRotationFromEuler(0, 0, randomRot);

                // 内部リストに登録
                this._activeParticles.push({
                    node: particleNode,
                    velocity: velocity,
                    lifetime: this.particleLifetime,
                });
            }
        } else {
            if (!this.bloodParticlePrefab) {
                console.warn('[BloodSplatter] bloodParticlePrefab が未設定のため、血飛沫パーティクルは生成されません。');
            }
        }

        // --- 血痕デカール生成 ---
        if (this.spawnDecal && this.decalPrefab && this.decalCount > 0) {
            for (let i = 0; i < this.decalCount; i++) {
                const decalNode = instantiate(this.decalPrefab);
                parent.addChild(decalNode);

                // 発生源周辺のランダム位置を決定(円形にばらけさせる)
                const r = this.decalOffsetRadius * Math.sqrt(Math.random());
                const theta = Math.random() * Math.PI * 2;
                const offset = new Vec3(
                    Math.cos(theta) * r,
                    Math.sin(theta) * r,
                    0
                );

                // world座標で配置
                const decalPos = originPos.clone().add(offset);
                decalNode.setWorldPosition(decalPos);

                // スケールランダム
                const decalScale = math.randomRange(this.decalRandomScaleMin, this.decalRandomScaleMax);
                decalNode.setScale(decalScale, decalScale, 1);

                // 回転ランダム
                if (this.decalRandomRotation) {
                    const rot = math.randomRange(0, 360);
                    decalNode.setRotationFromEuler(0, 0, rot);
                }

                // フェードアウト処理
                if (this.decalFadeOut) {
                    this._fadeOutAndDestroyDecal(decalNode, this.decalLifetime, this.decalFadeDuration);
                }
            }
        } else {
            if (this.spawnDecal && !this.decalPrefab) {
                console.warn('[BloodSplatter] spawnDecal は true ですが decalPrefab が未設定のため、血痕デカールは生成されません。');
            }
        }
    }

    /**
     * デカールノードを時間経過でフェードアウトさせ、最後に破棄する。
     * Sprite コンポーネントが無い場合はエラーログを出して即時破棄。
     */
    private _fadeOutAndDestroyDecal(decalNode: Node, delay: number, duration: number): void {
        const sprite = decalNode.getComponent(Sprite);
        if (!sprite) {
            console.warn('[BloodSplatter] decalPrefab に Sprite コンポーネントが見つかりません。フェードアウトできないため即時破棄します。');
            decalNode.destroy();
            return;
        }

        // 現在色を取得
        const startColor = sprite.color.clone();
        const endColor = new Color(startColor.r, startColor.g, startColor.b, 0);

        // delay後にフェードアウト
        tween(sprite)
            .delay(Math.max(0, delay))
            .to(duration, { color: endColor })
            .call(() => {
                decalNode.destroy();
            })
            .start();
    }
}

コードのポイント解説

  • onLoad()
    • Prefab が設定されていない場合に console.warn を出して知らせます。
    • 最小値・最大値のプロパティが逆転している場合は入れ替えて防御的に補正しています。
  • start()
    • autoPlayOnStart が true のときだけ spawnBlood() を 1 回呼び出してテストを容易にしています。
  • update(deltaTime)
    • 内部で管理している血飛沫パーティクル配列 _activeParticles を毎フレーム更新し、重力と速度で位置を動かしています。
    • 寿命が尽きた粒は destroy() して配列から削除します。
  • spawnBlood()
    • 外部から呼び出すメイン API です。被弾時などに呼び出してください。
    • 親ノードが無い場合は警告を出して処理を中断します。
    • 血飛沫パーティクル:
      • bloodParticlePrefabparticleCount 個インスタンス化。
      • 基準角度 particleBaseAngle と開き角度 particleSpreadAngle からランダムな飛翔方向を決定。
      • 速度・スケール・回転もランダム化してバリエーションを出しています。
    • 血痕デカール:
      • decalPrefabdecalCount 個インスタンス化。
      • 発生源の周囲 decalOffsetRadius 以内にランダム配置。
      • スケール・回転をランダム化。
      • decalFadeOut が true の場合は _fadeOutAndDestroyDecal() でフェードアウト処理を行います。
  • _fadeOutAndDestroyDecal()
    • デカールに Sprite コンポーネントが付いていることを確認し、無ければ警告を出して即破棄します。
    • tween(Sprite) を使って color.a を 0 にし、透明にしてからノードを破棄します。
    • これにより、血痕が徐々に消えていく自然な演出が可能です。

使用手順と動作確認

ここからは、実際に Cocos Creator 3.8.7 のエディタ上でこのコンポーネントを使う手順を説明します。

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

  1. Assets パネルで任意のフォルダ(例: assets/scripts)を右クリックします。
  2. Create → TypeScript を選択します。
  3. 新規スクリプトの名前を BloodSplatter.ts に変更します。
  4. ダブルクリックしてエディタ(VS Code など)で開き、上記の TypeScript コードを丸ごと貼り付けて保存します。

2. 血飛沫パーティクル用 Prefab の準備

簡単な血飛沫パーティクル用 Prefab を作成します。

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、ノード名を BloodParticle などに変更します。
  2. Inspector で Sprite コンポーネントの SpriteFrame に、小さな赤い丸・しぶきなどの画像を設定します。
    • なければ、赤い円や点の簡単なテクスチャを用意しても構いません。
  3. 必要に応じて UITransform のサイズや Sprite の色(Color)を調整し、血っぽい見た目にします。
  4. このノードを Prefab 化します:
    • Hierarchy から Assets パネルへドラッグ&ドロップ して Prefab を作成します。
    • Prefab 名を BloodParticlePrefab などに変更します。
  5. Hierarchy 上の元ノードは、不要であれば削除して構いません(Prefab は Assets に残ります)。

3. 血痕デカール用 Prefab の準備

  1. Hierarchy パネルで右クリック → Create → 2D Object → Sprite を選択し、ノード名を BloodDecal などに変更します。
  2. Inspector の Sprite コンポーネントで、床や壁に付着したような血痕テクスチャを設定します。
    • アルファ付き PNG で、にじんだ血だまり・血しぶきなど。
  3. このノードを同様に Prefab 化し、名前を BloodDecalPrefab などにします。
  4. 重要: フェードアウト機能を使う場合、この Prefab には必ず Sprite コンポーネント が付いている必要があります。
    • もし別のレンダラーを使う場合は、このスクリプトを拡張して対応してください。

4. テスト用ノードに BloodSplatter をアタッチ

  1. Hierarchy パネルで右クリック → Create → 2D Object → Node を選択し、ノード名を HitPoint などに変更します。
    • 既に敵キャラやプレイヤーのノードがある場合は、そのノードを使っても構いません。
  2. HitPoint ノードを選択し、Inspector の Add Component ボタンをクリックします。
  3. Custom → BloodSplatter を選択してコンポーネントを追加します。

5. インスペクタでプロパティを設定

HitPoint ノードにアタッチされた BloodSplatter コンポーネントのプロパティを設定します。

  • 血飛沫パーティクル関連
    • Blood Particle Prefab: 先ほど作成した BloodParticlePrefab をドラッグ&ドロップ。
    • Particle Count: 20〜40 あたりから試す。
    • Particle Min Speed: 150
    • Particle Max Speed: 400
    • Particle Gravity: -800(2D アクションならこのくらいがそれっぽい)
    • Particle Lifetime: 0.5〜0.8
    • Particle Base Angle: 90(上方向)、あるいは 60〜120 など。
    • Particle Spread Angle: 120〜180(広く飛び散らせたい場合は大きめに)。
    • Particle Random Scale Min/Max: 0.6 / 1.4 など。
  • 血痕デカール関連
    • Decal Prefab: BloodDecalPrefab をドラッグ&ドロップ。
    • Spawn Decal: true(血痕を生成したい場合)。
    • Decal Count: 1〜3。
    • Decal Offset Radius: 30〜60。血痕がどれくらい広がるか。
    • Decal Random Rotation: true。
    • Decal Random Scale Min/Max: 0.8 / 1.4。
    • Decal Fade Out: true(時間で消したい場合)。
    • Decal Lifetime: 5〜10 秒。
    • Decal Fade Duration: 1〜2 秒。
  • 共通
    • Auto Play On Start: 最初の動作確認用に true にしておくと、シーン再生時に自動で血飛沫が出ます。
    • Debug Log: 挙動を確認したい場合は true にしてログを見ます。

6. シーン再生で動作確認

  1. シーンにカメラと Canvas(または 2D の描画環境)があることを確認します。
  2. 再生ボタン(▶)を押してゲームを実行します。
  3. Auto Play On Start を true にしていれば、シーン開始と同時に HitPoint 位置から血飛沫が飛び散り、周囲に血痕が貼り付く様子が確認できます。

7. 実際のゲームでの呼び出し例

ゲーム中では、被弾時などのタイミングで spawnBlood() を呼び出します。例として、敵キャラのスクリプトから呼ぶ場合:


// Enemy.ts (例:敵キャラのスクリプト)
// ※ Enemy.ts 自体はこのガイドの範囲外ですが、呼び出し方のイメージとして。

import { _decorator, Component } from 'cc';
import { BloodSplatter } from './BloodSplatter';
const { ccclass, property } = _decorator;

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

    @property(BloodSplatter)
    public bloodSplatter: BloodSplatter | null = null;

    // 何らかの被弾処理の中で
    onHit() {
        if (this.bloodSplatter) {
            this.bloodSplatter.spawnBlood();
        }
        // HP減少などの処理...
    }
}

このように、BloodSplatter 自体は他のスクリプトに依存せず
「被弾した側のスクリプトから spawnBlood() を呼ぶ」だけで汎用的に利用できます。


まとめ

この記事では、Cocos Creator 3.8.7 + TypeScript で、単体で完結する血飛沫&血痕デカールコンポーネント BloodSplatter を実装しました。

  • Prefab 2 つ(血飛沫粒・血痕デカール)を用意してインスペクタに割り当てるだけで、どのノードでも使える。
  • パーティクル数・速度・重力・寿命・散らばり角度などを @property から細かく調整可能。
  • デカールの数・半径・スケール・回転・フェードアウト時間も柔軟にコントロールできる。
  • 物理エンジンや他の管理スクリプトに依存せず、防御的なチェックログ出力 でトラブルを最小限に抑えている。

このコンポーネントをプロジェクトの「血演出の標準パーツ」として再利用すれば、どの敵・どのプレイヤーにも簡単に同じ品質の血飛沫エフェクトを付与できます。
パラメータ違いのプリセットをいくつか用意しておけば、「弱い攻撃のときは控えめ」「クリティカル時は派手に」といったバリエーションも簡単に実現できます。

あとは、ゲームの世界観に合わせてテクスチャやパラメータをチューニングして、自分だけの血飛沫表現を作り込んでみてください。