Unityを触り始めた頃は、とりあえず Update() にプレイヤーの移動、ジャンプ、攻撃、カメラ制御…と全部詰め込んでしまいがちですよね。
動きは一応するけど、少し仕様を変えようとしただけで if と bool だらけの巨大スクリプトを前にフリーズ…なんて経験、あると思います。
こういう「なんでも入ったGodクラス」は、機能追加もデバッグも辛くなっていきます。
そこでおすすめなのが、「1つのコンポーネント = 1つのはっきりした役割」に分割していく設計です。
この記事では、空中で下入力すると急降下し、着地時に衝撃波判定を出す「GroundPound(ヒップドロップ)」を、
プレイヤーの移動ロジックから独立した専用コンポーネントとして実装してみましょう。
【Unity】空中アクションを1コンポーネントに分離!「GroundPound」コンポーネント
ここでは以下を満たす GroundPound コンポーネントを作ります。
- 空中でのみ発動できる(地上では無効)
- 下方向入力で急降下開始
- 急降下中は縦速度を強制的に落下方向へ
- 着地した瞬間に「衝撃波」用の当たり判定を一時的に有効化
- 他の移動スクリプトとは疎結合で使える
プレイヤーの基本移動は別スクリプトに任せて、
「ヒップドロップだけを担当する小さなスクリプト」として設計していきます。
フルコード:GroundPound.cs
using UnityEngine;
using UnityEngine.InputSystem; // 新Input System用
/// <summary>
/// 空中で下入力すると急降下し、着地時に衝撃波判定を出すコンポーネント。
/// ・Rigidbody を使った物理ベースのキャラクター向け
/// ・横移動やジャンプは別コンポーネントに任せる想定
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class GroundPound : MonoBehaviour
{
// ====== 設定パラメータ ======
[Header("Ground Pound 設定")]
[SerializeField]
[Tooltip("ヒップドロップ中に設定する下方向の速度(絶対値)")]
private float poundDownwardSpeed = 20f;
[SerializeField]
[Tooltip("ヒップドロップを開始できる最低の落下速度(上昇中には出したくない場合に使用)")]
private float minVerticalSpeedToStart = -0.1f;
[SerializeField]
[Tooltip("ヒップドロップ後の着地時に一時的に有効化される衝撃波のCollider")]
private Collider shockwaveCollider;
[SerializeField]
[Tooltip("衝撃波の当たり判定を有効にしておく時間(秒)")]
private float shockwaveActiveDuration = 0.1f;
[SerializeField]
[Tooltip("ヒップドロップ中にプレイヤーの横移動を制限するかどうか")]
private bool lockHorizontalMovementDuringPound = true;
[SerializeField]
[Tooltip("ヒップドロップ中に横移動速度をどれだけ残すか(0 = 完全停止, 1 = 変更しない)")]
private float horizontalSpeedMultiplier = 0.2f;
[Header("地面判定")]
[SerializeField]
[Tooltip("地面レイヤーを指定してください")]
private LayerMask groundLayer;
[SerializeField]
[Tooltip("地面判定に使う足元の位置")]
private Transform groundCheckPoint;
[SerializeField]
[Tooltip("地面判定の半径")]
private float groundCheckRadius = 0.2f;
[Header("入力設定")]
[SerializeField]
[Tooltip("下方向入力とみなす閾値(-1 ~ 1)。例: -0.5 以下なら下入力")]
private float downInputThreshold = -0.5f;
[SerializeField]
[Tooltip("ヒップドロップを開始するために使う InputAction(Vector2 など)")]
private InputActionReference moveAction;
// ====== 内部状態 ======
private Rigidbody rb;
// 現在ヒップドロップ中かどうか
private bool isGroundPounding = false;
// 直前フレームで地面にいたかどうか(着地判定用)
private bool wasGrounded = false;
// 衝撃波の有効化を制御するタイマー
private float shockwaveTimer = 0f;
// 外部から地面状態を上書きしたい場合用(任意)
private bool overrideGrounded = false;
private bool overrideGroundedValue = false;
private void Awake()
{
rb = GetComponent<Rigidbody>();
// 衝撃波Colliderは最初は無効にしておく
if (shockwaveCollider != null)
{
shockwaveCollider.enabled = false;
}
}
private void OnEnable()
{
// InputAction を有効化
if (moveAction != null && moveAction.action != null)
{
moveAction.action.Enable();
}
}
private void OnDisable()
{
// InputAction を無効化
if (moveAction != null && moveAction.action != null)
{
moveAction.action.Disable();
}
}
private void Update()
{
bool grounded = CheckGrounded();
// ヒップドロップの開始判定は Update で入力を読んで行う
HandleGroundPoundStart(grounded);
// 衝撃波の有効時間を管理
UpdateShockwaveTimer();
// 着地タイミングの検出(前フレームは空中、今フレームで地面)
if (!wasGrounded && grounded)
{
OnLanded();
}
wasGrounded = grounded;
}
private void FixedUpdate()
{
// 物理挙動の更新は FixedUpdate で
if (isGroundPounding)
{
ApplyGroundPoundPhysics();
}
}
/// <summary>
/// 現在地面にいるかどうかを判定する。
/// </summary>
private bool CheckGrounded()
{
if (overrideGrounded)
{
// 外部から上書きされている場合
return overrideGroundedValue;
}
if (groundCheckPoint == null)
{
// groundCheckPoint が未設定の場合は、Transform の足元付近を使う簡易実装
Vector3 checkPos = transform.position + Vector3.down * 0.5f;
return Physics.CheckSphere(checkPos, groundCheckRadius, groundLayer, QueryTriggerInteraction.Ignore);
}
else
{
return Physics.CheckSphere(groundCheckPoint.position, groundCheckRadius, groundLayer, QueryTriggerInteraction.Ignore);
}
}
/// <summary>
/// ヒップドロップを開始できるかどうかを判定し、開始する。
/// </summary>
private void HandleGroundPoundStart(bool grounded)
{
if (isGroundPounding)
{
// すでにヒップドロップ中なら、開始処理は行わない
return;
}
if (grounded)
{
// 地上ではヒップドロップを開始しない
return;
}
// InputAction から2D入力(WASD / 左スティックなど)を取得
Vector2 moveInput = Vector2.zero;
if (moveAction != null && moveAction.action != null)
{
moveInput = moveAction.action.ReadValue<Vector2>();
}
// 下方向入力が一定値を超えたらヒップドロップ開始
bool isDownPressed = moveInput.y <= downInputThreshold;
// 上昇中には出したくない場合、現在の縦速度もチェック
bool canStartByVerticalSpeed = rb.velocity.y <= minVerticalSpeedToStart;
if (isDownPressed && canStartByVerticalSpeed)
{
StartGroundPound();
}
}
/// <summary>
/// ヒップドロップの開始処理。
/// </summary>
private void StartGroundPound()
{
isGroundPounding = true;
Vector3 v = rb.velocity;
// 横移動を制限する場合
if (lockHorizontalMovementDuringPound)
{
v.x *= horizontalSpeedMultiplier;
v.z *= horizontalSpeedMultiplier;
}
// 下方向の速度を強制設定
v.y = -Mathf.Abs(poundDownwardSpeed);
rb.velocity = v;
}
/// <summary>
/// ヒップドロップ中の物理挙動を適用。
/// </summary>
private void ApplyGroundPoundPhysics()
{
Vector3 v = rb.velocity;
// 常に強制的に下向き速度を維持(重力で加速しすぎないようにするなど)
v.y = -Mathf.Abs(poundDownwardSpeed);
if (lockHorizontalMovementDuringPound)
{
v.x *= horizontalSpeedMultiplier;
v.z *= horizontalSpeedMultiplier;
}
rb.velocity = v;
}
/// <summary>
/// 着地時の処理。
/// </summary>
private void OnLanded()
{
if (!isGroundPounding)
{
// 通常の着地なら何もしない
return;
}
isGroundPounding = false;
// 衝撃波を有効化
if (shockwaveCollider != null)
{
shockwaveCollider.enabled = true;
shockwaveTimer = shockwaveActiveDuration;
}
// 着地時に縦速度をリセット(バウンドさせたくない場合)
Vector3 v = rb.velocity;
if (v.y < 0f)
{
v.y = 0f;
rb.velocity = v;
}
// ここでエフェクトやサウンドを再生してもよい
// 例:
// PlayGroundPoundEffect();
}
/// <summary>
/// 衝撃波Colliderの有効時間を管理。
/// </summary>
private void UpdateShockwaveTimer()
{
if (shockwaveCollider == null)
{
return;
}
if (shockwaveTimer > 0f)
{
shockwaveTimer -= Time.deltaTime;
if (shockwaveTimer <= 0f)
{
shockwaveCollider.enabled = false;
}
}
}
// ====== 外部から利用できる簡易API(任意) ======
/// <summary>
/// 外部のキャラクター制御から「今は地面にいる」などの情報を上書きしたい場合に使う。
/// 使わない場合は呼び出さなくてOK。
/// </summary>
public void SetGroundedOverride(bool useOverride, bool grounded)
{
overrideGrounded = useOverride;
overrideGroundedValue = grounded;
}
/// <summary>
/// 現在ヒップドロップ中かどうかを返す。
/// 他のスクリプト側で、入力やアニメーションを制御したいときに使用。
/// </summary>
public bool IsGroundPounding => isGroundPounding;
// 例: エフェクトを再生するためのフック
// 実装は利用側の自由に任せる
// private void PlayGroundPoundEffect()
// {
// // パーティクル再生やSE再生など
// }
}
使い方の手順
ここからは、実際にプレイヤーキャラクターにヒップドロップを追加する手順を見ていきます。
同じ手順で、例えば敵キャラの急降下攻撃や動く床の落下ギミックにも応用できます。
手順①:Rigidbody付きのキャラクターを用意する
- ヒップドロップさせたいオブジェクト(例:Player)に
Rigidbody(Use Gravity を ON)- カプセルコライダーなど、通常の当たり判定
が付いていることを確認します。
- 基本の移動やジャンプは、別のスクリプト(例:
PlayerMovement)に任せてOKです。
手順②:GroundPound コンポーネントを追加する
- プロジェクトビューで
GroundPound.csを作成し、上のコードをコピペして保存。 - ヒップドロップさせたい GameObject(Player など)に
GroundPoundコンポーネントをアタッチ。 - インスペクターで各パラメータを調整します。
- Pound Downward Speed:急降下の速さ(例:20~40)
- Ground Layer:地面として扱うレイヤー(例:Ground)
- Ground Check Point:足元に空の子オブジェクトを置いて指定すると精度が上がります
- Move Action:新Input Systemの移動アクション(WASD / スティックなどの Vector2)を割り当て
手順③:衝撃波用の当たり判定を作る
着地時に周囲の敵にダメージを与える「衝撃波」を作ってみましょう。
- Player の子オブジェクトとして
Shockwaveという名前の空オブジェクトを作成。 ShockwaveにSphereColliderなどを追加し、- Is Trigger を ON
- 半径を 1~3 程度に調整(範囲攻撃の広さ)
Shockwaveに「敵にダメージを与える」スクリプトを付けます。例として簡単なスクリプト:
using UnityEngine;
/// <summary>
/// 衝撃波の当たり判定。Trigger に入った敵にダメージを与える想定。
/// </summary>
public class ShockwaveDamage : MonoBehaviour
{
[SerializeField]
[Tooltip("与えるダメージ量")]
private int damage = 1;
private void OnTriggerEnter(Collider other)
{
// ここでは簡易的に「Enemy」タグを持つオブジェクトを対象にする
if (other.CompareTag("Enemy"))
{
// 本来は EnemyHealth コンポーネントなどにダメージを通知する
// 例:
// var health = other.GetComponent<EnemyHealth>();
// if (health != null) health.TakeDamage(damage);
Debug.Log($"Shockwave hit enemy: {other.name}, damage: {damage}");
}
}
}
- Player の
GroundPoundコンポーネントの Shockwave Collider に、
ShockwaveのSphereColliderをドラッグ&ドロップで割り当てます。
これで、ヒップドロップで着地した瞬間だけ衝撃波の Trigger が ON になり、
範囲内の敵にダメージを与える仕組みが完成します。
手順④:動作確認(プレイヤーの例)
- シーンを再生。
- ジャンプでプレイヤーを空中に浮かせる(ジャンプ処理は別スクリプトでOK)。
- 空中で下方向入力(例:Sキー、スティック下)を押す。
- プレイヤーが強く下に落下し、着地した瞬間に衝撃波の Trigger が一瞬有効になり、
敵にダメージログが出ていれば成功です。
同じコンポーネントを敵キャラに付ければ、「プレイヤーの上空から急降下してくる敵」も簡単に作れますし、
動く床に付ければ「プレイヤーが乗っているときだけ急降下してくる床」といったギミックも作れます。
メリットと応用
GroundPound を独立コンポーネントとして切り出すことで、次のようなメリットがあります。
- プレハブの再利用性が高い
「Rigidbody + GroundPound + ShockwaveDamage」のセットをプレハブ化しておけば、
プレイヤーだけでなく敵キャラやギミックにもポン付けできます。 - 責務が明確で保守しやすい
「移動」「ジャンプ」「ヒップドロップ」「攻撃」がそれぞれ別コンポーネントなら、
不具合の原因を特定しやすく、仕様変更にも強くなります。 - レベルデザインが楽になる
デザイナーは「この敵はヒップドロップあり」「この敵はなし」といった構成を
インスペクター上でコンポーネントの ON/OFF だけで切り替えられます。
例えば、ボス戦で「第2形態からだけ GroundPound を有効にする」なども、
GroundPound.enabled = true; を切り替えるだけで実現できます。
改造案:着地時にカメラを少し揺らす
ヒップドロップの着地に迫力を出すために、
OnLanded() の中から呼び出す「簡易カメラシェイク」を追加してみるのも面白いです。
/// <summary>
/// 簡易カメラシェイク(ローカル位置を少し揺らす)
/// メインカメラにアタッチして使う想定。
/// </summary>
public class SimpleCameraShake : MonoBehaviour
{
[SerializeField]
private float shakeDuration = 0.1f;
[SerializeField]
private float shakeMagnitude = 0.2f;
private Vector3 originalLocalPos;
private float shakeTimer = 0f;
private void Awake()
{
originalLocalPos = transform.localPosition;
}
private void Update()
{
if (shakeTimer > 0f)
{
shakeTimer -= Time.deltaTime;
Vector3 randomOffset = Random.insideUnitSphere * shakeMagnitude;
transform.localPosition = originalLocalPos + randomOffset;
if (shakeTimer <= 0f)
{
transform.localPosition = originalLocalPos;
}
}
}
/// <summary>
/// 外部からシェイクを開始する。
/// </summary>
public void Shake()
{
shakeTimer = shakeDuration;
}
}
GroundPound の OnLanded() 内で、メインカメラの SimpleCameraShake を呼び出せば、
ヒップドロップの着地に「ドシン!」という手応えを簡単に追加できます。
このように、1つの機能を1コンポーネントに閉じ込めておくと、
あとから「エフェクト追加」「カメラシェイク追加」「SE追加」などの拡張も、
他の機能を壊さずに安全に行えるようになります。コンポーネント指向で、気持ちよく機能追加していきましょう。
