Godotでアクションゲームやレースゲームを作っていると、「スピード感」をどう表現するかが悩みどころですよね。
よくあるのは、プレイヤーシーンに直接「集中線の処理」を書き込んだり、プレイヤーを継承した別クラスを作って、そこでエフェクトを追加するパターンです。

でもこのやり方だと、

  • プレイヤーのスクリプトが「移動ロジック」と「演出ロジック」でどんどん肥大化する
  • 敵や乗り物など、別のキャラに同じ集中線を付けたい時にコピペ地獄になる
  • 「UIとしての集中線」と「キャラに紐づく集中線」がごちゃっと混ざりがち

といった問題が出てきます。いわゆる「継承ベタベタ+巨大スクリプト」パターンですね。

そこで今回は、ノードにポン付けするだけで、移動速度に応じて画面端から中心に向かう集中線エフェクトを出してくれるコンポーネント、「SpeedLines」を用意しました。
プレイヤーでも、敵でも、カメラでも、「速度情報」さえ渡せば同じコンポーネントを使い回せます。

【Godot 4】スピード感をノードにポン付け!「SpeedLines」コンポーネント

このコンポーネントは、画面全体にオーバーレイする2Dエフェクトとして設計しています。
対象の「速度ベクトル」を受け取り、その大きさに応じて

  • 集中線の本数
  • 長さ
  • 透明度

などを変化させます。
描画は CanvasItem_draw() を使って、中央から外側に伸びるラインを毎フレーム描いています。


フルコード:SpeedLines.gd


extends Control
class_name SpeedLines
"""
画面端から中心に向かう「集中線」エフェクトコンポーネント。

・任意のノードの「速度ベクトル」を渡すことで、スピード感を表現
・カメラに追従させる UI 的なエフェクトとして使う想定
・プレイヤー以外(敵、車、弾丸カメラなど)にも再利用可能

想定する使い方:
  - 画面全体を覆う Control として配置(フルスクリーン)
  - 対象のノードから速度を渡す:
      speed_lines.velocity = player.velocity
  - もしくは、速度を通知するシグナルに接続して反映する
"""

@export_category("基本設定")
## 集中線を描く最大数(速度が最大のとき)
@export_range(4, 256, 1)
var max_lines: int = 80

## 速度がこの値以上のときに、集中線が最大強度になる
@export_range(10.0, 2000.0, 10.0)
var max_speed: float = 600.0

## 速度がこの値未満のときは、集中線を描かない(しきい値)
@export_range(0.0, 500.0, 10.0)
var min_speed: float = 150.0

## 集中線の基本色
@export_color_no_alpha
var line_color: Color = Color.WHITE

## 集中線の透明度(0〜1)
@export_range(0.0, 1.0, 0.01)
var base_alpha: float = 0.6

## 集中線の太さ(ピクセル)
@export_range(1.0, 10.0, 0.5)
var line_width: float = 2.0


@export_category("長さ・ゆらぎ")
## 線の最小長さ(ピクセル)
@export_range(10.0, 500.0, 5.0)
var min_length: float = 80.0

## 線の最大長さ(ピクセル)
@export_range(10.0, 800.0, 5.0)
var max_length: float = 300.0

## 線の先端をどれくらい「ぼかす」か(0〜1)
@export_range(0.0, 1.0, 0.01)
var fade_tail: float = 0.7

## ランダムなゆらぎの強さ(0で完全に均一)
@export_range(0.0, 1.0, 0.01)
var jitter_strength: float = 0.25


@export_category("方向・更新")
## 線の向きを「速度の逆方向」にする(デフォルト:true)
## 例:プレイヤーが右に進むと、線は左から右方向に向かって伸びる
@export
var use_inverse_direction: bool = true

## 毎フレーム、自動的に再描画するか
## false にすると、手動で queue_redraw() を呼ぶ必要がある
@export
var auto_redraw: bool = true

## 疑似ランダムのシード(同じ値だとパターンが安定する)
@export
var random_seed: int = 12345


@export_category("デバッグ")
## 実際の速度値をラベル表示する(デバッグ用)
@export
var show_debug_speed: bool = false

## ラベルのフォントサイズ
@export_range(8, 64, 1)
var debug_font_size: int = 16


## 対象の速度ベクトル(外部からセットする想定)
var velocity: Vector2 = Vector2.ZERO:
    set(value):
        velocity = value
        if auto_redraw:
            queue_redraw()

## 内部用の乱数生成器
var _rng := RandomNumberGenerator.new()


func _ready() -> void:
    # フルスクリーン UI として使う想定なので、マウス入力をブロックしないようにしておく
    mouse_filter = Control.MOUSE_FILTER_IGNORE
    _rng.seed = random_seed
    # 初回描画
    queue_redraw()


func _process(_delta: float) -> void:
    if auto_redraw:
        queue_redraw()


func _draw() -> void:
    var speed := velocity.length()
    if speed <= min_speed:
        # しきい値以下なら何も描かない
        return

    var center := get_viewport_rect().size * 0.5

    # 速度から「強度(0〜1)」を算出
    var t := clamp((speed - min_speed) / max(0.001, (max_speed - min_speed)), 0.0, 1.0)

    # 描画する線の本数
    var line_count: int = int(lerp(4.0, float(max_lines), t))

    # 速度ベクトルの向き
    var dir := velocity.normalized()
    if dir == Vector2.ZERO:
        return

    if use_inverse_direction:
        dir = -dir

    # 基本角度(速度方向)
    var base_angle := dir.angle()

    # 線を扇状にばらけさせる角度幅(ラジアン)
    # 速度が速いほど、やや広がるようにする
    var spread := lerp(deg_to_rad(5.0), deg_to_rad(25.0), t)

    # 透明度も速度に応じて変化
    var alpha := base_alpha * t

    # 疑似ランダムを毎フレーム同じパターンにしたい場合は seed を固定
    _rng.seed = random_seed

    # 背景を少し暗くする場合は、ここで半透明の矩形を描くのもアリ
    # draw_rect(Rect2(Vector2.ZERO, get_viewport_rect().size), Color(0, 0, 0, 0.1 * t), true)

    for i in line_count:
        var ratio := 0.0
        if line_count > 1:
            ratio = float(i) / float(line_count - 1)  # 0〜1

        # -0.5〜0.5 の範囲で左右に散らす
        var offset := (ratio - 0.5) * 2.0

        # 角度にランダムなゆらぎを加える
        var jitter_angle := (_rng.randf() - 0.5) * spread * jitter_strength * 2.0

        var angle := base_angle + offset * spread + jitter_angle
        var dir_i := Vector2.RIGHT.rotated(angle)

        # 長さもランダムに揺らす
        var len := lerp(min_length, max_length, t)
        var len_jitter := (0.5 + _rng.randf() * 0.5)  # 0.5〜1.0
        len *= lerp(1.0, len_jitter, jitter_strength)

        # 線の始点・終点
        # 始点は中心から少しだけ離した位置にして、真ん中が見えるようにする
        var inner_offset := len * 0.1
        var from := center + dir_i * inner_offset
        var to := center + dir_i * len

        # 線の先端ほど透明になるようにグラデーションをつける
        var col_start := line_color.with_alpha(alpha)
        var col_end := line_color.with_alpha(alpha * (1.0 - fade_tail))

        # 2本の線で簡易グラデーション
        var mid := from.lerp(to, 0.5)
        draw_line(from, mid, col_start, line_width)
        draw_line(mid, to, col_end, line_width)

    if show_debug_speed:
        _draw_debug_speed(speed)


func _draw_debug_speed(speed: float) -> void:
    var text := "Speed: %.1f" % speed
    var font := ThemeDB.fallback_font
    var font_size := debug_font_size
    var pos := Vector2(10, 10 + font_size)
    draw_string(font, pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size, Color(1, 1, 1, 0.8))


# --- 公開API(便利メソッド) ---

## 速度ベクトルを「大きさ+向き」でまとめてセットするユーティリティ
func set_speed_from_direction(direction: Vector2, speed: float) -> void:
    if direction == Vector2.ZERO:
        velocity = Vector2.ZERO
    else:
        velocity = direction.normalized() * speed


## 単純に「速度の大きさ」だけを更新したい場合用
func set_speed_magnitude(speed: float) -> void:
    var dir := velocity.normalized()
    velocity = dir * speed

使い方の手順

ここでは、2Dアクションゲームのプレイヤーに集中線を付ける例で説明します。

手順①:コンポーネントをプロジェクトに追加

  1. res://components/SpeedLines.gd など、好きな場所に上記コードを保存します。
  2. Godotエディタを再読み込みすると、ノード追加時に「SpeedLines」がスクリプトクラスとして選べるようになります。

手順②:UIレイヤーに SpeedLines ノードを配置

プレイヤーに直接ぶら下げても良いですが、カメラに追従するUIとして扱うのがおすすめです。例えばこんなシーン構成:

Main (Node2D)
 ├── Player (CharacterBody2D)
 │    ├── Sprite2D
 │    ├── CollisionShape2D
 │    └── Camera2D
 └── CanvasLayer
      └── SpeedLines (Control / class_name SpeedLines)
  • CanvasLayer を使うことで、ゲーム画面に対して常にフルスクリーンで集中線を表示できます。
  • SpeedLines ノードの LayoutFull Rect にして、画面全体を覆うようにしておきましょう。

手順③:プレイヤーの速度を SpeedLines に渡す

プレイヤーのスクリプトから、毎フレーム velocity を渡します。
(ここでは Godot 4 の典型的な CharacterBody2D ベースのプレイヤーを想定しています)


# Player.gd
extends CharacterBody2D

@export var move_speed: float = 300.0

var input_dir: Vector2 = Vector2.ZERO

# SpeedLines への参照(エディタからドラッグ&ドロップで割り当て)
@export var speed_lines: SpeedLines


func _physics_process(delta: float) -> void:
    input_dir = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    velocity = input_dir * move_speed
    move_and_slide()

    # SpeedLines に速度を渡す
    if speed_lines:
        speed_lines.velocity = velocity

これだけで、プレイヤーが一定以上の速度で動いたときに、画面中央から外側に向かって集中線が表示されます。
プレイヤーが止まると、速度が min_speed を下回るので、自動的に集中線は消えます。

手順④:敵や乗り物にも簡単に流用

同じ SpeedLines コンポーネントを、敵や車にも使い回すことができます。例えば、レースゲームの「ブースト中だけ集中線を出す車」の例:

RaceCar (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SpeedLines (Control / class_name SpeedLines)

# RaceCar.gd
extends CharacterBody2D

@onready var speed_lines: SpeedLines = $SpeedLines

var current_speed: float = 0.0
var is_boosting: bool = false


func _physics_process(delta: float) -> void:
    # ここでは例として、前方向に進むだけのシンプルな車
    var forward := Vector2.RIGHT.rotated(rotation)
    current_speed = 400.0
    if is_boosting:
        current_speed = 800.0

    velocity = forward * current_speed
    move_and_slide()

    # ブースト中だけ集中線を有効化
    if is_boosting:
        speed_lines.velocity = velocity
        speed_lines.visible = true
    else:
        speed_lines.velocity = Vector2.ZERO
        speed_lines.visible = false

このように、速度ベクトルを渡すだけで同じコンポーネントを複数のシーンで使い回せるのが、コンポーネント指向の強みですね。


メリットと応用

SpeedLines をコンポーネントとして切り出すことで、次のようなメリットがあります。

  • プレイヤーのスクリプトがスリムになる
    移動ロジックはプレイヤー、演出ロジックは SpeedLines に分離されるので、責務がはっきりします。
  • シーン構造がフラットで見通しが良い
    「PlayerWithSpeedLines」「EnemyWithSpeedLines」みたいな継承ツリーを増やさず、
    ただ SpeedLines ノードをアタッチするだけで機能追加できます。
  • パラメータ調整が楽
    集中線の本数・長さ・透明度・しきい値などを、エディタ上から即座に変更できます。
    プレイヤーごとに違うスピード感を演出するのも簡単です。
  • ゲーム全体の「スピード感」を一括管理できる
    例えば、メインシーンに1つだけ SpeedLines を置いて、
    今一番速いオブジェクトの速度を渡すようにすれば、「ゲーム全体の勢い」を表現するHUDとして使えます。

応用としては、

  • ダッシュ中だけ max_lines を増やして演出を派手にする
  • ブレーキ時は逆向きの集中線を出して「減速感」を演出する
  • シェーダーと組み合わせて、画面の周辺をブラーさせる

といった発展も考えられます。

改造案:ブースト中だけ「色」を変える

例えば、「ブースト中は集中線を青色にする」みたいな演出をしたい場合、SpeedLines に簡単な関数を追加するだけで対応できます。


# SpeedLines.gd の末尾あたりに追加

## ブースト状態に応じて色を切り替える
func set_boost_mode(is_boosting: bool) -> void:
    if is_boosting:
        line_color = Color.CYAN
        base_alpha = 0.8
        max_lines = 120
    else:
        line_color = Color.WHITE
        base_alpha = 0.6
        max_lines = 80
    queue_redraw()

これをプレイヤー側から


# Player.gd の例
func _on_boost_started() -> void:
    if speed_lines:
        speed_lines.set_boost_mode(true)

func _on_boost_ended() -> void:
    if speed_lines:
        speed_lines.set_boost_mode(false)

のように呼び出せば、ブースト演出もコンポーネント側にきれいに集約できます。
「継承より合成」で、演出コンポーネントをどんどん積み上げていきましょう。