Files
spellforge/scripts/autoloads/audio_manager.gd
T
2026-07-20 10:56:52 +08:00

111 lines
3.9 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## AudioManager — 音效池 + 节流(Autoload: AudioManager
## 权威来源:development_plan.md S6 P132 AudioStreamPlayer2D 池、0.1s 节流)
## 无音频素材时用程序化蜂鸣占位(同 API,放素材即替换)。SFX 总线 send→Master,受 SettingsManager 音量影响。
extends Node
const POOL_SIZE: int = 32
const THROTTLE_SEC: float = 0.1 # 同一 sound_id 两次播放最小间隔
## sound_id → 占位蜂鸣参数 [freq, ms](有真实 res://audio/sfx/<id>.* 时优先加载)
const _SFX_DEFS: Dictionary = {
"hit": [900.0, 60.0],
"kill": [420.0, 90.0],
"hurt": [180.0, 140.0],
"wave_complete": [660.0, 220.0],
"boss": [120.0, 400.0],
"shop": [780.0, 120.0],
"buy": [1040.0, 80.0],
"level_up": [880.0, 180.0],
}
var _players: Array = [] # Array[AudioStreamPlayer2D]
var _next: int = 0 # round-robin 游标
var _streams: Dictionary = {} # sound_id → AudioStream(懒构建缓存)
var _last_play: Dictionary = {} # sound_id → 上次播放 ticks_msec
func _ready() -> void:
_ensure_sfx_bus()
for i in POOL_SIZE:
var p := AudioStreamPlayer2D.new()
p.bus = "SFX"
add_child(p)
_players.append(p)
# 事件挂钩(无素材时静默蜂鸣)
EventBus.subscribe(EventID.ENEMY_KILLED, func(_p): play("kill"))
EventBus.subscribe(EventID.BULLET_HIT, func(p): play("hit", p.get("hit_pos", Vector2.ZERO)))
EventBus.subscribe(EventID.PLAYER_DAMAGED, func(_p): play("hurt"))
EventBus.subscribe(EventID.WAVE_COMPLETE, func(_p): play("wave_complete"))
EventBus.subscribe(EventID.BOSS_SPAWNED, func(_p): play("boss"))
EventBus.subscribe(EventID.SHOP_OPENED, func(_p): play("shop"))
EventBus.subscribe(EventID.LEVEL_UP, func(_p): play("level_up"))
func _ensure_sfx_bus() -> void:
if AudioServer.get_bus_index("SFX") < 0:
var idx: int = AudioServer.bus_count
AudioServer.add_bus(idx)
AudioServer.set_bus_name(idx, "SFX")
AudioServer.set_bus_send(idx, "Master") # 受 Master 音量(SettingsManager)影响
## 播放音效;返回 true=已触发,false=节流跳过 / 无可用流
func play(sound_id: String, world_pos: Vector2 = Vector2.ZERO) -> bool:
var now: int = Time.get_ticks_msec()
var last: int = int(_last_play.get(sound_id, -100000))
if now - last < int(THROTTLE_SEC * 1000.0):
return false # 0.1s 节流:抑制同一音效高频刷屏
var stream: AudioStream = _get_stream(sound_id)
if stream == null:
return false
_last_play[sound_id] = now
var p: AudioStreamPlayer2D = _players[_next]
_next = (_next + 1) % POOL_SIZE
p.stream = stream
p.global_position = world_pos
p.play()
return true
func _get_stream(sound_id: String) -> AudioStream:
if _streams.has(sound_id):
return _streams[sound_id]
# 优先真实素材
for ext in [".ogg", ".wav", ".mp3"]:
var path: String = "res://audio/sfx/%s%s" % [sound_id, ext]
if ResourceLoader.exists(path):
var s = load(path)
if s is AudioStream:
_streams[sound_id] = s
return s
# 回退:程序化蜂鸣
if _SFX_DEFS.has(sound_id):
var def: Array = _SFX_DEFS[sound_id]
var beep := _make_beep(float(def[0]), float(def[1]))
_streams[sound_id] = beep
return beep
_streams[sound_id] = null
return null
## 生成短蜂鸣(16-bit PCM 正弦 + 线性衰减包络)
func _make_beep(freq: float, ms: float) -> AudioStreamWAV:
var sr: int = 22050
var n: int = max(1, int(sr * ms / 1000.0))
var data := PackedByteArray()
data.resize(n * 2)
for i in n:
var t: float = float(i) / float(sr)
var env: float = 1.0 - float(i) / float(n)
var sample: int = int(sin(t * freq * TAU) * 11000.0 * env)
data.encode_s16(i * 2, sample)
var w := AudioStreamWAV.new()
w.format = AudioStreamWAV.FORMAT_16_BITS
w.mix_rate = sr
w.stereo = false
w.data = data
return w
## 当前正在播放的池实例数(测试 / 调试用)
func active_voices() -> int:
var c: int = 0
for p in _players:
if p.playing:
c += 1
return c