【Godot 4】CoinJingle (コイン音) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Godotでコインの取得音を実装するとき、つい「プレイヤーシーンにAudioStreamPlayerを直書きして、スクリプト側でpitch_scaleをいじる」みたいな実装をしがちですよね。最初はそれで動くんですが、

  • 敵もコインをドロップしたい → 敵にも同じロジックをコピペ…
  • ステージごとに効果音を変えたい → プレイヤースクリプトが条件分岐だらけ…
  • コインの連続取得でピッチを上げたい → 「最後に鳴った時間」や「連続カウント」の管理があちこちに…

と、だんだん「音のロジック」がプレイヤーやコイン本体のスクリプトにべったり張り付いてしまいます。継承を使って PlayerWithCoinSound みたいなクラスを増やしていくと、今度はクラス階層がどんどん深くなっていきます。

そこで今回は、

  • 「コインを取った」というイベントを受け取るだけで
  • 連続取得時にピッチを半音ずつ上げてくれて
  • 一定時間経つと自動でリセットしてくれる

という「音だけ担当」のコンポーネント CoinJingle を作ってみましょう。プレイヤーでも敵でも動く床でも、「コインを取る可能性があるノード」にペタっと貼れば使えるようにします。


【Godot 4】連続コインで半音アップ!「CoinJingle」コンポーネント

このコンポーネントは「継承」ではなく「合成(Composition)」で使う前提です。
プレイヤーやコインのスクリプトから coin_jingle.play_jingle() を呼ぶだけで、

  • 一定時間以内の連続コイン取得をカウント
  • カウントに応じてピッチを「半音単位」で上昇
  • 指定回数以上はピッチを頭打ち(上がりすぎ防止)
  • 最後の取得から一定時間経過で連続カウントをリセット

といった処理をすべてやってくれます。


フルコード:CoinJingle.gd


extends Node
class_name CoinJingle
## コイン取得時のジングルを管理するコンポーネント。
## 連続でコインを取ると、ピッチが半音ずつ上がっていく。

## --- 設定パラメータ(インスペクタから変更可能) ---

@export var audio_player_path: NodePath = NodePath("AudioStreamPlayer2D"):
	## コイン音を鳴らす AudioStreamPlayer / AudioStreamPlayer2D へのパス
	## - デフォルトでは、このコンポーネントの子にある AudioStreamPlayer2D を想定
	## - シーン側で別のAudioノードを使いたい場合は、ここを書き換えましょう
	set = set_audio_player_path

@export var base_pitch: float = 1.0:
	## 基本となるピッチスケール。
	## 1.0 で元の音程、0.5で1オクターブ下、2.0で1オクターブ上くらいのイメージです。
	set = set_base_pitch

@export var semitone_step: float = 1.0:
	## 連続取得ごとに上げる半音数。
	## 1.0 なら「半音1つ分」ずつ上がる。
	## 2.0 にすると「全音(半音2つ分)」ずつ上がっていきます。
	set = set_semitone_step

@export var max_steps: int = 8:
	## 連続取得による「ステップ」の上限。
	## 例: 8 なら、最大で半音8つ分(約1オクターブ弱)まで上がる。
	set = set_max_steps

@export var chain_reset_time: float = 0.6:
	## 連続取得とみなす「猶予時間(秒)」。
	## 最後にコインを取ってから、この秒数を超えると連続カウントをリセットします。
	set = set_chain_reset_time

@export var volume_db_offset: float = 0.0:
	## コイン音全体の音量オフセット(dB)。
	## ベースのAudioStreamPlayer側の音量に対して、相対的に増減します。
	set = set_volume_db_offset

## --- 内部状態 ---

var _audio_player: AudioStreamPlayer = null
var _current_step: int = 0
var _last_pick_time: float = -INF

func _ready() -> void:
	# シーンツリーに入ったタイミングでAudioPlayerを解決
	_resolve_audio_player()


# ---------------------------------------------------------
# 公開API
# ---------------------------------------------------------

func play_jingle() -> void:
	## コインを取得したときに呼び出すメイン関数。
	## - 連続取得カウントの更新
	## - ピッチの計算
	## - 実際の再生
	## をまとめて行います。
	if _audio_player == null:
		push_warning("CoinJingle: Audio player is not assigned or could not be found.")
		return

	var now := Time.get_ticks_msec() / 1000.0

	# 一定時間内なら連続取得とみなしてステップを増加、
	# それ以外ならリセット
	if now - _last_pick_time <= chain_reset_time:
		_current_step = min(_current_step + 1, max_steps)
	else:
		_current_step = 0

	_last_pick_time = now

	# 半音ステップからピッチスケールを計算
	# 半音 n 個分のピッチ比は 2^(n/12)
	var semitone_offset := float(_current_step) * semitone_step
	var pitch_scale := base_pitch * pow(2.0, semitone_offset / 12.0)

	# AudioPlayerに反映
	_audio_player.pitch_scale = pitch_scale

	# 音量オフセットも反映(AudioStreamPlayerとAudioStreamPlayer2Dは共通でvolume_dbを持つ)
	_audio_player.volume_db += volume_db_offset

	# すでに再生中でも一旦止めてから鳴らすと、連打時に頭が聞こえやすい
	if _audio_player.playing:
		_audio_player.stop()
	_audio_player.play()


func reset_chain() -> void:
	## 手動で連続取得状態をリセットしたい場合に呼ぶ関数。
	## 例: ステージ遷移時や死亡時など。
	_current_step = 0
	_last_pick_time = -INF
	if _audio_player:
		_audio_player.pitch_scale = base_pitch


# ---------------------------------------------------------
# エディタから触る用のsetter(バリデーション込み)
# ---------------------------------------------------------

func set_audio_player_path(path: NodePath) -> void:
	audio_player_path = path
	_resolve_audio_player()

func set_base_pitch(value: float) -> void:
	base_pitch = max(value, 0.01) # 0以下はNG
	if _audio_player:
		_audio_player.pitch_scale = base_pitch

func set_semitone_step(value: float) -> void:
	semitone_step = value

func set_max_steps(value: int) -> void:
	max_steps = max(value, 0)

func set_chain_reset_time(value: float) -> void:
	chain_reset_time = max(value, 0.0)

func set_volume_db_offset(value: float) -> void:
	volume_db_offset = value


# ---------------------------------------------------------
# 内部ユーティリティ
# ---------------------------------------------------------

func _resolve_audio_player() -> void:
	## audio_player_path から AudioStreamPlayer または AudioStreamPlayer2D を取得する。
	if not is_inside_tree():
		return

	if audio_player_path.is_empty():
		_audio_player = null
		return

	var node := get_node_or_null(audio_player_path)
	if node == null:
		_audio_player = null
		push_warning("CoinJingle: Node at path '%s' not found.".format([audio_player_path]))
		return

	if node is AudioStreamPlayer or node is AudioStreamPlayer2D:
		_audio_player = node
	else:
		_audio_player = null
		push_warning("CoinJingle: Node at path '%s' is not an AudioStreamPlayer.".format([audio_player_path]))

使い方の手順

ここでは、典型的な 2D アクションゲームを例に「プレイヤーがコインを取ると鳴る」ケースで説明します。

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

  1. 上記の CoinJingle.gd をプロジェクト内(例: res://components/CoinJingle.gd)に保存します。
  2. Godotエディタを再読み込みすると、ノード追加時の「スクリプト付きカスタムノード」として CoinJingle が選べるようになります。

手順②:プレイヤーシーンにコンポーネントをアタッチ

例として、以下のようなプレイヤーシーンを想定します:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AudioStreamPlayer2D
 └── CoinJingle (Node)
  1. Player シーンを開きます。
  2. Player の子として AudioStreamPlayer2D を追加し、
    • stream にコインのSE(例: coin.wav)を設定
    • 必要なら volume_dbbus を調整
  3. さらに子として Node を追加し、そのノードに CoinJingle.gd をアタッチします。
    (クラス名が効いていれば、直接「CoinJingle」ノードとして追加してもOKです)
  4. Inspector で audio_player_path"../AudioStreamPlayer2D" に設定します。
    (CoinJingle から見て親の AudioStreamPlayer2D を指す)

これで、「Player はコイン音のロジックを CoinJingle コンポーネントに丸投げ」できる状態になります。

手順③:コイン取得時に play_jingle() を呼ぶ

次に、コイン(Collectible)側のスクリプトからプレイヤーの CoinJingle を呼び出します。
例として、シンプルなコインシーンを考えます:

Coin (Area2D)
 ├── Sprite2D
 └── CollisionShape2D

コイン側のスクリプト Coin.gd の例:


extends Area2D

@export var score_value: int = 1

func _ready() -> void:
	# プレイヤーとの接触を検知する
	body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node) -> void:
	# プレイヤー以外には反応しない例
	if not body.is_in_group("player"):
		return

	# スコア加算などはここでやる
	if body.has_method("add_score"):
		body.add_score(score_value)

	# プレイヤーが CoinJingle コンポーネントを持っていれば再生する
	var jingle := body.get_node_or_null("CoinJingle")
	if jingle and jingle is CoinJingle:
		jingle.play_jingle()

	# コイン自体は消す
	queue_free()

ポイントは、

  • コインは「音をどう鳴らすか」を知らなくてよい
  • プレイヤーが CoinJingle を持っていれば、それに向かって play_jingle() を呼ぶだけ

という分離になっていることです。これで、「敵がコインを拾ったときも同じジングルを鳴らしたい」となったら、敵シーンにも CoinJingle を貼るだけで済みます。

手順④:パラメータを調整して「気持ちいい」設定を探す

最後に、インスペクタから以下のパラメータを調整して、ゲームに合う「気持ちよさ」を探しましょう:

  • base_pitch … 元の音程。BGMとの相性で微調整。
  • semitone_step … 半音何個分ずつ上げるか。1.0〜2.0あたりを試すと違いが分かりやすいです。
  • max_steps … どこまで上げるか。8〜12くらいにしておくと上がりすぎ防止になります。
  • chain_reset_time … 何秒以内なら連続取得とみなすか。0.3〜0.7秒あたりがアクションゲーム向きですね。
  • volume_db_offset … コイン音だけ少し目立たせたいときは +3dB くらいがおすすめ。

メリットと応用

この CoinJingle コンポーネントを導入することで、いくつか良いことがあります。

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

プレイヤーや敵のスクリプトに「音のピッチ管理ロジック」を書かなくてよくなります。

Before:
Player.gd
 ├─ コイン取得処理
 ├─ スコア計算
 ├─ 入力処理
 ├─ 移動処理
 └─ コイン音のピッチ管理(連続カウント、タイマー…)

After:
Player.gd
 ├─ コイン取得処理(CoinJingle.play_jingle() を呼ぶだけ)
 ├─ スコア計算
 ├─ 入力処理
 └─ 移動処理

CoinJingle.gd
 └─ コイン音のピッチ管理を完全に担当

責務が分離されて、Player.gd がだいぶ読みやすくなりますね。

2. どのキャラにも簡単に「気持ちいいコイン音」を付与できる

例えば、敵がコインを吸い寄せるギミックがあったとしても:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── AudioStreamPlayer2D
 └── CoinJingle (Node)

と構成して、コイン取得時に enemy_coin_jingle.play_jingle() を呼ぶだけで、プレイヤーと同じ「連続ピッチアップ」の気持ちよさを共有できます。
「敵用のプレイヤー継承クラス」をわざわざ作る必要はありません。

3. レベルデザイン側からも調整しやすい

パラメータがすべて @export されているので、レベルデザイナーがエディタ上で「このステージだけ半音2つ分ずつ上げて、超ハイテンションにしよう」みたいな調整を簡単にできます。

  • スコアが高くなる後半ステージだけ max_steps を増やす
  • ゆったりステージでは chain_reset_time を長めにして、途切れにくくする

といった「音によるゲーム体験の変化」をコードを書かずに実現できます。


改造案:ピッチを徐々に元に戻す「クールダウン」機能を追加

もう一歩踏み込んで、「連続取得が途切れた後、いきなりリセットではなく、徐々にピッチを戻していく」ような演出も面白いです。例えば、以下のような関数を CoinJingle に追加してみましょう:


func cool_down(delta: float, speed: float = 2.0) -> void:
	## 連続取得が途切れたときに、徐々にステップを下げていくクールダウン処理。
	## _process(delta) から呼び出すことを想定。
	if _current_step <= 0:
		return

	# delta と speed に応じて、徐々にステップを減らす
	var decrease := int(delta * speed)
	if decrease > 0:
		_current_step = max(_current_step - decrease, 0)
		# ピッチを更新
		var semitone_offset := float(_current_step) * semitone_step
		var pitch_scale := base_pitch * pow(2.0, semitone_offset / 12.0)
		if _audio_player:
			_audio_player.pitch_scale = pitch_scale

これを使う場合は、CoinJingle.gd_process(delta) を追加して、cool_down(delta) を呼べばOKです。
「連続取得が終わったあとも、しばらく高めのピッチが残って、少しずつ元に戻る」という、ちょっとリッチな演出になります。

こんな感じで、コンポーネントとして分離しておくと、「音の演出アイデア」をどんどん追加しやすくなります。継承より合成で、気持ちいいコイン取得体験を育てていきましょう。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!