Unityを触り始めた頃って、何でもかんでも Update() に書きがちですよね。プレイヤーの移動処理、カメラ追従、エフェクトの制御、UIの更新…全部ひとつのスクリプトに押し込んでしまうと、少し仕様を変えたいだけでもコード全体を読まないといけなくなります。

とくに「スピード感の演出」みたいなビジュアル系の処理をプレイヤー制御スクリプトの中に書いてしまうと、

  • 移動ロジックと見た目のロジックが混ざって読みにくい
  • 敵や乗り物など、別のオブジェクトで同じ演出を使い回しづらい
  • デザイナーさんがパラメータを調整しにくい

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

そこでこの記事では、「移動速度が上がると画面端から中心に向かう集中線エフェクトを出す」役割だけに責務を絞った 「SpeedLines」コンポーネント を作っていきます。プレイヤーの移動スクリプトとは独立させておくことで、後から別のオブジェクトにも簡単に流用できるようにしましょう。

【Unity】スピード感を足すだけ担当!「SpeedLines」コンポーネント

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

  • カメラにアタッチするだけで集中線エフェクトを描画
  • 対象オブジェクト(例: プレイヤー)の 速度 に応じてエフェクトの強さを自動調整
  • インスペクターから本数・長さ・太さ・色・フェード速度などを調整可能
  • Unity6(URP/HDRP/ビルトイン問わず)で動くよう、GL を使ったシンプルなライン描画

フルコード:SpeedLines.cs


using UnityEngine;

/// <summary>
/// 移動速度に応じて画面端から中心に向かう集中線を描画するコンポーネント。
/// カメラにアタッチして使うことを想定。
/// </summary>
[RequireComponent(typeof(Camera))]
public class SpeedLines : MonoBehaviour
{
    // --- 参照設定 ---

    [Header("速度の参照元")]
    [Tooltip("速度を計測する対象。例: プレイヤーのTransform")]
    [SerializeField] private Transform targetTransform;

    [Tooltip("速度を計算する際の座標空間。World = ワールド座標、Local = ローカル座標")]
    [SerializeField] private Space velocitySpace = Space.World;

    // --- 速度と強さのパラメータ ---

    [Header("速度とエフェクト強度")]
    [Tooltip("この速度以下では集中線は表示されない")]
    [SerializeField] private float minSpeedToShow = 5f;

    [Tooltip("この速度以上でエフェクト強度が最大になる")]
    [SerializeField] private float maxSpeedForFullEffect = 20f;

    [Tooltip("エフェクト強度の変化をスムーズにするための補間速度")]
    [SerializeField] private float intensityLerpSpeed = 5f;

    // --- 線の見た目 ---

    [Header("線の見た目")]
    [Tooltip("1フレームあたりに描画する線の本数")]
    [SerializeField] private int lineCount = 40;

    [Tooltip("線の長さ(画面の半径に対する割合)。0.0〜1.0くらいで調整")]
    [SerializeField] private float lineLength = 0.4f;

    [Tooltip("線の太さ(画面の高さに対する割合)。0.0〜0.01くらいで調整")]
    [SerializeField] private float lineThickness = 0.002f;

    [Tooltip("線の色(アルファ値はここでは無視される。後述のfadeIn/outで制御)")]
    [SerializeField] private Color lineColor = Color.white;

    [Tooltip("線の中心(画面中央からのオフセット)。(0,0)で画面中央")]
    [SerializeField] private Vector2 screenCenterOffset = Vector2.zero;

    // --- フェードとランダム性 ---

    [Header("フェードと揺らぎ")]
    [Tooltip("エフェクトがONになるときのフェードイン速度")]
    [SerializeField] private float fadeInSpeed = 4f;

    [Tooltip("エフェクトがOFFになるときのフェードアウト速度")]
    [SerializeField] private float fadeOutSpeed = 2f;

    [Tooltip("毎フレームのランダムシード。0にすると Time.time を元に自動で揺らぐ")]
    [SerializeField] private int fixedRandomSeed = 0;

    [Tooltip("線の角度にランダムな揺らぎを加える量(度数)")]
    [SerializeField] private float angleJitterDegree = 5f;

    [Tooltip("線の開始位置を少し外側にずらす割合(0で画面端から、0.5でさらに外から)")]
    [SerializeField] private float startOffsetFactor = 0.0f;

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

    private Camera _camera;
    private Vector3 _lastPosition;
    private bool _hasLastPosition;
    private float _currentIntensity; // 0.0〜1.0
    private Material _lineMaterial;

    // GL用のシンプルなマテリアルを生成
    private void Awake()
    {
        _camera = GetComponent<Camera>();
        CreateLineMaterial();
    }

    private void Start()
    {
        if (targetTransform != null)
        {
            _lastPosition = GetTargetPosition();
            _hasLastPosition = true;
        }
    }

    private void Update()
    {
        // 対象が設定されていなければ何もしない
        if (targetTransform == null)
        {
            // 徐々にフェードアウトさせる
            _currentIntensity = Mathf.MoveTowards(_currentIntensity, 0f, fadeOutSpeed * Time.deltaTime);
            return;
        }

        // 速度を計算
        Vector3 currentPos = GetTargetPosition();

        if (!_hasLastPosition)
        {
            _lastPosition = currentPos;
            _hasLastPosition = true;
            return;
        }

        float distance = (currentPos - _lastPosition).magnitude;
        float speed = distance / Mathf.Max(Time.deltaTime, 0.0001f);

        _lastPosition = currentPos;

        // 速度を 0〜1 の強度にマッピング
        float targetIntensity = 0f;
        if (speed >= minSpeedToShow)
        {
            float t = Mathf.InverseLerp(minSpeedToShow, maxSpeedForFullEffect, speed);
            targetIntensity = Mathf.Clamp01(t);
        }

        // 目標強度に向けて補間
        float lerpSpeed = targetIntensity > _currentIntensity ? fadeInSpeed : fadeOutSpeed;
        _currentIntensity = Mathf.MoveTowards(_currentIntensity, targetIntensity, lerpSpeed * Time.deltaTime);
    }

    /// <summary>
    /// カメラがシーンを描画した後に呼ばれる。ここでGLラインを描画する。
    /// </summary>
    private void OnPostRender()
    {
        if (_lineMaterial == null) return;
        if (_currentIntensity <= 0.001f) return;
        if (lineCount <= 0) return;

        // マテリアルをセット
        _lineMaterial.SetPass(0);

        // 2Dのスクリーン空間で描画(-1〜1の範囲)
        GL.PushMatrix();
        GL.LoadOrtho(); // 0〜1の正規化スクリーン座標

        GL.Begin(GL.QUADS);

        // ランダムシードの決定
        System.Random random;
        if (fixedRandomSeed != 0)
        {
            random = new System.Random(fixedRandomSeed);
        }
        else
        {
            // Time.time を元に毎フレーム変化するシード
            int seed = Mathf.FloorToInt(Time.time * 1000f);
            random = new System.Random(seed);
        }

        // スクリーン中心(0〜1空間)
        Vector2 center = new Vector2(0.5f + screenCenterOffset.x, 0.5f + screenCenterOffset.y);

        // スクリーンの半径(対角線の半分くらいを目安にする)
        float radius = 0.75f; // 0.5だと辺、0.75だと少し外まで

        // 実際の線の長さ
        float length = radius * Mathf.Clamp01(lineLength);

        // 線の太さ(縦方向の割合)
        float thickness = Mathf.Max(0f, lineThickness);

        // アルファを強度に応じて変化させる
        Color c = lineColor;
        c.a = _currentIntensity;

        for (int i = 0; i < lineCount; i++)
        {
            // 0〜360度のランダムな角度
            float baseAngle = (float)random.NextDouble() * 360f;

            // 揺らぎを加える
            float jitter = ((float)random.NextDouble() * 2f - 1f) * angleJitterDegree;
            float angleDeg = baseAngle + jitter;
            float angleRad = angleDeg * Mathf.Deg2Rad;

            // 方向ベクトル(単位ベクトル)
            Vector2 dir = new Vector2(Mathf.Cos(angleRad), Mathf.Sin(angleRad));

            // 線の開始位置と終了位置(スクリーン座標0〜1)
            float startDist = radius * startOffsetFactor;
            Vector2 start = center + dir * startDist;
            Vector2 end = center + dir * (startDist + length);

            // 線の太さ方向(法線ベクトル)
            Vector2 normal = new Vector2(-dir.y, dir.x) * thickness;

            // 4頂点を計算(QUADとして描画)
            Vector2 v0 = start - normal;
            Vector2 v1 = start + normal;
            Vector2 v2 = end + normal;
            Vector2 v3 = end - normal;

            GL.Color(c);

            GL.Vertex3(v0.x, v0.y, 0f);
            GL.Vertex3(v1.x, v1.y, 0f);
            GL.Vertex3(v2.x, v2.y, 0f);
            GL.Vertex3(v3.x, v3.y, 0f);
        }

        GL.End();
        GL.PopMatrix();
    }

    /// <summary>
    /// 対象の位置を、指定された座標空間で取得する。
    /// </summary>
    private Vector3 GetTargetPosition()
    {
        if (velocitySpace == Space.World)
        {
            return targetTransform.position;
        }
        else
        {
            // ローカル座標空間での位置を疑似的に扱う
            return targetTransform.localPosition;
        }
    }

    /// <summary>
    /// GLでラインを描画するためのシンプルなマテリアルを生成する。
    /// </summary>
    private void CreateLineMaterial()
    {
        if (_lineMaterial != null) return;

        // 非常にシンプルな頂点色シェーダー
        Shader shader = Shader.Find("Hidden/Internal-Colored");
        if (shader == null)
        {
            Debug.LogError("SpeedLines: シェーダー 'Hidden/Internal-Colored' が見つかりません。");
            return;
        }

        _lineMaterial = new Material(shader);
        _lineMaterial.hideFlags = HideFlags.HideAndDontSave;

        // アルファブレンドを有効化
        _lineMaterial.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
        _lineMaterial.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
        _lineMaterial.SetInt("_Cull", (int)UnityEngine.Rendering.CullMode.Off);
        _lineMaterial.SetInt("_ZWrite", 0);
    }

    /// <summary>
    /// 外部から対象Transformを動的に差し替えたい場合用のSetter。
    /// </summary>
    /// <param name="newTarget">速度を参照するTransform</param>
    public void SetTarget(Transform newTarget)
    {
        targetTransform = newTarget;
        _hasLastPosition = false; // 次フレームで再初期化
    }

    private void OnDestroy()
    {
        if (_lineMaterial != null)
        {
            DestroyImmediate(_lineMaterial);
        }
    }
}

使い方の手順

ここからは、実際にプレイヤーの移動に合わせて集中線を出す例で手順を説明します。

  1. コンポーネントを作成する
    • プロジェクトビューで SpeedLines.cs を作成し、上記コードをコピペして保存します。
    • コンパイルが通ると、インスペクターからアタッチできるようになります。
  2. カメラにアタッチする
    • シーン内の Main Camera(または使用中のカメラ)を選択します。
    • Add Component ボタンから SpeedLines を追加します。
    • これで描画自体はカメラ側で行われるようになります。
  3. 対象Transformを設定する(例: プレイヤー)
    • プレイヤー用のGameObject(例: Player)をシーンに置いておきます。
    • Main Camera のインスペクターにある SpeedLines コンポーネントの Target Transform に、Player の Transform をドラッグ&ドロップします。
    • これでプレイヤーの移動速度に応じて集中線が出るようになります。

    もし「動く床」や「高速で走る敵キャラ」にも同じ集中線を出したい場合は、

    • 別のカメラをそのオブジェクト専用に用意して SpeedLines をアタッチする
    • もしくはスクリプトから SetTarget() を呼んで対象を差し替える

    といった使い方ができます。

  4. パラメータを調整して好みの見た目にする
    代表的な調整ポイントは次の通りです。
    • Min Speed To Show: これより遅いときは集中線を出さない(歩きはOFF、ダッシュからONなど)。
    • Max Speed For Full Effect: この速度で集中線の強さが最大になる。
    • Line Count: 線の本数。増やしすぎると描画コストが上がるので注意。
    • Line Length: 線の長さ。0.4〜0.6くらいから調整すると扱いやすいです。
    • Line Thickness: 線の太さ。0.001〜0.004くらいがゲーム向きの範囲です。
    • Line Color: 線の色。白以外に、やや青みを入れるとSFっぽくなります。
    • Angle Jitter Degree: 線の角度の揺らぎ。0にすると完全に放射状、増やすと少しバラけてスピード感が増します。
    • Start Offset Factor: 線の開始位置。0だと画面端から、0.3などにすると画面のさらに外側から伸びてくるような印象になります。

    プレイヤーの移動スクリプト側では一切エフェクトのことを意識せず、SpeedLines は「速度を見て勝手に演出してくれる係」 として独立しているのがポイントです。

メリットと応用

この「SpeedLines」コンポーネントを使うメリットは、単にスピード感が出るだけではありません。

  • プレイヤー制御とエフェクト制御の責務分離
    プレイヤーの移動スクリプトは「どの方向にどれだけ動くか」だけを書き、スピードラインは「どのくらい速いかを見て画面に線を出す」だけに責務を分けられます。Resultとして、どちらのコードも読みやすくなります。
  • プレハブ化しやすく、レベルデザインが楽
    SpeedLines をアタッチしたカメラをプレハブにしておけば、別シーンでも同じスピード感をすぐ再利用できます。シーンごとに Line ColorLine Count を変えるだけで、ステージごとの雰囲気を簡単に変えられます。
  • 対象を差し替えるだけで別オブジェクトにも流用可能
    レースゲームであれば、プレイヤーだけでなくライバル車のリプレイカメラにも同じコンポーネントを使えます。空を飛ぶドラゴン、ロケット、ジェットコースターなど、速度感を出したいオブジェクトなら何にでも使い回せます。
  • アート側がパラメータを触りやすい
    線の本数・長さ・色などのパラメータはすべてインスペクターから調整可能なので、プログラマが触らなくても、デザイナーやレベルデザイナーがゲーム内で直接チューニングしやすい構成になっています。

さらに、少しコードを足すだけでいろいろな応用ができます。例えば、「ダッシュ中だけ線の色を変える」ようにしたい場合は、次のようなメソッドを追加して、プレイヤー側から呼び出すのもアリです。


    /// <summary>
    /// 外部から一時的に線の色を上書きする簡単な改造案。
    /// 例: ダッシュ中は青、通常時は白に戻す など。
    /// </summary>
    /// <param name="color">新しく設定したい線の色</param>
    public void OverrideLineColor(Color color)
    {
        lineColor = color;
    }

このように、小さなコンポーネントを組み合わせて「移動」「カメラ」「エフェクト」を分離しておくと、後からの仕様変更や演出追加がかなり楽になります。SpeedLines もそのひとつのパーツとして、シーンごと・オブジェクトごとにポンポン差し替えられるようにしておくと、プロジェクト全体の見通しが良くなりますね。