本节书摘来异步社区《OpenGL ES 3.x游戏开发(上卷)》一书中的第2章,第2.1节,作者: 吴亚峰 责编: 张涛,更多章节内容可以访问云栖社区“异步社区”公众号查看。
2.1 游戏中的音效
一款好游戏,除了具备优质的画面和较高的可玩性之外,还应该有出色的音效。音效一般指的是游戏中发生特定行为或进行特定操作时播放的效果音乐或为了渲染整体气氛播放的背景音乐等,如远处隆隆的炮声、由远而近的脚步声等。
开发人员通过精心准备的声音特效,并结合游戏酷炫的场景,可以渲染出一种紧张刺激的氛围,使玩家产生身临其境的感觉。这就像电影中的声音特效一样,假如没有了合适的音效,那么游戏和电影一样,真实感会大打折扣。
提示
按照作用的不同,可以将音效划分为即时音效和背景音乐。两种音效在Android中的实现技术不同,本节将向读者详细介绍两种音效在Android中的具体实现。
2.1.1 游戏中的即时音效
游戏中有时需要根据情况播放即时音效,如枪炮声、碰撞声等。即时音效的特点是短暂、可以重复、可以同时播放的。由于Android提供的MediaPlayer(媒体播放器)会占用大量的系统资源,而且播放时还需要进行缓冲,有较大的时延,因此使用MediaPlayer无法实现即时音效。
Android系统的设计者也考虑到了这个问题,为即时音效的实现提供了一个专门的类——SoundPool。SoundPool类用于管理和播放应用程序中的声音资源,使用该类时首先需要通过该类将声音资源加载到内存中,然后在需要即时音效的地方播放即可,几乎没有时延,可以满足游戏实时性的需要。
提示
由于SoundPool设计的初衷是用于无时延地播放游戏中的短促音效,因此实际开发中应该只将长度小于7s的声音资源放进SoundPool,否则可能会加载失败或内存占用过大。
SoundPool类的构造器及常用方法如表2-1所列。
2.1.2 即时音效的一个案例
了解了SoundPool类的基本操作方法之后,接下来就可以开发游戏中用到的即时音效了。本小节将向读者展示一个播放和停止即时音效的简单案例,其主要功能为,通过SoundPool声音池技术来实现一个即时音效的播放和停止,运行效果如图2-1和图2-2所示。
了解了本案例的运行效果后,接下来对其具体开发步骤进行介绍,具体如下所列。
(1)首先在Eclipse中创建名称为Sample2_1的项目,然后在项目目录下的res文件夹下创建raw文件夹。接着将需要被播放的短促音效对应的音频文件musictest.ogg复制到raw文件夹下,如图2-3所示。
提示
一般在Android手机平台上使用的即时音效文件越小越好,这有助于提高游戏的整体性能。对于同一个音效文件,在不改变其时长的情况下,可以采用降低采样率(如降低到16kbit/s)或由立体声改为单声道的方式来缩小体积。
(2)准备好声音资源后,接下来进行本案例中Sample2_1_Activity类的开发。该类中使用了声音池技术实现了即时音效的播放,其代码如下。
1 package com.bn.pp1; //声明包
2 ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
3 public class Sample2_1_Activity extends Activity {
4 SoundPool sp; //声明SoundPool的引用
5 HashMap<Integer, Integer> hm; //声明HashMap来存放声音文件
6 int currStreamId; //当前正播放的streamId
7 @Override
8 public void onCreate(Bundle savedInstanceState) { //重写onCreate方法
9 super.onCreate(savedInstanceState);
10 setContentView(R.layout.main); //跳转到主界面
11 initSoundPool(); //初始化声音池的方法
12 Button b1 = (Button) this.findViewById(R.id.Button01); //获取播放按钮
13 b1.setOnClickListener //为播放按钮添加监听器
14 (new OnClickListener() {
15 @Override
16 public void onClick(View v) {
17 playSound(1, 0); //播放1号声音资源,且播放一次
18 Toast.makeText(getBaseContext(),"播放即时音效",Toast.LENGTH_SHORT)
19 .show(); //提示播放即时音效
20 }});
21 Button b2 = (Button) this.findViewById(R.id.Button02); //获取停止按钮
22 b2.setOnClickListener //为停止按钮添加监听器
23 (new OnClickListener() {
24 @Override
25 public void onClick(View v) {
26 sp.stop(currStreamId); //停止正在播放的某个声音
27 Toast.makeText(getBaseContext(),"停止播放即时音效",Toast.
LENGTH_SHORT) .show(); //提示停止播放
28
29 }});}
30 public void initSoundPool() { //初始化声音池的方法
31 sp = new SoundPool(4, AudioManager.STREAM_MUSIC, 0); //创建SoundPool对象
32 hm = new HashMap<Integer, Integer>(); //创建HashMap对象
33 //加载声音文件musictest并且设置为1号声音放入hm中
34 hm.put(1, sp.load(this, R.raw.musictest, 1));
35 }
36 public void playSound(int sound, int loop) { //播放声音的方法
37 AudioManager am = (AudioManager) this //获取AudioManager引用
38 .getSystemService(Context.AUDIO_SERVICE);
39 float streamVolumeCurrent = am //获取当前音量
40 .getStreamVolume(AudioManager.STREAM_MUSIC);
41 float streamVolumeMax = am //获取系统最大音量
42 .getStreamMaxVolume(AudioManager.STREAM_MUSIC);
43 float volume = streamVolumeCurrent / streamVolumeMax;//计算得到播放音量
44 //调用SoundPool的play方法来播放声音文件
45 currStreamId = sp.play(hm.get(sound), volume, volume, 1, loop, 1.0f);
46 }}
- 第4-6行为声明所用到的SoundPool和HashMap对象的引用,其中SoundPool可以用来加载、播放、停止音效;HashMap用来管理音效id。currStreamId为当前正播放的streamId,用于对正在播放的音效进行管理。
第11行调用initSoundPool方法来对声音池进行初始化。 - 第12-28行为播放和停止按钮添加监听器,并在单击操作时显示Toast进行提示。
- 第30-35行为初始化声音池的方法,其首先创建SoundPool对象,然后加载音效文件到声音池并将生成的音效id存储进HashMap中。
- 第36-46行为播放声音的方法,其中调用SoundPool的play方法来实现即时音效的播放。
提示
通过以上的案例,读者可以看出使用SoundPool播放即时音效是非常简单的。今后的游戏开发中,只要是游戏中的即时音效都应该用此方式来实现。
2.1.3 背景音乐播放技术
背景音乐也可以采用前一小节的声音池技术,在播放背景音乐时,只需要把loop(播放次数参数)设置成-1进行无限循环即可。但由于SoundPool只适合播放不大于7秒的音效文件,限制较大。而背景音乐对时延并不敏感,因此在实际的游戏开发中,时长较长的背景音乐一般采用媒体播放器(MediaPlayer)来进行播放。
要想很好地使用MediaPlayer进行音/视频文件的播放,首先要熟悉MediaPlayer的生命周期。这样不仅有利于开发人员开发出更加合理的代码,而且可以达到充分利用系统资源的目的。
1.MediaPlayer的生命周期
MediaPlayer的生命周期包括10种状态,每种状态下可以调用相应的方法来实现音/视频文件的管理或播放。其各个状态及状态间的关系可以用一个简单的流程图来表示,如图2-4所示。
- Idle 状态。
使用new方法创建一个MediaPlayer对象或者调用了其reset方法时,该MediaPlayer对象处于Idle状态。
但通过以上两种不同方式进入的Idle状态还是有些区别的,主要体现为:如果在此状态下调用了getDuration等方法,并且是通过reset方法进入Idle状态的话会触发OnErrorListener.onError,同时MediaPlayer会进入Error状态;如果是新创建的MediaPlayer对象,则并不会触发onError,也不会进入Error状态。
- End状态。
通过release方法可以进入End状态,只要MediaPlayer对象不再被使用,就应当尽快将其通过release方法释放掉,以释放其占用的软、硬件资源,这其中有些资源是互斥的(相当于临界资源)。如果MediaPlayer对象进入了End状态,则不会再进入其他任何状态了。
- Initialized 状态。
这个状态比较简单,MediaPlayer调用setDataSource方法就进入了Initialized状态,表示此时要播放的文件已经设置好了。
- Prepared 状态。
初始化完成之后还需要通过调用prepare或prepareAsync方法进行准备,这两个方法一个是同步的,一个是异步的。只有进入了Prepared状态,才表明MediaPlayer到目前为止都工作正常,可以进行音乐文件的播放。
- Preparing 状态。
这个状态比较容易理解,主要是与prepareAsync异步准备方法配合,如果异步准备完成,会触发OnPreparedListener.onPrepared,进而进入Prepared状态。
- Started 状态。
MediaPlayer准备完成后,通过调用start方法,将进入Started状态。所谓Started状态,也就是播放中状态,开发中可以使用isPlaying方法测试MediaPlayer是否处于Started状态。
如果播放完毕,而又设置了循环播放,则MediaPlayer仍然会处于Started状态。类似地,如果在该状态下MediaPlayer调用了seekTo或者start方法均可以让MediaPlayer停留在Started状态。
- Paused 状态。
Started状态下调用pause方法可以暂停播放,从而进入Paused状态。MediaPlayer暂停后若再次调用start方法则可以继续进行播放,并转到Started状态。暂停状态时可以调用seekTo方法,这时此MediaPlayer的状态是不变的。
- Stopped状态。
Started或Paused状态下均可调用stop方法停止播放并进入Stopped状态,而处于Stopped状态的MediaPlayer要想重新播放,需要通过调用prepareAsync或prepare方法返回到先前的Prepared状态重新开始才可以。
- PlaybackCompleted状态。
文件正常播放完毕,而又没有设置循环播放的话就进入该状态,并会触发OnCompletionListener接口中的onCompletion方法。此时可以调用start方法重新从头播放文件,也可以调用stop方法停止播放,或者调用seekTo方法来重新定位播放位置。
- Error状态。
由于某种原因MediaPlayer出现了错误,就会触发OnErrorListener.onError回调方法,此时MediaPlayer将会进入Error状态。及时捕捉并妥善处理这些错误是很重要的,这可以帮助应用程序及时释放相关的软、硬件资源,也可以改善用户体验。
如果MediaPlayer进入了Error状态,可以通过调用reset方法来恢复,使得MediaPlayer重新返回到Idle状态。
提示
从上述对生命周期的介绍中可以看出,某些情况发生时MediaPlayer会回调特定监听接口中的事件处理方法。若读者在开发中希望使用回调,则需要首先向MediaPlayer注册实现指定监听接口的监听器。例如,可以使用setOnErrorListener方法注册实现OnErrorListener接口的监听器,当MediaPlayer进入Error状态时监听器中的onError方法就会被回调。
2.AudioManager类
AudioManager类在Android系统中主要用来进行音/视频播放时的音量控制,使用时的基本步骤如下所列。
首先需要调用Activity对象的getSystemService(Context.AUDIO_SERVICE)方法获取AudioManager对象。
然后再调用AudioManager类中的相关方法进行音量控制。
AudioManager类中的常用方法如表2-2所列。
提示
MediaPlayer类还可以对视频文件进行操作,由于本书只介绍其与游戏音效相关的功能,因此不再对其视频文件的播放功能进行介绍,有兴趣的读者可以自行查阅相关资料。
2.1.4 简易音乐播放器的实现
了解了MediaPlayer和AudioManager类的基本操作方法之后,就可以对游戏的背景音乐功能进行开发了。本小节将通过这两个类来实现一个简易的音乐播放器,其主要功能为对手机SD卡中的音乐文件进行播放,运行效果如图2-5所示。
提示
图2-5中从左到右分别为案例运行后,依次单击“播放音乐”按钮、“暂停播放”按钮、“增大音量”按钮后的效果图。停止播放音乐和减小音量的效果与图2-5中的已有效果类似,这里没有给出,请读者自行运行本案例进行查看。
了解了本案例的具体运行效果后,接下来就介绍本案例的具体开发步骤,具体如下所列。
(1)首先需要准备好要播放的音乐文件,本案例中使用的是著名的高山流水古曲,文件名为“gsls.mp3”。准备完音乐文件后,将该音乐文件通过DDMS导入到模拟器或真机的SD卡中,如图2-6所示。
提示
高山流水曲的音乐资源文件见随书中源代码/第2章目录下的gsls.mp3。导入时直接用鼠标光标将文件拖曳到DDMS下“File Explorer”面板中的“sdcard”目录下即可。
(2)音乐资源在SD卡中放置完成后,接下来进行本案例中Sample2_2_Activity类的开发。该类使用了MediaPlayer实现了背景音乐的播放,具体代码如下。
1 package com.bn.pp2;
2 ……//此处省略了部分类的引入代码,读者可自行查看随书的源代码
3 public class Sample2_2_Activity extends Activity {
4 MediaPlayer mp; //声明MediaPlayer的引用
5 AudioManager am; //声明AudioManager的引用
6 private int maxVolume; //最大音量值
7 private int currVolume; //当前音量值
8 private int stepVolume; //每次调整的音量幅度
9 @Override
10 public void onCreate(Bundle savedInstanceState){ //重写onCreate方法
11 super.onCreate(savedInstanceState);
12 setContentView(R.layout.main); //跳转到主界面
13 mp = new MediaPlayer(); //创建MediaPlayer实例对象
14 try {
15 mp.setDataSource("/sdcard/gsls.mp3"); //为MediaPlayer设置要播放文件资源
16 mp.prepare(); //MediaPlayer进行缓冲准备
17 }catch (Exception e) {
18 e.printStackTrace();
19 }
20 //获取AudioManager对象引用
21 am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE);
22 //获取最大音乐音量
23 maxVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
24 stepVolume = maxVolume / 6; //每次调整的音量大概为最大音量的1/6
25 Button bstart = (Button) this.findViewById(R.id.Button01); //获取开始按钮
26 bstart.setOnClickListener //为开始按钮添加监听器
27 (new OnClickListener() {
28 @Override
29 public void onClick(View v) {
30 mp.start(); //调用MediaPlayer的start方法来播放音乐
31 Toast.makeText(getBaseContext(), "开始播放'高山流水曲'",
32 Toast.LENGTH_LONG).show();
33 }});
34 Button bpause = (Button) this.findViewById(R.id.Button02); //获取暂停按钮
35 bpause.setOnClickListener //为暂停按钮添加监听器
36 (new OnClickListener() {
37 @Override
38 public void onClick(View v) {
39 mp.pause(); //调用MediaPlayer的pause方法暂停播放音乐
40 Toast.makeText(getBaseContext(), "暂停播放'高山流水曲'",
41 Toast.LENGTH_LONG).show();
42 }});
43 Button bstop = (Button) this.findViewById(R.id.Button03); //获取停止按钮
44 bstop.setOnClickListener //为停止按钮添加监听器
45 (new OnClickListener() {
46 @Override
47 public void onClick(View v) {
48 mp.stop(); //调用MediaPlayer的stop方法停止播放音乐
49 try {
50 mp.prepare(); //进入准备状态
51 } catch (IllegalStateException e) { //捕获异常
52 e.printStackTrace();
53 } catch (IOException e) { //捕获异常
54 e.printStackTrace();
55 }
56 Toast.makeText(getBaseContext(), "停止播放'高山流水曲'",
57 Toast.LENGTH_LONG).show();
58 }});
59 Button bUp = (Button) this.findViewById(R.id.Button04); //获取增大音量按钮
60 bUp.setOnClickListener //为增大音量按钮添加监听器
61 (new OnClickListener() {
62 @Override
63 public void onClick(View v) { //获取当前音量
64 currVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
65 int tmpVolume = currVolume + stepVolume;
//增加音量,但不超过最大音量值
66 currVolume = tmpVolume < maxVolume ? tmpVolume:maxVolume;
//临时音量
67 am.setStreamVolume(AudioManager.STREAM_MUSIC, currVolume,
68 AudioManager.FLAG_PLAY_SOUND);
69 Toast.makeText(getBaseContext(), "增大音量",
70 Toast.LENGTH_SHORT).show();
71 }});
72 Button bDown = (Button) this.findViewById(R.id.Button05);
//获取减小音量按钮
73 bDown.setOnClickListener //为减小音量按钮添加监听器
74 (new OnClickListener() {
75 @Override
76 public void onClick(View v) { //获取当前音量
77 currVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);
78 //减小音量,但不小于0
79 int tmpVolume = currVolume - stepVolume; //临时音量
80 currVolume = tmpVolume > 0 ? tmpVolume:0;
81 am.setStreamVolume(AudioManager.STREAM_MUSIC, currVolume,
82 AudioManager.FLAG_PLAY_SOUND);
83 Toast.makeText(getBaseContext(), "减小音量",
84 Toast.LENGTH_SHORT).show();
85 }});}}
- 第4-8行为声明需要用到的MediaPlayer和AudioManager对象的引用、最大音量值、当前音量值及每次调整的音量幅度等。
- 第13-19行获取了MediaPlayer类对象的引用,设置了音乐资源的路径,并调用prepare方法进入了准备状态。
- 第20-24行获取了AudioManager类对象的引用,同时获取了音乐播放的最大音量值,并设置每次调整的音量幅度为最大值的1/6。
- 第25-85行分别获取了开始按钮、暂停按钮、停止按钮以及增大和减小音量按钮对象的引用,并分别为这些按钮添加了监听器。这样在用户单击这些按钮时程序就能完成指定的功能了,例如,按下开始按钮开始播放音乐、按下减小音量按钮减小播放音量等。