Web 游戏音频
音频是任何游戏的重要组成部分;它增加反馈和氛围。基于网络的音频正在迅速成熟,但仍有许多浏览器差异需要解决。我们通常需要决定哪些音频部分对我们的游戏体验至关重要,哪些是可有可无但并非必不可少的部分,并相应地制定策略。本文提供了实现 Web 游戏音频的详细指南,着眼于目前在尽可能广泛的平台上的工作方式。
移动音频注意事项
到目前为止,提供 Web 音频支持最困难的平台是移动平台。不幸的是,这些也是人们经常用来玩游戏的平台。桌面浏览器和移动浏览器之间存在一些差异,这可能导致浏览器供应商做出一些选择,从而使 Web 音频难以供游戏开发者使用。现在让我们来看看这些。
自动播放
浏览器自动播放策略现在影响桌面*和*移动浏览器。有关它的更多信息,请参阅Google Developers 网站上的此处。
值得注意的是,如果满足以下条件,则允许自动播放声音:
- 用户已与该域进行交互。
- 在移动设备上,用户已使应用程序可安装。
许多浏览器将忽略您的游戏发出的任何自动播放音频的请求;相反,音频播放需要由用户发起的事件(例如点击或轻触)来启动。这意味着您必须构建音频播放以考虑这一点。通常通过提前加载音频并在用户发起的事件上进行预加载来缓解这种情况。
对于更被动的音频自动播放,例如游戏加载时立即开始的背景音乐,一个技巧是检测*任何*用户发起的事件并开始播放。对于游戏中使用的其他更主动的声音,我们可以考虑在按下“开始”按钮之类的操作时立即预加载它们。
要像这样预加载音频,我们希望播放它的一部分;因此,在您的音频样本末尾包含一段静音很有用。跳到、播放然后暂停该静音意味着我们现在可以使用 JavaScript 在任意点播放该文件。您可以在此处找到有关自动播放策略最佳实践的更多信息。
注意:如果浏览器允许您更改音量(请参见下文),则以零音量播放文件的一部分也可能有效。另请注意,播放并立即暂停音频并不能保证不会播放一小段音频。
注意:将 Web 应用程序添加到移动设备主屏幕可能会改变其功能。在 iOS 上自动播放的情况下,目前似乎就是这样。如果可能,您应该在多个设备和平台上尝试您的代码,以查看其工作方式。
有关自动播放支持,请参阅<audio>。
音量
移动浏览器中可能会禁用程序化音量控制。通常给出的理由是用户应该在操作系统级别控制音量,并且不应覆盖此设置。
有关音量控制的支持,请参阅HTMLMediaElement.volume。
缓冲和预加载
可能是为了缓解移动网络数据使用失控,我们还经常发现,在播放开始之前,缓冲已禁用。缓冲是浏览器提前下载媒体的过程,我们通常需要这样做以确保流畅播放。
HTMLMediaElement 接口提供了许多属性,可帮助确定轨道是否处于可播放状态。
注意:在许多方面,缓冲的概念已经过时。只要接受字节范围请求(这是默认行为),我们就可以跳转到音频中的特定点,而无需下载前面的内容。然而,预加载仍然有用——如果没有它,在播放开始之前总是需要一些客户端-服务器通信。
移动端变通方案
尽管移动浏览器可能会带来问题,但仍有办法解决上述问题。
音频精灵
音频精灵的名字来源于CSS 精灵,这是一种使用 CSS 和单个图形资源将其分解为一系列精灵的视觉技术。我们可以将相同的原理应用于音频,这样我们就不必拥有大量加载和播放耗时的小音频文件,而是拥有一个包含所有所需小音频片段的较大音频文件。要从文件中播放特定声音,我们只需使用每个音频精灵的已知开始和停止时间。
优点是我们可以预加载一段音频并让我们的精灵准备就绪。为此,我们只需播放并立即暂停较大的音频。您还将减少服务器请求的数量并节省带宽。
const myAudio = document.createElement("audio");
myAudio.src = "my-sprite.mp3";
myAudio.play();
myAudio.pause();
您需要采样当前时间才能知道何时停止。如果您的单个声音间隔至少 500 毫秒,那么使用 timeUpdate 事件(每 250 毫秒触发一次)就足够了。您的文件可能比实际需要的稍长,但静音压缩效果很好。
这是一个音频精灵播放器示例——首先让我们在 HTML 中设置用户界面
<audio id="myAudio" src="/shared-assets/audio/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.dataset.start;
stopTime = Number(button.dataset.stop);
myAudio.play();
});
}
myAudio.addEventListener("timeupdate", () => {
if (myAudio.currentTime > stopTime) {
myAudio.pause();
}
});
注意:在移动设备上,我们可能需要如上所述从用户发起的事件(例如按下开始按钮)触发此代码。
注意:注意比特率。以较低比特率编码音频意味着文件更小,但寻址精度更低。
背景音乐
游戏中的音乐可以产生强大的情感效果。您可以混合搭配各种音乐样本,并假设您可以控制音频元素的音量,则可以交叉渐变不同的音乐作品。使用playbackRate() 方法,您甚至可以调整音乐的速度而不影响音高,以使其更好地与动作同步。
所有这些都可以使用标准的<audio> 元素和相关的HTMLMediaElement 来实现,但使用更高级的Web Audio API 会变得更加容易和灵活。接下来让我们看看这个。
用于游戏的 Web Audio 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,这意味着现在可以为 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 函数来设置样本——我们可以将两个 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() 函数的第一个参数,该参数接受播放应该开始的绝对时间。请注意第二个参数(在新曲目中从哪里开始播放)是相对的
const tempo = 3.074074076;
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);
}
注意:如果第一个参数为 0 或小于上下文 currentTime,则播放将立即开始。
要尝试此操作,您可以采用与上面相同的多轨道源代码,但将 playTrack() 函数中的 if 语句替换为上面的代码。
位置音频
位置音频是使音频成为沉浸式游戏体验关键部分的重要技术。Web Audio API 不仅使我们能够在三维空间中定位多个音源,它还可以让我们应用滤镜,使音频听起来更逼真。
pannerNode 利用 Web Audio API 的位置功能,因此我们可以将有关游戏世界的更多信息关联到玩家。此处有一个教程,可帮助更详细地理解 pannerNode。
我们可以关联
- 物体的位置
- 物体的方向和运动
- 环境(洞穴、水下等)
这在通过 WebGL 渲染的三维环境中特别有用,Web Audio API 使音频能够与对象和视点绑定。