网页游戏音频
音频是任何游戏的重要组成部分;它增加了反馈和氛围。基于 Web 的音频正在快速成熟,但仍然存在许多需要解决的浏览器差异。我们通常需要确定哪些音频部分对游戏的体验至关重要,哪些音频部分虽然不错但并非必要,并据此制定策略。本文提供了有关为网页游戏实现音频的详细指南,探讨了目前在尽可能广泛的平台上有效的方法。
移动音频注意事项
到目前为止,提供 Web 音频支持最困难的平台是移动平台。不幸的是,这些也是人们经常用来玩游戏的平台。桌面浏览器和移动浏览器之间存在一些差异,这可能导致浏览器供应商做出了一些选择,使得 Web 音频难以让游戏开发人员使用。让我们现在看看这些差异。
自动播放
浏览器自动播放策略现在影响桌面和移动浏览器。有关此策略的更多信息,请参阅Google 开发者网站上的此文章。
值得注意的是,如果满足以下条件,则允许带声音的自动播放
- 用户已与该域名交互。
- 在移动设备上,用户已使应用程序可安装。
许多浏览器会忽略游戏发出的任何自动播放音频的请求;相反,音频的播放需要由用户发起的事件(例如点击或轻触)启动。这意味着你必须调整音频播放以考虑这一点。这通常可以通过预加载音频并在用户发起的事件中对其进行预处理来缓解。
对于更被动的音频自动播放,例如游戏加载后立即开始播放的背景音乐,一个技巧是检测任何用户发起的事件,然后开始播放。对于其他在游戏过程中使用的更主动的声音,我们可以考虑在按下“开始”按钮等操作后对其进行预处理。
要预处理音频,我们需要播放其中一部分;出于这个原因,在音频样本的末尾包含一段静音很有用。跳转到、播放,然后暂停这段静音,意味着我们现在可以使用 JavaScript 在任意点播放该文件。你可以在这里了解更多关于自动播放策略的最佳实践。
注意:如果浏览器允许你更改音量(见下文),播放部分文件时音量为零也可以。还要注意,播放并立即暂停音频并不能保证不会播放一小段音频。
注意:将 Web 应用添加到移动设备的主屏幕可能会改变其功能。在 iOS 上自动播放的情况下,目前似乎就是这样。如果可能,你应该在多个设备和平台上尝试你的代码,以查看其工作方式。
音量
移动浏览器中可能禁用了程序化音量控制。通常给出的理由是,用户应该能够在操作系统级别控制音量,并且不应该覆盖此设置。
缓冲和预加载
为了缓解移动网络数据过度使用,我们还经常发现,在播放开始之前缓冲功能被禁用。缓冲是指浏览器预先下载媒体的过程,我们通常需要执行此操作以确保流畅播放。
该HTMLMediaElement
接口附带许多属性,以帮助确定轨道是否处于可播放状态。
注意:在许多方面,缓冲的概念已经过时。只要接受字节范围请求(这是默认行为),我们应该能够跳转到音频中的特定位置,而无需下载前面的内容。但是,预加载仍然有用——如果没有它,在播放开始之前始终需要进行一些客户端-服务器通信。
并发音频播放
许多游戏的需求是需要同时播放多个音频;例如,可能同时播放背景音乐以及游戏过程中发生的各种事件的音效。尽管随着Web 音频 API的采用,这种情况很快就会得到改善,但目前支持最广泛的方法——使用普通的<audio>
元素——在移动设备上会导致结果参差不齐。
测试和支持
下表显示了哪些移动平台支持上述功能。
移动浏览器 | 版本 | 并发播放 | 自动播放 | 音量调节 | 预加载 |
---|---|---|---|---|---|
Chrome(Android) | 69+ | Y | Y | Y | Y |
Firefox(Android) | 62+ | Y | Y | Y | Y |
Edge 移动版 | Y | Y | Y | Y | |
Opera 移动版 | 46+ | Y | Y | Y | Y |
Safari(iOS) | 7+ | Y/N* | N | N | Y |
Android 浏览器 | 67+ | Y | Y | Y | Y |
你可以在这里查看移动和桌面 HTMLMediaElement 支持的完整兼容性图表。
注意:并发音频播放是使用我们的并发音频测试示例进行测试的,我们尝试使用标准音频 API 同时播放三段音频。
注意:简单的自动播放功能是使用我们的自动播放测试示例进行测试的。
注意:音量可变性是使用我们的音量测试示例进行测试的。
移动端解决方法
尽管移动浏览器可能会带来问题,但有一些方法可以解决上述问题。
音频精灵
音频精灵借鉴了CSS 精灵的名称,这是一种使用 CSS 和单个图形资源将其分解为一系列精灵的可视化技术。我们可以将相同的原理应用于音频,这样,与其使用大量需要加载和播放的小型音频文件,不如使用一个包含所有需要的小型音频片段的大型音频文件。要播放文件中特定的声音,我们只需使用每个音频精灵的已知开始和结束时间。
优点是我们可以预处理一部分音频,并准备好我们的精灵。为此,我们可以播放并立即暂停较大的音频片段。你还可以减少服务器请求次数并节省带宽。
const myAudio = document.createElement("audio");
myAudio.src = "mysprite.mp3";
myAudio.play();
myAudio.pause();
你需要对当前时间进行采样,以了解何时停止。如果将各个声音间隔至少 500 毫秒,则使用timeUpdate
事件(每 250 毫秒触发一次)就足够了。你的文件可能比严格需要的略长,但静音压缩效果很好。
以下是一个音频精灵播放器的示例——首先让我们在 HTML 中设置用户界面
<audio id="myAudio" src="http://jPlayer.org/tmp/countdown.mp3"></audio>
<button data-start="18" data-stop="19">0</button>
<button data-start="16" data-stop="17">1</button>
<button data-start="14" data-stop="15">2</button>
<button data-start="12" data-stop="13">3</button>
<button data-start="10" data-stop="11">4</button>
<button data-start="8" data-stop="9">5</button>
<button data-start="6" data-stop="7">6</button>
<button data-start="4" data-stop="5">7</button>
<button data-start="2" data-stop="3">8</button>
<button data-start="0" data-stop="1">9</button>
现在我们有了带有以秒为单位的开始和结束时间的按钮。“countdown.mp3” MP3 文件包含每 2 秒说出的一个数字,我们的想法是在按下相应的按钮时播放该数字。
让我们添加一些 JavaScript 来使其工作
const myAudio = document.getElementById("myAudio");
const buttons = document.getElementsByTagName("button");
let stopTime = 0;
for (const button of buttons) {
button.addEventListener(
"click",
() => {
myAudio.currentTime = button.getAttribute("data-start");
stopTime = button.getAttribute("data-stop");
myAudio.play();
},
false,
);
}
myAudio.addEventListener(
"timeupdate",
() => {
if (myAudio.currentTime > stopTime) {
myAudio.pause();
}
},
false,
);
注意:你可以在 JSFiddle 上试用我们的音频精灵播放器。
注意:在移动设备上,我们可能需要从用户发起的事件(例如按下开始按钮)触发此代码,如上所述。
注意:注意比特率。以较低的比特率编码音频意味着文件尺寸更小,但查找精度更低。
背景音乐
游戏中的音乐可以产生强大的情感效果。你可以混合和匹配各种音乐样本,并假设你可以控制音频元素的音量,你可以淡入淡出不同的音乐片段。使用playbackRate()
方法,你甚至可以调整音乐的速度而不影响音调,以使其更好地与动作同步。
所有这些都可以使用标准的<audio>
元素和关联的HTMLMediaElement
来实现,但在更高级的Web 音频 API中,它变得更加容易和灵活。接下来让我们看看这一点。
Web 音频 API 用于游戏
Web Audio API 受到所有现代桌面和移动浏览器支持,除了 Opera Mini。考虑到这一点,在许多情况下使用 Web Audio API 是一种可接受的方法(有关浏览器兼容性的更多信息,请参阅Can I use Web Audio API 页面)。Web Audio API 是一种高级音频 JavaScript API,非常适合游戏音频。开发者可以生成音频和处理音频样本,以及在 3D 游戏空间中定位声音。
一个可行的跨浏览器策略是使用标准的 <audio>
元素提供基本音频,并在支持的情况下使用 Web Audio API 增强体验。
注意:值得注意的是,iOS Safari 现在支持 Web Audio API,这意味着现在可以编写基于 Web 的游戏,为 iOS 提供原生质量的音频。
由于 Web Audio API 允许精确控制音频播放的时机,我们可以用它在特定时刻播放样本,这是游戏沉浸式体验的关键方面。毕竟,你希望爆炸声伴随着一声巨响,而不是在爆炸之后才响起。
使用 Web Audio API 播放背景音乐
虽然我们可以使用 <audio>
元素提供不会根据游戏环境变化的线性背景音乐,但 Web Audio API 非常适合实现更具动态性的音乐体验。你可能希望音乐根据你是否试图营造悬念或以某种方式鼓励玩家而发生变化。音乐是游戏体验的重要组成部分,根据你正在制作的游戏类型,你可能希望投入大量精力来使其完美。
使你的音乐配乐更具动态性的一种方法是将其分解成组件循环或音轨。这通常也是音乐家创作音乐的方式,而 Web Audio API 非常擅长保持这些部分的同步。一旦你有了构成作品的各个音轨,你就可以根据需要引入和移除音轨。
你还可以将滤镜或效果应用于音乐。你的角色是否在洞穴中?增加回声。也许你有水下场景,在此期间你可以应用一个使声音变得模糊的滤镜。
让我们看看一些用于动态调整音乐基础音轨的 Web Audio API 技术。
加载音轨
使用 Web Audio API,你可以使用Fetch API或XMLHttpRequest
分别加载单独的音轨和循环,这意味着你可以同步或并行加载它们。同步加载可能意味着你的部分音乐更早准备好,你可以在其他音乐加载时开始播放它们。
无论哪种方式,你可能都需要同步音轨或循环。Web Audio API 包含一个内部时钟的概念,它在你创建音频上下文时开始计时。你需要考虑创建音频上下文和第一个音频音轨开始播放之间的时间。记录此偏移量并查询正在播放的音轨的当前时间,即可获得足够的信息来同步不同的音频片段。
为了实际演示,让我们布局一些单独的音轨
<section id="tracks">
<ul>
<li data-loading="true">
<a href="leadguitar.mp3" class="track">Lead Guitar</a>
<p class="loading-text">Loading…</p>
<button data-playing="false" aria-describedby="guitar-play-label">
<span id="guitar-play-label">Play</span>
</button>
</li>
<li data-loading="true">
<a href="bassguitar.mp3" class="track">Bass Guitar</a>
<p class="loading-text">Loading…</p>
<button data-playing="false" aria-describedby="bass-play-label">
<span id="bass-play-label">Play</span>
</button>
</li>
<li data-loading="true">
<a href="drums.mp3" class="track">Drums</a>
<p class="loading-text">Loading…</p>
<button data-playing="false" aria-describedby="drums-play-label">
<span id="drums-play-label">Play</span>
</button>
</li>
<li data-loading="true">
<a href="horns.mp3" class="track">Horns</a>
<p class="loading-text">Loading…</p>
<button data-playing="false" aria-describedby="horns-play-label">
<span id="horns-play-label">Play</span>
</button>
</li>
<li data-loading="true">
<a href="clav.mp3" class="track">Clavi</a>
<p class="loading-text">Loading…</p>
<button data-playing="false" aria-describedby="clavi-play-label">
<span id="clavi-play-label">Play</span>
</button>
</li>
</ul>
<p class="sourced">
All tracks sourced from <a href="https://jplayer.org/">jplayer.org</a>
</p>
</section>
所有这些音轨都具有相同的节奏,并且设计为彼此同步,因此我们需要确保它们已加载并可供 API 使用在我们能够播放它们之前。我们可以使用 JavaScript 的async
/await
功能来实现这一点。
一旦它们可以播放,我们需要确保它们从其他音轨可能正在播放的正确位置开始,以便它们同步。
让我们创建音频上下文
const audioCtx = new AudioContext();
现在让我们选择所有<li>
元素;稍后我们可以利用这些元素访问音轨文件路径和每个单独的播放按钮。
const trackEls = document.querySelectorAll("li");
我们希望确保每个文件都已加载并解码成缓冲区,然后再使用它,因此让我们创建一个 async
函数来允许我们执行此操作
async function getFile(filepath) {
const response = await fetch(filepath);
const arrayBuffer = await response.arrayBuffer();
const audioBuffer = await audioCtx.decodeAudioData(arrayBuffer);
return audioBuffer;
}
然后,我们可以在调用此函数时使用 await
运算符,这确保我们可以在它执行完毕后运行后续代码。
让我们创建另一个 async
函数来设置样本——我们可以将这两个异步函数组合成一个不错的 Promise 模式,以便在每个文件加载并缓冲后执行进一步的操作
async function loadFile(filePath) {
const track = await getFile(filePath);
return track;
}
让我们再创建一个 playTrack()
函数,我们可以在获取文件后调用它。这里我们需要一个偏移量,因此如果我们已经开始播放一个文件,我们就会记录需要从哪里开始播放另一个文件。
start()
接受两个可选参数。第一个是何时开始播放,第二个是播放位置,即我们的偏移量。
let offset = 0;
function playTrack(audioBuffer) {
const trackSource = audioCtx.createBufferSource();
trackSource.buffer = audioBuffer;
trackSource.connect(audioCtx.destination);
if (offset === 0) {
trackSource.start();
offset = audioCtx.currentTime;
} else {
trackSource.start(0, audioCtx.currentTime - offset);
}
return trackSource;
}
最后,让我们遍历我们的 <li>
元素,为每个元素获取正确的文件,然后通过隐藏“加载”文本并显示播放按钮来允许播放
trackEls.forEach((el, i) => {
// Get children
const anchor = el.querySelector("a");
const loadText = el.querySelector("p");
const playButton = el.querySelector("button");
// Load file
loadFile(anchor.href).then((track) => {
// Set loading to false
el.dataset.loading = "false";
// Hide loading text
loadText.style.display = "none";
// Show button
playButton.style.display = "inline-block";
// Allow play on click
playButton.addEventListener("click", () => {
// Check if context is in suspended state (autoplay policy)
if (audioCtx.state === "suspended") {
audioCtx.resume();
}
playTrack(track);
playButton.dataset.playing = true;
});
});
});
在你的游戏世界中,你可能有一些在不同情况下播放的循环和样本,并且能够与其他音轨同步以获得更无缝的体验可能很有用。
注意:此示例在引入下一部分之前不会等待节拍结束;如果我们知道音轨的 BPM(每分钟节拍数),我们可以做到这一点。
如果你想让新音轨的引入听起来更自然,你可以让它在节拍/小节/乐句或任何你想要将背景音乐分成块的单位开始播放。
在播放要同步的音轨之前,你应该计算出距离下一个节拍/小节等的开始还有多长时间。
这是一段代码,给定一个速度(节拍/小节的秒数),它将计算出等待多长时间才能播放下一部分——你将结果值传递给 start()
函数的第一个参数,该参数接受该播放应开始的绝对时间。请注意第二个参数(从新音轨中的哪个位置开始播放)是相对的
if (offset === 0) {
source.start();
offset = context.currentTime;
} else {
const relativeTime = context.currentTime - offset;
const beats = relativeTime / tempo;
const remainder = beats - Math.floor(beats);
const delay = tempo - remainder * tempo;
source.start(context.currentTime + delay, relativeTime + delay);
}
注意:你可以在 JSFiddle 上尝试我们的等待计算器代码(在这种情况下,我已同步到小节)。
注意:如果第一个参数为 0 或小于上下文 currentTime
,则播放将立即开始。
位置音频
位置音频可以成为使音频成为沉浸式游戏体验关键部分的重要技术。Web Audio API 不仅使我们能够在三维空间中定位多个音频源,还可以让我们应用滤镜使这些音频听起来更逼真。
pannerNode
利用 Web Audio API 的位置功能,因此我们可以将更多关于游戏世界的信息关联到玩家身上。这里有一个教程,可以帮助你更详细地了解 pannerNode
。
我们可以关联
- 物体的位置
- 物体方向和运动
- 环境(洞穴、水下等)
这在使用 WebGL 渲染的三维环境中尤其有用,在这些环境中,Web Audio API 可以将音频与物体和视角关联起来。