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;
}
}
使い方の手順
-
① 押されるオブジェクト(親)を用意する
例として「動かせる箱」を作りたい場合:- 空の GameObject を作成し、名前を
PushableBoxなどにする。 - この親オブジェクトに
Rigidbodyを追加する(押される本体)。 - 必要に応じて
Constraintsで回転や移動軸をロックする(例: Y回転だけ許可)。
- 空の GameObject を作成し、名前を
-
② 当たり判定用の子オブジェクトを作成して PushableObject を付ける
PushableBoxの子として Cube などのメッシュ付きオブジェクトを作る。- 子オブジェクトに
Collider(BoxColliderなど)を追加する。 - その子オブジェクトに、上記の
PushableObjectコンポーネントを追加する。 - インスペクターで
Push Force(押す強さ)やNormal Scaleを調整する。 Reset()により自動で親のRigidbodyが設定されていることを確認する。
これで「押される箱」のプレハブが完成です。シーン内に何個でも配置できます。
-
③ プレイヤー側を用意する
例: シンプルなプレイヤーキャラ- プレイヤーオブジェクトに
RigidbodyとColliderを付ける(キャラコントローラでもOK)。 - タグに
Playerを設定する(PushableObjectのplayerTagと一致させる)。 - 移動スクリプトは「移動だけ」を担当させる。押す処理は一切書かないのがポイントです。
これで、プレイヤーが箱にぶつかると、
PushableObjectが自動的に親Rigidbodyを押してくれます。 - プレイヤーオブジェクトに
-
④ 実例パターン
-
プレイヤーが押せる箱
上記の通り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つの責務に絞ったコンポーネントに小さなフックを足していくと、プロジェクト全体がスッキリして管理しやすくなりますよ。
