Unityを触り始めた頃は、つい「とりあえず Update() に全部書く」スタイルになりがちですよね。HPの管理、移動、攻撃パターン、アニメーション、SE再生…すべてを1つのスクリプトに押し込んでしまうと、ボスの挙動を少し変えたいだけでも巨大なコードをスクロールして探すハメになります。
特にボス戦の「形態変化(フェーズ)」は、ロジックが複雑になりやすい代表例です。
「HPが半分を切ったら攻撃パターンを変える」「見た目も変える」「BGMも変えたい」などを1つのクラスに直書きすると、あっという間にGodクラス化してしまいます。
そこでこの記事では、「HPが半分を切ったら攻撃パターンや見た目を切り替える」部分だけに責務を絞ったコンポーネント 「BossPhase」 を作ってみましょう。
ボスの移動ロジックや攻撃ロジックは別コンポーネントに任せ、BossPhase は「いつ、どのフェーズにいるか」を決めるだけにすると、コードの見通しがぐっと良くなります。
【Unity】HPで形態変化を制御!「BossPhase」コンポーネント
以下に、Unity6(C#)で動作する BossPhase コンポーネントのフルコードを示します。
このコンポーネントは:
- HPを監視して「通常フェーズ」「怒りフェーズ(HP半分以下)」「撃破」の状態を管理
- フェーズごとに有効化する攻撃コンポーネントの切り替え
- 見た目(MeshRenderer や Animator パラメータ)の切り替え
- インスペクターで設定可能なイベント(UnityEvent)の発火
までを担当します。ボスの攻撃パターンそのものは、他のコンポーネントに分離しておきましょう。
ソースコード(フル)
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// ボスのHPに応じて「フェーズ(形態)」を管理するコンポーネント。
/// - HPが最大のとき: 通常フェーズ
/// - HPが半分以下のとき: 怒りフェーズ
/// - HPが0以下のとき: 撃破
///
/// 攻撃コンポーネントの有効/無効や、見た目の切り替えをここで行います。
/// 攻撃ロジック自体は別コンポーネントに任せることで、責務を分離します。
/// </summary>
[DisallowMultipleComponent]
public class BossPhase : MonoBehaviour
{
/// <summary>最大HP(初期HP)</summary>
[Header("HP 設定")]
[SerializeField]
private int maxHp = 100;
/// <summary>現在HP(インスペクターで確認用)</summary>
[SerializeField, Tooltip("現在HP。ゲーム開始時に maxHp で初期化されます。")]
private int currentHp;
/// <summary>怒りフェーズに入る割合(0〜1)。0.5 なら HP が半分以下で怒りフェーズ。</summary>
[SerializeField, Range(0.1f, 0.9f)]
private float rageThresholdRate = 0.5f;
/// <summary>現在のフェーズを表す列挙型</summary>
public enum Phase
{
Normal, // 通常
Rage, // 怒り(第二形態)
Dead // 撃破
}
[Header("フェーズ状態(読み取り専用)")]
[SerializeField, Tooltip("現在のフェーズ。インスペクターで確認用です。")]
private Phase currentPhase = Phase.Normal;
/// <summary>現在フェーズを外部から参照したい場合用のプロパティ(読み取り専用)</summary>
public Phase CurrentPhase => currentPhase;
[Header("フェーズごとの攻撃コンポーネント")]
[SerializeField, Tooltip("通常フェーズで有効化したいコンポーネント(例: 通常攻撃スクリプト)")]
private Behaviour[] normalPhaseBehaviours;
[SerializeField, Tooltip("怒りフェーズで有効化したいコンポーネント(例: 強化攻撃スクリプト)")]
private Behaviour[] ragePhaseBehaviours;
[Header("見た目の切り替え")]
[SerializeField, Tooltip("通常フェーズで表示したい Renderer(メッシュやスキンメッシュなど)")]
private Renderer[] normalRenderers;
[SerializeField, Tooltip("怒りフェーズで表示したい Renderer(色違いモデルなど)")]
private Renderer[] rageRenderers;
[SerializeField, Tooltip("Animator がある場合、フェーズを表す int パラメータ名(空なら無視)")]
private string phaseAnimatorParameter = "Phase";
[SerializeField, Tooltip("Animator コンポーネント(任意)。設定されていなければ自動取得を試みます。")]
private Animator animator;
[Header("イベント(インスペクターから設定可能)")]
[SerializeField, Tooltip("怒りフェーズに突入したときに呼ばれるイベント")]
private UnityEvent onEnterRagePhase;
[SerializeField, Tooltip("ボスが撃破されたときに呼ばれるイベント")]
private UnityEvent onDead;
// 内部用:怒りフェーズへ入るHP閾値
private int rageThresholdHp;
// 内部用:既に Dead フェーズへ入ったかどうか
private bool isDead = false;
private void Reset()
{
// コンポーネント追加時に、Animator を自動で探して設定しておく
animator = GetComponentInChildren<Animator>();
maxHp = Mathf.Max(1, maxHp);
currentHp = maxHp;
}
private void Awake()
{
// HP初期化
maxHp = Mathf.Max(1, maxHp);
currentHp = maxHp;
// 怒りフェーズに入るHP閾値を計算
rageThresholdHp = Mathf.FloorToInt(maxHp * rageThresholdRate);
// Animator が未設定なら自動取得
if (animator == null)
{
animator = GetComponentInChildren<Animator>();
}
// 開始時は通常フェーズとしてセットアップ
SetPhase(Phase.Normal, force: true);
}
/// <summary>
/// ダメージを与える公開メソッド。
/// 他のスクリプト(例: 弾やプレイヤー攻撃判定)がここを呼び出します。
/// マイナス値を渡すと回復として扱います。
/// </summary>
/// <param name="amount">与えるダメージ量(例: 10)。負の値で回復。</param>
public void ApplyDamage(int amount)
{
if (isDead)
{
// すでに撃破済みなら何もしない
return;
}
// HPを減算(または回復)
currentHp -= amount;
// HPを 0〜maxHp の範囲にクランプ
currentHp = Mathf.Clamp(currentHp, 0, maxHp);
// HPに応じてフェーズを更新
UpdatePhaseByHp();
}
/// <summary>
/// HPに応じてフェーズを更新する内部メソッド。
/// </summary>
private void UpdatePhaseByHp()
{
// すでに Dead フェーズなら何もしない
if (isDead)
{
return;
}
if (currentHp <= 0)
{
// 撃破
SetPhase(Phase.Dead);
return;
}
if (currentHp <= rageThresholdHp)
{
// 怒りフェーズ
SetPhase(Phase.Rage);
}
else
{
// 通常フェーズ
SetPhase(Phase.Normal);
}
}
/// <summary>
/// 実際にフェーズを切り替える処理。
/// 攻撃コンポーネントの有効/無効や、見た目の切り替えをここで行います。
/// </summary>
/// <param name="nextPhase">切り替え先のフェーズ</param>
/// <param name="force">同じフェーズでも強制的に再適用したい場合 true</param>
private void SetPhase(Phase nextPhase, bool force = false)
{
if (!force && currentPhase == nextPhase)
{
// すでに同じフェーズなら何もしない
return;
}
currentPhase = nextPhase;
// Animator パラメータの更新
UpdateAnimatorParameter();
switch (currentPhase)
{
case Phase.Normal:
isDead = false;
SetBehavioursEnabled(normalPhaseBehaviours, true);
SetBehavioursEnabled(ragePhaseBehaviours, false);
SetRenderersEnabled(normalRenderers, true);
SetRenderersEnabled(rageRenderers, false);
break;
case Phase.Rage:
isDead = false;
SetBehavioursEnabled(normalPhaseBehaviours, false);
SetBehavioursEnabled(ragePhaseBehaviours, true);
SetRenderersEnabled(normalRenderers, false);
SetRenderersEnabled(rageRenderers, true);
// 怒りフェーズ突入イベント
onEnterRagePhase?.Invoke();
break;
case Phase.Dead:
isDead = true;
// すべての攻撃コンポーネントを無効化
SetBehavioursEnabled(normalPhaseBehaviours, false);
SetBehavioursEnabled(ragePhaseBehaviours, false);
// 見た目は好みで。ここでは両方有効のままにしておく
// SetRenderersEnabled(normalRenderers, false);
// SetRenderersEnabled(rageRenderers, false);
// 撃破イベント
onDead?.Invoke();
break;
}
}
/// <summary>
/// Animator にフェーズを表す int パラメータを渡す。
/// パラメータ名が空、または Animator が未設定なら何もしない。
/// </summary>
private void UpdateAnimatorParameter()
{
if (animator == null) return;
if (string.IsNullOrEmpty(phaseAnimatorParameter)) return;
// Phase の enum を int にキャストして渡す
animator.SetInteger(phaseAnimatorParameter, (int)currentPhase);
}
/// <summary>
/// Behaviour(MonoBehaviour, NavMeshAgent など)配列の有効/無効を一括で切り替える。
/// </summary>
private void SetBehavioursEnabled(Behaviour[] behaviours, bool enabled)
{
if (behaviours == null) return;
foreach (var behaviour in behaviours)
{
if (behaviour == null) continue;
behaviour.enabled = enabled;
}
}
/// <summary>
/// Renderer 配列の表示/非表示を一括で切り替える。
/// </summary>
private void SetRenderersEnabled(Renderer[] renderers, bool enabled)
{
if (renderers == null) return;
foreach (var r in renderers)
{
if (r == null) continue;
r.enabled = enabled;
}
}
// --- デバッグ用:インスペクターから手動でダメージを与えるためのヘルパー ---
#if UNITY_EDITOR
[Header("デバッグ用(エディタ限定)")]
[SerializeField, Tooltip("エディタ上からテストダメージを与えるための値")]
private int debugDamage = 10;
[ContextMenu("Debug: Apply Damage")]
private void DebugApplyDamage()
{
ApplyDamage(debugDamage);
Debug.Log($"[BossPhase] Debug ダメージ {debugDamage} を適用。現在HP: {currentHp}, フェーズ: {currentPhase}");
}
#endif
}
使い方の手順
-
ボス用のプレハブを用意する
例として「巨大なドラゴンボス」を想定します。
プレハブ構成例:- 親オブジェクト:
DragonBoss - 子オブジェクト:
Model_Normal(通常見た目) - 子オブジェクト:
Model_Rage(怒りフェーズ見た目、色違いなど) - 子オブジェクト:
Attack_Normal(通常攻撃スクリプト付き) - 子オブジェクト:
Attack_Rage(怒り攻撃スクリプト付き)
- 親オブジェクト:
-
「BossPhase」コンポーネントを追加して設定する
DragonBossの親オブジェクトにBossPhaseをアタッチします。- HP 設定で
maxHpをボスの耐久力に合わせて設定(例: 500)。 - rageThresholdRate を
0.5にして「HP半分以下で怒りフェーズ」にします。 - フェーズごとの攻撃コンポーネント:
normalPhaseBehavioursにAttack_Normalのスクリプトをドラッグ&ドロップ。ragePhaseBehavioursにAttack_Rageのスクリプトをドラッグ&ドロップ。
- 見た目の切り替え:
normalRenderersにModel_NormalのMeshRendererを登録。rageRenderersにModel_RageのMeshRendererを登録。
- Animator を使っている場合は:
animatorにドラゴンのAnimatorを設定。- Animator Controller に
int Phaseパラメータを作成し、ステート遷移条件に使う。 phaseAnimatorParameterを"Phase"に設定。
-
ダメージを与える側から「ApplyDamage」を呼ぶ
例として、プレイヤーの弾がボスに当たったときにダメージを与えるコンポーネントを用意します。using UnityEngine; /// <summary> /// プレイヤーの弾。ボスに当たったらダメージを与える簡易例。 /// </summary> [RequireComponent(typeof(Collider))] public class PlayerBullet : MonoBehaviour { [SerializeField] private int damage = 10; private void OnTriggerEnter(Collider other) { // BossPhase を持っているオブジェクトに当たったらダメージを与える var bossPhase = other.GetComponentInParent<BossPhase>(); if (bossPhase != null) { bossPhase.ApplyDamage(damage); // 弾を破壊 Destroy(gameObject); } } }このように、「HPを減らす」ことだけをBossPhaseに任せることで、弾側のスクリプトはシンプルに保てます。
-
イベントを使って演出を追加する
- インスペクターの
On Enter Rage Phase(怒りフェーズ突入)イベントに:- 画面エフェクト用のコンポーネント(例: カメラシェイク、画面フラッシュ)
- BGMを切り替えるスクリプト
を登録しておくと、コードを書かずに演出を追加できます。
On Deadに「ドロップアイテム生成」「次のシーンに遷移」などを登録しておくと、撃破時の処理を柔軟に組めます。
- インスペクターの
メリットと応用
BossPhase を導入することで、以下のようなメリットがあります。
- 責務が明確:HPとフェーズ管理だけを担当するので、クラスが肥大化しません。
- プレハブの再利用性アップ:ボスごとに HP や閾値、攻撃コンポーネント、見た目を差し替えるだけで、別のボスを簡単に作れます。
- レベルデザインが楽になる:UnityEvent によって、フェーズ移行タイミングで演出を差し込めるため、スクリプトを書き換えずにゲームデザイナーが調整できます。
- テストしやすい:コンテキストメニュー(
Debug: Apply Damage)でエディタ上から簡単にダメージを与えられるので、「怒りフェーズのテスト」「撃破演出のテスト」が素早く行えます。
応用として、フェーズを2段階以上に増やしたり、時間経過でもフェーズが変化するようにしたりといった拡張も簡単に行えます。
例えば「一定時間ごとに自動で回復する」改造を入れて、放置するとボスが回復するギミックを作ることもできます。
以下は、そのような回復ギミックを追加する例のスニペットです(BossPhase クラスに追記する形)。
/// <summary>
/// 一定間隔で自動回復させる簡易例。
/// 例えば「プレイヤーが離れているとボスが回復する」などのギミックに使えます。
/// </summary>
private void HealOverTime(float interval, int healAmountPerTick)
{
// コルーチンで実装してもよいですが、
// シンプルな例として InvokeRepeating を使います。
CancelInvoke(nameof(HealTick));
_healAmountPerTick = healAmountPerTick;
InvokeRepeating(nameof(HealTick), interval, interval);
}
private int _healAmountPerTick;
private void HealTick()
{
if (isDead) return;
// ApplyDamage に負の値を渡すことで回復として扱う
ApplyDamage(-_healAmountPerTick);
}
このように、「HPとフェーズの管理」コンポーネントとして BossPhase を分離しておくと、後からの改造やギミック追加がとてもやりやすくなります。
巨大なボス用スクリプトを1つ作るのではなく、小さな責務ごとにコンポーネントを分けていく設計を意識してみてください。
