Unityを触り始めた頃によくあるのが、Update() の中に「移動処理」「攻撃処理」「ダメージ処理」「状態管理」などを全部詰め込んでしまう書き方ですね。最初は動くので気持ちいいのですが、スタンやノックバック、スロー状態などの「一時的な状態異常」を入れ始めると一気にコードがカオスになりがちです。

たとえば、

  • if (isStunned) { ... } があちこちに増える
  • 物理挙動を止めたいけど、移動ロジックと密結合していて止めづらい
  • 見た目のエフェクト(頭上のピヨりマーク)も同じクラスに書いてしまう

こうなると、ちょっとした仕様変更でも既存の巨大スクリプトを壊しがちです。

そこでこの記事では、「スタン状態」という機能だけに責任を絞ったコンポーネント StunEffect を作って、

  • 親オブジェクトの物理挙動(Physics)を一時停止する
  • スタン中だけ頭上にピヨりマークを表示する

という処理を、シンプルに再利用可能な形で実装していきます。

【Unity】スタン状態を丸ごとカプセル化!「StunEffect」コンポーネント

以下が、Unity6(C#)でそのまま使える StunEffect コンポーネントの完全版コードです。
Rigidbody で動くプレイヤーや敵にアタッチして使う想定になっています。


using System.Collections;
using UnityEngine;

/// <summary>親オブジェクトにスタン効果を与えるコンポーネント</summary>
[RequireComponent(typeof(Transform))]
public class StunEffect : MonoBehaviour
{
    // --- 参照設定 ---

    [Header("スタン対象の Rigidbody")]
    [Tooltip("スタン中に物理挙動を停止する対象。未指定なら親階層から自動取得を試みます。")]
    [SerializeField] private Rigidbody _targetRigidbody;

    [Header("ピヨりマークのプレハブ")]
    [Tooltip("スタン中に頭上に表示するピヨりマークのPrefab。")]
    [SerializeField] private GameObject _stunIconPrefab;

    [Header("ピヨりマークのオフセット")]
    [Tooltip("ターゲットの位置からどれだけ上にピヨりマークを表示するか。")]
    [SerializeField] private Vector3 _iconOffset = new Vector3(0f, 2f, 0f);

    [Header("スタン設定")]
    [Tooltip("スタン中にRigidbodyの速度をゼロにするかどうか。")]
    [SerializeField] private bool _zeroVelocityOnStun = true;

    [Tooltip("スタン中にRigidbodyを完全に停止(isKinematic = true)するかどうか。")]
    [SerializeField] private bool _setKinematicOnStun = true;

    // --- 内部状態 ---

    /// <summary>現在スタン中かどうか</summary>
    public bool IsStunned => _isStunned;
    private bool _isStunned;

    // 生成されたピヨりマークのインスタンス
    private GameObject _spawnedIcon;

    // Rigidbody の元の状態を保存しておく
    private bool _originalIsKinematic;
    private RigidbodyConstraints _originalConstraints;

    private Coroutine _stunCoroutine;

    private void Awake()
    {
        // Rigidbody が未指定なら、親階層を含めて自動取得
        if (_targetRigidbody == null)
        {
            _targetRigidbody = GetComponentInParent<Rigidbody>();
        }

        if (_targetRigidbody == null)
        {
            Debug.LogWarning(
                $"[StunEffect] Rigidbody が見つかりませんでした。物理停止は行われません。" +
                $" ゲームオブジェクト: {gameObject.name}"
            );
        }
    }

    /// <summary>
    /// 外部から呼び出してスタンを開始する。
    /// すでにスタン中の場合は、スタン時間を延長する。
    /// </summary>
    /// <param name="duration">スタン継続時間(秒)</param>
    public void ApplyStun(float duration)
    {
        if (duration <= 0f)
        {
            // 0秒以下なら何もしない
            return;
        }

        // すでにスタン中なら、コルーチンをリスタートして時間を延長
        if (_stunCoroutine != null)
        {
            StopCoroutine(_stunCoroutine);
        }

        _stunCoroutine = StartCoroutine(StunRoutine(duration));
    }

    /// <summary>
    /// 強制的にスタンを解除する(外部からの手動解除用)。
    /// </summary>
    public void ForceEndStun()
    {
        if (_stunCoroutine != null)
        {
            StopCoroutine(_stunCoroutine);
            _stunCoroutine = null;
        }

        if (_isStunned)
        {
            EndStunInternal();
        }
    }

    /// <summary>
    /// スタン処理本体のコルーチン。
    /// </summary>
    private IEnumerator StunRoutine(float duration)
    {
        BeginStunInternal();

        float timer = 0f;
        while (timer < duration)
        {
            timer += Time.deltaTime;
            // ピヨりマークをターゲットの頭上に追従させる
            UpdateIconPosition();
            yield return null;
        }

        EndStunInternal();
        _stunCoroutine = null;
    }

    /// <summary>
    /// スタン開始時の内部処理。
    /// </summary>
    private void BeginStunInternal()
    {
        if (_isStunned)
        {
            // 念のため二重適用を防ぐ
            return;
        }

        _isStunned = true;

        // Rigidbody の状態を保存してから変更
        if (_targetRigidbody != null)
        {
            _originalIsKinematic = _targetRigidbody.isKinematic;
            _originalConstraints = _targetRigidbody.constraints;

            if (_zeroVelocityOnStun)
            {
                _targetRigidbody.velocity = Vector3.zero;
                _targetRigidbody.angularVelocity = Vector3.zero;
            }

            if (_setKinematicOnStun)
            {
                _targetRigidbody.isKinematic = true;
            }
            else
            {
                // 位置を固定したい場合は、Constraints を使う案もある
                // ここでは例として、Y回転だけ許可するなども検討可能
                // 今回は変更しないので何もしない
            }
        }

        // ピヨりマークを生成
        SpawnIcon();
    }

    /// <summary>
    /// スタン終了時の内部処理。
    /// </summary>
    private void EndStunInternal()
    {
        if (!_isStunned)
        {
            return;
        }

        _isStunned = false;

        // Rigidbody の状態を元に戻す
        if (_targetRigidbody != null)
        {
            _targetRigidbody.isKinematic = _originalIsKinematic;
            _targetRigidbody.constraints = _originalConstraints;
        }

        // ピヨりマークを削除
        DespawnIcon();
    }

    /// <summary>
    /// ピヨりマークを生成する。
    /// </summary>
    private void SpawnIcon()
    {
        if (_stunIconPrefab == null)
        {
            // プレハブが設定されていない場合は、警告だけ出して処理続行
            Debug.LogWarning(
                $"[StunEffect] StunIconPrefab が設定されていません。ピヨりマークは表示されません。" +
                $" ゲームオブジェクト: {gameObject.name}"
            );
            return;
        }

        if (_spawnedIcon != null)
        {
            // すでに生成されている場合は一度消して作り直す
            Destroy(_spawnedIcon);
        }

        // ターゲットの Transform(Rigidbody があればそれ、なければこのコンポーネントの Transform)
        Transform targetTransform = (_targetRigidbody != null)
            ? _targetRigidbody.transform
            : transform;

        Vector3 spawnPos = targetTransform.position + _iconOffset;
        _spawnedIcon = Instantiate(_stunIconPrefab, spawnPos, Quaternion.identity);

        // ターゲットの子にしておくと、移動に追従してくれる
        _spawnedIcon.transform.SetParent(targetTransform, worldPositionStays: true);
    }

    /// <summary>
    /// ピヨりマークを削除する。
    /// </summary>
    private void DespawnIcon()
    {
        if (_spawnedIcon != null)
        {
            Destroy(_spawnedIcon);
            _spawnedIcon = null;
        }
    }

    /// <summary>
    /// ピヨりマークの位置を更新する。
    /// 親に追従させたいけど、オフセットを調整したい場合に使う。
    /// </summary>
    private void UpdateIconPosition()
    {
        if (_spawnedIcon == null)
        {
            return;
        }

        Transform targetTransform = (_targetRigidbody != null)
            ? _targetRigidbody.transform
            : transform;

        _spawnedIcon.transform.position = targetTransform.position + _iconOffset;
    }

    private void OnDisable()
    {
        // オブジェクトが無効化されたときは、スタン状態をリセットしておく
        if (_isStunned)
        {
            ForceEndStun();
        }
    }
}

使い方の手順

ここからは、プレイヤーや敵キャラに実際にスタン効果を付ける手順を見ていきましょう。

① ピヨりマーク用のプレハブを用意する

  • Hierarchy で右クリック → Create Empty などで空の GameObject を作成
  • Sprite Renderer や 3D モデル、UI Image など、好きな見た目の「ピヨりマーク」を配置
  • 完成したら、Project ビューにドラッグ&ドロップして Prefab 化(例: StunIcon.prefab

② プレイヤー or 敵オブジェクトに Rigidbody を設定

  • プレイヤー(例: Player)や敵(例: Enemy)の GameObject を選択
  • Add Component から Rigidbody を追加
  • すでに移動スクリプトなどで Rigidbody を使っている場合は、そのままでOKです

③ StunEffect コンポーネントをアタッチして設定

  1. 同じくプレイヤー or 敵の GameObject に StunEffectAdd Component で追加
  2. Target Rigidbody は空欄でも、親階層から自動取得されます(明示的に指定してもOK)
  3. Stun Icon Prefab に、先ほど作った StunIcon.prefab をドラッグ&ドロップ
  4. Icon Offset を調整して、頭上にちょうどよく表示される位置にします(例: (0, 2, 0)

④ 攻撃などからスタンを発動させる

最後に、攻撃スクリプトやトラップスクリプトから ApplyStun を呼び出します。
例として、「敵の攻撃がプレイヤーに当たったら 2 秒間スタンさせる」コードを示します。


using UnityEngine;

public class EnemyAttack : MonoBehaviour
{
    [SerializeField] private float _stunDuration = 2f;

    private void OnCollisionEnter(Collision collision)
    {
        // 衝突した相手に StunEffect が付いていればスタンさせる
        StunEffect stun = collision.gameObject.GetComponent<StunEffect>();
        if (stun != null)
        {
            stun.ApplyStun(_stunDuration);
        }
    }
}

このようにしておくと、

  • プレイヤーに付けた StunEffect がスタン処理を担当
  • 敵の攻撃側は「スタン時間を渡して呼ぶだけ」

という、責務が分離された状態になります。

他にも、

  • 動く床に StunEffect を付けて、特定のトリガーに入ったら床を一時停止する
  • ボスのギミックで、一定HP以下になったらボス自身をスタンさせる

といった使い方も簡単です。

メリットと応用

StunEffect をコンポーネントとして切り出すことで、次のようなメリットがあります。

  • プレハブ単位で「スタン機能付き」を管理できる
    プレイヤーや敵の Prefab に StunEffect を付けておけば、「この敵はスタンする」「この敵はスタンしない」をインスペクター上の有無で切り替えできます。
  • レベルデザインが楽になる
    シーン上に配置したオブジェクト(敵、トラップ、ギミックなど)に StunEffect を付けておけば、
    攻撃側は「GetComponent<StunEffect>() があればスタンさせる」という汎用処理で済みます。
    つまり、攻撃側は相手の具体的なクラスを知らなくてよくなるので、シーンを組み替えても壊れにくくなります。
  • 見た目とロジックの分離
    ピヨりマークのプレハブを差し替えるだけで、エフェクトの見た目を変えられます。
    スクリプト側は「スタン中は頭上に何かを表示する」という責務だけを持ち、
    実際に何を表示するかは Prefab 側に委ねる設計になっています。
  • 状態異常ごとのコンポーネント化への足がかり
    スタン以外にも、スロー、毒、バリアなどをそれぞれ別コンポーネントとして実装するスタイルに発展させやすくなります。

最後に、簡単な「改造案」として、スタン終了時にコールバックを飛ばす処理を追加する例を示します。
これを使えば、「スタンが解けたら自動で反撃モーションに入る」などの演出がしやすくなります。


using System;
using UnityEngine;

public class StunEffectWithCallback : StunEffect
{
    // スタン終了時に通知するイベント
    public event Action OnStunEnded;

    // StunEffect 側の EndStunInternal をフックしたい場合の例
    // (実際には StunEffect 本体に virtual メソッドを用意しておくとより綺麗です)
    private void OnDisable()
    {
        // 無効化時にスタンが終わったとみなして通知
        OnStunEnded?.Invoke();
    }
}

実運用では、StunEffect 本体に public event Action OnStunEnded; を追加し、
EndStunInternal() の最後で OnStunEnded?.Invoke(); を呼び出す形にすると、
「スタン終了時にだけ何かしたい」コンポーネントを別途生やせるようになります。

このように、スタンという一つの機能をコンポーネントとして切り出しておくと、
後からの仕様変更や演出追加にも強いプロジェクト構成になります。ぜひ自分のプロジェクト用にカスタマイズしてみてください。