Files
cocos/assets/scripts/audio/GameAudio.ts
刘宇飞 d393302388 Complete Cocos Creator port with level bundles, themes, and tooling.
Adds level prefabs, theme assets, audio, extensions, and deployment scripts for the Unity WebGL migration.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 15:30:58 +08:00

124 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<keyof typeof CLIPS, 'background'>;
/**
* 对齐 Unity关卡加载时 curLevel 上循环播放 Backgroud.mp3
* 音效路径与 Player.prefab 一致。
*/
export class GameAudio {
private static readonly cache = new Map<string, AudioClip>();
private static readonly loading = new Map<string, Promise<AudioClip | null>>();
private static sfxHost: Node | null = null;
static async preload(): Promise<void> {
await Promise.all(Object.values(CLIPS).map((p) => GameAudio.loadClip(p)));
}
static loadClip(path: string): Promise<AudioClip | null> {
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<AudioClip | null>((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<void> {
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<boolean> {
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();
}
}
}