## 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/.* 时优先加载) 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