Godotで効果音を鳴らしていると、同じジャンプ音・攻撃音・足音が何度も連続して「ポコポコ」「カンカン」と鳴って、どうしても機械的でチープに聞こえてしまうことがありますよね。標準的なやり方だと、

  • AudioStreamPlayer / AudioStreamPlayer2D / 3D をそれぞれ継承してカスタムクラスを作る
  • シーンごとに pitch_scale をちょっと変えたプレイヤーを複数用意する
  • 毎回スクリプト側で randf_range() を書いてピッチをいじる

…と、どれも「書く場所」がバラけたり、「深い継承ツリー」や「複製されたノード」が増えがちです。音を鳴らすたびに pitch_scale をちょい変えするだけなのに、管理コストが高いんですよね。

そこでこの記事では、どの AudioStreamPlayer 系ノードにもポン付けできる「コンポーネント」スタイルの RandomPitch を用意して、

  • プレイヤーや敵、UIボタンなど、どのシーンにも同じスクリプトをアタッチするだけ
  • 継承ではなく「合成(Composition)」で音のバリエーションを付ける
  • ノード階層は浅いまま、音の表現力だけ上げる

という方向で解決していきましょう。

【Godot 4】単調なSEにサヨナラ!「RandomPitch」コンポーネント

今回作る RandomPitch は、「音が鳴るたびに pitch_scale をランダムに微調整する」だけの、シンプルだけど汎用性の高いコンポーネントです。

  • AudioStreamPlayer / AudioStreamPlayer2D / AudioStreamPlayer3D いずれにも対応
  • 既存の play() 呼び出しを一切変えなくても動く(通知フック方式)
  • 「足音だけちょっとブレさせたい」「攻撃音はブレ幅を大きめに」など、音種ごとに設定を変えられる

という設計にしてあります。

フルコード:RandomPitch.gd


## RandomPitch.gd
## 任意の AudioStreamPlayer 系ノードの子に置くことで、
## 再生のたびに pitch_scale をランダムに揺らしてくれるコンポーネントです。
##
## 想定親ノード:
## - AudioStreamPlayer
## - AudioStreamPlayer2D
## - AudioStreamPlayer3D
##
## 使い方の例:
##   - 足音SE: 0.9 ~ 1.1 の範囲で毎回ピッチを変える
##   - 攻撃SE: 0.8 ~ 1.2 くらいまで大きく揺らして迫力を出す
##   - UIボタン: ごくわずか (0.98 ~ 1.02) だけ揺らして耳障りを軽減

extends Node
class_name RandomPitch

## ランダムピッチを有効にするかどうか。
## 一時的にオフにしたいときはインスペクタからチェックを外すだけでOK。
@export var enabled: bool = true

## ピッチの基準値。
## 通常は 1.0 のままでOK。音程全体を少し高く/低くしたいときに使います。
@export_range(0.1, 3.0, 0.01)
@export var base_pitch: float = 1.0

## ランダム変化の幅(最小側)。
## 実際のピッチは base_pitch * randf_range(min_multiplier, max_multiplier) で決まります。
## 例: base_pitch = 1.0, min_multiplier = 0.9, max_multiplier = 1.1
##     → 0.9 ~ 1.1 の範囲でランダム
@export_range(0.1, 3.0, 0.01)
@export var min_multiplier: float = 0.95

## ランダム変化の幅(最大側)。
@export_range(0.1, 3.0, 3.0)
@export var max_multiplier: float = 1.05

## フレームごとに変えるのではなく、「再生の直前にだけ変える」コンポーネントなので
## _process は不要です。シグナルや通知をフックして動きます。

## 親にアタッチされた AudioStreamPlayer 系ノードをキャッシュしておく。
var _player: Object = null

func _ready() -> void:
    ## 親ノードが AudioStreamPlayer / 2D / 3D のいずれかか確認します。
    _player = _find_audio_player()
    if _player == null:
        push_warning("[RandomPitch] 親ノードが AudioStreamPlayer / 2D / 3D ではありません。機能しません。")
        return

    ## すでに音が再生中にシーンが読み込まれるケースはレアですが、
    ## 将来的な拡張を考えてシグナル接続しておきます。
    ## ここでは "finished" を拾うだけで、主な仕事は _notification() で行います。
    if _player.has_signal("finished"):
        _player.finished.connect(_on_player_finished)

    ## Godot 4 では、ノードのライフサイクル通知を _notification で受け取れます。
    ## AudioStreamPlayer が play() されると NOTIFICATION_POST_ENTER_TREE → NOTIFICATION_INTERNAL_PROCESS
    ## などが飛んできますが、ここではより直接的に
    ## 「再生直前の pitch_scale をセットする」ために、play() の直前/直後をフックします。
    ## とはいえ、スクリプトから play() をオーバーライドするのは避けたいので、
    ## 通知ベースで「再生開始」を検知します。

    ## 親の pitch_scale を初期化しておきます。
    _apply_random_pitch()

func _notification(what: int) -> void:
    ## 通知を使って「再生開始」を検知する方法はいくつかありますが、
    ## シンプルに毎フレーム監視するのではなく、
    ## 「前フレームと今フレームの playing 状態の変化」を見る方式にします。
    if what == NOTIFICATION_INTERNAL_PROCESS:
        if _player == null or !enabled:
            return

        ## 前回の状態を static 変数的に保持する代わりに、
        ## プレイヤー側にメタデータを仕込む方法を使います。
        var was_playing: bool = _player.get_meta_or_null("_random_pitch_was_playing") as bool
        var is_playing: bool = _player.playing

        if is_playing and !was_playing:
            ## ここで「今フレームから再生が始まった」と判定できるので、
            ## 再生直前にピッチをランダム化します。
            _apply_random_pitch()

        _player.set_meta("_random_pitch_was_playing", is_playing)

func _find_audio_player() -> Object:
    ## 親ノードを取得し、AudioStreamPlayer 系かどうかをチェックします。
    var parent := get_parent()
    if parent == null:
        return null

    if parent is AudioStreamPlayer:
        return parent
    if parent is AudioStreamPlayer2D:
        return parent
    if parent is AudioStreamPlayer3D:
        return parent

    return null

func _apply_random_pitch() -> void:
    if _player == null or !enabled:
        return

    ## min/max の値が逆転している場合に備えて補正しておきます。
    var min_val := min(min_multiplier, max_multiplier)
    var max_val := max(min_multiplier, max_multiplier)

    ## ランダムな倍率を決定します。
    var random_mul := randf_range(min_val, max_val)

    ## 実際の pitch_scale = base_pitch * random_mul
    var new_pitch := base_pitch * random_mul

    ## AudioStreamPlayer 系はすべて pitch_scale プロパティを持っています。
    _player.pitch_scale = new_pitch

func _on_player_finished() -> void:
    ## 再生終了時に何かしたい場合のフック。
    ## 今回は特に何もしませんが、将来的な拡張ポイントとして残しておきます。
    pass


## --- 便利メソッド群(任意で呼び出し可) --- ##

## 一度だけ明示的にピッチを更新したいときに使えます。
func randomize_now() -> void:
    _apply_random_pitch()

## 設定をまとめて変更するためのヘルパー。
func configure(base: float, min_mul: float, max_mul: float) -> void:
    base_pitch = base
    min_multiplier = min_mul
    max_multiplier = max_mul

使い方の手順

ここからは、実際に「プレイヤーの足音」「敵の攻撃音」「UIボタンのクリック音」を例にしながら、RandomPitch コンポーネントの使い方を見ていきましょう。

① スクリプトファイルを用意する
上のコードを res://components/audio/RandomPitch.gd など、プロジェクト内の分かりやすい場所に保存します。
class_name RandomPitch を付けているので、どこからでも「RandomPitch」として参照できます。

② AudioStreamPlayer の子としてノードを追加
例として、2Dゲームのプレイヤーの足音SEを考えます。シーン構成はこんな感じにしておきましょう。

Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── FootstepPlayer (AudioStreamPlayer2D)
│ └── RandomPitch (Node) <-- ★ このノードに RandomPitch.gd をアタッチ
└── AnotherComponent (Node)

FootstepPlayer は普通の AudioStreamPlayer2D です。

その子として Node を追加し、スクリプトに RandomPitch.gd を指定します。

名前は「RandomPitch」にしておくと分かりやすいですね。

③ インスペクタでパラメータを調整
RandomPitch ノードを選択すると、以下のプロパティが見えます。


  • enabled: 有効/無効の切り替え

  • base_pitch: ピッチの基準値(通常は 1.0)

  • min_multiplier: ランダム倍率の最小値

  • max_multiplier: ランダム倍率の最大値


例えば足音なら、



  • base_pitch = 1.0

  • min_multiplier = 0.9

  • max_multiplier = 1.1


くらいにしておくと、毎回ちょっとだけ高さの違う足音になってくれます。


④ いつも通り play() を呼ぶだけ
プレイヤーのスクリプト側では、特別なことは何もする必要はありません。
いつも通り FootstepPlayer.play() を呼ぶだけで、再生直前に RandomPitch が pitch_scale をランダムにセットしてくれます。

# Player.gd(例)
extends CharacterBody2D

@onready var footstep_player: AudioStreamPlayer2D = $FootstepPlayer

func _physics_process(delta: float) -> void:
if is_on_floor() and abs(velocity.x) > 10.0:
if !footstep_player.playing:
footstep_player.play()

 

これだけで、全ての足音が「微妙に違う音程」で鳴るようになります。

別の使用例:敵の攻撃音とUIボタン

同じコンポーネントを、敵やUIにもそのまま使い回せます。

敵の攻撃音

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AttackPlayer (AudioStreamPlayer2D)
 │    └── RandomPitch (Node)
 └── HealthComponent (Node)
  • 敵A: min_multiplier = 0.8, max_multiplier = 1.2(大きく揺らして迫力重視)
  • 敵B: min_multiplier = 0.95, max_multiplier = 1.05(微妙な違いだけ出す)

どちらも AttackPlayer.play() を呼ぶだけでOKです。

UIボタンのクリック音

MainMenu (Control)
 ├── StartButton (Button)
 │    └── ClickPlayer (AudioStreamPlayer)
 │         └── RandomPitch (Node)
 └── OptionsButton (Button)
      └── ClickPlayer (AudioStreamPlayer)
           └── RandomPitch (Node)

UIはあまり音が暴れると違和感が出るので、

  • base_pitch = 1.0
  • min_multiplier = 0.98
  • max_multiplier = 1.02

くらいのごくわずかな揺らぎにしておくと、「ずっと同じ音が鳴っている感じ」が薄れて心地よくなります。

メリットと応用

この RandomPitch コンポーネントを使うことで、いくつか嬉しいポイントがあります。

  • 継承しないのでシーン構造がシンプル
    AudioStreamPlayer を継承した「CustomAudioStreamPlayer」みたいなクラスを量産せずに済みます。
    親はあくまで標準の AudioStreamPlayer、そこに「RandomPitch コンポーネント」を子としてぶら下げるだけです。
  • どのシーンにも同じコンポーネントをポン付けできる
    プレイヤー、敵、UI、環境音…どこでも同じ RandomPitch.gd を再利用できます。
    「足音の揺らぎをもう少し大きくしたい」と思ったら、そのシーンの RandomPitch だけ調整すればOK。
  • スクリプト側のロジックがスッキリ
    毎回 player.pitch_scale = randf_range(...) と書かなくてよくなります。
    「音を鳴らすコード」は play() だけに集中できるので、読みやすくてメンテしやすいです。
  • ゲーム全体の「耳触り」が良くなる
    足音や攻撃音のような頻繁に鳴るSEほど、単調さがストレスになります。
    ピッチを少し揺らすだけで、かなり「生っぽい」印象に変わります。

改造案:音量も一緒にランダム化する

「ピッチだけじゃなくて、音量も少しバラけさせたい」というケースも多いと思います。その場合は、こんな感じで _apply_random_pitch() を拡張して、音量用の export 変数と処理を足してみましょう。


# 追記: 音量ランダム用のパラメータ
@export_range(0.0, 1.0, 0.01)
var min_volume_db: float = -3.0

@export_range(-60.0, 0.0, 0.01)
var max_volume_db: float = 0.0

func _apply_random_pitch() -> void:
    if _player == null or !enabled:
        return

    var min_val := min(min_multiplier, max_multiplier)
    var max_val := max(min_multiplier, max_multiplier)
    var random_mul := randf_range(min_val, max_val)
    var new_pitch := base_pitch * random_mul
    _player.pitch_scale = new_pitch

    # ここから音量ランダム化
    var v_min := min(min_volume_db, max_volume_db)
    var v_max := max(min_volume_db, max_volume_db)
    var new_volume_db := randf_range(v_min, v_max)
    _player.volume_db = new_volume_db

こんなふうに、コンポーネントとして独立していると、「音のゆらぎ」を担当する責務をこの1ファイルに閉じ込めておけます。継承に頼らず、必要な振る舞いを「合成」していくスタイルで、Godotのオーディオ周りもどんどん整理していきましょう。