Unityでゲームを作り始めると、つい何でもかんでも Update() に書いてしまいがちですよね。
プレイヤーの移動、カメラ制御、アイテム取得、スコア管理…すべて1つの巨大スクリプトに詰め込んでしまうと、以下のような問題が出てきます。

  • 処理の責務がごちゃ混ぜになり、バグの原因箇所が追いづらい
  • 別のシーン・別のプロジェクトで再利用しづらい
  • ちょっとした仕様変更でも、巨大クラス全体を読み解く必要がある

そこでおすすめなのが、「1つのコンポーネント = 1つの責務」に分割する設計です。
今回紹介する 「MagnetAttractor」コンポーネント は、

  • 「一定距離内にあるアイテム(Rigidbody)を、親オブジェクトへ引き寄せる」

という単一の役割だけを持つ、小さなコンポーネントです。
プレイヤーにアタッチすれば「マグネットコイン取得」、敵にアタッチすれば「周囲の弾を吸収するシールド」、動く床にアタッチすれば「乗っているアイテムを中央に寄せる」など、いろいろ応用できます。

【Unity】近くのアイテムをスイーッと吸い寄せる!「MagnetAttractor」コンポーネント

以下が、Unity6(C#)で動作するフルコードです。
「親の周囲にあるアイテム(Rigidbody)」を検知し、親の位置に向かって力を加えて吸い寄せます。


using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 一定半径内の Rigidbody を親オブジェクトに向かって引き寄せるコンポーネント。
/// 「マグネットコイン」や「アイテム吸引フィールド」のような表現に使えます。
/// </summary>
[DisallowMultipleComponent]
public class MagnetAttractor : MonoBehaviour
{
    // 吸引する対象を絞り込むためのレイヤーマスク
    // 例: "Item" レイヤーだけを対象にする
    [SerializeField]
    private LayerMask targetLayerMask = ~0; // デフォルトは全レイヤー

    // 吸引を行う半径
    [SerializeField]
    [Min(0f)]
    private float attractionRadius = 5f;

    // 吸引の強さ(力のスケール)
    [SerializeField]
    [Min(0f)]
    private float attractionForce = 10f;

    // 距離に応じて力を減衰させるかどうか
    [SerializeField]
    private bool useDistanceFalloff = true;

    // 距離0に近づいたときに無限大の力にならないようにするためのクランプ値
    [SerializeField]
    [Min(0.01f)]
    private float minDistance = 0.1f;

    // 1フレームにどれくらいの回数、物理演算に力を加えるか
    // FixedUpdate() 内で使用するので、Time.fixedDeltaTime と組み合わせて調整可能
    [SerializeField]
    private ForceMode forceMode = ForceMode.Acceleration;

    // デバッグ用: シーンビューに吸引範囲を表示するか
    [SerializeField]
    private bool drawGizmos = true;

    // デバッグ用: 吸引範囲の色
    [SerializeField]
    private Color gizmoColor = new Color(0.3f, 0.8f, 1f, 0.25f);

    // オーバーラップ用の一時バッファ(GC対策)
    private readonly Collider[] _overlapResults = new Collider[64];

    // 毎フレームの物理ステップで吸引処理を行う
    private void FixedUpdate()
    {
        // 物理演算は FixedUpdate() で行うのが基本
        AttractRigidbodies();
    }

    /// <summary>
    /// attractionRadius 内にある Rigidbody を探し、親の位置に向かって力を加える。
    /// </summary>
    private void AttractRigidbodies()
    {
        Vector3 center = transform.position;

        // OverlapSphereNonAlloc を使うことで、毎フレームの GC を抑える
        int hitCount = Physics.OverlapSphereNonAlloc(
            center,
            attractionRadius,
            _overlapResults,
            targetLayerMask,
            QueryTriggerInteraction.Ignore // トリガーは無視(必要なら変更)
        );

        for (int i = 0; i < hitCount; i++)
        {
            Collider col = _overlapResults[i];
            if (col == null)
            {
                continue;
            }

            // 自分自身のコライダーは無視
            if (col.transform == transform)
            {
                continue;
            }

            // Rigidbody を持っているかチェック
            Rigidbody rb = col.attachedRigidbody;
            if (rb == null)
            {
                continue;
            }

            // Kinematic な Rigidbody には力を加えても動かないのでスキップ
            if (rb.isKinematic)
            {
                continue;
            }

            // 方向ベクトルと距離を計算
            Vector3 toCenter = center - rb.worldCenterOfMass;
            float distance = toCenter.magnitude;

            // 半径外や、ほぼゼロ距離の場合はスキップ
            if (distance <= 0.0001f || distance > attractionRadius)
            {
                continue;
            }

            // 正規化した方向
            Vector3 direction = toCenter / distance;

            // 距離に応じた減衰を計算
            float strength = attractionForce;

            if (useDistanceFalloff)
            {
                // 距離が近いほど強く、遠いほど弱くする簡易モデル
                // 例: 1 / distance で減衰させる(ただし minDistance でクランプ)
                float d = Mathf.Max(distance, minDistance);
                strength *= 1f / d;
            }

            // 最終的な力ベクトル
            Vector3 force = direction * strength;

            // Rigidbody に力を加える
            rb.AddForce(force, forceMode);
        }
    }

    /// <summary>
    /// シーンビュー上で吸引範囲を可視化する
    /// </summary>
    private void OnDrawGizmosSelected()
    {
        if (!drawGizmos)
        {
            return;
        }

        Gizmos.color = gizmoColor;
        Gizmos.DrawSphere(transform.position, attractionRadius);
    }

    #region 公開API(インスペクタからではなくコードから制御したい場合に使用)

    /// <summary>
    /// 吸引半径を動的に変更する
    /// 例: プレイヤーがパワーアップアイテムを取ったときに範囲拡大する等
    /// </summary>
    /// <param name="radius">新しい吸引半径</param>
    public void SetAttractionRadius(float radius)
    {
        attractionRadius = Mathf.Max(0f, radius);
    }

    /// <summary>
    /// 吸引力を動的に変更する
    /// </summary>
    /// <param name="force">新しい吸引力</param>
    public void SetAttractionForce(float force)
    {
        attractionForce = Mathf.Max(0f, force);
    }

    #endregion
}

使い方の手順

ここでは、「プレイヤーの周囲にあるコインを吸い寄せる」例で説明します。
敵や動く床に使う場合も、基本の手順は同じです。

① アイテム(吸い寄せたいオブジェクト)の準備

  • シーン内またはプレハブとして「Coin」などのアイテムオブジェクトを作成します。
  • 以下のコンポーネントをアタッチします:
    • MeshRenderer / SpriteRenderer(見た目用)
    • ColliderBoxCollider / SphereCollider など、Is Trigger は OFF
    • RigidbodyUse Gravity: ON / Is Kinematic: OFF
  • レイヤーを「Item」などに設定しておくと管理しやすいです。

② MagnetAttractor スクリプトを作成

  • プロジェクトビューで MagnetAttractor.cs を作成し、上記のコードをコピペして保存します。

③ 親オブジェクト(吸引元)にアタッチ

  • 例: プレイヤーオブジェクト(Player)を選択します。
  • Inspector で Add ComponentMagnetAttractor を追加します。
  • インスペクタ上で以下の値を調整します:
    • Target Layer Mask: 「Item」レイヤーのみにチェック(または吸い寄せたいレイヤー)
    • Attraction Radius: 例: 5〜10 くらいから試す
    • Attraction Force: 例: 10〜30 くらいから試す
    • Use Distance Falloff: ON にすると、近いほど強く吸い寄せられます
    • Force Mode: とりあえず Acceleration のままでOK

④ 実行して動作確認

  • シーンに複数のコイン(Rigidbody 付き)をばらまきます。
  • プレイヤー(MagnetAttractor 付き)を中心に近づけてみましょう。
  • コインがプレイヤーの方へスイーッと吸い寄せられたら成功です。

同じ要領で、

  • 敵にアタッチして「周囲の弾丸を吸い込むボス」
  • 動く床にアタッチして「床の中央にアイテムが集まるギミック」
  • 宝箱にアタッチして「開いた瞬間、周囲のゴールドが吸い込まれる演出」

といった使い方もできます。

メリットと応用

1. プレハブの再利用性が高い

MagnetAttractor は「周囲の Rigidbody を吸い寄せる」という単一の責務だけを持っています。
そのため、

  • 「MagnetAttractor 付きプレイヤー」プレハブ
  • 「MagnetAttractor 付き敵」プレハブ
  • 「MagnetAttractor 付きギミック」プレハブ

といった形で、オブジェクトごとに役割を変えつつも、同じコンポーネントを使い回せるのが大きなメリットです。

2. レベルデザインが楽になる

レベルデザイナー視点では、

  • シーン上に MagnetAttractor を持つオブジェクトをポンポン配置するだけで、
  • 「ここに近づくとアイテムが寄ってくる」「ここは敵の弾が吸い込まれるゾーン」

といったギミックを簡単に追加できます。
コードを書き換えずに、インスペクタから Attraction RadiusAttraction Force を調整するだけで、ゲームバランスの微調整もやりやすくなります。

3. 物理挙動と組み合わせた自然な演出

Rigidbody.AddForce() を使っているため、アイテムがスッと加速したり、慣性で少しオーバーシュートしたりと、物理ベースの自然な動きになります。
カーブを描いて吸い込まれるような表現も簡単に作れますね。

改造案:プレイヤーのパワーアップでマグネット範囲を一時的に拡大する

例えば、「マグネットパワーアップアイテム」を取ったときだけ、一定時間だけ吸引範囲を広げたい場合は、MagnetAttractor を持つオブジェクト側から、以下のような関数を呼び出す形で実装できます。


using System.Collections;
using UnityEngine;

public class MagnetPowerUpExample : MonoBehaviour
{
    [SerializeField]
    private MagnetAttractor magnetAttractor;

    [SerializeField]
    private float boostedRadius = 10f;

    [SerializeField]
    private float boostDuration = 5f;

    private float _originalRadius;

    // 例えば「マグネットアイテムを取得したとき」にこのメソッドを呼ぶ
    public void ActivateMagnetBoost()
    {
        if (magnetAttractor == null)
        {
            return;
        }

        StopAllCoroutines();
        StartCoroutine(MagnetBoostRoutine());
    }

    private IEnumerator MagnetBoostRoutine()
    {
        // 元の値を保存
        _originalRadius = magnetAttractor != null ? GetCurrentRadius() : 0f;

        // 半径を拡大
        magnetAttractor.SetAttractionRadius(boostedRadius);

        // 一定時間待つ
        yield return new WaitForSeconds(boostDuration);

        // 元の半径に戻す
        magnetAttractor.SetAttractionRadius(_originalRadius);
    }

    // MagnetAttractor の private フィールドを直接参照しないためのラッパー
    // 実際には MagnetAttractor 側に Getter を追加してもよいです。
    private float GetCurrentRadius()
    {
        // ここでは仮に 5 を返す例。実運用では MagnetAttractor に
        // public float CurrentRadius => attractionRadius;
        // のようなプロパティを追加して参照するとよいです。
        return 5f;
    }
}

このように、MagnetAttractor は「吸い寄せる」ことだけを担当し、
「いつ・どのくらい強く吸い寄せるか」は別コンポーネント(ここでは MagnetPowerUpExample)に任せると、
責務が分かれていて保守しやすい構成になります。

巨大な God クラスを避けて、小さなコンポーネントを組み合わせる設計を意識すると、
後からの仕様変更や新ギミック追加がかなり楽になります。
ぜひ自分のプロジェクトでも「MagnetAttractor」をベースに、いろいろなマグネット系ギミックを試してみてください。