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
}
}
使い方の手順
-
血しぶきパーティクルプレハブを作る
- Hierarchy で空の GameObject を作成し、「BloodParticle」などにリネーム。
Particle Systemコンポーネントを追加し、血しぶきっぽい見た目に調整。- 完了したら Project ビューにドラッグしてプレハブ化し、元オブジェクトは削除してOK。
-
血痕デカールプレハブを作る
もっともシンプルな例として:- Hierarchy で「3D Object > Quad」を作成し、「BloodDecal」などにリネーム。
- Quad に血痕テクスチャを貼ったマテリアルを設定(シェーダは
TransparentやURP/Litの透明系など)。 - 影を落としたくない場合は、Mesh Renderer の「Cast Shadows」「Receive Shadows」をオフに。
- Project ビューにドラッグしてプレハブ化し、元オブジェクトは削除してOK。
-
BloodSplatter コンポーネントを配置して設定する
- Hierarchy で空の GameObject を作成し、「BloodEffectsManager」などにリネーム。
- そこに
BloodSplatterコンポーネントを追加。 - インスペクタで以下を設定:
- Blood Particle Prefab に 1 で作ったプレハブをドラッグ。
- Blood Decal Prefab に 2 で作ったプレハブをドラッグ。
- Decal Layer Mask に「床・壁」のレイヤーを指定(例: Default, Environment など)。
- 必要に応じて Decal Lifetime や Scale Min/Max を調整。
- 整理したい場合は、Effects Parent に「BloodDecalsRoot」などの空オブジェクトを指定しておくと、階層がスッキリします。
-
ダメージ処理から 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を行い、RaycastHitをOnHitに渡す形が定番です。 - 敵キャラ、プレイヤー、NPC など、同じ
BloodSplatterを共有してもOKですし、個別に違う設定を持たせても構いません。
- FPS 風の銃なら、弾丸スクリプト側で
メリットと応用
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 に任せる」という形にして、メインのゲームロジックをスッキリ保ちましょう。
