初次提交
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
## 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
|
||||
Reference in New Issue
Block a user