Unityを触り始めた頃、つい何でもかんでも Update() に書いてしまいがちですよね。プレイヤーの移動、入力処理、エフェクトの制御、ギミックの動き…すべて一つの巨大なスクリプトに詰め込むと、少し仕様変更が入っただけでコードを追うのが一気にしんどくなります。

特に「風で押し流されるエリア」や「ベルトコンベア」「水流」などの継続的な力を与えるギミックを、プレイヤー側のスクリプトに書いてしまうと、

  • シーンごとに挙動を変えたい時にプレイヤースクリプトを毎回編集する
  • 敵や動くオブジェクトにも同じ影響を与えたいのに、コピペ地獄になる
  • レベルデザイナーがパラメータをいじりにくい

といった問題が出てきます。

そこで今回は、「風の通り道」を表現する専用コンポーネント WindTunnel を用意して、エリア側に「風のロジック」を閉じ込めてしまいましょう。オブジェクトには Rigidbody さえ付いていれば、エリアに入るだけで自動的に風の力が掛かるようになります。

【Unity】風でオブジェクトを押し流すエリアを作ろう!「WindTunnel」コンポーネント

このコンポーネントは、指定したコライダー領域(2D/3D対応)内にいる Rigidbody に対して、一定方向へ継続的な力を加え続けます。風向きや強さはインスペクターから調整できるので、レベルデザインもしやすくなります。

フルコード


using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 風の通り道を表現するコンポーネント。
/// 指定したコライダー領域内にいる Rigidbody / Rigidbody2D に
/// 一定方向の力を継続的に加え続ける。
/// 
/// - 3D物理(Rigidbody + Collider + isTrigger)
/// - 2D物理(Rigidbody2D + Collider2D + isTrigger)
/// の両方に対応しています。
/// </summary>
[DisallowMultipleComponent]
public class WindTunnel : MonoBehaviour
{
    /// <summary>
    /// 風の向き(ワールド座標系)。
    /// </summary>
    [Header("風の基本設定")]
    [Tooltip("ワールド座標系での風の向き(正規化は自動で行われます)。")]
    [SerializeField] private Vector3 windDirection = Vector3.right;

    /// <summary>
    /// 風の強さ。1秒あたりに与える加速度の大きさ。
    /// </summary>
    [Tooltip("風の強さ(大きいほど強く押し流されます)。")]
    [SerializeField] private float windStrength = 10f;

    /// <summary>
    /// 風力のタイプ(加速度 or 力)。
    /// Acceleration: 質量に関係なく同じ加速度を与える
    /// Force: Rigidbody.mass によって影響が変わる
    /// </summary>
    private enum ForceModeType
    {
        Acceleration3D,
        Force3D,
        Acceleration2D,
        Force2D
    }

    [Tooltip("3D/2D それぞれで力の種類を選べます。基本は Acceleration 系がおすすめです。")]
    [SerializeField] private ForceModeType forceModeType = ForceModeType.Acceleration3D;

    /// <summary>
    /// 風の強さに Time.deltaTime を掛けるかどうか。
    /// 通常は true(フレームレート非依存)でOK。
    /// </summary>
    [Tooltip("true にするとフレームレート非依存の挙動になります。")]
    [SerializeField] private bool useDeltaTime = true;

    /// <summary>
    /// 2D物理を使うかどうかのヒント(自動判定も行います)。
    /// </summary>
    [Header("物理設定")]
    [Tooltip("2D物理を使うエリアの場合はチェック推奨(自動でも判定します)。")]
    [SerializeField] private bool is2DHint = false;

    /// <summary>
    /// トリガーとして使うコライダー(自動取得)。
    /// 2D/3D のどちらか片方のみアタッチしてください。
    /// </summary>
    [Header("内部状態(確認用)")]
    [SerializeField] private Collider triggerCollider3D;
    [SerializeField] private Collider2D triggerCollider2D;

    // 現在風のエリア内にいる Rigidbody / Rigidbody2D を保持するリスト
    private readonly List<Rigidbody> bodies3D = new List<Rigidbody>();
    private readonly List<Rigidbody2D> bodies2D = new List<Rigidbody2D>();

    // 2D か 3D かを内部的に判断するフラグ
    private bool use2D = false;

    private void Reset()
    {
        // コンポーネント追加時にトリガーコライダー設定を補助する
        triggerCollider3D = GetComponent<Collider>();
        triggerCollider2D = GetComponent<Collider2D>();

        // どちらも無い場合は BoxCollider を追加してトリガーにする(3D優先)
        if (triggerCollider3D == null && triggerCollider2D == null)
        {
            // デフォルトでは 3D の BoxCollider を追加
            triggerCollider3D = gameObject.AddComponent<BoxCollider>();
            triggerCollider3D.isTrigger = true;
        }
        else
        {
            // 既存コライダーがある場合は isTrigger をオンにしておく
            if (triggerCollider3D != null)
            {
                triggerCollider3D.isTrigger = true;
            }

            if (triggerCollider2D != null)
            {
                triggerCollider2D.isTrigger = true;
            }
        }
    }

    private void Awake()
    {
        // Awake 時に 2D/3D を判定
        triggerCollider3D = GetComponent<Collider>();
        triggerCollider2D = GetComponent<Collider2D>();

        // 2D ヒント or Collider2D が付いているなら 2D とみなす
        use2D = is2DHint || triggerCollider2D != null;

        // どちらのコライダーも無い場合は警告
        if (triggerCollider3D == null && triggerCollider2D == null)
        {
            Debug.LogWarning(
                $"[WindTunnel] {name} に Collider / Collider2D が見つかりません。" +
                "トリガー用コライダーを追加してください。");
        }
    }

    private void OnValidate()
    {
        // 風向きがゼロベクトルの場合は警告を出す
        if (windDirection == Vector3.zero)
        {
            Debug.LogWarning($"[WindTunnel] {name} の windDirection が (0,0,0) です。風が発生しません。");
        }

        // 強さが負の値にならないようにクランプ
        if (windStrength < 0f)
        {
            windStrength = 0f;
        }
    }

    private void FixedUpdate()
    {
        // 風向きを正規化して、強さを掛けたベクトルを作る
        Vector3 dir = windDirection.sqrMagnitude > 0.0001f
            ? windDirection.normalized
            : Vector3.zero;

        if (dir == Vector3.zero || windStrength <= 0f)
        {
            // 風向きが無い or 強さが0以下なら何もしない
            return;
        }

        float dt = useDeltaTime ? Time.fixedDeltaTime : 1f;

        // 3D 物理に対して力を加える
        if (!use2D)
        {
            Vector3 force = dir * windStrength * dt;

            for (int i = bodies3D.Count - 1; i >= 0; i--)
            {
                Rigidbody rb = bodies3D[i];
                if (rb == null)
                {
                    // 破棄された場合などはリストから削除
                    bodies3D.RemoveAt(i);
                    continue;
                }

                switch (forceModeType)
                {
                    case ForceModeType.Acceleration3D:
                        rb.AddForce(force, ForceMode.Acceleration);
                        break;

                    case ForceModeType.Force3D:
                        rb.AddForce(force, ForceMode.Force);
                        break;

                    // 他のモードが選ばれていても 3D なら最低限 Force として扱う
                    default:
                        rb.AddForce(force, ForceMode.Force);
                        break;
                }
            }
        }
        // 2D 物理に対して力を加える
        else
        {
            Vector2 force2D = (Vector2)(dir * windStrength * dt);

            for (int i = bodies2D.Count - 1; i >= 0; i--)
            {
                Rigidbody2D rb2D = bodies2D[i];
                if (rb2D == null)
                {
                    bodies2D.RemoveAt(i);
                    continue;
                }

                switch (forceModeType)
                {
                    case ForceModeType.Acceleration2D:
                        rb2D.AddForce(force2D, ForceMode2D.Force);
                        break;

                    case ForceModeType.Force2D:
                        rb2D.AddForce(force2D, ForceMode2D.Force);
                        break;

                    // 他のモードが選ばれていても 2D なら最低限 Force として扱う
                    default:
                        rb2D.AddForce(force2D, ForceMode2D.Force);
                        break;
                }
            }
        }
    }

    #region 3D Trigger Events

    private void OnTriggerEnter(Collider other)
    {
        if (use2D) return; // 2D モード時は無視

        // 侵入してきたオブジェクトから Rigidbody を探す
        Rigidbody rb = other.attachedRigidbody;
        if (rb == null) return;

        // まだリストに無ければ追加
        if (!bodies3D.Contains(rb))
        {
            bodies3D.Add(rb);
        }
    }

    private void OnTriggerExit(Collider other)
    {
        if (use2D) return; // 2D モード時は無視

        Rigidbody rb = other.attachedRigidbody;
        if (rb == null) return;

        bodies3D.Remove(rb);
    }

    #endregion

    #region 2D Trigger Events

    private void OnTriggerEnter2D(Collider2D other)
    {
        if (!use2D) return; // 3D モード時は無視

        Rigidbody2D rb2D = other.attachedRigidbody;
        if (rb2D == null) return;

        if (!bodies2D.Contains(rb2D))
        {
            bodies2D.Add(rb2D);
        }
    }

    private void OnTriggerExit2D(Collider2D other)
    {
        if (!use2D) return; // 3D モード時は無視

        Rigidbody2D rb2D = other.attachedRigidbody;
        if (rb2D == null) return;

        bodies2D.Remove(rb2D);
    }

    #endregion

    /// <summary>
    /// インスペクターからも呼べる、風向きをゲームオブジェクトの forward に合わせるヘルパー。
    /// </summary>
    [ContextMenu("風向きをこのオブジェクトの Forward に合わせる")]
    private void AlignDirectionToForward()
    {
        windDirection = transform.forward;
    }
}

使い方の手順

  1. エリア用の GameObject を用意する
    例として、3Dゲームなら「WindZone_01」という空の GameObject を作り、風が吹く範囲をイメージしやすいように子オブジェクトに Cube などを置いておくと便利です(見た目用の MeshRenderer は任意)。
  2. トリガーコライダーを設定する
    • 3Dの場合: BoxColliderSphereCollider を追加し、Is Trigger にチェックを入れる。
    • 2Dの場合: BoxCollider2DCircleCollider2D を追加し、Is Trigger にチェックを入れる。

    スクリプトの Reset 時に自動で isTrigger をオンにしてくれますが、目視で確認しておくと安心です。

  3. WindTunnel コンポーネントをアタッチする
    エリア用 GameObject に上記の WindTunnel スクリプトを追加します。
    インスペクターから以下を調整しましょう。
    • Wind Direction: 風の向き(例: X+ 方向なら (1, 0, 0))。
      オブジェクトの向きに合わせたい場合は、コンテキストメニュー「風向きをこのオブジェクトの Forward に合わせる」を使うと便利です。
    • Wind Strength: 風の強さ。まずは 5〜20 あたりから試すと良いです。
    • Force Mode Type: 基本は Acceleration3D または Acceleration2D がおすすめです。
    • Is 2D Hint: 2Dゲームで使う場合はチェックを入れておくと 2D モードが確実になります。
  4. 対象オブジェクトに Rigidbody を付ける
    風の影響を受けさせたいオブジェクト(プレイヤー、敵、木箱、ギミックなど)に
    • 3D: Rigidbody + 通常の Collider
    • 2D: Rigidbody2D + Collider2D

    を付けておきます。
    これで、そのオブジェクトが WindTunnel のトリガー領域に入ると、自動的に風の力が掛かるようになります。

具体的な使用例としては、以下のようなものが作りやすくなります。

  • プレイヤーを押し流す強風エリア
    崖の上に横風の WindTunnel を置き、プレイヤーが近づくと横に流されて足場から落ちやすくなるギミックとして使えます。
  • 敵やオブジェクトもまとめて流す水流
    川の流れを WindTunnel で表現し、木箱や敵も一緒に下流へ流されるようにすれば、ステージ全体の一体感が出ます。
  • 動く床の代わりになるベルトコンベア
    水平方向の WindTunnel を床の上に薄く配置すると、プレイヤーや箱がベルトコンベアのように動かされます。床自体は動かさずに済むので、コリジョンの管理がシンプルになります。

メリットと応用

WindTunnel コンポーネントを使うことで、以下のようなメリットがあります。

  • プレイヤーや敵のスクリプトを汚さない
    「風の影響を受けるかどうか」の判断や力の計算を、プレイヤー側に一切書かなくて済みます。Rigidbody が付いていれば、ただエリアに入るだけで勝手に押し流されます。
  • プレハブ単位でギミックを再利用できる
    風向き・強さ・エリア形状を調整した GameObject をプレハブ化しておけば、別シーンでもドラッグ&ドロップだけで同じギミックを再利用できます。
  • レベルデザインがインスペクター操作だけで完結
    スクリプトを触らずに、レベルデザイナーが「ここは少し強風」「ここはゆるい上昇気流」といった調整をインスペクター上で完結できます。
  • SRP(単一責任の原則)に沿った分離
    WindTunnel は「エリア内の物体に風の力を与える」という単一の責務に絞っているため、挙動の追跡やデバッグがしやすくなります。

さらに、少し改造すると「時間で風の強さが変化する」「オン・オフをスイッチで切り替える」などの応用も簡単です。

例えば、風の強さをサイン波で周期的に変化させる改造案はこんな感じです。


// WindTunnel クラス内に追加する例
[SerializeField] private bool useWave = false;
[SerializeField] private float waveAmplitude = 5f;
[SerializeField] private float waveFrequency = 1f;

private float baseWindStrength;

/// <summary>
/// 風の強さを時間経過で揺らす(サイン波)
/// Awake などで baseWindStrength = windStrength; をセットしておくと良いです。
/// </summary>
private void UpdateWindStrengthByWave()
{
    if (!useWave) return;

    // サイン波で -1〜1 の値を得る
    float wave = Mathf.Sin(Time.time * waveFrequency);

    // 振幅を掛けて強さに加算
    windStrength = Mathf.Max(0f, baseWindStrength + wave * waveAmplitude);
}

この関数を FixedUpdate() の先頭で呼び出せば、「強くなったり弱くなったりする風のエリア」が簡単に作れます。こうして小さな責務のコンポーネントを積み重ねていくと、Godクラスに頼らない拡張しやすいプロジェクト構成になっていきますね。