111 lines
3.9 KiB
GDScript
111 lines
3.9 KiB
GDScript
## AudioManager — 音效池 + 节流(Autoload: AudioManager)
|
||
## 权威来源:development_plan.md S6 P1(32 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
|