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);
}
"""
使い方の手順
ここでは、プレイヤーがダッシュしたときに集中線を出す例で説明します。
① スクリプトをプロジェクトに追加
res://components/SpeedLineEffect.gdなど、好きな場所に上記コードを保存します。- Godot エディタを再読み込みすると、ノード追加ダイアログの「CanvasLayer」カテゴリに SpeedLineEffect が出てくるはずです。
② プレイヤーシーンにアタッチ
例として、典型的な 2D プレイヤーシーンはこんな感じだとします:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── SpeedLineEffect (CanvasLayer) ← これを追加
- Player シーンを開く
- 右クリック → 「子ノードを追加」 →
SpeedLineEffectを検索して追加 - インスペクタで以下のように設定
velocity_source:PARENT_HAS_VELOCITY_PROPERTY(CharacterBody2D のvelocityを使う)speed_threshold:例)200(この速度以上で集中線オン)max_intensity:例)0.8line_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_colorやline_densityを変えるだけ
ノード構成を変えずに「速いものには全部同じコンポーネントを付ける」というスタイルにできるので、継承ツリーをいじらずにエフェクトだけ合成できるのがポイントですね。
メリットと応用
この SpeedLineEffect コンポーネントを使うメリットをいくつか挙げてみます。
- シーン構造がスッキリ
プレイヤーや敵のスクリプトから「CanvasLayer を探して ColorRect のマテリアルを書き換える」ようなコードが消えます。
速度の更新は本体、エフェクトの表示はコンポーネントという役割分担が明確になります。 - 再利用性が高い
どのシーンでも「速く動くもの」にポン付けできるので、SpeedLineEffectのチューニングを 1 箇所で済ませられます。 - テストやデバッグがしやすい
コンポーネントを単体でシーンに置いて、update_speed()をテスト的に呼ぶことで、見た目だけをデバッグすることもできます。 - 継承に縛られない
「高速移動するキャラ用のベースクラス」を作らなくても、普通のCharacterBody2DやNode2Dにコンポーネントを追加するだけで機能を合成できます。
ちょい改造案:ブースト時にだけ色を変える
例えば「ブーストボタンを押している間だけ、集中線を赤くする」といった演出も、コンポーネント側にちょっとフックを追加するだけで実現できます。
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 エフェクトをどんどん合成していきましょう。
