Unityを触り始めた頃は、つい「被弾処理もエフェクトもサウンドも全部プレイヤーの Update() に書いてしまう」ことが多いですよね。
ダメージ判定の中に血しぶきの生成、カメラシェイク、スコア加算…と機能が雪だるま式に増えていくと、気づいたら1,000行級の「Godクラス」が出来上がってしまいます。

こうなると、

  • エフェクトだけ仕様変更したいのに、ダメージ処理まで巻き込んで壊れる
  • 敵キャラにも同じエフェクトを使いたいのに、プレイヤースクリプトにベッタリ依存していて再利用できない
  • プレハブごとに微妙な調整をしたいのに、1つの巨大スクリプトを共有していて地獄

といった問題が起きがちです。

そこでこの記事では、「被弾時の血しぶき&血痕デカール」を 専用の小さなコンポーネント に切り出します。
被弾イベント側は「ここで血を出して」とだけ呼び出し、実際のパーティクル再生や壁・床への血痕貼り付けは BloodSplatter コンポーネントに丸投げする構成です。

【Unity】被弾演出をコンポーネントに分離!「BloodSplatter」コンポーネント

ここでは、

  • 被弾位置と法線(衝突面の向き)を渡すと
  • その場で血のパーティクルを散らし
  • 壁や床に血痕デカール(静止画)を貼り付ける

という責務だけを持つ BloodSplatter コンポーネントを実装します。

前提となる準備(アセット側)

  • 血しぶき用の Particle System プレハブ(例: BloodParticlePrefab
  • 血痕用の デカール用プレハブ(Quad + 血テクスチャのマテリアルなど。例: BloodDecalPrefab

を用意しておく想定でコードを書いています。

BloodSplatter.cs(フルコード)


using UnityEngine;

/// <summary>
/// 被弾時に血しぶきパーティクルと血痕デカールを生成するコンポーネント。
/// 「どこで・どの向きに」血を出すかだけを引数で受け取り、
/// パーティクルとデカールの生成・寿命管理を担当します。
/// </summary>
public class BloodSplatter : MonoBehaviour
{
    [Header("パーティクル設定")]
    [SerializeField]
    private GameObject bloodParticlePrefab; // 血しぶき用パーティクルのプレハブ

    [SerializeField]
    [Tooltip("パーティクルが自動停止するまでの時間 + 安全マージン(秒)")]
    private float particleLifeTime = 3f;

    [Header("デカール設定")]
    [SerializeField]
    private GameObject bloodDecalPrefab; // 血痕デカール用プレハブ(Quadなど)

    [SerializeField]
    [Tooltip("デカールの寿命(秒)。0以下なら破棄しない")]
    private float decalLifeTime = 30f;

    [SerializeField]
    [Tooltip("デカールを壁に少しだけ浮かせるオフセット値")]
    private float decalSurfaceOffset = 0.01f;

    [SerializeField]
    [Tooltip("デカールのランダムスケール範囲(最小値)")]
    private float decalScaleMin = 0.7f;

    [SerializeField]
    [Tooltip("デカールのランダムスケール範囲(最大値)")]
    private float decalScaleMax = 1.3f;

    [SerializeField]
    [Tooltip("デカールの回転をランダムにするかどうか")]
    private bool randomizeDecalRotation = true;

    [Header("レイヤーマスク")]
    [SerializeField]
    [Tooltip("血痕を貼り付けてよいレイヤー。床や壁などを指定")]
    private LayerMask decalLayerMask = ~0; // デフォルトは全レイヤー許可

    [Header("その他設定")]
    [SerializeField]
    [Tooltip("パーティクルやデカールを階層整理するための親Transform(任意)")]
    private Transform effectsParent;

    /// <summary>
    /// 外部から呼び出す公開メソッド。
    /// 被弾位置と被弾面の法線を渡すと、血しぶきと血痕を生成します。
    /// </summary>
    /// <param name="hitPoint">被弾したワールド座標</param>
    /// <param name="hitNormal">被弾面の法線ベクトル(RaycastHit.normal など)</param>
    public void SpawnBlood(Vector3 hitPoint, Vector3 hitNormal)
    {
        // 1. 血しぶきパーティクルの生成
        SpawnBloodParticle(hitPoint, hitNormal);

        // 2. 血痕デカールの生成
        SpawnBloodDecal(hitPoint, hitNormal);
    }

    /// <summary>
    /// 血しぶきパーティクルを生成して、自動的に破棄します。
    /// </summary>
    private void SpawnBloodParticle(Vector3 hitPoint, Vector3 hitNormal)
    {
        if (bloodParticlePrefab == null)
        {
            // プレハブ未設定の場合は何もしない
            return;
        }

        // パーティクルの向きを、被弾面の法線方向に向ける
        Quaternion rotation = Quaternion.LookRotation(hitNormal);

        // 親Transformが指定されていればそこにぶら下げる
        Transform parent = effectsParent != null ? effectsParent : null;

        GameObject particleObj = Instantiate(
            bloodParticlePrefab,
            hitPoint,
            rotation,
            parent
        );

        // パーティクルの寿命後に自動でGameObjectを破棄
        if (particleLifeTime > 0f)
        {
            Destroy(particleObj, particleLifeTime);
        }
    }

    /// <summary>
    /// 血痕デカールを被弾面に貼り付けます。
    /// Raycast を使って、実際の壁・床の表面位置に合わせて配置します。
    /// </summary>
    private void SpawnBloodDecal(Vector3 hitPoint, Vector3 hitNormal)
    {
        if (bloodDecalPrefab == null)
        {
            // プレハブ未設定の場合は何もしない
            return;
        }

        // Raycast で「血痕を貼れる実際のサーフェス」を探す
        // 被弾位置から少しだけ法線方向に戻して、逆方向へRayを飛ばすイメージ
        Vector3 rayOrigin = hitPoint + hitNormal * 0.1f;
        Vector3 rayDirection = -hitNormal;

        if (Physics.Raycast(rayOrigin, rayDirection, out RaycastHit hitInfo, 1f, decalLayerMask, QueryTriggerInteraction.Ignore))
        {
            // 実際のヒット地点と法線を使用してデカールを配置
            Vector3 decalPosition = hitInfo.point + hitInfo.normal * decalSurfaceOffset;
            Quaternion decalRotation = Quaternion.LookRotation(hitInfo.normal);

            // Z軸回転をランダムにして、同じテクスチャでもバリエーションを出す
            if (randomizeDecalRotation)
            {
                float randomZ = Random.Range(0f, 360f);
                decalRotation *= Quaternion.Euler(0f, 0f, randomZ);
            }

            Transform parent = effectsParent != null ? effectsParent : hitInfo.transform;

            GameObject decalObj = Instantiate(
                bloodDecalPrefab,
                decalPosition,
                decalRotation,
                parent
            );

            // ランダムスケールを適用
            float scale = Random.Range(decalScaleMin, decalScaleMax);
            decalObj.transform.localScale *= scale;

            // デカールの寿命管理
            if (decalLifeTime > 0f)
            {
                Destroy(decalObj, decalLifeTime);
            }
        }
        else
        {
            // Raycast に失敗した場合(例: 空中でヒットした弾など)は、
            // 被弾位置と法線をそのまま使ってデカールを貼る。
            Vector3 decalPosition = hitPoint + hitNormal * decalSurfaceOffset;
            Quaternion decalRotation = Quaternion.LookRotation(hitNormal);

            if (randomizeDecalRotation)
            {
                float randomZ = Random.Range(0f, 360f);
                decalRotation *= Quaternion.Euler(0f, 0f, randomZ);
            }

            Transform parent = effectsParent != null ? effectsParent : null;

            GameObject decalObj = Instantiate(
                bloodDecalPrefab,
                decalPosition,
                decalRotation,
                parent
            );

            float scale = Random.Range(decalScaleMin, decalScaleMax);
            decalObj.transform.localScale *= scale;

            if (decalLifeTime > 0f)
            {
                Destroy(decalObj, decalLifeTime);
            }
        }
    }

    // ---------------------------------------------------------
    // 以下はサンプル用:簡易的なテストメソッド
    // 実際のゲームでは、ダメージ処理側から SpawnBlood を呼び出します。
    // ---------------------------------------------------------

    /// <summary>
    /// デバッグ用:Sceneビュー上で右クリックした地点に血を出すテスト。
    /// ※ 実運用では使わない想定ですが、動作確認用として残しています。
    /// </summary>
    private void Update()
    {
#if UNITY_EDITOR
        // エディタ上でマウス右クリックした地点に血痕を生成
        if (Input.GetMouseButtonDown(1))
        {
            Camera cam = Camera.main;
            if (cam == null) return;

            Ray ray = cam.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out RaycastHit hitInfo, 100f, decalLayerMask, QueryTriggerInteraction.Ignore))
            {
                SpawnBlood(hitInfo.point, hitInfo.normal);
            }
        }
#endif
    }
}

使い方の手順

  1. 血しぶきパーティクルプレハブを作る
    • Hierarchy で空の GameObject を作成し、「BloodParticle」などにリネーム。
    • Particle System コンポーネントを追加し、血しぶきっぽい見た目に調整。
    • 完了したら Project ビューにドラッグしてプレハブ化し、元オブジェクトは削除してOK。
  2. 血痕デカールプレハブを作る
    もっともシンプルな例として:
    • Hierarchy で「3D Object > Quad」を作成し、「BloodDecal」などにリネーム。
    • Quad に血痕テクスチャを貼ったマテリアルを設定(シェーダは TransparentURP/Lit の透明系など)。
    • 影を落としたくない場合は、Mesh Renderer の「Cast Shadows」「Receive Shadows」をオフに。
    • Project ビューにドラッグしてプレハブ化し、元オブジェクトは削除してOK。
  3. BloodSplatter コンポーネントを配置して設定する
    • Hierarchy で空の GameObject を作成し、「BloodEffectsManager」などにリネーム。
    • そこに BloodSplatter コンポーネントを追加。
    • インスペクタで以下を設定:
      • Blood Particle Prefab に 1 で作ったプレハブをドラッグ。
      • Blood Decal Prefab に 2 で作ったプレハブをドラッグ。
      • Decal Layer Mask に「床・壁」のレイヤーを指定(例: Default, Environment など)。
      • 必要に応じて Decal LifetimeScale Min/Max を調整。
    • 整理したい場合は、Effects Parent に「BloodDecalsRoot」などの空オブジェクトを指定しておくと、階層がスッキリします。
  4. ダメージ処理から SpawnBlood を呼び出す
    具体例として、プレイヤーや敵キャラの「被弾スクリプト」から呼び出します。
    
    using UnityEngine;
    
    /// <summary>簡易的な被弾処理の例。弾丸から呼ばれる想定。</summary>
    public class SimpleHitReceiver : MonoBehaviour
    {
        [SerializeField]
        private BloodSplatter bloodSplatter; // Hierarchy 上の BloodEffectsManager をアサイン
    
        public void OnHit(RaycastHit hitInfo)
        {
            // ここでHPを減らしたり、ノックバックしたりする
            // health -= damage; など
    
            // 血しぶき&血痕を生成
            if (bloodSplatter != null)
            {
                bloodSplatter.SpawnBlood(hitInfo.point, hitInfo.normal);
            }
        }
    }
    
    • FPS 風の銃なら、弾丸スクリプト側で Physics.Raycast を行い、RaycastHitOnHit に渡す形が定番です。
    • 敵キャラ、プレイヤー、NPC など、同じ BloodSplatter を共有してもOKですし、個別に違う設定を持たせても構いません。

メリットと応用

BloodSplatter を独立させることで、

  • ダメージ処理と演出処理を完全に分離できるので、HPやステートマシンのコードがスリムになる
  • 「敵の血は濃い赤、プレイヤーは薄い赤」など、プレハブ単位でエフェクトを差し替えるのが簡単になる
  • レベルデザイナーが「このステージは血痕がすぐ消えるようにしたい」など、シーンごとにデカール寿命を変えることもインスペクタからポチポチ調整できる
  • パフォーマンス調整(寿命短縮・スケール縮小・レイヤー制限など)も、この1コンポーネントだけ触ればよい

プレハブ管理の観点でも、

  • BloodEffectsManager を 1 つ用意しておき、全キャラクターがそれを参照する
  • 特殊なボス戦だけ、別シーン専用の BloodSplatter を置いて派手なエフェクトにする

といった構成がしやすくなります。
「エフェクトはエフェクト用のコンポーネントに閉じ込める」というルールを徹底すると、プロジェクト全体がかなり見通しよくなりますね。

改造案:一定数を超えた古い血痕を自動で消す

大量の敵がいるゲームでは、血痕デカールが増えすぎると描画コストが気になります。
例えば「常に最新100個だけ残す」ような機能を加えると、パフォーマンス管理が楽になります。

以下は、簡単な「古いデカールを削除する」メソッド例です(BloodSplatter に追記する想定)。


using System.Collections.Generic;

public class BloodSplatter : MonoBehaviour
{
    // ... 既存コード ...

    [SerializeField]
    private int maxDecalCount = 100;

    private readonly List<GameObject> spawnedDecals = new List<GameObject>();

    private void RegisterDecal(GameObject decal)
    {
        spawnedDecals.Add(decal);

        // 上限を超えたら、一番古いデカールから破棄
        while (spawnedDecals.Count > maxDecalCount)
        {
            GameObject oldest = spawnedDecals[0];
            spawnedDecals.RemoveAt(0);

            if (oldest != null)
            {
                Destroy(oldest);
            }
        }
    }
}

そして SpawnBloodDecal 内で Instantiate した直後に RegisterDecal(decalObj); を呼ぶようにすれば、「血痕の総数を自動制限する BloodSplatter」に進化させられます。

このように、小さな責務のコンポーネントを用意しておくと、仕様変更や最適化のときにも安心して手を入れられます。
「被弾演出は全部 BloodSplatter に任せる」という形にして、メインのゲームロジックをスッキリ保ちましょう。