Unityを触り始めたころ、「とりあえず全部 Update に書いてしまう」という実装をしがちですよね。プレイヤーの移動、アニメーションの切り替え、エフェクトの制御、UIの更新……すべてが1つのスクリプトの Update に詰め込まれていくと、あっという間に数百行の Godクラスができあがってしまいます。

そうなると、

  • どのコードがどのオブジェクトの見た目を変えているのか分かりづらい
  • ちょっとした演出(例えば「ふわふわ浮かせたい」)を追加したいだけなのに、既存コードに手を入れる必要が出てくる
  • プレハブごとに挙動を変えたいとき、条件分岐だらけになる

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

この記事では「ふわふわ浮かせる」というよくある演出を、1つの小さなコンポーネントに切り出します。
オブジェクトにアタッチするだけで、親オブジェクトの position.y を正弦波(Mathf.Sin)で揺らして、空中に浮いているように見せる SineWaveHover コンポーネントを実装していきましょう。

【Unity】ふわっと浮かせて存在感アップ!「SineWaveHover」コンポーネント

今回作る SineWaveHover は、

  • オブジェクトの「元の高さ」を基準に
  • 時間経過に応じて Mathf.Sin で上下に揺らす
  • 振幅(どれくらい上下するか)と周期(どれくらいの速さで揺れるか)をインスペクターから調整できる

という、小さくまとまった「浮遊アニメーション専用」コンポーネントです。

フルコード:SineWaveHover.cs


using UnityEngine;

/// <summary>
/// 正弦波で Transform の Y 座標を上下させて、ふわふわ浮いているように見せるコンポーネント。
/// ・元の高さを基準に上下に揺らす
/// ・振幅、高さオフセット、速度などをインスペクターから調整可能
/// ・Update に全部書くのではなく、「浮遊だけ」を担当する小さなコンポーネントとして設計
/// </summary>
[DisallowMultipleComponent] // 同じコンポーネントを重ね付けしてバグるのを防止
public class SineWaveHover : MonoBehaviour
{
    // --- 設定項目(インスペクターから編集) ---

    [Header("浮遊設定")]

    [Tooltip("上下にどれくらい揺らすか(振幅)。0.25〜0.5 くらいが扱いやすいです。")]
    [SerializeField] private float amplitude = 0.25f;

    [Tooltip("1秒あたりの揺れの速さ。2〜4 くらいで「ふわふわ」感が出ます。")]
    [SerializeField] private float frequency = 2.0f;

    [Tooltip("ベースの高さに足すオフセット。浮遊開始位置を少し持ち上げたいときに使います。")]
    [SerializeField] private float heightOffset = 0.0f;

    [Header("挙動オプション")]

    [Tooltip("ワールド座標ではなくローカル座標の Y を揺らす場合は ON。")]
    [SerializeField] private bool useLocalPosition = false;

    [Tooltip("再生開始時にランダムな位相を与えて、複数オブジェクトの揺れをずらす。")]
    [SerializeField] private bool randomizePhaseOnStart = true;

    [Tooltip("位相のランダム範囲(-pi〜pi を基準にスケール)。1 で ±π、0.5 で ±π/2 程度。")]
    [SerializeField] [Range(0f, 1f)] private float phaseRandomRange = 1.0f;

    // --- 内部状態 ---

    // 開始時の基準位置(Y以外も保持しておき、X/Zは変えないようにする)
    private Vector3 _basePosition;

    // 正弦波の位相オフセット(複数オブジェクトの揺れをずらすため)
    private float _phaseOffset;

    // 経過時間を自前で管理したいとき用(Time.time でも良いが、制御しやすいように)
    private float _time;

    private void Awake()
    {
        // 開始時の位置を基準位置として保存しておく
        _basePosition = useLocalPosition ? transform.localPosition : transform.position;

        // 位相オフセットを初期化
        _phaseOffset = 0f;
    }

    private void Start()
    {
        // 複数並べたときに同じタイミングで揺れていると「機械的」に見えるので、
        // オプションでランダム位相を与えて少しずらします。
        if (randomizePhaseOnStart && phaseRandomRange > 0f)
        {
            // -π〜+π の範囲でランダムな値を取り、その範囲を phaseRandomRange でスケール
            float maxPhase = Mathf.PI * phaseRandomRange;
            _phaseOffset = Random.Range(-maxPhase, maxPhase);
        }
    }

    private void Update()
    {
        // 経過時間を進める
        // Time.time を直接使っても良いですが、将来的に一時停止やスロー演出を
        // 実装したくなったとき、自前カウンタの方が制御しやすいです。
        _time += Time.deltaTime;

        // 正弦波の値を計算(-1〜+1 の範囲)
        // sin(ωt + φ) の ω が frequency(角速度に相当)、φ が位相オフセット
        float sine = Mathf.Sin(_time * frequency + _phaseOffset);

        // 振幅を掛けて「どれくらい上下させるか」を決める
        float hoverOffset = sine * amplitude;

        // 実際に適用する高さ(基準位置 + オフセット + 正弦波)
        float targetY = _basePosition.y + heightOffset + hoverOffset;

        if (useLocalPosition)
        {
            // ローカル座標での Y のみを変更
            Vector3 localPos = transform.localPosition;
            localPos.y = targetY;
            transform.localPosition = localPos;
        }
        else
        {
            // ワールド座標での Y のみを変更
            Vector3 worldPos = transform.position;
            worldPos.y = targetY;
            transform.position = worldPos;
        }
    }

    /// <summary>
    /// 実行中にベース位置をリセットしたいときに呼ぶヘルパー。
    /// 例えば、エディタ上で高さを調整した後に「ここを新しい基準にしたい」場合など。
    /// </summary>
    public void ResetBasePosition()
    {
        _basePosition = useLocalPosition ? transform.localPosition : transform.position;
    }

    /// <summary>
    /// 実行中に揺れの強さを変えたいとき用のセッター。
    /// </summary>
    public void SetAmplitude(float newAmplitude)
    {
        amplitude = Mathf.Max(0f, newAmplitude);
    }

    /// <summary>
    /// 実行中に揺れの速さを変えたいとき用のセッター。
    /// </summary>
    public void SetFrequency(float newFrequency)
    {
        frequency = Mathf.Max(0f, newFrequency);
    }
}

使い方の手順

ここでは、代表的な使用例として「浮遊するアイテム」「ふわふわしている敵」「上下する足場」の3パターンを想定して手順を説明します。

  1. スクリプトをプロジェクトに追加する
    • Project ウィンドウで任意のフォルダ(例:Scripts/Effects)を右クリック → Create > C# Script
    • 名前を SineWaveHover に変更。
    • 自動生成された中身を、上記のフルコードで丸ごと置き換えて保存。
  2. 浮かせたいオブジェクトにアタッチする
    例として「浮遊するアイテム」を作る場合:
    • Hierarchy で Right Click > 3D Object > Cube などで仮のアイテムを作成。
    • その GameObject に SineWaveHover コンポーネントをドラッグ&ドロップで追加。
    • インスペクターで以下のように調整してみましょう:
      • Amplitude: 0.25
      • Frequency: 2.0
      • Height Offset: 0.5(少し持ち上げたい場合)
      • Use Local Position: オフ(ワールド空間で上下させる)
      • Randomize Phase On Start: オン(複数のアイテムが同時に揺れないように)
  3. 敵キャラやエフェクトにもそのまま流用する
    例:ふわふわ浮いている敵キャラ
    • 敵キャラのプレハブ(例:Enemy_Flying)を開く。
    • ルートの GameObject、もしくは「見た目用」の子オブジェクトに SineWaveHover を追加。
    • 敵の移動ロジックは別コンポーネント(例:EnemyPatrol)に分けておき、このコンポーネントは「浮遊の見た目」だけを担当させるとシンプルです。
    • Use Local Position をオンにしておくと、敵のルートが移動しても、そのローカル位置を基準にふわふわしてくれます。
  4. 動く床(足場)として使う
    例:上下する足場
    • 3D オブジェクト(Cube など)で足場を作成。
    • SineWaveHover をアタッチ。
    • 振幅を少し大きめ(例:Amplitude = 1.0)に設定し、Frequency = 1.0 程度でゆっくり揺らすと、上下する足場として機能します。
    • この足場に別のコンポーネントで「左右移動」や「回転」を付け足すこともできますが、それぞれを別コンポーネントに分けておくと、プレハブの組み合わせだけで多様なギミックを作れて便利です。

メリットと応用

SineWaveHover のように「浮遊演出だけ」を担当するコンポーネントを用意しておくと、プレハブ管理やレベルデザインがかなり楽になります。

  • 見た目の演出をロジックから完全に切り離せる
    敵の AI やプレイヤーの操作ロジックに「ふわふわさせるコード」を混ぜないので、挙動の修正・調整がしやすくなります。
    例えば「この敵は浮遊させないでいいや」と思ったら、コンポーネントを外すだけで済みます。
  • プレハブの再利用性が高まる
    同じアイテムプレハブに対して、シーンごとに振幅や高さオフセットを変えるだけで、違う雰囲気の浮遊演出を簡単に作れます。
    「このステージではゆっくり」「ボス部屋では激しく」など、レベルデザイナーがインスペクターだけで調整可能です。
  • 組み合わせるだけで多様なギミックを作れる
    水平方向の移動コンポーネント、回転コンポーネント、色変更コンポーネントなどと組み合わせることで、コードを書き足さなくても多様な動きを作れます。
    「動く床 + 浮遊」「回転するコイン + 浮遊」など、足し算でギミックを増やしていけるのがコンポーネント指向の良さですね。

応用として、例えば「特定のタイミングで浮遊を一時停止する」機能を追加したい場合、SineWaveHover を直接いじらず、別コンポーネントから制御するのもアリです。
以下は「一定時間だけ浮遊を止める」簡単な改造案の例です。


using UnityEngine;

/// <summary>
/// SineWaveHover を一時停止・再開する簡単な制御コンポーネントの例。
/// 同じ GameObject に SineWaveHover が付いている前提です。
/// </summary>
[RequireComponent(typeof(SineWaveHover))]
public class SineWaveHoverPauser : MonoBehaviour
{
    [SerializeField] private float pauseDuration = 1.0f;

    private SineWaveHover _hover;
    private bool _isPaused;
    private float _pauseTimer;

    private void Awake()
    {
        _hover = GetComponent<SineWaveHover>();
    }

    private void Update()
    {
        if (!_isPaused) return;

        _pauseTimer += Time.deltaTime;
        if (_pauseTimer >= pauseDuration)
        {
            ResumeHover();
        }
    }

    /// <summary>
    /// 外部から呼び出して浮遊を一時停止させる。
    /// 実際の停止方法は、frequency を 0 にするだけのシンプルなもの。
    /// </summary>
    public void PauseHover()
    {
        if (_isPaused) return;

        _isPaused = true;
        _pauseTimer = 0f;
        _hover.SetFrequency(0f);
    }

    private void ResumeHover()
    {
        _isPaused = false;
        // 再開時の周波数はお好みで。ここでは 2.0 を固定値で戻しています。
        _hover.SetFrequency(2.0f);
    }
}

SineWaveHover 自体はあくまで「正弦波で揺らすだけ」の小さな責務にとどめておき、
停止や同期、イベント連動などの複雑な制御は、別コンポーネントで組み合わせていくと、結果として保守しやすいプロジェクトになります。

Update に全部を書きがちな処理を、こうした小さなコンポーネントに分割していく習慣をつけていきましょう。