Unityを触り始めると、つい何でもかんでも Update() に書いてしまいがちですよね。プレイヤーの移動、ジャンプ、カメラ、当たり判定、UI更新…全部1つのスクリプトに詰め込むと、どこを直せばいいのか分からなくなり、バグも増えていきます。

特に「プレイヤーが物体を押す」ような処理は、PlayerController に押し込みがちですが、そうすると「プレイヤー以外は押せない」「押される側のパラメータを変えたいのにプレイヤー側のコードをいじる必要がある」など、設計がどんどん苦しくなります。

そこでこの記事では、「押される側」にだけアタッチすれば動く、小さな責務のコンポーネントとして PushableObject を作っていきます。
プレイヤーは「ただぶつかるだけ」、押されるロジックは「押されるオブジェクト側」が持つ。この分離ができると、レベルデザインもプレハブ管理もかなり楽になりますよ。

【Unity】ぶつかったら自動で押せる!「PushableObject」コンポーネント

以下は、「プレイヤーが親に衝突したとき、親(Rigidbody など)を反対側へ押す」ための、完結したコンポーネントです。
ここでは「押される対象となる Rigidbody(または CharacterController など)」を 親オブジェクト に持たせ、その子やコリジョン用オブジェクトに PushableObject を付ける想定で作っています。

フルコード


using UnityEngine;

/// <summary>
/// プレイヤーなどがこのオブジェクトに衝突したとき、
/// 親オブジェクトに対して「反対側へ押す力」を加えるコンポーネント。
/// 
/// - 押される対象(Rigidbody)は親階層から自動取得
/// - プレイヤー側は「Rigidbodyを持つコライダーでぶつかるだけ」でOK
/// - 小さな責務: 「押されるロジック」だけを担当
/// </summary>
[RequireComponent(typeof(Collider))]
public class PushableObject : MonoBehaviour
{
    // 親にある Rigidbody(押される本体)
    [SerializeField]
    private Rigidbody parentRigidbody;

    // 押す力の強さ(スカラー値)
    [SerializeField]
    [Tooltip("プレイヤーがぶつかったときに親に加える押し力の強さ")]
    private float pushForce = 5f;

    // 押し方向の補正(1.0でそのまま、0.5で半分など)
    [SerializeField]
    [Tooltip("法線方向に対する補正。1でそのまま、0で押さない")]
    private float normalScale = 1f;

    // プレイヤーとみなすタグ
    [SerializeField]
    [Tooltip("このタグを持つオブジェクトにぶつかったときだけ押されます")]
    private string playerTag = "Player";

    // 一度の衝突で何度も押し続けないためのフラグ
    [SerializeField]
    [Tooltip("同じ接触中に何度もAddForceしたくない場合はオンにする")]
    private bool applyOncePerContact = true;

    // 接触中かどうかを管理するためのフラグ
    private bool hasPushedInCurrentContact = false;

    // このコライダー
    private Collider ownCollider;

    private void Reset()
    {
        // Resetはコンポーネント追加時にInspector用の初期値を設定する用途に便利
        // 親のRigidbodyを自動で探す
        TryAutoAssignParentRigidbody();
    }

    private void Awake()
    {
        ownCollider = GetComponent<Collider>();

        // 安全のため、Awakeでも親Rigidbodyを探す
        if (parentRigidbody == null)
        {
            TryAutoAssignParentRigidbody();
        }

        // コライダーがTriggerかどうかはシーンに応じて選択できるようにする
        // ここでは強制しない(Triggerでも非Triggerでも動く実装にする)
    }

    /// <summary>
    /// 親階層からRigidbodyを探して自動設定する
    /// </summary>
    private void TryAutoAssignParentRigidbody()
    {
        // 自身を含まず親階層から探す
        parentRigidbody = GetComponentInParent<Rigidbody>();
    }

    // 非Triggerコライダー用
    private void OnCollisionEnter(Collision collision)
    {
        // 衝突相手がプレイヤーかどうかチェック
        if (!IsPlayer(collision.gameObject))
        {
            return;
        }

        // 衝突点から押す方向を計算して押す
        Vector3 pushDir = CalculatePushDirection(collision);
        ApplyPush(pushDir);
    }

    private void OnCollisionStay(Collision collision)
    {
        if (!IsPlayer(collision.gameObject))
        {
            return;
        }

        if (!applyOncePerContact)
        {
            Vector3 pushDir = CalculatePushDirection(collision);
            ApplyPush(pushDir);
        }
    }

    private void OnCollisionExit(Collision collision)
    {
        if (!IsPlayer(collision.gameObject))
        {
            return;
        }

        // 接触終了時にフラグをリセット
        hasPushedInCurrentContact = false;
    }

    // Triggerコライダー用
    private void OnTriggerEnter(Collider other)
    {
        if (!IsPlayer(other.gameObject))
        {
            return;
        }

        // Triggerの場合は、プレイヤーの位置と押されるオブジェクトの位置から方向を計算
        Vector3 pushDir = CalculatePushDirectionFromPositions(other.transform.position);
        ApplyPush(pushDir);
    }

    private void OnTriggerStay(Collider other)
    {
        if (!IsPlayer(other.gameObject))
        {
            return;
        }

        if (!applyOncePerContact)
        {
            Vector3 pushDir = CalculatePushDirectionFromPositions(other.transform.position);
            ApplyPush(pushDir);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (!IsPlayer(other.gameObject))
        {
            return;
        }

        hasPushedInCurrentContact = false;
    }

    /// <summary>
    /// プレイヤーかどうかをタグで判定
    /// </summary>
    private bool IsPlayer(GameObject obj)
    {
        // playerTagが空ならタグ指定なし(何にぶつかっても押す)
        if (string.IsNullOrEmpty(playerTag))
        {
            return true;
        }

        return obj.CompareTag(playerTag);
    }

    /// <summary>
    /// Collision情報から押す方向を計算する
    /// 「プレイヤーが親に衝突した際、親を反対側へ押す」イメージ。
    /// </summary>
    private Vector3 CalculatePushDirection(Collision collision)
    {
        // 衝突点の法線は「相手から見た自分への向き」になることが多い
        // ここでは「法線の反対方向」に押すことで、相手側に押し返す。
        // 例: プレイヤーが右から左にぶつかってきた場合、法線は右向き、
        //      その反対方向(左)へ押すことで、プレイヤーから遠ざかるように動く。
        Vector3 averageNormal = Vector3.zero;

        foreach (var contact in collision.contacts)
        {
            averageNormal += contact.normal;
        }

        if (collision.contactCount > 0)
        {
            averageNormal /= collision.contactCount;
        }

        // 法線の反対方向 = 押される方向
        Vector3 pushDir = -averageNormal * normalScale;

        // 水平方向だけにしたい場合はy成分を0にするなどもあり
        // pushDir.y = 0f;

        if (pushDir.sqrMagnitude < 0.0001f)
        {
            // 方向がほぼゼロの場合は、プレイヤーと親の位置関係から計算し直す
            pushDir = CalculatePushDirectionFromPositions(collision.transform.position);
        }

        return pushDir.normalized;
    }

    /// <summary>
    /// プレイヤー位置と親Rigidbody位置から押す方向を計算する
    /// </summary>
    private Vector3 CalculatePushDirectionFromPositions(Vector3 playerPosition)
    {
        if (parentRigidbody == null)
        {
            return Vector3.zero;
        }

        // プレイヤーから見て親がどちら側にいるかを計算
        // プレイヤー → 親 のベクトル
        Vector3 dirPlayerToParent = parentRigidbody.worldCenterOfMass - playerPosition;

        // 「プレイヤーが親に衝突した際、親を反対側へ押す」なので、
        // プレイヤーから見て親が右側にいるなら、さらに右へ押すイメージ。
        Vector3 pushDir = dirPlayerToParent.normalized * normalScale;

        // 必要なら垂直方向をカット
        // pushDir.y = 0f;

        return pushDir;
    }

    /// <summary>
    /// 実際に親Rigidbodyに力を加える処理
    /// </summary>
    private void ApplyPush(Vector3 direction)
    {
        if (parentRigidbody == null)
        {
            Debug.LogWarning($"[PushableObject] 親にRigidbodyが見つかりませんでした: {name}");
            return;
        }

        if (direction.sqrMagnitude < 0.0001f)
        {
            return;
        }

        if (applyOncePerContact && hasPushedInCurrentContact)
        {
            // すでにこの接触中に押しているなら何もしない
            return;
        }

        // AddForceで瞬間的な力を加える
        parentRigidbody.AddForce(direction.normalized * pushForce, ForceMode.Impulse);

        hasPushedInCurrentContact = true;
    }
}

使い方の手順

  1. ① 押されるオブジェクト(親)を用意する
    例として「動かせる箱」を作りたい場合:
    • 空の GameObject を作成し、名前を PushableBox などにする。
    • この親オブジェクトに Rigidbody を追加する(押される本体)。
    • 必要に応じて Constraints で回転や移動軸をロックする(例: Y回転だけ許可)。
  2. ② 当たり判定用の子オブジェクトを作成して PushableObject を付ける
    • PushableBox の子として Cube などのメッシュ付きオブジェクトを作る。
    • 子オブジェクトに Collider(BoxColliderなど)を追加する。
    • その子オブジェクトに、上記の PushableObject コンポーネントを追加する。
    • インスペクターで Push Force(押す強さ)や Normal Scale を調整する。
    • Reset() により自動で親の Rigidbody が設定されていることを確認する。

    これで「押される箱」のプレハブが完成です。シーン内に何個でも配置できます。

  3. ③ プレイヤー側を用意する
    例: シンプルなプレイヤーキャラ
    • プレイヤーオブジェクトに RigidbodyCollider を付ける(キャラコントローラでもOK)。
    • タグに Player を設定する(PushableObjectplayerTag と一致させる)。
    • 移動スクリプトは「移動だけ」を担当させる。押す処理は一切書かないのがポイントです。

    これで、プレイヤーが箱にぶつかると、PushableObject が自動的に親Rigidbodyを押してくれます。

  4. ④ 実例パターン
    • プレイヤーが押せる箱
      上記の通り PushableBox プレハブを作り、ステージに並べるだけ。
      押す強さを変えれば、重い箱・軽い箱を簡単に作り分けできます。
    • 敵キャラを押し返すシールド
      シールドオブジェクトをプレイヤーの子に配置し、シールドのコライダーに PushableObject を付ける。
      親Rigidbody を敵側にしておけば、「シールドに当たると敵が弾き飛ばされる」ギミックも作れます。
    • 動く足場をプレイヤーが押して動かす
      動く床の親に Rigidbody、その側面に PushableObject を付けたコライダーを配置。
      プレイヤーが横からぶつかると、床がその方向へ動く「押して動くエレベーター」のような表現も可能です。

メリットと応用

PushableObject を使うことで、「押すロジック」を押される側に閉じ込められるのが大きなメリットです。

  • プレイヤー側のスクリプトは「移動」「入力処理」などに専念できる。
  • 押されるオブジェクトごとに、pushForce を変えるだけで「軽い/重い」を簡単に表現できる。
  • レベルデザイナーは「とりあえずこのプレハブを置けば押せるギミックになる」という状態を作れる。
  • プレイヤー以外のオブジェクト(敵、弾、ギミック)にも Player タグを付け替えるだけで押し挙動を共有できる。

コンポーネント指向で「押される」という単一の責務に分解しておくと、別のゲームでもそのまま再利用しやすくなるのも嬉しいポイントですね。

さらに、ちょっとした改造を加えることで、表現の幅を広げられます。例えば、押されたときに効果音やパーティクルを出す処理を足してみましょう。


    /// <summary>
    /// 押された瞬間に呼び出す演出用のフック
    /// (PushableObject 内に追記する想定)
    /// </summary>
    [SerializeField]
    private AudioSource pushSE;

    [SerializeField]
    private ParticleSystem pushEffect;

    private void PlayPushFeedback()
    {
        // 効果音を再生
        if (pushSE != null)
        {
            pushSE.Play();
        }

        // パーティクルを再生
        if (pushEffect != null)
        {
            pushEffect.Play();
        }
    }

そして ApplyPush の最後で PlayPushFeedback() を呼べば、「押されたときだけ音やエフェクトが出る箱」が簡単に作れます。
このように、1つの責務に絞ったコンポーネントに小さなフックを足していくと、プロジェクト全体がスッキリして管理しやすくなりますよ。