Godot 4でキャラクターの足音を実装するとき、ついこんな構成にしがちですよね。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AudioStreamPlayer2D (歩き用)
 ├── AudioStreamPlayer2D (走り用)
 ├── AudioStreamPlayer2D (砂用)
 └── Player.gd(中に足音ロジック全部入り)

Player.gd の中で

  • 「歩き中ならこの AudioStreamPlayer2D を再生」
  • 「走り中ならこっち」
  • 「ピッチをちょっと変えたいから pitch_scale を毎回ランダムに…」

みたいな処理を書いていくと、だんだん プレイヤースクリプトが肥大化 していきます。さらに、敵キャラや動く床などにも足音を付けたくなると、同じようなロジックをコピペしてしまいがちです。

しかも、足音を毎回同じピッチで鳴らしていると、どうしても「ポコポコポコ…」と 機械的なループ感 が出てしまいます。せっかくいい音源を用意しても、ゲーム全体の質感がチープに見えてしまうんですよね。

そこで登場するのが、今回のコンポーネント 「FootstepRandomizer」 です。足音を鳴らす直前に pitch_scale0.9〜1.1 の範囲でランダムに変更し、同じ音源でも毎回ちょっと違うニュアンスで再生してくれます。しかも、プレイヤー・敵・動く床など、どのノードにもぽんっとアタッチして使えるコンポーネントとして独立させます。


【Godot 4】足音に「生っぽさ」をプラス!「FootstepRandomizer」コンポーネント

今回は、AudioStreamPlayer / AudioStreamPlayer2D / AudioStreamPlayer3D などに簡単に足音ランダム化を追加できるコンポーネントを作ります。

  • コンポーネント自身は Node として実装
  • 内部で指定した AudioStreamPlayer ノードを取得してピッチをいじる
  • play_footstep() を呼ぶだけで、ランダムピッチ + 再生
  • 既存のプレイヤー / 敵スクリプトからは「コンポーネントを呼ぶだけ」にする

という「継承より合成」スタイルですね。


GDScript フルコード


## 足音のピッチを毎回ランダムにして再生するコンポーネント
## どんなノードにもアタッチして使えるように、Node を継承しています。
class_name FootstepRandomizer
extends Node

## --- 設定パラメータ(@export) -------------------------

## 足音を再生する AudioStreamPlayer 系ノードへのパス。
## ルート(このコンポーネントが付いているノード)からの相対パスで指定します。
## 例: "FootstepPlayer" / "Audio/FootstepPlayer" など
@export_node_path("AudioStreamPlayer") var audio_player_path: NodePath

## ピッチの最小値。
## デフォルト 0.9。低いほど「重く / 遅く」聞こえます。
@export_range(0.5, 2.0, 0.01) var min_pitch: float = 0.9

## ピッチの最大値。
## デフォルト 1.1。高いほど「軽く / 速く」聞こえます。
@export_range(0.5, 2.0, 0.01) var max_pitch: float = 1.1

## 足音を鳴らす間隔の最小秒数(オプション)。
## 連打しすぎて「マシンガン足音」にならないようにするためのクールダウン。
@export_range(0.0, 1.0, 0.01) var min_interval_sec: float = 0.0

## すでに再生中でも、次の足音を上書きしてよいかどうか。
## false にすると、「前の足音が鳴り終わるまでは次を鳴らさない」動作になります。
@export var allow_overlap: bool = true

## 足音の音量(dB)。0.0 がデフォルト、負の値で小さく、正の値で大きく。
@export_range(-40.0, 6.0, 0.1) var volume_db: float = 0.0

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

var _audio_player: AudioStreamPlayer
var _last_play_time: float = -1000.0


func _ready() -> void:
    ## エディタ上でパスが設定されていれば AudioStreamPlayer を取得します。
    if audio_player_path != NodePath():
        _audio_player = get_node_or_null(audio_player_path)
    else:
        _audio_player = null

    if _audio_player == null:
        push_warning("[FootstepRandomizer] audio_player_path が設定されていないか、ノードが見つかりません。")

    ## ピッチの範囲が逆転していたら自動で補正
    if min_pitch > max_pitch:
        push_warning("[FootstepRandomizer] min_pitch が max_pitch より大きいので入れ替えます。")
        var tmp := min_pitch
        min_pitch = max_pitch
        max_pitch = tmp


## 外部から呼び出すメイン API。
## 例: プレイヤーの移動処理から `footstep_randomizer.play_footstep()` を呼ぶだけ。
func play_footstep() -> void:
    if _audio_player == null:
        push_warning("[FootstepRandomizer] AudioStreamPlayer が設定されていません。")
        return

    ## クールダウンチェック
    var now := Time.get_ticks_msec() / 1000.0
    if now - _last_play_time < min_interval_sec:
        return

    ## すでに再生中で、上書きを許可しない設定なら何もしない
    if not allow_overlap and _audio_player.playing:
        return

    ## ピッチをランダムに設定
    _audio_player.pitch_scale = randf_range(min_pitch, max_pitch)

    ## 音量も設定(AudioStreamPlayer 側で上書きされていない場合)
    _audio_player.volume_db = volume_db

    ## 再生
    _audio_player.play()

    _last_play_time = now


## 足音のピッチ範囲を動的に変更したいとき用のヘルパー。
## 例: 走っているときは (0.95, 1.15)、歩いているときは (0.9, 1.1) など。
func set_pitch_range(new_min: float, new_max: float) -> void:
    min_pitch = new_min
    max_pitch = new_max
    if min_pitch > max_pitch:
        var tmp := min_pitch
        min_pitch = max_pitch
        max_pitch = tmp


## AudioStreamPlayer を動的に差し替えたい場合用。
## 例: 地面の材質に応じて別の AudioStreamPlayer に切り替えるなど。
func set_audio_player(node: AudioStreamPlayer) -> void:
    _audio_player = node
    if _audio_player:
        audio_player_path = _audio_player.get_path()

使い方の手順

ここからは、実際に「プレイヤー」「敵」「動く床」に足音を付ける例で進めていきましょう。

手順①:コンポーネントスクリプトを用意する

  1. 上の FootstepRandomizer.gd をプロジェクト内(例: res://components/FootstepRandomizer.gd)に保存します。
  2. Godot エディタを再読み込みすると、ノード追加ダイアログの「スクリプトクラス」タブFootstepRandomizer が出てくるはずです。

手順②:プレイヤーシーンに組み込む

例として 2D プレイヤーのシーン構成はこんな感じにします。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── FootstepPlayer (AudioStreamPlayer2D)
 └── FootstepRandomizer (Node)
  1. Player シーンを開く。
  2. AudioStreamPlayer2D を追加し、名前を FootstepPlayer に変更。
  3. FootstepPlayer.stream に足音の AudioStream(WAV/OGG)を設定。
  4. FootstepPlayer.autoplay は「オフ」にしておきます。
  5. FootstepRandomizer ノードを追加(クラス一覧から FootstepRandomizer を選択)。
  6. インスペクタで audio_player_path../FootstepPlayer に設定。

これで、プレイヤーノードから FootstepRandomizer を呼び出せば足音が鳴る状態になります。

手順③:プレイヤースクリプトから呼び出す

プレイヤー側のスクリプトを、できるだけ「足音ロジックから切り離す」形にしてみます。


extends CharacterBody2D

@export var move_speed: float = 200.0

var _footstep_timer: float = 0.0
@export_range(0.05, 0.5, 0.01) var footstep_interval: float = 0.2

## FootstepRandomizer への参照
@onready var footstep_randomizer: FootstepRandomizer = $FootstepRandomizer


func _physics_process(delta: float) -> void:
    var input_dir := Vector2.ZERO
    input_dir.x = Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left")
    input_dir.y = Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")

    if input_dir.length() > 0.0:
        input_dir = input_dir.normalized()
        velocity = input_dir * move_speed
    else:
        velocity = Vector2.ZERO

    move_and_slide()

    _update_footstep(delta, input_dir)


func _update_footstep(delta: float, input_dir: Vector2) -> void:
    ## 足が動いているときだけカウント
    if input_dir.length() > 0.1:
        _footstep_timer -= delta
        if _footstep_timer <= 0.0:
            ## ここでコンポーネントに丸投げ
            footstep_randomizer.play_footstep()
            _footstep_timer = footstep_interval
    else:
        ## 止まっているときはタイマーをリセット
        _footstep_timer = 0.0

プレイヤー側は「いつ足音を鳴らすか」だけを考えていて、「どう鳴らすか(ピッチをどうするか)」は FootstepRandomizer に完全に委譲しているのがポイントです。

手順④:敵や動く床にも再利用する

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

例1:敵キャラ
Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── FootstepPlayer (AudioStreamPlayer2D)
 └── FootstepRandomizer (Node)

敵スクリプト側では、パトロール移動のステップに合わせて FootstepRandomizer.play_footstep() を呼ぶだけです。


extends CharacterBody2D

@onready var footstep_randomizer: FootstepRandomizer = $FootstepRandomizer

func _physics_process(delta: float) -> void:
    ## これは単純な左右パトロールの例
    velocity.x = 100.0
    move_and_slide()

    if is_on_floor():
        ## 一定距離動いたら足音を鳴らす、などのロジックに合わせて呼ぶ
        footstep_randomizer.play_footstep()
例2:動く床
MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── FootstepPlayer (AudioStreamPlayer2D)
 └── FootstepRandomizer (Node)

床が一定距離動いたタイミングや、プレイヤーが乗った瞬間などで play_footstep() を呼ぶことで、「ギシギシ…」という足音(というより軋み音)をランダムピッチで鳴らすこともできます。


メリットと応用

1. シーン構造がスッキリする

足音の「ピッチランダム化」「クールダウン」「音量調整」といったロジックが FootstepRandomizer にまとまるので、プレイヤー・敵などのメインスクリプトは 移動やAIなどの本質的な処理だけ に集中できます。

2. 再利用性が高い(継承地獄を回避)

「足音付きプレイヤー」「足音付き敵」「足音付き動く床」などをそれぞれ継承で作るのではなく、どのノードにも同じコンポーネントをアタッチ するだけで済みます。将来、足音の仕様を変えたくなっても FootstepRandomizer.gd を直せば全員に反映されます。

3. レベルデザインがラクになる

レベルデザイナーがシーンを組むときも、「足音を付けたいキャラ/オブジェクトに FootstepRandomizer をポン付けする」だけで済みます。深いノード階層の中の特定スクリプトを編集する必要がないので、事故も減ります。

4. 応用:材質別の足音、走り/歩きでピッチを変える

  • 地面が「草」「石」「金属」などのときに、AudioStreamPlayer を差し替える
  • 走っているときはピッチを少し高め、歩きのときは標準に戻す

といった拡張も、コンポーネントのメソッドを少し増やすだけで実現できます。

改造案:材質ごとにピッチレンジを変える

例えば、「草の上ではピッチ高め」「石の上では低め」といった質感を出したい場合、以下のような補助メソッドを追加できます。


## 材質タグに応じてピッチ範囲を切り替える例
## 例: set_material("grass") / set_material("stone")
func set_material(material: String) -> void:
    match material:
        "grass":
            set_pitch_range(1.0, 1.2)
        "stone":
            set_pitch_range(0.9, 1.05)
        "metal":
            set_pitch_range(0.95, 1.15)
        _:
            ## デフォルト
            set_pitch_range(0.9, 1.1)

プレイヤーや敵側で「今踏んでいるタイルの材質」を判定して set_material() を呼べば、同じ足音素材でも足場によって雰囲気を変えることができます。

こんな感じで、足音のような「どのキャラも持っているけど本質ではないロジック」は、どんどんコンポーネント化していくと、プロジェクト全体がかなり見通しよくなりますね。継承より合成、どんどんやっていきましょう。