Unityを触り始めた頃は、Update() の中に「移動処理」「入力処理」「UI更新」「エフェクト制御」などを全部まとめて書いてしまいがちですよね。最初は動くのですが、あとから「ダッシュを追加したい」「UIを変更したい」といった要望が出てくると、巨大な PlayerController.cs を開くたびに憂鬱になります。

とくに「スピード感のある演出」を入れようとすると、プレイヤーの移動ロジックに直接エフェクト処理を書き足してしまいがちです。

  • 移動速度の計算
  • カメラの追従
  • ダッシュのクールダウン
  • 速度に応じた集中線エフェクト

これらを1つのクラスで面倒を見ると、テストも再利用も難しくなります。そこで、「移動速度に応じた集中線エフェクトだけ」を担当する小さなコンポーネントとして切り出しておくと、プレイヤーにも乗り物にも簡単に使い回せて便利です。

この記事では、移動速度が上がったときに画面端に集中線エフェクトを表示するためのコンポーネント SpeedLineEffect を作っていきます。プレイヤー本体のスクリプトは「移動」に専念させて、演出はこのコンポーネントに丸投げする構成ですね。

【Unity】ダッシュ時のスピード感を底上げ!「SpeedLineEffect」コンポーネント

ここでは以下のような要件を満たすコンポーネントを実装します。

  • Rigidbody の速度から現在の移動スピードを取得
  • 一定以上のスピードで、画面端に集中線エフェクト(UI)を表示
  • スピードに応じてエフェクトの強さを補間(フェードイン / フェードアウト)
  • 集中線は ColorRect + Shader を想定(ここではマテリアルの色・強度を操作)
  • プレイヤーだけでなく、敵や乗り物にも簡単にアタッチして使い回し可能

フルコード


using UnityEngine;
using UnityEngine.UI;

namespace Samples.VisualEffects
{
    /// <summary>
    /// 移動速度に応じて、画面端の集中線エフェクトを制御するコンポーネント。
    /// 
    /// - Rigidbody の速度からスピードを取得
    /// - 一定以上の速度でエフェクトを表示
    /// - スピードに応じてエフェクトの強さ(アルファや強度)を補間
    /// 
    /// 想定環境:
    /// - UI(Canvas)上に配置された Image / RawImage / ColorRect など
    /// - 集中線用のマテリアル(Shader付き)を割り当てる
    /// 
    /// 責務を「スピードに応じたエフェクト制御」に限定し、
    /// 移動ロジックとは完全に分離しているのがポイントです。
    /// </summary>
    [DisallowMultipleComponent]
    public class SpeedLineEffect : MonoBehaviour
    {
        // --- 参照系 ---

        [Header("速度の参照元")]
        [Tooltip("移動速度を取得する Rigidbody。プレイヤーや乗り物にアタッチされたものを指定します。")]
        [SerializeField] private Rigidbody targetRigidbody;

        [Header("集中線エフェクトの表示先")]
        [Tooltip("画面端に配置した集中線用の UI (Image / RawImage など)。ColorRect でも OK。")]
        [SerializeField] private Graphic speedLineGraphic;

        [Header("マテリアル制御")]
        [Tooltip("集中線用マテリアル。null の場合は speedLineGraphic の material を使用します。")]
        [SerializeField] private Material speedLineMaterialOverride;

        [Tooltip("シェーダー側で強度を受け取るプロパティ名(float)。例: _Intensity")]
        [SerializeField] private string intensityPropertyName = "_Intensity";

        [Tooltip("シェーダー側で色を受け取るプロパティ名(Color)。空なら色は制御しません。")]
        [SerializeField] private string colorPropertyName = "_Color";

        // --- パラメータ ---

        [Header("速度しきい値")]
        [Tooltip("この速度を下回ると集中線エフェクトは完全にオフになります。")]
        [SerializeField] private float minSpeed = 5f;

        [Tooltip("この速度以上で集中線エフェクトは最大強度になります。")]
        [SerializeField] private float maxSpeed = 20f;

        [Header("強度・フェード設定")]
        [Tooltip("エフェクトの最小強度(minSpeed ちょうどのとき)。")]
        [SerializeField] private float minIntensity = 0.0f;

        [Tooltip("エフェクトの最大強度(maxSpeed 以上のとき)。")]
        [SerializeField] private float maxIntensity = 1.0f;

        [Tooltip("強度を補間するときのレスポンス速度。大きいほど素早く変化します。")]
        [SerializeField] private float intensityLerpSpeed = 8f;

        [Header("色設定(任意)")]
        [Tooltip("集中線のベースカラー。colorPropertyName が設定されている場合のみ使用します。")]
        [SerializeField] private Color baseColor = Color.white;

        [Tooltip("色のアルファを速度に応じて変化させるかどうか。")]
        [SerializeField] private bool useAlphaWithIntensity = true;

        [Header("自動セットオプション")]
        [Tooltip("targetRigidbody が未設定なら、親階層から自動取得を試みます。")]
        [SerializeField] private bool autoFindRigidbodyFromParent = true;

        [Tooltip("speedLineGraphic が未設定なら、自身の GameObject から自動取得を試みます。")]
        [SerializeField] private bool autoFindGraphicOnThisObject = true;

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

        private Material runtimeMaterial;   // 実際に操作するマテリアル (インスタンス)
        private int intensityPropertyId;
        private int colorPropertyId;
        private float currentIntensity;     // 補間後の現在強度

        private void Awake()
        {
            // --- 参照の自動取得 ---

            if (targetRigidbody == null && autoFindRigidbodyFromParent)
            {
                // 親階層から Rigidbody を探す
                targetRigidbody = GetComponentInParent<Rigidbody>();
            }

            if (speedLineGraphic == null && autoFindGraphicOnThisObject)
            {
                // 自身にアタッチされた Graphic を探す (Image, RawImage, Text などをまとめた基底クラス)
                speedLineGraphic = GetComponent<Graphic>();
            }

            // 参照チェック(ここで見つからなければ、あとで警告を出す)
            if (speedLineGraphic != null)
            {
                // マテリアルの決定
                if (speedLineMaterialOverride != null)
                {
                    // インスペクターで指定されたマテリアルを複製して使用
                    runtimeMaterial = Instantiate(speedLineMaterialOverride);
                    speedLineGraphic.material = runtimeMaterial;
                }
                else
                {
                    // Graphic が持っているマテリアルを複製して使用
                    if (speedLineGraphic.material != null)
                    {
                        runtimeMaterial = Instantiate(speedLineGraphic.material);
                        speedLineGraphic.material = runtimeMaterial;
                    }
                    else
                    {
                        Debug.LogWarning(
                            $"[SpeedLineEffect] {name} にマテリアルが設定されていません。エフェクトを制御できません。",
                            this
                        );
                    }
                }
            }

            // プロパティIDをキャッシュ
            if (!string.IsNullOrEmpty(intensityPropertyName))
            {
                intensityPropertyId = Shader.PropertyToID(intensityPropertyName);
            }

            if (!string.IsNullOrEmpty(colorPropertyName))
            {
                colorPropertyId = Shader.PropertyToID(colorPropertyName);
            }

            // 初期状態はエフェクトオフ
            currentIntensity = 0f;
            ApplyToMaterial(0f);
        }

        private void Update()
        {
            if (targetRigidbody == null)
            {
                // Rigidbody が未設定なら何もできないので早期リターン
                return;
            }

            if (runtimeMaterial == null)
            {
                // マテリアルが無い場合も何もできない
                return;
            }

            // --- 現在速度の取得 ---

            float speed = targetRigidbody.velocity.magnitude;

            // --- 速度から目標強度を計算 ---

            float targetIntensity = CalculateTargetIntensity(speed);

            // --- スムーズに補間 ---

            currentIntensity = Mathf.Lerp(
                currentIntensity,
                targetIntensity,
                intensityLerpSpeed * Time.deltaTime
            );

            // --- マテリアルへ適用 ---

            ApplyToMaterial(currentIntensity);
        }

        /// <summary>
        /// 現在の速度から、0〜maxIntensity の範囲で目標強度を計算する。
        /// </summary>
        private float CalculateTargetIntensity(float speed)
        {
            // しきい値より遅い場合は 0
            if (speed <= minSpeed)
            {
                return 0f;
            }

            // しきい値より速い場合は 0〜1 に正規化
            float t = Mathf.InverseLerp(minSpeed, maxSpeed, speed);

            // 正規化された t を minIntensity〜maxIntensity にマッピング
            float intensity = Mathf.Lerp(minIntensity, maxIntensity, t);

            return intensity;
        }

        /// <summary>
        /// 計算された強度をマテリアルへ反映する。
        /// </summary>
        private void ApplyToMaterial(float intensity)
        {
            if (runtimeMaterial == null)
            {
                return;
            }

            // Intensity プロパティの適用
            if (intensityPropertyId != 0)
            {
                runtimeMaterial.SetFloat(intensityPropertyId, intensity);
            }

            // Color プロパティの適用(任意)
            if (colorPropertyId != 0)
            {
                Color c = baseColor;

                if (useAlphaWithIntensity)
                {
                    // アルファに強度を掛けて、フェードイン/アウトさせる
                    c.a = baseColor.a * intensity;
                }

                runtimeMaterial.SetColor(colorPropertyId, c);
            }

            // UI Graphic のアルファもまとめて変えたい場合は、こちらを使う方法もあります。
            // (デフォルトでは Shader の Color だけ制御しています)
            //
            // if (speedLineGraphic != null && useAlphaWithIntensity)
            // {
            //     Color gc = speedLineGraphic.color;
            //     gc.a = intensity;
            //     speedLineGraphic.color = gc;
            // }
        }

        /// <summary>
        /// エディタ上でパラメータ変更時に、即座に反映されるようにする(調整用)。
        /// 実行中のみ動作します。
        /// </summary>
        private void OnValidate()
        {
            if (!Application.isPlaying)
            {
                return;
            }

            // パラメータの整合性チェック
            maxSpeed = Mathf.Max(minSpeed + 0.01f, maxSpeed);
            maxIntensity = Mathf.Max(minIntensity, maxIntensity);

            // 現在の強度でマテリアルを更新(インスペクタ調整時の見た目確認用)
            ApplyToMaterial(currentIntensity);
        }

        /// <summary>
        /// 外部から強度をリセットしたい場合用のヘルパー。
        /// 例: リスポーン時にエフェクトを強制的にオフにする。
        /// </summary>
        public void ResetEffect()
        {
            currentIntensity = 0f;
            ApplyToMaterial(0f);
        }
    }
}

使い方の手順

  1. 集中線用のUIを用意する
    • Hierarchy で Canvas を作成(UI > Canvas)。
    • Canvas の子に Image(または RawImage / ColorRect)を作成。
    • RectTransform を画面全体を覆うように広げ、集中線テクスチャを割り当てたスプライト / テクスチャを設定します。
    • 集中線用の Shader(ColorRect+Shader)を使ったマテリアルを作り、Image の Material に設定します。
      例: Shader 側に _Intensity(float)と _Color(Color)を用意しておくと、そのまま制御できます。
  2. SpeedLineEffect コンポーネントをアタッチ
    • 集中線 Image の GameObject を選択し、Add Component から SpeedLineEffect を追加します。
    • SpeedLineEffectSpeed Line Graphic に、その Image コンポーネントが自動で入っていなければドラッグ&ドロップで設定します。
    • Speed Line Material Override に、集中線用マテリアルを指定するか、Image に設定したものをそのまま使う場合は空のままでOKです。
    • Shader のプロパティ名に合わせて、Intensity Property Name(例: _Intensity)、Color Property Name(例: _Color)を設定します。
  3. 速度を参照する Rigidbody を指定
    • プレイヤーキャラクター(例: Player)の GameObject に Rigidbody が付いていることを確認します。
    • Canvas の下にある集中線 Image を選択し、SpeedLineEffectTarget Rigidbody にプレイヤーの Rigidbody をドラッグ&ドロップします。
    • もし集中線 UI をプレイヤーの子オブジェクトとして階層に入れている場合は、Auto Find Rigidbody From Parent をオンにしておけば自動で拾ってくれます。
  4. パラメータを調整して完成
    • Min Speed:この速度を超えたあたりから集中線がうっすら出始めるように設定(例: 5)。
    • Max Speed:この速度以上で集中線が最大になるように設定(例: 20)。
    • Min Intensity / Max Intensity:Shader の見え方に合わせて強度を調整。
    • Intensity Lerp Speed:値を大きくすると、ダッシュ開始・終了時の切り替わりがキビキビします。小さくすると、じわっと変化します。
    • Base ColorUse Alpha With Intensity で、色やフェードの雰囲気を調整します。

    プレイしてみて、プレイヤーがダッシュしたときに画面端の集中線が強くなり、止まるとスッと消えるようなら成功です。

具体的な使用例

  • プレイヤーキャラのダッシュ演出
    3D アクションゲームで、プレイヤーがダッシュボタンを押したときに Rigidbody.velocity が一気に上がる場合、Max Speed をダッシュ速度に合わせておくと、ダッシュ時だけ集中線がドンと出ます。
  • 乗り物・車のスピード感アップ
    レースゲームの車オブジェクトに Rigidbody が付いていれば、その Rigidbody を Target Rigidbody に指定するだけで、最高速付近だけ集中線が強くなるようにできます。UI はカメラに追従する Canvas に置いておけば OK です。
  • 敵ボスの突進攻撃演出
    敵ボスがプレイヤーに向かって高速突進する攻撃のときだけ、ボス用の SpeedLineEffect を有効化すると、「今からやばい攻撃が来る」という演出としても使えます。ボス専用の Canvas / Camera を用意するパターンでも動きます。

メリットと応用

SpeedLineEffect を導入することで、以下のようなメリットがあります。

  • プレイヤーの移動スクリプトを汚さない
    「速度に応じたエフェクト制御」という責務を切り出しているので、PlayerMovementCarController などのクラスは、物理挙動や入力処理だけに集中できます。
    後から「集中線の仕様を変えたい」「別のエフェクトに差し替えたい」となっても、このコンポーネントだけを触ればよくなります。
  • プレハブ化しやすく、レベルデザインが楽
    集中線付きの UI を丸ごとプレハブにしておけば、どのシーンにもドラッグ&ドロップで持ち込めるようになります。
    乗り物ごとに違う集中線を出したい場合も、プレハブを複製してマテリアルやパラメータを変えるだけで済みます。
  • Rigidbody を持つあらゆるオブジェクトに再利用可能
    Rigidbody さえあれば、プレイヤー、敵、乗り物、動く床など、なんでも速度ベースで演出できます。
    たとえば「高速で動くエレベーターに乗ったときだけ、画面端に縦方向の集中線を出す」といったギミックも簡単に作れます。

さらに、コンポーネントを小さく保つことで、テストや差し替えも容易になります。「速度参照元を Rigidbody から別コンポーネントに変える」などの拡張も、インターフェースを差し込むだけで対応しやすくなりますね。

改造案:カメラのFOVも連動させてもっとスピード感を出す

集中線だけでなく、カメラの視野角(FOV)も速度に応じて少し広げると、さらにスピード感が増します。
以下は SpeedLineEffect に追加できる、カメラ FOV 連動用の簡単なメソッド例です。


[SerializeField] private Camera targetCamera;
[SerializeField] private float baseFov = 60f;
[SerializeField] private float maxFov = 75f;

/// <summary>
/// 現在の強度をもとに、カメラの FOV を補間して反映する。
/// Update() の最後あたりから呼び出す想定です。
/// </summary>
private void ApplyFovByIntensity()
{
    if (targetCamera == null)
    {
        return;
    }

    // currentIntensity は 0〜maxIntensity の範囲
    float t = Mathf.InverseLerp(minIntensity, maxIntensity, currentIntensity);
    float fov = Mathf.Lerp(baseFov, maxFov, t);
    targetCamera.fieldOfView = fov;
}

このように、SpeedLineEffect を「速度に応じた演出ハブ」として育てていくと、移動ロジックに一切手を入れずに演出だけ強化していくことができます。コンポーネント指向で小さく分けておくと、こうした改造も怖くないですね。