Unityを触り始めた頃にありがちなのが、Update() の中に「移動」「入力」「エフェクト」「ダメージ処理」「爆発処理」などを全部まとめて書いてしまうパターンです。
最初は動くので気持ちいいのですが、あとから「爆発の仕様をちょっと変えたい」「敵にも同じ爆風を使いたい」となったときに、巨大なスクリプトの中から関連コードを探すハメになり、修正のたびにバグが増えていきます。
こういうときに意識したいのが「1コンポーネント = 1つの責務」という考え方ですね。
爆発の物理的な押し出しだけを担当する小さなコンポーネントに分けておけば、プレイヤーのスクリプトや敵のAIスクリプトとは独立して、爆風ロジックだけを再利用・調整できます。
この記事では、爆発時に、範囲内の Rigidbody を中心から外側へ吹き飛ばす専用コンポーネントとして、ExplosionForce を実装していきます。
【Unity】一発で吹き飛ばす爆風物理!「ExplosionForce」コンポーネント
今回作る ExplosionForce は、こんなイメージで使います。
- 爆発エフェクトのプレハブにアタッチしておく
- 爆発が発生したタイミングで
Explode()を呼ぶ(または自動で Start 時に爆発) - 指定した半径内の Rigidbody に対して、爆心地から外側へ力を加える
物理的な押し出しだけに責務を絞ることで、「ダメージ計算」「エフェクト表示」「サウンド再生」などとはきれいに分離できます。
フルコード:ExplosionForce.cs
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 爆発時に、指定半径内の Rigidbody に爆風の力を与えるコンポーネント。
/// ・物理的な押し出しのみを担当(ダメージやエフェクトは別コンポーネントに分離推奨)
/// ・OnDrawGizmos で爆風範囲を可視化可能
/// </summary>
[DisallowMultipleComponent]
public class ExplosionForce : MonoBehaviour
{
// --- 爆風の基本パラメータ ---
[Header("Explosion Settings")]
[Tooltip("爆風の中心位置。未指定ならこのオブジェクトの Transform.position を使用")]
[SerializeField] private Transform explosionCenter;
[Tooltip("爆風の最大半径(この距離までの Rigidbody に力を与える)")]
[SerializeField] private float radius = 5f;
[Tooltip("爆風の強さ(大きいほど強く吹き飛ばす)")]
[SerializeField] private float force = 10f;
[Tooltip("爆風の上向き補正。0 なら完全に水平、値を上げると少し上方向に持ち上がる")]
[SerializeField] private float upwardsModifier = 0.0f;
[Tooltip("力の加え方(Impulse 推奨:瞬間的な爆発力)")]
[SerializeField] private ForceMode forceMode = ForceMode.Impulse;
[Header("Falloff Settings")]
[Tooltip("距離による減衰を有効にするか(true: 爆心地に近いほど強く、遠いほど弱くなる)")]
[SerializeField] private bool useDistanceFalloff = true;
[Tooltip("減衰カーブ。横軸: 距離0〜1(0:中心,1:半径端) 縦軸: 強さの倍率")]
[SerializeField] private AnimationCurve falloffCurve = AnimationCurve.EaseInOut(0, 1, 1, 0);
[Header("Layer / Filter Settings")]
[Tooltip("爆風の対象とするレイヤー(例: Player, Enemy, DynamicObjects など)")]
[SerializeField] private LayerMask targetLayers = ~0; // デフォルトは全レイヤー
[Tooltip("自分自身にアタッチされている Rigidbody を除外するか")]
[SerializeField] private bool ignoreSelfRigidbody = true;
[Header("Timing")]
[Tooltip("Start() で自動的に爆発させるか(プレハブ生成と同時に爆発させたい場合など)")]
[SerializeField] private bool explodeOnStart = true;
[Tooltip("自動爆発の場合、生成から何秒後に爆発させるか")]
[SerializeField] private float explodeDelay = 0.0f;
[Header("Debug")]
[Tooltip("Scene ビューに爆風の半径をワイヤーフレームで表示するか")]
[SerializeField] private bool drawGizmos = true;
[Tooltip("Gizmos の色")]
[SerializeField] private Color gizmoColor = new Color(1f, 0.5f, 0f, 0.3f);
// すでに爆発済みかどうかのフラグ(誤爆防止)
private bool hasExploded = false;
// 自身にアタッチされている Rigidbody(ignoreSelfRigidbody 用)
private Rigidbody selfRigidbody;
private void Awake()
{
// 自身の Rigidbody をキャッシュ(存在しない場合は null)
selfRigidbody = GetComponent<Rigidbody>();
}
private void Start()
{
// 自動爆発が有効なら、Start で爆発を予約
if (explodeOnStart)
{
if (explodeDelay <= 0f)
{
Explode();
}
else
{
Invoke(nameof(Explode), explodeDelay);
}
}
}
/// <summary>
/// 爆発を実行し、半径内の Rigidbody に力を与えるメイン関数。
/// 外部のスクリプトから手動で呼び出してもOK。
/// 例: GetComponent<ExplosionForce>().Explode();
/// </summary>
public void Explode()
{
// 二重に爆発させないようにガード
if (hasExploded)
{
return;
}
hasExploded = true;
// 爆心地の取得(未指定なら自分の位置)
Vector3 center = explosionCenter != null
? explosionCenter.position
: transform.position;
// 半径内にあるコライダーをすべて取得
Collider[] hitColliders = Physics.OverlapSphere(center, radius, targetLayers, QueryTriggerInteraction.Ignore);
// 重複した Rigidbody への多重適用を防ぐため、一時的な HashSet で管理
HashSet<Rigidbody> processedRigidbodies = new HashSet<Rigidbody>();
foreach (Collider col in hitColliders)
{
if (col.attachedRigidbody == null)
{
// Rigidbody を持たないオブジェクトはスキップ
continue;
}
Rigidbody rb = col.attachedRigidbody;
// 自分自身の Rigidbody を除外するオプション
if (ignoreSelfRigidbody && rb == selfRigidbody)
{
continue;
}
// 同じ Rigidbody に対しては一度だけ処理する
if (processedRigidbodies.Contains(rb))
{
continue;
}
processedRigidbodies.Add(rb);
// 爆心地から対象へのベクトル
Vector3 direction = (rb.worldCenterOfMass - center);
// 爆心地と完全に同じ位置にいる場合、方向が出せないのでランダム方向に吹き飛ばす
if (direction.sqrMagnitude <= Mathf.Epsilon)
{
direction = Random.onUnitSphere;
}
float distance = direction.magnitude;
Vector3 normalizedDirection = direction / distance;
// 距離による減衰係数を計算(0〜1)
float distanceFactor = 1f;
if (useDistanceFalloff)
{
// 0:中心, 1:半径の端
float t = Mathf.Clamp01(distance / radius);
distanceFactor = Mathf.Clamp01(falloffCurve.Evaluate(t));
}
// 実際に適用する力の大きさ
float finalForce = force * distanceFactor;
// 上向き補正を適用(Unity の AddExplosionForce と同様のイメージ)
Vector3 finalDirection = normalizedDirection;
if (!Mathf.Approximately(upwardsModifier, 0f))
{
// 上方向成分を足して少し持ち上がるようにする
finalDirection += Vector3.up * upwardsModifier;
finalDirection.Normalize();
}
// 最終的な力のベクトル
Vector3 forceVector = finalDirection * finalForce;
// Rigidbody に力を加える
rb.AddForce(forceVector, forceMode);
}
}
/// <summary>
/// Scene ビューで爆風半径を可視化する。
/// レベルデザイン時の調整に便利。
/// </summary>
private void OnDrawGizmosSelected()
{
if (!drawGizmos)
{
return;
}
Gizmos.color = gizmoColor;
Vector3 center = explosionCenter != null
? explosionCenter.position
: transform.position;
Gizmos.DrawWireSphere(center, radius);
}
/// <summary>
/// 外部からパラメータを一括設定したいとき用のヘルパー関数。
/// 爆発プレハブをスクリプトから生成する場合などに便利。
/// </summary>
public void Setup(float radius, float force, float upwardsModifier, LayerMask targetLayers)
{
this.radius = radius;
this.force = force;
this.upwardsModifier = upwardsModifier;
this.targetLayers = targetLayers;
}
}
使い方の手順
ここでは、代表的な3パターンの使い方を例にしながら手順を説明します。
手順①:プレハブに ExplosionForce を仕込む
- Hierarchy で空の GameObject を作成し、名前を
Explosionなどにします。 ExplosionForce.csをこの GameObject にアタッチします。- 必要であれば、パーティクル(爆発エフェクト)や AudioSource(爆発音)などを子オブジェクトとしてぶら下げます。
- この GameObject を Project ビューにドラッグしてプレハブ化します。
これで「爆風だけを扱うプレハブ」ができました。
パラメータは Inspector から自由に調整できます。
- Radius: 爆風の届く距離
- Force: 吹き飛ばしの強さ
- Upwards Modifier: 0.5〜1.0 くらいにすると、少し上方向に飛びやすくなって気持ちいいです
- Target Layers: Player, Enemy, DynamicObjects など、動かしたいレイヤーを指定
手順②:プレイヤーのグレネードに使う例
例えば、プレイヤーが投げるグレネードが一定時間後に爆発する場合、グレネードのプレハブ側で ExplosionForce を呼び出します。
using UnityEngine;
public class Grenade : MonoBehaviour
{
[SerializeField] private ExplosionForce explosionForce;
[SerializeField] private float fuseTime = 2.0f; // 起爆までの時間
private void Start()
{
// 一定時間後に爆発させる
Invoke(nameof(Detonate), fuseTime);
}
private void Detonate()
{
if (explosionForce != null)
{
explosionForce.Explode();
}
// 爆発エフェクトなどを再生したあと、自分自身を削除する想定
Destroy(gameObject, 0.1f);
}
}
このように、グレネードは「いつ爆発するか」だけを知っていて、爆風の物理処理は ExplosionForce に丸投げできます。
責務が分かれているので、爆風の仕様変更があっても ExplosionForce だけをいじれば済みます。
手順③:敵の自爆攻撃に使う例
敵がプレイヤーに近づいて自爆するタイプの攻撃でも、同じ ExplosionForce を再利用できます。
- 敵のプレハブに ExplosionForce をアタッチしておく
- HP が 0 になったり、一定条件を満たしたときに
Explode()を呼ぶ
敵の AI ロジックは「いつ自爆するか」を決めるだけで、吹き飛ばしの挙動は共通化できます。
手順④:動く床や破壊可能オブジェクトへの利用例
動く床や破壊可能オブジェクトにも Rigidbody を付けておけば、ExplosionForce でまとめて吹き飛ばせます。
- 「破壊可能オブジェクト」レイヤーを作成し、それらのオブジェクトに設定
- ExplosionForce の Target Layers にそのレイヤーを含める
レベルデザイン時には、Scene ビューで ExplosionForce のオブジェクトを配置し、OnDrawGizmos のワイヤーフレームを見ながら半径を調整すると、どの範囲までオブジェクトが吹き飛ぶか一目で分かって便利です。
メリットと応用
ExplosionForce を単独のコンポーネントとして切り出すメリットは多いです。
- 責務が明確:爆風の物理処理だけを担当するので、ダメージやエフェクトとは独立して調整可能。
- プレハブの再利用性が高い:プレイヤー用グレネード、敵の自爆、トラップなど、あらゆる爆発系ギミックで同じコンポーネントを使い回せる。
- レベルデザインが楽:Gizmos で爆風範囲が見えるため、「ここで爆発したらこの箱が落ちる」といった配置調整が直感的。
- テストしやすい:ExplosionForce だけをシーンに置いて、パラメータをいじりながら挙動を確認できる。
また、減衰カーブを AnimationCurve で持っているので、
- 中心付近はほぼ一定の強さ、端に近づくほど急激に弱くする
- 逆に、中心は弱め・中間距離が一番強いなど、変わった爆風パターン
といった「ゲームらしいチューニング」も Inspector から直感的に行えます。
改造案:タグで対象をフィルタリングする
もっと細かく対象を絞りたい場合、「特定のタグを持つ Rigidbody にのみ爆風を適用する」ような改造も簡単にできます。
例えば、ExplosionForce 内に次のようなヘルパー関数を追加して、Explode() から呼ぶロジックを切り替えるのもアリです。
/// <summary>
/// 指定したタグを持つ Rigidbody のみを爆風対象とするサンプル関数。
/// 例: "Player" だけ吹き飛ばしたい場合など。
/// </summary>
private void ApplyExplosionToTaggedRigidbodies(Vector3 center, string targetTag)
{
Collider[] hitColliders = Physics.OverlapSphere(center, radius, targetLayers, QueryTriggerInteraction.Ignore);
foreach (Collider col in hitColliders)
{
if (!col.CompareTag(targetTag))
{
continue;
}
Rigidbody rb = col.attachedRigidbody;
if (rb == null)
{
continue;
}
// ここから先は Explode() 内のロジックとほぼ同様に力を加える
Vector3 direction = (rb.worldCenterOfMass - center);
if (direction.sqrMagnitude <= Mathf.Epsilon)
{
direction = Random.onUnitSphere;
}
float distance = direction.magnitude;
Vector3 normalizedDirection = direction / distance;
float distanceFactor = 1f;
if (useDistanceFalloff)
{
float t = Mathf.Clamp01(distance / radius);
distanceFactor = Mathf.Clamp01(falloffCurve.Evaluate(t));
}
float finalForce = force * distanceFactor;
Vector3 finalDirection = normalizedDirection;
if (!Mathf.Approximately(upwardsModifier, 0f))
{
finalDirection += Vector3.up * upwardsModifier;
finalDirection.Normalize();
}
Vector3 forceVector = finalDirection * finalForce;
rb.AddForce(forceVector, forceMode);
}
}
このように、ExplosionForce は「爆風の物理」を担当する小さなモジュールとして設計しておくと、タグやダメージ処理などのゲーム固有ロジックを、別コンポーネントやヘルパー関数として後からいくらでも積み増しできるようになります。
巨大な God クラスを作る前に、まずはこうした小さなコンポーネントから積み上げていきましょう。
