Unityを触り始めると、つい何でもかんでも Update() に書きがちですよね。
「プレイヤーとの距離を毎フレーム計算して、近づいたら処理を有効化する」みたいなロジックも、巨大なプレイヤースクリプトやゲームマネージャーに全部押し込んでしまうと、すぐにカオスになります。

そうなると…

  • どのオブジェクトがどの条件で動き出すのかが分かりづらい
  • Prefab を複製しても、シーンごとに微妙な差分が出てしまう
  • パフォーマンス改善のための「距離による有効/無効化」が後付けしづらい

そこでこの記事では、「プレイヤーが一定距離に近づくまで、親の処理や物理演算を無効化しておく」という責務だけを持つコンポーネント
DistanceActivator を作って、オブジェクトごとにポン付けで制御できる形にしてみましょう。

【Unity】近づいたら起動する省エネギミック!「DistanceActivator」コンポーネント

DistanceActivator は、ざっくり言うとこんなことをやります。

  • シーン開始時に「親オブジェクトの挙動」を止めておく(BehaviourRigidbody の無効化)
  • プレイヤーとの距離を監視し、閾値以内に入ったらまとめて有効化
  • オプションで「離れたらまた無効化」することも可能

プレイヤー、敵、トラップ、動く床など、「プレイヤーが近づくまで眠らせておきたいオブジェクト」にアタッチするだけで使えるようにしていきます。


フルコード:DistanceActivator.cs


using System.Collections.Generic;
using UnityEngine;

namespace Samples
{
    /// <summary>
    /// プレイヤーとの距離に応じて、親オブジェクトのコンポーネントや物理を有効/無効化するコンポーネント。
    /// 
    /// ・Updateで距離チェックのみを行い、実際の挙動は他のコンポーネントに任せる方針。
    /// ・「近づいたら起動」「離れたら停止」のような省エネ挙動を、Prefab単位で簡単に設定できる。
    /// </summary>
    public class DistanceActivator : MonoBehaviour
    {
        // --- 参照設定 ---

        [Header("ターゲット設定")]
        [Tooltip("距離判定の基準となるプレイヤーTransform。\n未指定の場合は、Playerタグ付きオブジェクトを自動検索します。")]
        [SerializeField] private Transform targetPlayer;

        [Header("距離設定")]
        [Tooltip("この距離以内にプレイヤーが入ったら有効化します。")]
        [SerializeField] private float activateDistance = 20f;

        [Tooltip("この距離より外にプレイヤーが離れたら無効化します。\n0以下の場合は「一度有効化したら無効化しない」モードになります。")]
        [SerializeField] private float deactivateDistance = 0f;

        [Header("チェック頻度")]
        [Tooltip("距離チェックを行う間隔(秒)。\n0にすると毎フレームチェックします。")]
        [SerializeField] private float checkInterval = 0.2f;

        [Header("対象オブジェクト")]
        [Tooltip("有効/無効を切り替えたいルートオブジェクト。\n未設定の場合は、このコンポーネントが付いているGameObjectが対象になります。")]
        [SerializeField] private GameObject targetRoot;

        [Header("制御する要素")]
        [Tooltip("Behaviour(MonoBehaviour, Animator, Colliderなど)をまとめて有効/無効化します。")]
        [SerializeField] private bool controlBehaviours = true;

        [Tooltip("Rigidbody / Rigidbody2D の simulation を有効/無効化します。")]
        [SerializeField] private bool controlRigidbody = true;

        [Tooltip("Renderer(メッシュ/スプライト)の表示をON/OFFします。")]
        [SerializeField] private bool controlRenderers = false;

        [Header("その他オプション")]
        [Tooltip("シーン開始時に自動で「無効状態」から始めるかどうか。")]
        [SerializeField] private bool startDisabled = true;

        [Tooltip("有効化/無効化時にログを出すかどうか。(デバッグ用)")]
        [SerializeField] private bool debugLog = false;


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

        // 管理対象のBehaviour(MonoBehaviour, Collider, Animatorなどを含む)
        private readonly List<Behaviour> _behaviours = new List<Behaviour>();

        // 管理対象のRigidbody / Rigidbody2D
        private readonly List<Rigidbody> _rigidbodies = new List<Rigidbody>();
        private readonly List<Rigidbody2D> _rigidbodies2D = new List<Rigidbody2D>();

        // 管理対象のRenderer
        private readonly List<Renderer> _renderers = new List<Renderer>();

        // 有効状態フラグ
        private bool _isActivated = false;

        // チェック用タイマー
        private float _checkTimer = 0f;


        private void Awake()
        {
            // targetRoot が未設定なら、自分自身を対象にする
            if (targetRoot == null)
            {
                targetRoot = gameObject;
            }

            // 管理対象コンポーネントを収集
            CacheComponents();
        }

        private void Start()
        {
            // プレイヤー参照が未設定なら、タグ検索してみる
            if (targetPlayer == null)
            {
                var playerObj = GameObject.FindGameObjectWithTag("Player");
                if (playerObj != null)
                {
                    targetPlayer = playerObj.transform;
                }
                else
                {
                    if (debugLog)
                    {
                        Debug.LogWarning($"[DistanceActivator] Playerタグのオブジェクトが見つかりませんでした。({name})");
                    }
                }
            }

            // 開始時に無効状態から始める設定なら、一度強制的に無効化
            if (startDisabled)
            {
                SetActivated(false, force: true);
            }
            else
            {
                // 現在の状態を初期値として扱う
                _isActivated = true;
            }
        }

        private void Update()
        {
            if (targetPlayer == null)
            {
                // プレイヤーが見つからない場合は何もしない
                return;
            }

            // チェック間隔が0の場合は毎フレーム
            if (checkInterval > 0f)
            {
                _checkTimer -= Time.deltaTime;
                if (_checkTimer > 0f)
                {
                    return;
                }
                _checkTimer = checkInterval;
            }

            float sqrDistance = (targetPlayer.position - transform.position).sqrMagnitude;
            float activateSqr = activateDistance * activateDistance;

            // 一度有効化したら無効化しないモード
            bool canDeactivate = deactivateDistance > 0f;
            float deactivateSqr = deactivateDistance * deactivateDistance;

            if (!_isActivated)
            {
                // まだ無効状態:距離が十分近づいたら有効化
                if (sqrDistance <= activateSqr)
                {
                    SetActivated(true);
                }
            }
            else
            {
                // すでに有効状態:オプションで離れたら無効化
                if (canDeactivate && sqrDistance >= deactivateSqr)
                {
                    SetActivated(false);
                }
            }
        }

        /// <summary>
        /// 管理対象コンポーネントをキャッシュする。
        /// Awakeで一度だけ呼び出すことを想定。
        /// </summary>
        private void CacheComponents()
        {
            _behaviours.Clear();
            _rigidbodies.Clear();
            _rigidbodies2D.Clear();
            _renderers.Clear();

            if (targetRoot == null) return;

            // 子階層も含めて全てのコンポーネントを取得
            var behaviours = targetRoot.GetComponentsInChildren<Behaviour>(includeInactive: true);
            var rigidbodies = targetRoot.GetComponentsInChildren<Rigidbody>(includeInactive: true);
            var rigidbodies2D = targetRoot.GetComponentsInChildren<Rigidbody2D>(includeInactive: true);
            var renderers = targetRoot.GetComponentsInChildren<Renderer>(includeInactive: true);

            // 自分自身(DistanceActivator)は制御対象から除外する
            foreach (var b in behaviours)
            {
                if (b == this) continue;
                _behaviours.Add(b);
            }

            _rigidbodies.AddRange(rigidbodies);
            _rigidbodies2D.AddRange(rigidbodies2D);
            _renderers.AddRange(renderers);
        }

        /// <summary>
        /// 有効/無効状態を切り替えるメイン処理。
        /// &lt/summary>
        /// <param name="active">trueで有効化、falseで無効化</param>
        /// <param name="force">現在と同じ状態でも強制的に適用したい場合はtrue</param>
        private void SetActivated(bool active, bool force = false)
        {
            if (!force && _isActivated == active)
            {
                // 状態が変わらないなら何もしない
                return;
            }

            _isActivated = active;

            if (controlBehaviours)
            {
                foreach (var behaviour in _behaviours)
                {
                    if (behaviour == null) continue;
                    behaviour.enabled = active;
                }
            }

            if (controlRigidbody)
            {
                foreach (var rb in _rigidbodies)
                {
                    if (rb == null) continue;
                    // isKinematic ではなく simulation を止めることで、元の設定を壊さない
                    rb.simulated = active;
                }

                foreach (var rb2d in _rigidbodies2D)
                {
                    if (rb2d == null) continue;
                    rb2d.simulated = active;
                }
            }

            if (controlRenderers)
            {
                foreach (var renderer in _renderers)
                {
                    if (renderer == null) continue;
                    renderer.enabled = active;
                }
            }

            if (debugLog)
            {
                Debug.Log($"[DistanceActivator] {(active ? "Activated" : "Deactivated")} : {targetRoot.name}", this);
            }
        }

#if UNITY_EDITOR
        private void OnValidate()
        {
            // エディタ上で値を変更した時にも、最低限の整合性を保つ
            if (activateDistance < 0f) activateDistance = 0f;
            if (deactivateDistance < 0f) deactivateDistance = 0f;

            // 非アクティブでも動くように、Awakeと同等のキャッシュを再実行
            if (targetRoot == null)
            {
                targetRoot = gameObject;
            }

            CacheComponents();
        }
#endif
    }
}

使い方の手順

ここでは、プレイヤーが近づくと動き出す敵近づくと動き出す動く床 を例に、実際の使い方を見ていきます。

手順① スクリプトをプロジェクトに追加する

  1. DistanceActivator.cs という名前のC#スクリプトを作成します。
  2. 上記のコードを丸ごとコピペして保存します。

手順② 対象オブジェクトにアタッチする

  • 敵キャラのPrefab(例: EnemyGoblin)を開く
  • ルートのGameObject(または任意の親)に DistanceActivator を追加
  • targetRoot を空欄のままにすると、そのオブジェクト自身と子階層が対象になります

もし 「敵の見た目は常に表示したいけど、AIだけ止めておきたい」 などの場合は、
AI用の親オブジェクトを作って、そこに DistanceActivator を付けると制御しやすいです。

手順③ インスペクターでパラメータを設定する

  • targetPlayer
    • 通常は空欄でOK(Player タグ付きオブジェクトを自動検索)
    • マルチプレイなどで明示的に指定したい場合は、プレイヤーの Transform を割り当てる
  • activateDistance
    • プレイヤーがこの距離以内に入ったら有効化されます
    • 例: 敵なら 25〜40、動く床なら 15〜25 くらいから調整すると良いです
  • deactivateDistance
    • 0以下: 一度有効化したら、そのまま最後まで動き続ける
    • 正の値: その距離より外に出たら 再び無効化(ループする仕掛けなどに便利)
    • 例: activateDistance = 30, deactivateDistance = 40 のように少し広めに取るとヒステリシスが効いて安定します
  • checkInterval
    • 0: 毎フレーム距離チェック(精度重視・やや重め)
    • 0.1〜0.5: 多くのケースではこれで十分。パフォーマンスにも優しいです
  • controlBehaviours / controlRigidbody / controlRenderers
    • 「何を止めたいか」に応じてON/OFFを切り替えます
    • 敵AIだけ止めたいなら controlBehaviours = true、物理だけ止めたいなら controlRigidbody = true など
  • startDisabled
    • ON: シーン開始時は完全に眠った状態からスタート
    • OFF: 現在の有効状態を維持(テスト中に一時的にOFFにすると挙動確認が楽)

手順④ 具体的な使用例

例1:プレイヤーが近づくと動き出す敵AI
  1. 敵のルートオブジェクトに、EnemyAI, NavMeshAgent, Animator などが付いているとします。
  2. 同じオブジェクトに DistanceActivator を追加します。
  3. 設定例:
    • activateDistance = 30
    • deactivateDistance = 0(一度起動したら止めない)
    • controlBehaviours = true, controlRigidbody = false, controlRenderers = false
    • startDisabled = true
  4. これで、プレイヤーが30m以内に近づくまで、敵のAIやAnimatorなどが一切動かない状態になります。
例2:近づくと動き出す「動く床」
  1. 動く床用のスクリプト MovingPlatform を持ったオブジェクトを用意します。
  2. そのオブジェクトに Rigidbody または Rigidbody2D が付いているとします。
  3. 同じオブジェクトに DistanceActivator を追加し、設定例:
    • activateDistance = 20
    • deactivateDistance = 30(プレイヤーが遠ざかったらまた停止させる)
    • controlBehaviours = true, controlRigidbody = true
    • startDisabled = true
  4. これで、プレイヤーが近くにいるときだけ床が動き、遠くに行くとまた止まる「省エネ床」になります。
例3:遠くのギミックを丸ごと眠らせておく

大きなステージで、遠くのトラップや環境ギミックが大量にあるとき、
それぞれのルートオブジェクトに DistanceActivator を付けておくと、プレイヤーが近づいたエリアだけ が動作するようにできます。

  • 「崩れる足場」「回転するトゲ」「炎の噴き出し」など、1エリアごとにまとめて親オブジェクトを作る
  • その親に DistanceActivator を付けて、子の挙動を全部まとめてON/OFF
  • Prefab化しておけば、レベルデザイン時にポンポン配置するだけで自動的に省エネ化されたギミックが並びます

メリットと応用

メリット1:Update地獄からの脱出

DistanceActivator の責務は「プレイヤーとの距離に応じてON/OFFすること」だけです。
実際の挙動(敵AI、動く床、トラップの制御など)は、それぞれ専用のコンポーネントに任せます。

これにより、

  • 巨大なプレイヤースクリプトやゲームマネージャーに「敵の起動条件」ロジックを書かずに済む
  • 各Prefabが「自分で自分をいつ起動するか」を完結に表現できる
  • 後から「この敵だけ起動距離を変えたい」といった調整がインスペクターで完結する

メリット2:Prefabベースのレベルデザインが楽になる

DistanceActivator を含んだPrefabを作っておけば、

  • シーンにポンと置くだけで「近づいたら動き出すオブジェクト」になる
  • 起動距離やチェック間隔を個別に調整して、重いシーンでもパフォーマンスを確保しやすい
  • 「このエリアの敵は遠くからでも動いていてほしい」など、例外的な挙動もPrefab単位で差し替えられる

レベルデザイナーが「DistanceActivator付きPrefab」をカタログ的に使えるようにしておくと、
エンジニアが逐一スクリプトを書き足さなくても、ゲーム全体の省エネ設計 を維持しやすくなります。

メリット3:将来的な拡張ポイントが明確

責務がはっきり分かれているので、将来「距離だけでなく視界チェックもしたい」「音を聞きつけて起動したい」といった要望が出ても、
DistanceActivator をベースにした別コンポーネントを作るだけで対応しやすくなります。


改造案:有効化時に一度だけイベントを飛ばす

「起動した瞬間にだけエフェクトを出したい」「サウンドを鳴らしたい」といったケース向けに、
SetActivated の中から呼び出すフック関数を追加するのもアリですね。


/// <summary>
/// 有効化された瞬間に一度だけ呼ばれるフック。
/// ここでSE再生やパーティクル再生などを行う。
/// </summary>
private void OnActivatedOnce()
{
    // 例:アタッチされているAudioSourceでSEを鳴らす
    var audio = GetComponent<AudioSource>();
    if (audio != null)
    {
        audio.Play();
    }

    // 例:パーティクルを再生
    var particle = GetComponentInChildren<ParticleSystem>();
    if (particle != null)
    {
        particle.Play();
    }
}

そして SetActivated(true) に切り替わった瞬間だけ OnActivatedOnce() を呼ぶようにすれば、
「起動エフェクト付きのDistanceActivator」に簡単に発展させられます。

このように、小さな責務のコンポーネントを積み重ねていくことで、
Godクラスに頼らない、拡張しやすいUnityプロジェクトを育てていきましょう。