Godotで「ダッシュ中だけ集中線エフェクトを出したい」と思ったとき、ありがちな実装はこんな感じですよね。

  • プレイヤーシーンに CanvasLayer を生やす
  • その下に ColorRect を置いてシェーダーを書く
  • プレイヤースクリプトから $CanvasLayer/ColorRect を直接いじる

最初はこれでも動きますが、だんだん問題が出てきます。

  • プレイヤー、敵、乗り物など「速く動くもの」全部に同じ処理をコピペする羽目になる
  • エフェクトの見た目を調整したいのに、プレイヤースクリプトがどんどん肥大化していく
  • 「UI系ノード(CanvasLayer)」と「ゲームロジック(移動速度判定)」がガッツリ結合していてテストしづらい

そこで登場するのが、今回のコンポーネント SpeedLineEffect です。
移動速度の監視と、集中線エフェクトの表示制御をひとつのコンポーネントに閉じ込めて、どのキャラクターにもポン付けできるようにしてしまいましょう。

【Godot 4】ダッシュ時に画面端へ集中線!「SpeedLineEffect」コンポーネント

このコンポーネントはざっくり言うと:

  • 親ノード(プレイヤーや敵など)の移動速度を毎フレーム監視
  • 一定以上の速度になったら、画面端に集中線エフェクトをフェードイン
  • 遅くなったらフェードアウト

ということを自動でやってくれる UI制御+ロジック一体型コンポーネントです。
エフェクトは ColorRect + Shader で実装し、シェーダーパラメータもエクスポート変数で調整可能にしてあります。


フルソースコード(SpeedLineEffect.gd)


extends CanvasLayer
class_name SpeedLineEffect
## 移動速度に応じて画面端に集中線エフェクトを表示するコンポーネント。
## 親ノード(例: Player)が移動しているとき、その速度を監視して
## 一定以上なら集中線をフェードイン、未満ならフェードアウトします。
##
## 想定:
## - 親ノードは 2D の移動体 (CharacterBody2D, RigidBody2D, Node2D など)
## - 親の速度ベクトルをどう取得するかは @export で選択可能

@export_category("速度判定")
## 速度の取得方法
enum VelocitySource {
	PARENT_HAS_VELOCITY_PROPERTY, ## 親が velocity プロパティを持っている (例: CharacterBody2D)
	PARENT_IS_NODE2D_AND_WE_DIFF_POSITION, ## Node2D の位置差分から速度を推定
	CUSTOM_SIGNAL, ## 外部から update_speed() を呼んでもらう
}

@export var velocity_source: VelocitySource = VelocitySource.PARENT_HAS_VELOCITY_PROPERTY

## 集中線が有効になる速度のしきい値(ピクセル/秒)
@export var speed_threshold: float = 200.0

## しきい値を超えたときに最終的に到達する強さ(0.0〜1.0)
@export_range(0.0, 1.0, 0.01)
var max_intensity: float = 1.0

## 強さの変化スピード(フェードイン・アウトの補間速度)
@export var intensity_lerp_speed: float = 5.0

@export_category("シェーダー設定")
## 集中線のベースカラー
@export var line_color: Color = Color(1, 1, 1, 0.8)

## 集中線の密度(値を大きくすると線が細かくなる)
@export var line_density: float = 40.0

## 集中線のブラー量(0 でカリッとした線、値を上げるとぼかし)
@export var line_blur: float = 0.4

## 集中線の中心方向(画面のどこに向かって集中するか)
## 例: (0.5, 0.5) で画面中央、(0.5, 0.8) でやや下寄り
@export var focus_point: Vector2 = Vector2(0.5, 0.5)

## プレイヤーの進行方向に応じて集中線の向きを変えるか
@export var align_with_velocity: bool = true

@export_category("パフォーマンス")
## 集中線を描画する解像度のスケール
## 1.0 = フル解像度, 0.5 = 横/縦半分の解像度で描画 (軽くなるが少し荒くなる)
@export_range(0.25, 1.0, 0.05)
var render_scale: float = 1.0


# --- 内部用変数 ---

var _color_rect: ColorRect
var _material: ShaderMaterial
var _current_intensity: float = 0.0
var _target_intensity: float = 0.0

var _last_global_position: Vector2
var _current_speed: float = 0.0


func _ready() -> void:
	# このコンポーネントは UI 用なので、2Dシーンの上に常に表示される CanvasLayer を使います。
	# 自分自身の下に ColorRect を自動生成し、そこにシェーダーを適用します。
	_create_color_rect()
	_setup_shader()
	
	# 初期位置を記録(Node2D から速度を推定する場合に使用)
	if owner and owner is Node2D:
		_last_global_position = (owner as Node2D).global_position
	
	# 最初は非表示寄りにしておく
	_update_shader_intensity()


func _process(delta: float) -> void:
	# 毎フレーム、速度を更新してターゲット強度を決める
	_update_speed_from_source(delta)
	_update_target_intensity()
	_update_current_intensity(delta)
	_update_shader_intensity()
	_update_shader_direction()


# =========================
# 速度関連
# =========================

func _update_speed_from_source(delta: float) -> void:
	if not owner:
		_current_speed = 0.0
		return
	
	match velocity_source:
		VelocitySource.PARENT_HAS_VELOCITY_PROPERTY:
			# CharacterBody2D など velocity プロパティを持つノードを想定
			if "velocity" in owner:
				var v: Variant = owner.get("velocity")
				if v is Vector2:
					_current_speed = (v as Vector2).length()
				else:
					_current_speed = 0.0
			else:
				_current_speed = 0.0
		
		VelocitySource.PARENT_IS_NODE2D_AND_WE_DIFF_POSITION:
			if owner is Node2D:
				var node := owner as Node2D
				var current_pos: Vector2 = node.global_position
				var distance: float = current_pos.distance_to(_last_global_position)
				# delta 秒間に distance 動いたので、速度 = distance / delta
				if delta > 0.0:
					_current_speed = distance / delta
				else:
					_current_speed = 0.0
				_last_global_position = current_pos
			else:
				_current_speed = 0.0
		
		VelocitySource.CUSTOM_SIGNAL:
			# 外部から update_speed() を呼んでもらう前提なので、ここでは何もしない
			pass


## 外部から任意のタイミングで速度を更新したい場合に使う。
## 例: プレイヤーのスクリプトから:
##   $SpeedLineEffect.update_speed(velocity.length())
func update_speed(speed: float) -> void:
	_current_speed = max(speed, 0.0)


func _update_target_intensity() -> void:
	if _current_speed >= speed_threshold:
		_target_intensity = max_intensity
	else:
		_target_intensity = 0.0


func _update_current_intensity(delta: float) -> void:
	# 線形補間でなめらかに変化させる
	_current_intensity = lerp(_current_intensity, _target_intensity, 1.0 - exp(-intensity_lerp_speed * delta))


# =========================
# シェーダー関連
# =========================

func _create_color_rect() -> void:
	_color_rect = ColorRect.new()
	_color_rect.name = "SpeedLineRect"
	_color_rect.anchor_left = 0.0
	_color_rect.anchor_top = 0.0
	_color_rect.anchor_right = 1.0
	_color_rect.anchor_bottom = 1.0
	_color_rect.offset_left = 0.0
	_color_rect.offset_top = 0.0
	_color_rect.offset_right = 0.0
	_color_rect.offset_bottom = 0.0
	
	# 透明なベースカラー。実際の見た目はシェーダーで決める
	_color_rect.color = Color(0, 0, 0, 0)
	
	add_child(_color_rect)


func _setup_shader() -> void:
	var shader_code := _get_shader_code()
	var shader := Shader.new()
	shader.code = shader_code
	
	_material = ShaderMaterial.new()
	_material.shader = shader
	_color_rect.material = _material
	
	# 初期パラメータを反映
	_material.set_shader_parameter("u_intensity", _current_intensity)
	_material.set_shader_parameter("u_line_color", line_color)
	_material.set_shader_parameter("u_line_density", line_density)
	_material.set_shader_parameter("u_line_blur", line_blur)
	_material.set_shader_parameter("u_focus_point", focus_point)
	_material.set_shader_parameter("u_render_scale", render_scale)
	_material.set_shader_parameter("u_direction_angle", 0.0)


func _update_shader_intensity() -> void:
	if not _material:
		return
	
	_material.set_shader_parameter("u_intensity", _current_intensity)
	_material.set_shader_parameter("u_line_color", line_color)
	_material.set_shader_parameter("u_line_density", line_density)
	_material.set_shader_parameter("u_line_blur", line_blur)
	_material.set_shader_parameter("u_focus_point", focus_point)
	_material.set_shader_parameter("u_render_scale", render_scale)


func _update_shader_direction() -> void:
	if not _material or not align_with_velocity:
		return
	
	if _current_speed <= 0.1:
		_material.set_shader_parameter("u_direction_angle", 0.0)
		return
	
	var vel: Vector2
	
	match velocity_source:
		VelocitySource.PARENT_HAS_VELOCITY_PROPERTY:
			if owner and "velocity" in owner:
				var v: Variant = owner.get("velocity")
				if v is Vector2:
					vel = v
		VelocitySource.PARENT_IS_NODE2D_AND_WE_DIFF_POSITION:
			# すでに _current_speed は更新済みだが、方向は position 差分から再計算する必要がある
			if owner and owner is Node2D:
				var node := owner as Node2D
				var current_pos: Vector2 = node.global_position
				var dir: Vector2 = current_pos - _last_global_position
				vel = dir
		VelocitySource.CUSTOM_SIGNAL:
			# CUSTOM の場合は向きの情報がないので、ここでは 0 にしておく。
			# 必要なら別途 set_direction_angle() を追加してもよい。
			vel = Vector2.ZERO
	
	if vel.length() > 0.1:
		# 進行方向とは逆向きに線が伸びるようにしたいので +PI を足す
		var angle: float = vel.angle() + PI
		_material.set_shader_parameter("u_direction_angle", angle)


## 外部から進行方向を指定したい場合に使用(align_with_velocity = false のときなど)
func set_direction_angle(angle_radians: float) -> void:
	if _material:
		_material.set_shader_parameter("u_direction_angle", angle_radians)


func _get_shader_code() -> String:
	# シンプルな集中線シェーダー。
	# 画面のある一点 (u_focus_point) に向かって線が収束するようなパターンを生成します。
	# u_intensity が 0 だと完全に透明、1 で最大の強さになります。
	return """
shader_type canvas_item;

uniform float u_intensity : hint_range(0.0, 1.0) = 0.0;
uniform vec4  u_line_color : source_color = vec4(1.0, 1.0, 1.0, 0.8);
uniform float u_line_density = 40.0;
uniform float u_line_blur = 0.4;
uniform vec2  u_focus_point = vec2(0.5, 0.5);
uniform float u_render_scale = 1.0;
uniform float u_direction_angle = 0.0;

void fragment() {
	// 何も表示しない場合は早期リターン
	if (u_intensity < 0.01) {
		COLOR = vec4(0.0);
		return;
	}
	
	// UV を中心からのベクトルに変換
	vec2 uv = UV;
	
	// 解像度スケーリング(低解像度で計算して軽くする)
	uv = (uv - 0.5) * u_render_scale + 0.5;
	
	vec2 dir = uv - u_focus_point;
	
	// 方向を回転(プレイヤーの進行方向に合わせる)
	float s = sin(u_direction_angle);
	float c = cos(u_direction_angle);
	vec2 rotated = vec2(
		dir.x * c - dir.y * s,
		dir.x * s + dir.y * c
	);
	
	// 角度成分を取り出す
	float angle = atan(rotated.y, rotated.x);
	
	// 角度に対して周期的なパターンを生成
	float line = sin(angle * u_line_density);
	line = abs(line); // 線を正の値に
	
	// ブラー的に滑らかにする
	line = pow(1.0 - line, 1.0 / max(u_line_blur, 0.001));
	
	// 中心から遠いほど強くする(スピード線っぽさ)
	float dist = length(dir);
	float edge_boost = smoothstep(0.2, 0.8, dist);
	
	float alpha = line * edge_boost * u_intensity * u_line_color.a;
	
	COLOR = vec4(u_line_color.rgb, alpha);
}
"""



使い方の手順

ここでは、プレイヤーがダッシュしたときに集中線を出す例で説明します。

① スクリプトをプロジェクトに追加

  1. res://components/SpeedLineEffect.gd など、好きな場所に上記コードを保存します。
  2. Godot エディタを再読み込みすると、ノード追加ダイアログの「CanvasLayer」カテゴリに SpeedLineEffect が出てくるはずです。

② プレイヤーシーンにアタッチ

例として、典型的な 2D プレイヤーシーンはこんな感じだとします:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SpeedLineEffect (CanvasLayer)   ← これを追加
  1. Player シーンを開く
  2. 右クリック → 「子ノードを追加」 → SpeedLineEffect を検索して追加
  3. インスペクタで以下のように設定
    • velocity_sourcePARENT_HAS_VELOCITY_PROPERTY(CharacterBody2D の velocity を使う)
    • speed_threshold:例)200(この速度以上で集中線オン)
    • max_intensity:例)0.8
    • line_density:例)40 ~ 70(数字が大きいほど線が細かく)
    • focus_point(0.5, 0.5)(画面中央に集中)

CharacterBody2D はデフォルトで velocity: Vector2 プロパティを持っているので、特別なコードは不要です。
いつも通りに velocity を更新しているだけで、SpeedLineEffect が勝手に速度を監視してくれます。

③ ダッシュ/ブースト時だけ強めにしたい場合

「通常移動ではあまり出さず、ダッシュ時だけガッツリ出したい」場合は、単純に speed_threshold をダッシュ速度に合わせるのがおすすめです。

例えば、プレイヤー側がこんな感じだとします:


# Player.gd (CharacterBody2D)
extends CharacterBody2D

const WALK_SPEED := 150.0
const DASH_SPEED := 350.0

var is_dashing := false

func _physics_process(delta: float) -> void:
	var input_dir := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	
	if Input.is_action_just_pressed("dash"):
		is_dashing = true
	elif Input.is_action_just_released("dash"):
		is_dashing = false
	
	var speed := is_dashing ? DASH_SPEED : WALK_SPEED
	velocity = input_dir * speed
	move_and_slide()

この場合、SpeedLineEffect 側で speed_threshold = 250 くらいにしておけば、ダッシュ速度のときだけ集中線がオンになります。

④ 敵や乗り物にも再利用する

同じコンポーネントを、例えば高速で突進してくる敵や、乗り物シーンにもそのまま使えます。

例:突進する敵シーン

ChargerEnemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── SpeedLineEffect (CanvasLayer)
  • 敵 AI が velocity を更新していれば、SpeedLineEffect は何も知らなくても勝手に反応
  • プレイヤーと敵で見た目を変えたいなら、それぞれの line_colorline_density を変えるだけ

ノード構成を変えずに「速いものには全部同じコンポーネントを付ける」というスタイルにできるので、継承ツリーをいじらずにエフェクトだけ合成できるのがポイントですね。


メリットと応用

この SpeedLineEffect コンポーネントを使うメリットをいくつか挙げてみます。

  • シーン構造がスッキリ
    プレイヤーや敵のスクリプトから「CanvasLayer を探して ColorRect のマテリアルを書き換える」ようなコードが消えます。
    速度の更新は本体、エフェクトの表示はコンポーネントという役割分担が明確になります。
  • 再利用性が高い
    どのシーンでも「速く動くもの」にポン付けできるので、SpeedLineEffect のチューニングを 1 箇所で済ませられます。
  • テストやデバッグがしやすい
    コンポーネントを単体でシーンに置いて、update_speed() をテスト的に呼ぶことで、見た目だけをデバッグすることもできます。
  • 継承に縛られない
    「高速移動するキャラ用のベースクラス」を作らなくても、普通の CharacterBody2DNode2D にコンポーネントを追加するだけで機能を合成できます。

ちょい改造案:ブースト時にだけ色を変える

例えば「ブーストボタンを押している間だけ、集中線を赤くする」といった演出も、コンポーネント側にちょっとフックを追加するだけで実現できます。

SpeedLineEffect.gd に、こんなメソッドを追加してみましょう:


## 一時的に色をオーバーライドする簡易 API。
## override_color を null にすると元の line_color に戻す。
func set_override_color(override_color: Color) -> void:
	if not _material:
		return
	# シェーダー側に u_line_color をそのまま渡しているだけなので、
	# ここで直接上書きすれば即座に反映されます。
	_material.set_shader_parameter("u_line_color", override_color)

そしてプレイヤー側では:


# Player.gd の一部
@onready var speed_line: SpeedLineEffect = $SpeedLineEffect

func _process(delta: float) -> void:
	if Input.is_action_pressed("dash"):
		speed_line.set_override_color(Color(1, 0.3, 0.3, 0.9)) # 赤っぽい
	else:
		speed_line.set_override_color(speed_line.line_color)   # 元の色に戻す

こんな感じで、コンポーネントの外側から「エフェクトの味付け」だけを変えることができます。
ロジック本体(速度判定やフェード処理)は変えずに、見た目だけ差し替えられるのはコンポーネント指向の強みですね。

このコンポーネントをベースに、「ブレーキ時に逆向きの集中線」「被弾時に画面端を赤くフラッシュ」など、UI エフェクトをどんどん合成していきましょう。