import { AudioClip, AudioSource, director, Node, resources } from 'cc'; /** Unity Assets/Art/Audio → resources/audio/ */ const CLIPS = { background: 'audio/Backgroud', move: 'audio/Move', jump: 'audio/Jump', vehicleMove: 'audio/FlyingCarpetMove', fail: 'audio/Fail', success: 'audio/Success', coins: 'audio/GetCoins', } as const; type SfxKey = Exclude; /** * 对齐 Unity:关卡加载时 curLevel 上循环播放 Backgroud.mp3; * 音效路径与 Player.prefab 一致。 */ export class GameAudio { private static readonly cache = new Map(); private static readonly loading = new Map>(); private static sfxHost: Node | null = null; static async preload(): Promise { await Promise.all(Object.values(CLIPS).map((p) => GameAudio.loadClip(p))); } static loadClip(path: string): Promise { const hit = GameAudio.cache.get(path); if (hit) return Promise.resolve(hit); const pending = GameAudio.loading.get(path); if (pending) return pending; const task = new Promise((resolve) => { resources.load(path, AudioClip, (err, clip) => { GameAudio.loading.delete(path); if (err || !clip) { console.warn(`[GameAudio] 加载失败: ${path}`, err); resolve(null); return; } GameAudio.cache.set(path, clip); resolve(clip); }); }); GameAudio.loading.set(path, task); return task; } /** 在关卡根节点播放循环背景音乐(对齐 Unity createNewLevel) */ static async playBackground(levelRoot: Node): Promise { if (!levelRoot?.isValid) return; const clip = await GameAudio.loadClip(CLIPS.background); if (!clip || !levelRoot.isValid) return; let host = levelRoot.getChildByName('_BGM'); if (!host) { host = new Node('_BGM'); host.parent = levelRoot; } const src = host.getComponent(AudioSource) ?? host.addComponent(AudioSource); src.clip = clip; src.loop = true; src.playOnAwake = false; src.volume = 1; if (!src.playing) { src.play(); } } static playSfx(key: SfxKey, host?: Node) { void GameAudio.playSfxAsync(key, host); } /** 对齐 Unity:同一 AudioSource 播放中则不重复触发移动/跳跃音效 */ static async playSfxOnSource(src: AudioSource, key: SfxKey): Promise { if (!src?.node?.isValid || src.playing) return false; const clip = await GameAudio.loadClip(CLIPS[key]); if (!clip) return false; src.clip = clip; src.loop = false; src.volume = 1; src.play(); return true; } private static async playSfxAsync(key: SfxKey, host?: Node) { const clip = await GameAudio.loadClip(CLIPS[key]); if (!clip) return; const root = host?.isValid ? host : GameAudio.ensureSfxHost(); if (!root?.isValid) return; const src = root.getComponent(AudioSource) ?? root.addComponent(AudioSource); src.playOneShot(clip, 1); } private static ensureSfxHost(): Node | null { if (GameAudio.sfxHost?.isValid) return GameAudio.sfxHost; const scene = director.getScene(); if (!scene) return null; let host = scene.getChildByName('_GameSFX'); if (!host) { host = new Node('_GameSFX'); host.parent = scene; } GameAudio.sfxHost = host; return host; } /** 浏览器需用户交互后才能播放音频,首次点击 HUD 时恢复 */ static resumeAll() { const scene = director.getScene(); if (!scene) return; for (const src of scene.getComponentsInChildren(AudioSource)) { if (src.clip && !src.playing) src.play(); } } }