攻撃ボイスって、ついプレイヤーシーンのスクリプトに直書きしがちですよね。

  • 攻撃処理の中に AudioStreamPlayer.play() が混ざって読みにくい
  • 敵キャラ・味方キャラごとに似たようなボイス再生コードをコピペしがち
  • 「えい!」「やあ!」などのバリエーションを増やしたくなると、
    条件分岐やランダム処理がどんどん肥大化する

Godot 4 ではシーンとスクリプトの継承でゴリ押しするよりも、「ボイス再生」という責務だけを持つコンポーネントに分離して、
プレイヤーや敵にポン付けできる形にした方が圧倒的に管理しやすいです。

そこで今回は、攻撃時に「えい!」「やあ!」「とりゃ!」などの候補から
ランダムに1つを再生するコンポーネントRandomVoice を用意しました。

【Godot 4】攻撃ボイスをコンポーネント化!「RandomVoice」コンポーネント

このコンポーネントは、ざっくり言うと:

  • 複数のボイス(AudioStream)をインスペクタで登録
  • play_random() を呼ぶだけで、ランダムに1つ再生
  • 同時再生の抑制・クールダウン・音量調整などをまとめて管理

という「どのキャラにも付けられるボイス再生ユニット」です。
攻撃処理は攻撃処理、ボイスはボイスで分離しておくことで、コンポーネント指向な設計に一歩近づけますね。

フルコード(GDScript / Godot 4)


extends Node
class_name RandomVoice
## 攻撃ボイスなどをランダム再生するコンポーネント
##
## ・インスペクタで AudioStream を複数登録
## ・play_random() を呼ぶだけでランダムに1つ再生
## ・同時再生の抑制やクールダウンもオプションで設定可能

@export_category("Voice List")
@export var voices: Array[AudioStream] = []
## 再生候補のボイス一覧。
## 例: 「えい!」「やあ!」「とりゃ!」などの音声ファイルを登録する。
## 空のままだと何も再生されません。

@export_category("Playback Settings")
@export_range(0.0, 1.0, 0.01)
var base_volume_db: float = 0.0
## 基本の音量 (dB)。
## 0.0 がデフォルト音量、-6.0 で少し小さく、+3.0 で少し大きく、といった調整ができます。

@export_range(0.0, 1.0, 0.01)
var random_pitch_min: float = 0.95
@export_range(0.0, 1.5, 0.01)
var random_pitch_max: float = 1.05
## ピッチ(再生速度)のランダム幅。
## 1.0 が通常。少しだけブレさせると「同じ音源でも毎回違う感じ」にできます。

@export_category("Behavior")
@export var prevent_overlap: bool = true
## true の場合、すでにボイス再生中なら新しいボイスは再生しない。
## 攻撃連打でボイスがぐちゃぐちゃに重なるのを防ぎたいときに便利です。

@export var cooldown_time: float = 0.1
## ボイス再生後、次に再生できるまでの最小時間(秒)。
## 連続攻撃のときに毎回鳴らすと騒がしすぎるので、少し間を空けたい場合に使います。
## 0.0 にするとクールダウンなし。

@export var auto_create_player: bool = true
## true の場合、このコンポーネントが内部で AudioStreamPlayer を自動生成します。
## false にしておけば、自前の AudioStreamPlayer を子ノードとして用意して使うこともできます。

@export var player_path: NodePath
## 使用する AudioStreamPlayer のパス。
## 未設定で auto_create_player = true の場合、自動的に子ノード "VoicePlayer" を作成して使います。
## 既にシーン上に AudioStreamPlayer を置いている場合は、そのノードへのパスを指定してください。

var _player: AudioStreamPlayer
var _last_play_time: float = -100.0  # 最後に再生した時間(秒)

func _ready() -> void:
    _setup_player()

func _setup_player() -> void:
    # すでにプレイヤーが設定済みなら何もしない
    if _player:
        return

    if player_path != NodePath():
        # ユーザーが指定した AudioStreamPlayer を使う
        var node := get_node_or_null(player_path)
        if node and node is AudioStreamPlayer:
            _player = node
        else:
            push_warning("RandomVoice: player_path に指定されたノードが AudioStreamPlayer ではありません。自動生成にフォールバックします。")

    if not _player and auto_create_player:
        # 自動生成モード:子ノードに AudioStreamPlayer を作成
        _player = AudioStreamPlayer.new()
        _player.name = "VoicePlayer"
        add_child(_player)

    if not _player:
        push_warning("RandomVoice: AudioStreamPlayer が見つかりません。player_path を設定するか auto_create_player を有効にしてください。")

func can_play() -> bool:
    ## 現在の状態でボイスを再生できるかどうかを返す。
    ##
    ## - AudioStreamPlayer が存在するか
    ## - voices が空でないか
    ## - prevent_overlap / cooldown_time の条件を満たしているか
    if not _player:
        return false
    if voices.is_empty():
        return false

    # 再生中の重なりを防ぐ
    if prevent_overlap and _player.playing:
        return false

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

    return true

func play_random() -> void:
    ## 登録されているボイスからランダムに1つ再生する。
    ##
    ## 再生できない条件(can_play() が false)の場合は何もしません。
    _setup_player()

    if not can_play():
        return

    # ランダムに1つ選択
    var index := randi() % voices.size()
    var stream: AudioStream = voices[index]
    if not stream:
        return

    # ピッチをランダムに設定
    var pitch := randf_range(random_pitch_min, random_pitch_max)
    _player.pitch_scale = pitch

    # 音量設定
    _player.volume_db = base_volume_db

    # ストリームを差し替えて再生
    _player.stream = stream
    _player.play()

    _last_play_time = Time.get_ticks_msec() / 1000.0

func stop() -> void:
    ## 再生中のボイスを停止する。
    if _player and _player.playing:
        _player.stop()

func is_playing() -> bool:
    ## 現在ボイスが再生中かどうかを返す。
    if not _player:
        return false
    return _player.playing

使い方の手順

例として、プレイヤーが攻撃したときに「えい!」「やあ!」「とりゃ!」のどれかを再生するケースで説明します。

シーン構成例

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── RandomVoice (Node)      <-- 今回のコンポーネント
 └── AnimationPlayer (任意)

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

  1. res://components/RandomVoice.gd など、好きな場所に上記コードを保存します。
  2. Godot を再読み込みすると、インスペクタの「スクリプトを追加」から RandomVoice がクラスとして選べるようになります。

手順②:Player シーンにアタッチする

  1. Player シーンを開きます。
  2. Player の子として Node を1つ追加し、名前を RandomVoice に変更します(名前は任意ですが分かりやすく)。
  3. そのノードに RandomVoice.gd をアタッチします。

最終的な構成イメージ:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── RandomVoice (Node) [script: RandomVoice.gd]

手順③:ボイス音源を登録する

  1. 「えい!」「やあ!」「とりゃ!」などのボイス音源(wav / ogg など)をインポートしておきます。
  2. RandomVoice ノードを選択し、インスペクタの Voices 配列に AudioStream を追加します。
    例:
    • res://audio/voice_attack_1.ogg(えい!)
    • res://audio/voice_attack_2.ogg(やあ!)
    • res://audio/voice_attack_3.ogg(とりゃ!)
  3. 必要に応じて Base Volume DbRandom Pitch Min/Max を調整します。
  4. 特にこだわりがなければ、auto_create_player = true のままでOKです(内部で AudioStreamPlayer を自動生成します)。

手順④:攻撃処理から呼び出す

あとは、攻撃が成立したタイミングで play_random() を呼ぶだけです。
プレイヤーのスクリプト(例:Player.gd)でこう書きます:


extends CharacterBody2D

@onready var random_voice: RandomVoice = $RandomVoice

func _process(delta: float) -> void:
    if Input.is_action_just_pressed("attack"):
        _do_attack()

func _do_attack() -> void:
    # ここに攻撃判定処理やアニメーション再生などを書く
    # 例: animation_player.play("attack")

    # 攻撃ボイスをランダム再生
    random_voice.play_random()

これで、攻撃ボタンを押すたびに「えい!」「やあ!」「とりゃ!」のいずれかがランダムに再生されます。
クールダウンや重なり防止もコンポーネント側で面倒を見てくれるので、
攻撃処理のコードは一切ボイス再生の細かいロジックを意識しなくて済むのがポイントですね。

敵キャラや動く床にもそのまま流用

同じ RandomVoice を、敵キャラやギミックにもそのまま付けられます。

例:敵キャラが攻撃するときにうなり声をランダム再生する場合:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── RandomVoice (Node)

敵のスクリプト:


extends CharacterBody2D

@onready var random_voice: RandomVoice = $RandomVoice

func attack_player() -> void:
    # 攻撃ロジック...
    random_voice.play_random()

また、動く床が踏まれたときに「ギィ…」「ミシッ…」と鳴るような演出も、
同じコンポーネントを使って実現できます。

MovingPlatform (StaticBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── RandomVoice (Node)

動く床スクリプトの例:


extends StaticBody2D

@onready var random_voice: RandomVoice = $RandomVoice

func on_player_step() -> void:
    # プレイヤーが乗ったときに呼ばれる想定
    random_voice.play_random()

メリットと応用

RandomVoice コンポーネントを使うメリットはたくさんあります。

  • シーン構造がスッキリする
    攻撃処理・移動処理・ボイス再生がそれぞれ独立したコンポーネントになるので、
    1つのスクリプトが「なんでも屋」になるのを防げます。
  • 使い回しが簡単
    プレイヤー、敵、ギミックなど、どのノードにも同じコンポーネントをペタッと貼るだけ。
    「攻撃ボイス」「ダメージボイス」「ジャンプボイス」など、用途別に複数インスタンスを作るのも簡単です。
  • 継承地獄からの解放
    「ボイス付きキャラ用のベースクラス」を作って継承ツリーを深くする必要がありません。
    必要なノードに RandomVoice をアタッチすれば OK、という合成(Composition)スタイルになります。
  • レベルデザインの調整がしやすい
    各シーンごとにインスペクタからボイスリストや音量、ピッチ幅をいじれるので、
    「この敵だけちょっと声を低く」「このギミックだけクールダウン長め」といった調整が楽です。

改造案:確率付きで「レアボイス」を混ぜる

応用として、「たまにだけレアボイスを再生する」機能を追加してみましょう。
シンプルに1つの関数を追加するだけで実現できます。


func play_with_rare(rare_voice: AudioStream, rare_chance: float = 0.1) -> void:
    ## 一定確率でレアボイスを再生し、それ以外は通常の play_random() を行う。
    ##
    ## rare_voice: レアボイスの AudioStream
    ## rare_chance: 0.0〜1.0 の確率(0.1 なら 10%)
    _setup_player()

    if not can_play():
        return

    if rare_voice and randf() < rare_chance:
        # レアボイスを再生
        var pitch := randf_range(random_pitch_min, random_pitch_max)
        _player.pitch_scale = pitch
        _player.volume_db = base_volume_db
        _player.stream = rare_voice
        _player.play()
        _last_play_time = Time.get_ticks_msec() / 1000.0
    else:
        # 通常のランダム再生
        play_random()

このように、RandomVoice をベースに少しずつ機能を足していくことで、
「コンポーネントを育てていく」感覚でプロジェクト全体の再利用性を高めていけます。
継承より合成、そして小さなコンポーネントを積み上げていくスタイルで、Godot 4 ライフを快適にしていきましょう。