Godot 4でキャラの足音をつけようとすると、こんな「あるある」にぶつかりがちです。

  • アニメーションのフレームごとに AudioStreamPlayer2D を直書きして、アニメーションが増えるたびにイベントをコピペ…
  • 歩き・走り・ダッシュで足音のテンポや音量を変えたいけど、AnimationPlayer のトラック調整がどんどんカオスに…
  • プレイヤー、敵、NPC、動く床など「足音が欲しいノード」それぞれに似たようなスクリプトを継承で量産…

Godot標準のやり方(キャラクターシーンを継承して、そこで足音処理を書く)だと、

  • 「足音だけ変えたい」のにキャラの基底クラスを触る必要がある
  • 足音ロジックがキャラの移動・アニメーションロジックとベタ結合になる
  • 敵やNPCにも足音を付けたいとき、継承ツリーを見直すハメになる

そこで「継承より合成」の出番です。
キャラの種類に関係なく、足音だけを担当するコンポーネントを 1 ノードとしてアタッチしてしまいましょう。

今回紹介する 「FootstepAudio」コンポーネント は、

  • 親ノードの 移動速度アニメーション状態 を監視
  • 歩行中のみ一定間隔で足音を再生
  • 速度に応じて足音のテンポを自動調整(走るとテンポアップ)

といった処理を、どのキャラにもポン付けできるようにしたものです。

【Godot 4】歩く・走るテンポも自動調整!「FootstepAudio」コンポーネント

フルコード


## FootstepAudio.gd
## 親ノードの移動速度やアニメーション状態を監視して
## 足音を再生するコンポーネント。
##
## 想定する親ノード:
## - CharacterBody2D / CharacterBody3D
## - RigidBody2D / 3D など「velocity」を持っているもの
## - もしくは、自前で `get_linear_velocity()` を実装しているノード
##
## 足音はこのノード自身に付けた AudioStreamPlayer2D/3D から再生します。

extends Node
class_name FootstepAudio

@export_group("参照ノード")
## 親のアニメーション状態を監視するための AnimationPlayer (任意)
@export var animation_player: AnimationPlayer
## 親のアニメーション状態を監視するための AnimatedSprite2D (任意)
@export var animated_sprite_2d: AnimatedSprite2D
## 親のアニメーション状態を監視するための AnimationTree (任意)
@export var animation_tree: AnimationTree
## 足音を再生する AudioStreamPlayer2D/3D
@export var audio_player: Node

@export_group("移動検知")
## 親ノードの速度を直接参照するかどうか。
## true の場合、親に `velocity` プロパティ (Vector2/Vector3) がある前提。
@export var use_parent_velocity: bool = true

## 親ノードの速度を取得するためのメソッド名。
## 例: "get_linear_velocity" (RigidBody系など)
@export var velocity_method_name: StringName = &""

## 「歩行中」とみなす最小速度。
## この値より小さいと足音は鳴りません。
@export var move_speed_threshold: float = 20.0

@export_group("テンポ設定")
## 通常歩行時の足音の間隔(秒)。
@export var base_interval: float = 0.4

## 速度がこの値のときに base_interval を使う想定。
## それより速ければ自動的に間隔が短くなります。
@export var reference_speed: float = 100.0

## 足音間隔の最小値(これ以上は速くならない)。
@export var min_interval: float = 0.15

## アニメーション名による補正。
## 例: {"run": 0.7, "walk": 1.0} のようにすると、
## "run" アニメーションのときは interval * 0.7 になります。
@export var animation_interval_scale: Dictionary = {}

@export_group("アニメーション判定")
## 「歩行中」とみなすアニメーション名のリスト。
## AnimationPlayer / AnimatedSprite2D / AnimationTree のいずれかで使用。
@export var walking_animation_names: Array[StringName] = [ &"walk", &"run" ]

## AnimationTree を使う場合、現在のアニメーション名を取るためのパラメータパス。
## 例: "parameters/playback" (AnimationNodeStateMachinePlayback)
@export var animtree_state_path: NodePath

@export_group("サウンド設定")
## 再生する足音のバリエーション。
## AudioStreamRandomizer を使う場合は 1 つだけ設定してもOK。
@export var footstep_streams: Array[AudioStream] = []

## 速度に応じてピッチを変えるかどうか。
@export var pitch_scale_by_speed: bool = true

## ピッチの基本値。
@export_range(0.1, 4.0, 0.01)
@export var base_pitch_scale: float = 1.0

## 最高速度付近でどれくらいピッチを上げるか。
## 例: 1.2 なら速く走ると 1.2 倍のピッチになる。
@export_range(1.0, 4.0, 0.01)
@export var max_pitch_scale: float = 1.3

## ピッチ補正の基準速度。
@export var pitch_reference_speed: float = 200.0


var _time_accum: float = 0.0
var _current_interval: float = 0.4
var _parent: Node


func _ready() -> void:
    _parent = get_parent()
    if _parent == null:
        push_warning("FootstepAudio: 親ノードが存在しません。何もできません。")

    # デフォルトで自分の子ノードから AudioStreamPlayer2D/3D を探す
    if audio_player == null:
        audio_player = _find_audio_player()
        if audio_player == null:
            push_warning("FootstepAudio: AudioStreamPlayer2D/3D が見つかりません。足音は鳴りません。")

    # base_interval を初期の現在間隔としてセット
    _current_interval = base_interval


func _physics_process(delta: float) -> void:
    if _parent == null:
        return

    var speed := _get_parent_speed()
    var is_moving := speed >= move_speed_threshold
    var current_anim := _get_current_animation_name()
    var is_walking_anim := _is_walking_animation(current_anim)

    # 「動いている && 歩行系アニメーション中」のときだけ足音を鳴らす
    if not (is_moving and is_walking_anim):
        _time_accum = 0.0
        return

    # 速度とアニメーションから現在のインターバルを計算
    _current_interval = _compute_interval(speed, current_anim)

    _time_accum += delta
    if _time_accum >= _current_interval:
        _time_accum = 0.0
        _play_footstep(speed)


# 親の速度ベクトルからスカラー速度を取得
func _get_parent_speed() -> float:
    var vel: Variant

    if use_parent_velocity and _parent != null and _parent.has_variable("velocity"):
        vel = _parent.get("velocity")
    elif velocity_method_name != StringName() and _parent != null and _parent.has_method(velocity_method_name):
        vel = _parent.call(velocity_method_name)
    else:
        return 0.0

    if vel is Vector2:
        return (vel as Vector2).length()
    elif vel is Vector3:
        return (vel as Vector3).length()
    else:
        return 0.0


# 現在のアニメーション名を取得
func _get_current_animation_name() -> StringName:
    # AnimationPlayer 優先
    if animation_player:
        return animation_player.current_animation

    # AnimatedSprite2D
    if animated_sprite_2d:
        return animated_sprite_2d.animation

    # AnimationTree の StateMachine 再生状態から取る
    if animation_tree and animtree_state_path != NodePath():
        var playback := animation_tree.get(animtree_state_path)
        if playback and playback is AnimationNodeStateMachinePlayback:
            return (playback as AnimationNodeStateMachinePlayback).get_current_node()

    return StringName() # 空


# 「歩行中アニメーションかどうか」を判定
func _is_walking_animation(anim_name: StringName) -> bool:
    if anim_name == StringName():
        # アニメーションが取れない場合は「速度だけで判定」してほしいケースもあるので
        # ここでは true を返す実装にしておく。
        return true
    return anim_name in walking_animation_names


# 速度とアニメーション名から足音の間隔を決める
func _compute_interval(speed: float, anim_name: StringName) -> float:
    if reference_speed <= 0.0:
        return base_interval

    # 速度に応じて間隔を縮める
    var speed_ratio := clamp(speed / reference_speed, 0.0, 10.0)
    # 速いほど間隔が短くなる(ただし min_interval まで)
    var interval := base_interval / max(speed_ratio, 0.2)
    interval = max(interval, min_interval)

    # アニメーション名ごとのスケールを適用
    if animation_interval_scale.has(anim_name):
        var scale := float(animation_interval_scale[anim_name])
        interval *= scale

    return interval


# 足音を実際に再生する
func _play_footstep(speed: float) -> void:
    if audio_player == null:
        return
    if not (audio_player is AudioStreamPlayer2D or audio_player is AudioStreamPlayer3D or audio_player is AudioStreamPlayer):
        push_warning("FootstepAudio: audio_player は AudioStreamPlayer 系である必要があります。")
        return

    var player := audio_player as AudioStreamPlayer

    # 足音バリエーションが設定されていれば、その中からランダムに選ぶ
    if footstep_streams.size() > 0:
        var idx := randi() % footstep_streams.size()
        player.stream = footstep_streams[idx]

    # 速度に応じてピッチを補正
    if pitch_scale_by_speed and pitch_reference_speed > 0.0:
        var ratio := clamp(speed / pitch_reference_speed, 0.0, 1.0)
        var pitch := lerp(base_pitch_scale, max_pitch_scale, ratio)
        player.pitch_scale = pitch
    else:
        player.pitch_scale = base_pitch_scale

    # 連続再生時に音が潰れないよう、再生中でも一度 stop してから play
    if player.playing:
        player.stop()
    player.play()


# 子孫ノードから AudioStreamPlayer2D / 3D を探す
func _find_audio_player() -> Node:
    var candidates := get_children()
    for c in candidates:
        if c is AudioStreamPlayer2D or c is AudioStreamPlayer3D or c is AudioStreamPlayer:
            return c
    # 再帰的に探してもよい
    for c in candidates:
        if c is Node:
            var found := (c as Node).find_child("AudioStreamPlayer2D", true, false)
            if found:
                return found
    return null

使い方の手順

典型的な 2D プレイヤーキャラ(CharacterBody2D)に足音を付ける例で解説します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AnimationPlayer
 ├── AudioStreamPlayer2D
 └── FootstepAudio (Node)

① コンポーネントを用意する

  1. FootstepAudio.gd をプロジェクト内(例: res://components/audio/FootstepAudio.gd)に保存します。
  2. 任意のシーン(プレイヤー、敵、NPC など)を開き、子ノードとして Node を追加し、スクリプトに FootstepAudio.gd をアタッチします。

② 足音を鳴らす AudioStreamPlayer2D を配置

  1. 同じシーン内に AudioStreamPlayer2D を追加します。名前は何でもOKですが、例として FootstepPlayer とします。
  2. 足音用の AudioStream(wav/ogg など)を FootstepPlayer.stream に設定しておきます。
  3. FootstepAudio のインスペクタで audio_playerFootstepPlayer をドラッグ&ドロップします。
    (設定しない場合でも、自動で子ノードから探してくれますが、明示的に指定した方が安全です)

③ 親の速度とアニメーションを紐づける

親が CharacterBody2D で、velocity プロパティを普通に使っている場合、デフォルト設定のままでOKです。

  • use_parent_velocity = true
  • 親に var velocity: Vector2 が存在していること

アニメーション判定は次のどれかを使います。

  • AnimationPlayer を使う場合
    • プレイヤーに AnimationPlayer を追加
    • 歩行アニメーション名を walk、走りを run などにしておく
    • FootstepAudio.animation_player にその AnimationPlayer を指定
    • walking_animation_names["walk", "run"] を設定
  • AnimatedSprite2D を使う場合
    • FootstepAudio.animated_sprite_2dAnimatedSprite2D を指定
    • アニメーション名 walk, run などを walking_animation_names に登録
  • AnimationTree + StateMachine を使う場合
    • FootstepAudio.animation_treeAnimationTree を指定
    • animtree_state_path"parameters/playback" を設定
    • ステートマシンのステート名(例: Walk, Run)を walking_animation_names に登録

これで、「速度が一定以上 && 歩行系アニメーションのときだけ足音が鳴る」状態になります。

④ 速度に応じたテンポ・ピッチの調整

次に、歩き・走りでテンポが変わるようにします。

  • base_interval = 0.4(通常歩きの足音間隔)
  • reference_speed = 100.0(この速度のときに 0.4 秒間隔)
  • 走るときの速度が 200 くらいなら、移動速度が 200 のとき base_interval / (200 / 100) = 0.2 秒間隔になります。
  • min_interval = 0.15 を下回らないようにして、超高速移動でも破綻しないようにしています。

アニメーションごとのテンポ差を付けたい場合は、


animation_interval_scale = {
    "walk": 1.0,  # デフォルト
    "run": 0.7,   # 走りは少しテンポ速く
}

のように設定しておくと、run アニメーション中はさらに 0.7 倍の間隔になります。

ピッチについては、

  • pitch_scale_by_speed = true
  • base_pitch_scale = 1.0
  • max_pitch_scale = 1.3
  • pitch_reference_speed = 200.0

とすると、速度 0 〜 200 の範囲でピッチが 1.0 → 1.3 の間でスムーズに変化します。
走っているときだけ少し高い足音になるので、体感的なスピード感が出ますね。


別の使用例:敵キャラや動く床にもポン付け

敵キャラ (Enemy) に足音を付ける

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AnimationTree
 ├── AudioStreamPlayer2D
 └── FootstepAudio (Node)
  • 敵 AI は別スクリプト(EnemyAI.gd)で書く
  • 足音は FootstepAudio が勝手にやってくれる

こうしておくと、

  • 敵の挙動(パスファインディングや攻撃パターン)をいじっても足音ロジックに一切触れなくて済む
  • 新しい敵タイプを作るときも FootstepAudio をコピペで付けるだけ

動く床 (MovingPlatform) に足音を付ける

例えば「ギギギ…」という機械音を、足音コンポーネントの応用で鳴らすこともできます。

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AudioStreamPlayer2D
 └── FootstepAudio (Node)

MovingPlatform 側に var velocity: Vector2 を持たせて、移動中だけ機械音を鳴らす…という使い方も可能です。
命名は「FootstepAudio」のままでもいいですし、共通の「MovementLoopAudio」みたいな名前にリネームしてもいいですね。


メリットと応用

FootstepAudio コンポーネントを使う最大のメリットは、足音ロジックをキャラ本体から綺麗に切り離せることです。

  • シーン構造がスッキリ
    キャラクターのスクリプトは「入力・物理・アニメーション制御」に集中させて、
    足音は FootstepAudio に丸投げできます。
  • 使い回しが簡単
    プレイヤー、敵、NPC、動く床など、「動いて音が鳴るもの」には全部このコンポーネントを付けるだけ
  • 後からのチューニングが楽
    足音のテンポやピッチ、アニメーションとの紐づけを FootstepAudio のエクスポート変数だけで調整できます。
    キャラごとに微妙に違う足音にしたいときにも、コンポーネント単位で完結します。
  • 深い継承ツリーからの解放
    BaseCharacter に足音処理を追加して、全部の子クラスを巻き込む」といった重い判断をしなくて済みます。
    足音が必要なキャラだけにコンポーネントをアタッチすればOKです。

コンポーネント指向にしておくと、ゲーム後半で「やっぱり足音の仕様変えたいな…」となったときも、
FootstepAudio.gd を 1 箇所直すだけで全キャラに反映されるのが気持ちいいですね。

改造案:接地しているときだけ足音を鳴らす

プラットフォーマー系だと、空中ジャンプ中や落下中には足音を鳴らしたくないことが多いです。
親が CharacterBody2D の場合、こんな感じで「接地中のみ」判定を足すのもアリです。


func _is_on_floor_safely() -> bool:
    # 親が CharacterBody2D / 3D なら is_on_floor() を使う
    if _parent and _parent is CharacterBody2D:
        return (_parent as CharacterBody2D).is_on_floor()
    if _parent and _parent is CharacterBody3D:
        return (_parent as CharacterBody3D).is_on_floor()
    # それ以外は常に true として扱う(必要に応じて拡張)
    return true

そして _physics_process() の中で、


func _physics_process(delta: float) -> void:
    if _parent == null:
        return

    var speed := _get_parent_speed()
    var is_moving := speed >= move_speed_threshold
    var current_anim := _get_current_animation_name()
    var is_walking_anim := _is_walking_animation(current_anim)

    if not _is_on_floor_safely():
        _time_accum = 0.0
        return

    if not (is_moving and is_walking_anim):
        _time_accum = 0.0
        return

    _current_interval = _compute_interval(speed, current_anim)
    _time_accum += delta
    if _time_accum >= _current_interval:
        _time_accum = 0.0
        _play_footstep(speed)

のように差し込めば、空中では足音が鳴らなくなります。
このように「接地判定」「床の材質による足音差し替え」「ダッシュ中は別サウンド」など、
足音に関するロジックを全部 FootstepAudio 側に寄せていくと、キャラ本体のスクリプトがどんどんシンプルになっていきます。

ぜひ、自分のプロジェクト流の足音コンポーネントにカスタマイズしてみてください。