初次提交

This commit is contained in:
2026-07-20 10:56:52 +08:00
commit 7bcc0026e0
462 changed files with 50191 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
## 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