Godot 4でキャラクターの足音を実装するとき、ついこんな構成にしがちですよね。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── AudioStreamPlayer2D (歩き用) ├── AudioStreamPlayer2D (走り用) ├── AudioStreamPlayer2D (砂用) └── Player.gd(中に足音ロジック全部入り)
Player.gd の中で
- 「歩き中ならこの AudioStreamPlayer2D を再生」
- 「走り中ならこっち」
- 「ピッチをちょっと変えたいから
pitch_scaleを毎回ランダムに…」
みたいな処理を書いていくと、だんだん プレイヤースクリプトが肥大化 していきます。さらに、敵キャラや動く床などにも足音を付けたくなると、同じようなロジックをコピペしてしまいがちです。
しかも、足音を毎回同じピッチで鳴らしていると、どうしても「ポコポコポコ…」と 機械的なループ感 が出てしまいます。せっかくいい音源を用意しても、ゲーム全体の質感がチープに見えてしまうんですよね。
そこで登場するのが、今回のコンポーネント 「FootstepRandomizer」 です。足音を鳴らす直前に pitch_scale を 0.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()
使い方の手順
ここからは、実際に「プレイヤー」「敵」「動く床」に足音を付ける例で進めていきましょう。
手順①:コンポーネントスクリプトを用意する
- 上の
FootstepRandomizer.gdをプロジェクト内(例:res://components/FootstepRandomizer.gd)に保存します。 - Godot エディタを再読み込みすると、ノード追加ダイアログの「スクリプトクラス」タブに
FootstepRandomizerが出てくるはずです。
手順②:プレイヤーシーンに組み込む
例として 2D プレイヤーのシーン構成はこんな感じにします。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── FootstepPlayer (AudioStreamPlayer2D) └── FootstepRandomizer (Node)
- Player シーンを開く。
AudioStreamPlayer2Dを追加し、名前をFootstepPlayerに変更。FootstepPlayer.streamに足音の AudioStream(WAV/OGG)を設定。FootstepPlayer.autoplayは「オフ」にしておきます。FootstepRandomizerノードを追加(クラス一覧から FootstepRandomizer を選択)。- インスペクタで
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() を呼べば、同じ足音素材でも足場によって雰囲気を変えることができます。
こんな感じで、足音のような「どのキャラも持っているけど本質ではないロジック」は、どんどんコンポーネント化していくと、プロジェクト全体がかなり見通しよくなりますね。継承より合成、どんどんやっていきましょう。
