Pixi.js 游戏开发教程 / Pixi.js 声音集成(Howler.js)
20. 声音集成(Howler.js)
概述
Pixi.js 不包含音频系统。Howler.js 是最流行的 Web 音频库,支持 Web Audio API 和 HTML5 Audio 回退,跨浏览器兼容性极佳。本章讲解如何将 Howler.js 与 Pixi.js 游戏集成。
Howler.js 安装与配置
npm install howler
npm install @types/howler # TypeScript 类型
import { Howl, Howler } from 'howler';
// 基本用法
const sound = new Howl({
src: ['/file/sound.mp3'],
volume: 0.8,
loop: false,
preload: true,
});
// 播放
const id = sound.play();
// 全局设置
Howler.autoUnlock = true; // 自动解锁音频上下文
Howler.html5PoolSize = 10; // HTML5 Audio 对象池大小
⚠️ 注意:浏览器要求用户交互(点击、按键)后才能播放音频。Howler.js 会自动处理音频上下文的解锁,但建议在首次用户交互后再播放声音。
Howl 对象详解
播放/暂停/停止
const bgm = new Howl({
src: ['/file/bgm.mp3', '/file/bgm.ogg'], // 多格式回退
volume: 0.5,
loop: true,
preload: true,
});
// 播放(返回声音实例 ID)
const id = bgm.play();
// 暂停(保留播放位置)
bgm.pause(id);
// 从暂停位置恢复
bgm.play(id);
// 停止(重置到开头)
bgm.stop(id);
// 判断状态
bgm.playing(id); // 是否正在播放
bgm.state(); // 'unloaded' | 'loading' | 'loaded'
音量控制
// 设置全局音量(影响所有声音)
Howler.volume(0.8);
// 设置单个声音的音量
bgm.volume(0.3);
// 设置某个实例的音量
const sfxId = sfx.play();
sfx.volume(0.5, sfxId);
// 获取当前音量
const currentVol = bgm.volume(); // 返回当前音量值
播放速率
// 调整播放速率(变调效果)
sfx.rate(1.5, sfxId); // 1.5 倍速
sfx.rate(0.5, sfxId); // 0.5 倍速(慢动作)
sfx.rate(1.0, sfxId); // 正常速度
事件回调
const sound = new Howl({
src: ['/file/sound.mp3'],
onload: () => console.log('加载完成'),
onloaderror: (id, err) => console.error('加载失败:', err),
onplay: (id) => console.log('开始播放'),
onend: (id) => console.log('播放结束'),
onpause: (id) => console.log('暂停'),
onstop: (id) => console.log('停止'),
onfade: (id) => console.log('淡入淡出完成'),
});
// 也可以通过链式方法绑定
sound.on('end', (id) => {
console.log('播放结束,ID:', id);
});
音频精灵(Sprite)
音频精灵将多个短音效打包在一个音频文件中,通过时间区间播放:
// 音频精灵定义
const sfx = new Howl({
src: ['/file/sfx-sprite.mp3', '/file/sfx-sprite.ogg'],
sprite: {
shoot: [0, 300], // 开始时间 0ms,持续 300ms
explosion: [500, 800], // 开始时间 500ms,持续 800ms
coin: [1500, 200], // 开始时间 1500ms,持续 200ms
jump: [1800, 400],
hit: [2400, 150],
powerup: [2700, 600],
click: [3500, 100],
},
});
// 播放精灵中的片段
sfx.play('shoot');
sfx.play('explosion');
sfx.play('coin');
// 可以同时播放同一个片段的多个实例
sfx.play('shoot'); // 第一个实例
sfx.play('shoot'); // 第二个实例(不会互相覆盖)
💡 提示:音频精灵非常适合打包 UI 音效。20 个小音效打包成一个文件只需一次 HTTP 请求。推荐使用 Audacity 制作音频精灵。
制作音频精灵
# 使用 audiosprite 工具(npm 包)
npm install -g audiosprite
# 合并多个音频文件
audiosprite --output sfx-sprite \
--format howler \
--export mp3,ogg \
shoot.wav explosion.wav coin.wav jump.wav
3D 空间音频
Howler.js 支持 3D 空间音效,基于听者位置产生方向感:
const fireSound = new Howl({
src: ['/file/fire.mp3'],
loop: true,
});
// 设置声音源的 3D 位置(相对于听者)
const id = fireSound.play();
fireSound.pos(x, y, z, id); // x, y, z 为 3D 坐标
// 设置听者位置(通常是玩家/相机位置)
Howler.pos(listenerX, listenerY, listenerZ);
// 设置朝向(可选)
Howler.orientation(0, 0, -1, 0, 1, 0); // 前方向, 上方向
// 更新声音位置(每帧调用)
app.ticker.add(() => {
// 听者跟随玩家
Howler.pos(player.x, player.y, 0);
// 篝火声音位置
fireSound.pos(campfire.x, campfire.y, 0, fireId);
});
衰减模型
// 设置距离衰减模型
Howler.panningModel('HRTF'); // 'HRTF'(更真实)或 'equalpower'(更简单)
// 设置最大距离
fireSound.pannerAttr({
panningModel: 'HRTF',
refDistance: 100, // 参考距离
maxDistance: 1000, // 最大距离
rolloffFactor: 1, // 衰减速率
distanceModel: 'inverse', // 'linear' | 'inverse' | 'exponential'
}, id);
音频格式选择
| 格式 | 压缩 | 浏览器支持 | 推荐用途 |
|---|---|---|---|
| MP3 | 有损 | 全部 | 背景音乐 |
| OGG | 有损 | Chrome/Firefox | 备选格式 |
| WebM | 有损 | 现代浏览器 | 高压缩率 |
| WAV | 无损 | 全部 | 短音效 |
💡 提示:建议同时提供 MP3 和 OGG 两种格式。MP3 保证 Safari 兼容,OGG 在 Chrome/Firefox 中更小。
const bgm = new Howl({
src: ['/file/bgm.mp3', '/file/bgm.ogg'],
// Howler 自动选择浏览器支持的最佳格式
});
音频加载策略
class AudioLoader {
constructor() {
this.sounds = new Map();
}
// 预加载关键音效
async preload(manifest) {
const promises = [];
for (const [name, config] of Object.entries(manifest)) {
const sound = new Howl({
src: config.src,
preload: true,
sprite: config.sprite,
volume: config.volume ?? 1,
loop: config.loop ?? false,
});
this.sounds.set(name, sound);
promises.push(new Promise((resolve, reject) => {
sound.once('load', resolve);
sound.once('loaderror', reject);
}));
}
return Promise.all(promises);
}
get(name) {
return this.sounds.get(name);
}
play(name, sprite) {
const sound = this.sounds.get(name);
if (sound) return sound.play(sprite);
}
}
// 使用
const audio = new AudioLoader();
await audio.preload({
bgm: { src: ['/file/bgm.mp3', '/file/bgm.ogg'], loop: true, volume: 0.4 },
shoot: { src: ['/file/sfx.mp3'], sprite: { shoot: [0, 300] } },
explosion: { src: ['/file/sfx.mp3'], sprite: { explosion: [500, 800] } },
});
背景音乐循环
class BGMManager {
constructor() {
this.current = null;
this.currentName = '';
this.targetVolume = 0.5;
}
play(name, src, volume = 0.5) {
if (this.currentName === name && this.current?.playing()) return;
// 停止当前音乐
if (this.current) {
this.fadeOut(this.current, 500).then(() => {
this.current.stop();
});
}
// 播放新音乐
this.currentName = name;
this.current = new Howl({
src,
loop: true,
volume: 0,
});
this.current.play();
this.fadeIn(this.current, 1000, volume);
}
fadeIn(sound, duration, targetVolume) {
sound.fade(0, targetVolume, duration);
}
fadeOut(sound, duration) {
return new Promise((resolve) => {
const currentVol = sound.volume();
sound.fade(currentVol, 0, duration);
sound.once('fade', resolve);
});
}
pause() {
if (this.current?.playing()) {
this.current.pause();
}
}
resume() {
if (this.current && !this.current.playing()) {
this.current.play();
}
}
}
// 使用
const bgmManager = new BGMManager();
// 进入战斗
bgmManager.play('battle', ['/file/battle.mp3', '/file/battle.ogg'], 0.6);
// 回到探索
bgmManager.play('explore', ['/file/explore.mp3', '/file/explore.ogg'], 0.4);
音效触发
射击音效
const shootSfx = new Howl({
src: ['/file/shoot.mp3'],
volume: 0.3,
rate: 1.0,
});
function onShoot() {
// 随机微调音高,避免听觉疲劳
const rate = 0.9 + Math.random() * 0.2; // 0.9-1.1
shootSfx.rate(rate);
shootSfx.play();
}
爆炸音效
const explosionSfx = new Howl({
src: ['/file/explosion.mp3'],
volume: 0.6,
});
function onExplosion(x, y) {
const id = explosionSfx.play();
// 3D 空间音效
if (Howler.usingWebAudio) {
explosionSfx.pos(x, y, 0, id);
// 根据距离衰减音量
const dx = x - player.x;
const dy = y - player.y;
const dist = Math.sqrt(dx * dx + dy * dy);
const maxDist = 800;
const volume = Math.max(0, 1 - dist / maxDist);
explosionSfx.volume(volume * 0.6, id);
}
}
UI 点击音效
const uiClickSfx = new Howl({
src: ['/file/click.mp3'],
volume: 0.4,
});
// 全局按钮音效
function addClickSound(button) {
button.on('pointerdown', () => {
uiClickSfx.play();
});
}
音频淡入淡出
// 淡入
const sound = new Howl({ src: ['/file/bgm.mp3'], volume: 0 });
sound.play();
sound.fade(0, 0.8, 2000); // 2 秒从 0 淡入到 0.8
// 淡出
sound.fade(0.8, 0, 1000); // 1 秒从 0.8 淡出到 0
sound.once('fade', () => {
sound.stop();
});
// 场景切换时的音频过渡
async function sceneTransition() {
const currentBgm = bgmManager.current;
if (currentBgm) {
currentBgm.fade(currentBgm.volume(), 0, 500);
}
// ... 切换场景 ...
newBgm.play();
newBgm.fade(0, 0.5, 1000);
}
音频管理器封装
class AudioManager {
private sounds: Map<string, Howl> = new Map();
private bgmManager: BGMManager;
private sfxVolume = 1.0;
private bgmVolume = 0.5;
private masterVolume = 1.0;
private muted = false;
constructor() {
this.bgmManager = new BGMManager();
}
// 注册音效
registerSfx(name: string, src: string[], sprite?: Record<string, [number, number]>) {
this.sounds.set(name, new Howl({
src,
sprite,
volume: this.sfxVolume,
}));
}
// 播放音效
playSfx(name: string, sprite?: string): number | null {
if (this.muted) return null;
const sound = this.sounds.get(name);
if (sound) {
const id = sound.play(sprite);
sound.volume(this.sfxVolume * this.masterVolume, id);
return id;
}
return null;
}
// 播放背景音乐
playBgm(name: string, src: string[]) {
if (this.muted) return;
this.bgmManager.play(name, src, this.bgmVolume * this.masterVolume);
}
// 设置主音量
setMasterVolume(v: number) {
this.masterVolume = Math.max(0, Math.min(1, v));
Howler.volume(this.masterVolume);
}
// 设置音效音量
setSfxVolume(v: number) {
this.sfxVolume = Math.max(0, Math.min(1, v));
}
// 设置背景音乐音量
setBgmVolume(v: number) {
this.bgmVolume = Math.max(0, Math.min(1, v));
if (this.bgmManager.current) {
this.bgmManager.current.volume(this.bgmVolume * this.masterVolume);
}
}
// 静音切换
toggleMute() {
this.muted = !this.muted;
Howler.mute(this.muted);
}
// 暂停所有
pauseAll() {
Howler.volume(0);
}
// 恢复所有
resumeAll() {
Howler.volume(this.masterVolume);
}
}
// 全局实例
const audioManager = new AudioManager();
// 注册所有音效
audioManager.registerSfx('shoot', ['/file/shoot.mp3']);
audioManager.registerSfx('explosion', ['/file/explosion.mp3']);
audioManager.registerSfx('coin', ['/file/coin.mp3']);
audioManager.registerSfx('jump', ['/file/jump.mp3']);
// 使用
player.on('shoot', () => audioManager.playSfx('shoot'));
player.on('jump', () => audioManager.playSfx('jump'));
enemy.on('death', () => audioManager.playSfx('explosion'));
// 切换背景音乐
audioManager.playBgm('explore', ['/file/explore.mp3', '/file/explore.ogg']);
设置界面集成
// 音量滑块
function createVolumeSlider(label, initialValue, onChange) {
const container = new Container();
const text = new Text({
text: label,
style: { fontSize: 14, fill: '#ffffff' },
});
container.addChild(text);
const sliderBg = new Graphics();
sliderBg.rect(0, 0, 150, 8);
sliderBg.fill(0x555555);
sliderBg.x = 100;
sliderBg.y = 4;
container.addChild(sliderBg);
const sliderFill = new Graphics();
sliderFill.rect(0, 0, 150 * initialValue, 8);
sliderFill.fill(0x00aaff);
sliderFill.x = 100;
sliderFill.y = 4;
container.addChild(sliderFill);
sliderBg.eventMode = 'static';
sliderBg.on('pointerdown', (e) => {
const local = sliderBg.toLocal(e.global);
const ratio = Math.max(0, Math.min(1, local.x / 150));
sliderFill.clear();
sliderFill.rect(0, 0, 150 * ratio, 8);
sliderFill.fill(0x00aaff);
onChange(ratio);
});
return container;
}
// 设置菜单中
const masterSlider = createVolumeSlider('主音量', 1.0, (v) => audioManager.setMasterVolume(v));
const bgmSlider = createVolumeSlider('背景音乐', 0.5, (v) => audioManager.setBgmVolume(v));
const sfxSlider = createVolumeSlider('音效', 1.0, (v) => audioManager.setSfxVolume(v));
masterSlider.y = 200;
bgmSlider.y = 230;
sfxSlider.y = 260;
settingsPanel.addChild(masterSlider, bgmSlider, sfxSlider);