支持的浏览器:
难度:中等
注意:本文讨论的API尚未最终确定,仍在不断变化。请在自己的项目中谨慎使用。
介绍
音频在很大程度上使得多媒体体验非常引人注目。如果你曾经尝试在关闭声音的情况下看电影,你就很可能已经注意到了这一点。
游戏也不例外!我最喜爱的视频游戏的回忆里包含了音乐和声效。在二十年后的今天,大多情况下,当玩我最爱的游戏时,我仍然不能把“塞尔达”里近藤浩二的乐曲和马特大气的暗黑配乐从我的头脑里驱逐掉。这同样适用于音效,例如魔兽里单位实时点击的响应,以及任天堂的经典例子。
游戏的音频提出了一些有趣的挑战。要创建令人着迷的游戏音乐,设计人员需要调节潜在的不可预知的状态。实际上,部分游戏能持续未知的时间长度,声音可以与环境互动,并以复杂的方式混合起来,例如室内效果和相对声音定位。最后,可能有大量的一次播放声效,这需要不错的混合效果和在渲染时没有性能损失。
网页上的游戏音频
简单的游戏使用<audio>
标签可能就足够了。然而,许多浏览器提供的简陋实现导致音频毛刺和高延迟的出现。这可能只是暂时性的问题,因为厂商们都在努力改进各自的实现。要了解<audio>
标签的支持情况,我们可以使用areweplayingyet.org所提供的优秀测试工具。
一旦深入<audio>
标签规范,就会清楚了解到有很多事情根本不能用它实现。这并不奇怪,因为它主要被设计来支持多媒体播放。这些限制包括:
- 无法为声音信号使用滤波器
- 无法访问原始的PCM(宇捷:即WAV)数据
- 没有来源和听众位置、方向的概念
- 没有细粒度的计时
在下文中,我将深入介绍一些用WebAudio API编写游戏音频方面的内容。在入门教程里可以了解到此API的简单介绍。
背景音乐
游戏里往往有循环播放的背景音乐。例如,一个背景音轨如下:
如果你的循环音乐很短并且已知,会相当的烦人。当玩家被困在一个区域或者关卡上,会同时连续播放相同的背景音乐,我们可能需要逐渐淡出来防止让玩家厌烦。另一种策略是,根据游戏中的上下文,把不同的音效强度通过逐渐的淡入淡出混合起来。
如果你的玩家在一个史诗般的BOSS关卡里,可能需要对几个不同的情绪范围进行混音,例如从艺术氛围到有心理暗示的氛围再到激烈的氛围。音乐合成软件通常允许你通过选择音轨集合来导出几种混音(它们具有同样长度)。这样音轨之间就有某种内部一致性,避免出现从一个音轨切换到另一个时出现不和谐的转换过渡。
然后,利用WebAudio API,你可以使用某些类例如BufferLoader通过XHR导入所有这些音效样本(这在介绍网络音频API的文章中进行了深入介绍)。加载音效需要时间,所以这些在游戏中使用的音效在每一关开始时,应该在页面加载时同时载入,或者在播放器播放时增量加载。
接下来,你需要为每个节点创建一个源,并为每个源创建一个增益节点,连接图如下:
完成之后,你可以在一个循环中同时回放这些音效源,因为它们都具有相同的长度,WebAudio API将保证它们保持一致。由于最后的BOSS战时音效风格会变得相近或更不同,游戏可以使用类似于下面的增量算法来改变链中各节点对应的增益值:
- // Assume gains is an array of AudioGainNode, normVal is the intensity
- // between 0 and 1.
- var value = normVal * (gains.length - 1);
- // First reset gains on all nodes.
- for (var i = 0; i < gains.length; i++) {
- gains[i].gain.value = 0;
- }
- // Decide which two nodes we are currently between, and do an equal
- // power crossfade between them.
- var leftNode = Math.floor(value);
- // Normalize the value between 0 and 1.
- var x = value - leftNode;
- var gain1 = Math.cos(x * 0.5*Math.PI);
- var gain2 = Math.cos((1.0 - x) * 0.5*Math.PI);
- // Set the two gains accordingly.
- gains[leftNode].gain.value = gain1;
- // Check to make sure that there's a right node.
- if (leftNode < gains.length - 1) {
- // If there is, adjust its gain.
- gains[leftNode + 1].gain.value = gain2;
- }
在上述方法中,有两个音效源同时播放,我们使用同等功率的曲线(如介绍所述)从它们之间淡入淡出。下面的示例使用了这一策略,演示的背景音乐在魔兽争霸2的主题上逐渐增强:
缺少的环节:Web Audio的Audio标签
现在许多游戏开发商为背景音乐使用<audio>
标签,因为它非常适合流媒体内容。现在你可以通过<audio>
标签把内容带入网络音频的上下文。
<audio>标签支持流媒体相当有用,因为它可以让你立即播放背景音乐,而无须等待下载所有内容。在网络音频API支持音频流之后,你可以操作或分析它们。下面的例子为通过<audio>
标签播放的音乐使用了一个低通滤波器:
- var audioElement = document.querySelector('audio');
- var mediaSourceNode = context.createMediaElementSource(audioElement);
- // Create the filter
- var filter = context.createBiquadFilter();
- // Create the audio graph.
- mediaSourceNode.connect(filter);
- filter.connect(context.destination);
关于<audio>
标签和网络音频API整合更多的讨论,可以看看这篇短文。
音效
游戏经常在响应用户输入或者游戏状态改变时播放声音效果。但是像背景音乐一样,音效可以很快的让用户厌倦。 为了避免这种情况,最好有一个音效池放置相似但是不同的音效。 这可以从轻微变化到急剧变化间通过固定长度来过渡,像魔兽系列里点击各单位的时候。
游戏音效的另外一个关键点是可以同时有多个。想象一下,你与多个演员拍摄枪战时。每个机枪每秒触发多次,造成几十个音效同时播放。从多个源同时播放音效,还要对音效源精确计时,是网络音频API真正的亮点。
下面的例子演示了由多个单独子弹样本组成的机枪,其创建了多个播放时间错开的声源。
- var time = context.currentTime;
- for (var i = 0; i < rounds; i++) {
- var source = this.makeSource(this.buffers[M4A1]);
- source.noteOn(time + i * interval);
- }
下面是这个代码的效果:
如果你觉得声音太响了,我感到抱歉。我们将在后面的章节讨论测量和动态压缩。
现在,如果你游戏里所有的机枪都像这样响起,那将相当无聊。当然,它们会基于目标的距离和相对位置而有所差异(稍后讨论),但即使这样做可能还不够。幸运的是,网络音频API提供了对上面的示例进行轻松调整的方式,主要有两种:
1. 发射子弹时间上微妙的变化
2. 改变每个音效的播放速率(同时改变音高),以更好地模拟现实世界中的随机性。
这两种方法的效果如下:
对于这些技术在现实生活中的实际例子,可以看看台球桌的演示 ,它采用了随机抽样和变化的播放速率来表现更有趣的球的碰撞声。
3D定位音效
游戏往往设定在一个2D或者3D的世界里。在这样的情况下,立体定位的音频可以大大增加沉浸感的体验。幸运的是,网络音频API带来了内置硬件加速的位置音频特性,可以直接的使用。 顺便说一下,你应该确保有立体声扬声器(最好是耳机)来运行下面的例子。 在下面的示例中,你可以通过在画布上滚动鼠标滚轮来更改声源的角度。
上面的例子中,有一个监听者在画布正中(人的图标),同时鼠标控制声源(喇叭图标)的位置,这是使用AudioPannerNode实现这种效果的简单例子。它的基本思想是通过设置音频信号源的位置响应鼠标的移动,如下所示:
- PositionSample.prototype.changePosition = function(position) {
- // Position coordinates are in normalized canvas coordinates
- // with -0.5 < x, y < 0.5
- if (position) {
- if (!this.isPlaying) {
- this.play();
- }
- var mul = 2;
- var x = position.x / this.size.width;
- var y = -position.y / this.size.height;
- this.panner.setPosition(x * mul, y * mul, -0.5);
- } else {
- this.stop();
- }
- };
关于网络音频空间化处理需要了解的事情:
- 监听者默认在原点(0,0,0)。
- 网络音频位置API没有单位,所以我引入了一个乘数使得演示的声效更好。
- 网络音频采用Y-型直角坐标系(和大多数计算机图形系统相反)。 这就是为什么我在上面的代码片段进行了y轴的变换。
高级:音锥
定位模型非常强大,而且相当先进,主要基于OpenAL。详细信息请查看上述规范的第3和第4节。
在有单一的AudioListener连接到网络音频API的情况下,它可以通过位置和方向配置空间。每个源可以通过一个AudioPannerNode(音频声像节点)来使得音频输入空间化。声像节点有位置和方向,以及距离和方向性模型。
距离模型指定的增益取决于和源的接近程度,而方向模型可以通过指定内外锥来配置,以决定监听者在内部锥里,在内外锥之间,或在外部锥之外时增益的大小(通常为负值)。
- var panner = context