Godot で車を作るとき、つい Car シーンの中に「移動ロジック」「入力処理」「エンジン音制御」「エフェクト」…と全部盛りにしてしまいがちですよね。
さらに Godot 標準の書き方だと、AudioStreamPlayer を車シーンに直書きして、スクリプトの中で pitch_scale をガリガリいじる…みたいな「継承+巨大スクリプト」構成になりやすいです。
このやり方だと:
- 別の車シーンを作るたびに、同じような「エンジン音ロジック」をコピペする羽目になる
- エンジン音だけ別のチームメンバーが調整したくても、巨大スクリプトを触らないといけない
- 「敵車」「プレイヤー車」「ゴーストカー」など、挙動は違うけどエンジン音の仕様は同じ…というときに使い回しがしづらい
そこで、「継承より合成」の発想で、エンジン音だけを担当するコンポーネントを用意しておくとかなりスッキリします。
今回紹介する EngineSound コンポーネント は、車の速度(Velocity)に合わせてエンジン音のピッチを自動で上げ下げしてくれる小さな Node です。
車本体は「速度を決める」ことだけに集中し、音はこのコンポーネントに丸投げしてしまいましょう。
【Godot 4】速度連動でエンジンが唸る!「EngineSound」コンポーネント
以下は Godot 4 用の GDScript フルコードです。
class_name EngineSound 付きなので、どのシーンからでも「EngineSound」として追加できます。
extends Node
class_name EngineSound
## 車両などの「速度」に応じて AudioStreamPlayer の pitch_scale を変化させるコンポーネント。
## - 速度の取得方法を2種類サポート:
## 1) 直接 velocity ベクトルを参照する (CharacterBody2D/3D, RigidBody など)
## 2) 速度のスカラー値を公開している任意ノードから取得する
## - 速度 0 のときはアイドリング音、最大速度付近で高回転音になるイメージ。
@export_group("参照ノード")
## 速度を持っているノード(例: 車本体の CharacterBody2D 等)
@export var target_body: NodePath
## エンジン音を再生する AudioStreamPlayer / AudioStreamPlayer2D / 3D
@export var audio_player_path: NodePath
@export_group("速度の取得方法")
## true の場合: target_body.velocity から速度を取得する (CharacterBody2D/3D など)
## false の場合: target_body の任意プロパティから取得する (例: current_speed)
@export var use_velocity_property: bool = true
## use_velocity_property = false のときに参照するプロパティ名
## 例: "current_speed", "speed", "linear_velocity_length" など
@export var speed_property_name: StringName = "current_speed"
## 速度ベクトルの長さを使うか (2D/3D 共通)
## use_velocity_property = true のときのみ有効
@export var is_3d: bool = false
@export_group("ピッチ設定")
## 最小ピッチ (停止〜低速時)
@export_range(0.1, 2.0, 0.01)
var min_pitch: float = 0.8
## 最大ピッチ (最高速時)
@export_range(0.1, 4.0, 0.01)
var max_pitch: float = 1.6
## 速度 0〜max_speed を min_pitch〜max_pitch にマッピングする
## これを超えてもピッチはクランプされる
@export var max_speed: float = 200.0
## ピッチ変化のスムージング係数 (0 だと即時反映、1 に近いほどなめらか)
@export_range(0.0, 1.0, 0.01)
var smoothing: float = 0.2
@export_group("音量・自動再生")
## 停止時に音量を下げるか (アイドリング音が別で用意できない場合の簡易対策)
@export var lower_volume_on_stop: bool = true
## lower_volume_on_stop が true のとき、停止時の音量 (dB)
@export_range(-80.0, 0.0, 0.1)
var idle_volume_db: float = -6.0
## lower_volume_on_stop が true のとき、最高速時の音量 (dB)
@export_range(-80.0, 0.0, 0.1)
var max_volume_db: float = 0.0
## コンポーネントが ready になったとき、自動的に再生を開始するか
@export var autoplay: bool = true
## 速度がこの値未満なら「停止」に近いとみなす
@export var stop_threshold: float = 0.5
# 内部用キャッシュ
var _audio_player: AudioStreamPlayer
var _target: Node
var _current_pitch: float
func _ready() -> void:
# 参照ノードの解決
if target_body != NodePath(""):
_target = get_node_or_null(target_body)
else:
_target = get_parent() # 未指定なら親ノードを対象とする (車本体にアタッチする想定)
if _target == null:
push_warning("EngineSound: target_body が見つかりません。速度を取得できません。")
if audio_player_path != NodePath(""):
_audio_player = get_node_or_null(audio_player_path)
else:
# 未指定なら、自分の子から AudioStreamPlayer 系を探す
_audio_player = _find_audio_player_in_children()
if _audio_player == null:
push_warning("EngineSound: AudioStreamPlayer が見つかりません。ピッチを制御できません。")
else:
_current_pitch = min_pitch
_audio_player.pitch_scale = _current_pitch
if autoplay and _audio_player.stream != null:
_audio_player.play()
func _process(delta: float) -> void:
if _audio_player == null or _target == null:
return
var speed: float = _get_speed()
var t := 0.0
if max_speed > 0.0:
# 0〜1 に正規化してクランプ
t = clamp(speed / max_speed, 0.0, 1.0)
# 速度に応じてピッチを線形補間
var target_pitch := lerp(min_pitch, max_pitch, t)
# スムージング (指数的な補間に近い動き)
if smoothing > 0.0:
_current_pitch = lerp(_current_pitch, target_pitch, 1.0 - pow(1.0 - smoothing, delta * 60.0))
else:
_current_pitch = target_pitch
_audio_player.pitch_scale = _current_pitch
# オプション: 速度に応じて音量も変える
if lower_volume_on_stop:
var vol_t := clamp(speed / max_speed, 0.0, 1.0)
var target_db := lerp(idle_volume_db, max_volume_db, vol_t)
_audio_player.volume_db = target_db
func _get_speed() -> float:
# 速度の取得ロジックをまとめた関数
if use_velocity_property:
# CharacterBody2D / 3D, RigidBody などの velocity ベクトルを参照
if is_3d:
# 3D の場合: velocity: Vector3 を想定
if "velocity" in _target:
var v3: Vector3 = _target.velocity
return v3.length()
else:
# 2D の場合: velocity: Vector2 を想定
if "velocity" in _target:
var v2: Vector2 = _target.velocity
return v2.length()
else:
# 任意の speed プロパティを参照 (float を想定)
if speed_property_name in _target:
var s = _target.get(speed_property_name)
if typeof(s) == TYPE_FLOAT or typeof(s) == TYPE_INT:
return float(s)
return 0.0
func _find_audio_player_in_children() -> AudioStreamPlayer:
# 自分の子孫から最初に見つかった AudioStreamPlayer 系を返すヘルパー
for child in get_children():
if child is AudioStreamPlayer:
return child
# ネストされている場合もチェックしたいときは再帰的に探してもよい
return null
使い方の手順
ここでは 2D のレーシングゲームを例にして、「プレイヤー車」と「敵車」の両方に同じ EngineSound コンポーネントを付けるパターンを紹介します。
① シーン構成を用意する
まずはプレイヤー車のシーン構成例です。
PlayerCar (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── AudioStreamPlayer2D # エンジン音のループ素材をセット └── EngineSound (Node) # ← このコンポーネントをアタッチ
敵車もほぼ同じ構成で OK です。
EnemyCar (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── AudioStreamPlayer2D # 敵専用のエンジン音素材にしてもよい └── EngineSound (Node)
EngineSound は ただの Node なので、どのシーンにもペタッと貼り付けられます。
車の挙動は PlayerCar.gd / EnemyCar.gd に任せて、音は EngineSound に任せる、という役割分担ですね。
② AudioStreamPlayer にエンジン音ループを設定する
AudioStreamPlayer2Dを選択し、streamにエンジン音のループ素材(WAV/OGG 等)を設定します。- ループ素材がない場合は、短いエンジン音をループ再生できるように編集しておくと自然になります。
この時点ではまだピッチは変化しません。普通の BGM のように鳴るだけです。
③ EngineSound のエクスポート変数を設定する
EngineSound ノードを選択し、インスペクタから以下を設定します。
- target_body: 車本体の
CharacterBody2Dを指定(未設定なら自動で親を対象) - audio_player_path: 子の
AudioStreamPlayer2Dを指定(未設定なら子から自動探索) - use_velocity_property:
trueのままで OK(CharacterBody2D.velocityを使う) - is_3d: 2D なので
falseのまま - max_speed: 自分のゲームで想定している最高速度に合わせて調整(例: 300.0)
- min_pitch / max_pitch: 好みに応じて調整(例: 0.9〜1.8)
- smoothing: 0.15〜0.3 あたりにすると自然な加速感が出ます
- lower_volume_on_stop: アイドリング音が素材に含まれているなら
true推奨
プレイヤー車と敵車で「エンジン音のキャラ」を変えたい場合は:
- プレイヤー車:
max_pitch = 1.6、idle_volume_db = -4.0 - 敵車:
max_pitch = 2.0、idle_volume_db = -8.0(ちょっと遠くで鳴ってる感じ)
のようにパラメータだけ変えてあげると、同じコンポーネントでも違うキャラ付けができます。
④ 自前の速度プロパティを使う場合(MovingPlatform など)
車以外にも、例えば「動く床」の速度に応じてモーター音を変えたい、というケースもあります。
この場合、「velocity プロパティを持っていないノード」でも、EngineSound をそのまま使えます。
MovingPlatform (Node2D) ├── Sprite2D ├── AudioStreamPlayer2D └── EngineSound
例えば MovingPlatform.gd がこんな感じだとします:
extends Node2D
@export var speed: float = 120.0
var current_speed: float = 0.0
func _process(delta: float) -> void:
# ここでは単純に左右に往復する例
var dir := sin(Time.get_ticks_msec() / 500.0)
current_speed = abs(dir) * speed
position.x += dir * speed * delta
このとき、EngineSound 側では:
- use_velocity_property:
false - speed_property_name:
"current_speed"
と設定すれば、MovingPlatform の current_speed を読んでピッチを変えてくれます。
「速度の定義」は各オブジェクトに任せて、EngineSound は「スカラーの speed さえくれれば何でも鳴らすよ」というスタンスです。
メリットと応用
EngineSound をコンポーネントとして切り出すことで、次のようなメリットがあります。
- 車クラスがスリムになる
移動ロジックと音ロジックが分離されるので、「車の挙動を直したいだけなのに、音のコードが邪魔」という状態を避けられます。 - どのオブジェクトにも再利用できる
車、バイク、動く床、エレベーター、ジェットパックなど、「速度に応じて音が変わるもの」なら何でも同じコンポーネントで対応できます。 - シーン階層が浅くても機能を盛り込める
「エンジン音専用のサブシーン」を作らずに、EngineSoundノードをポンと足すだけで機能追加できるので、ノード階層が無駄に深くなりません。 - サウンド担当が触りやすい
サウンドデザイナーが Godot エディタ上でmin_pitchやmax_pitchをいじるだけで音のキャラを調整でき、ゲームロジックには触らなくて済みます。
応用としては、例えば「ターボボタンを押している間だけピッチをさらに上げる」「ドリフト中は別のレイヤーの音を足す」などが考えられます。
以下は、ターボ中だけピッチをブーストする改造案の一例です。
## EngineSound に追加できる簡易ターボブースト機能の例
@export_group("ターボブースト")
@export var turbo_pitch_boost: float = 0.3 # ターボ中にどれだけピッチを上乗せするか
var turbo_active: bool = false
func set_turbo_active(active: bool) -> void:
turbo_active = active
func _process(delta: float) -> void:
if _audio_player == null or _target == null:
return
var speed: float = _get_speed()
var t := 0.0
if max_speed > 0.0:
t = clamp(speed / max_speed, 0.0, 1.0)
var target_pitch := lerp(min_pitch, max_pitch, t)
# ターボ中はピッチを上乗せ
if turbo_active:
target_pitch += turbo_pitch_boost
if smoothing > 0.0:
_current_pitch = lerp(_current_pitch, target_pitch, 1.0 - pow(1.0 - smoothing, delta * 60.0))
else:
_current_pitch = target_pitch
_audio_player.pitch_scale = _current_pitch
if lower_volume_on_stop:
var vol_t := clamp(speed / max_speed, 0.0, 1.0)
var target_db := lerp(idle_volume_db, max_volume_db, vol_t)
_audio_player.volume_db = target_db
プレイヤー側からは、例えば Input.is_action_pressed("turbo") を見て engine_sound.set_turbo_active(true/false) を呼ぶだけです。
このように、エンジン音に関する機能追加はすべてコンポーネント側に閉じ込めることで、車クラスをこれ以上太らせずに済みます。
「速度に応じて音が変化する」というのはゲームの気持ちよさに直結する要素なので、ぜひコンポーネント化してガンガン使い回していきましょう。




