《Android 3D游戏开发技术宝典——OpenGL ES 2.0》——2.1节游戏中的音效

简介:

本节书摘来自异步社区《Android 3D游戏开发技术宝典——OpenGL ES 2.0》一书中的第2章,第2.1节游戏中的音效,作者 吴亚峰,更多章节内容可以访问云栖社区“异步社区”公众号查看

2.1 游戏中的音效
Android 3D游戏开发技术宝典——OpenGL ES 2.0
一款好游戏,除了具备优质的画面和较高的可玩性之外,还应该有出色的音效。音效一般指的是游戏中发生特定行为或进行特定操作时播放的效果音乐或为了渲染整体气氛播放的背景音,如远处隆隆的炮声、怪物死亡的惨叫声、由远而近的脚步声等。

通过开发人员精心准备的声音特效,结合游戏的场景,可以渲染出一种紧张刺激的氛围,使玩家产生身临其境的感觉。这就像电影中的声音特效一样,假如没有了合适的音效,那么游戏和电影一样,真实感会大打折扣。

提示 按照作用的不同,可以将音效划分为即时音效和背景音乐。两种音效在Android中的实现技术是不同的,本节将向读者详细介绍两种音效在Android中的具体实现。

2.1.1 游戏中的即时音效
游戏中有时需要根据情况播放即时音效,如枪炮声、碰撞声等。即时音效的特点是短暂、可以重复、可以同时播放。由于Android提供的MediaPlayer(媒体播放器)会占用大量的系统资源,而且播放时还需要进行缓冲,有较大的时延,因此使用MediaPlayer无法实现即时音效。

Android系统的设计者也考虑到了这个问题,为即时音效的实现提供了一个专门的类——SoundPool。SoundPool类用于管理和播放应用程序中的声音资源,使用该类时首先需要通过该类将声音资源加载到内存中,然后在需要即时音效的地方播放即可,其几乎没有时延,可以满足游戏实时性的需要。

提示 由于SoundPool设计的初衷是用于无时延地播放游戏中的短促音效,因此实际开发中应该只将长度小于7s的声音资源放进SoundPool,否则可能加载失败或内存占用过大。
SoundPool类的构造器及常用方法如表2-1所列。


9b61a684e462874ea343899dfa12c9262f06e79e

2.1.2 即时音效的一个案例
了解了SoundPool类的基本操作方法之后,接下来就可以开发游戏中用到的即时音效了。本小节将向读者展示一个播放和停止即时音效的简单案例,其主要功能为,通过SoundPool声音池技术来实现一个即时音效的播放和停止,运行效果如图2-1和图2-2所示。

了解了本案例的运行效果后,接下来对其具体开发步骤进行介绍,具体如下所列。

(1)首先在Eclipse中新建名称为Sample2_1的项目,然后在项目目录下的res文件夹下新建raw文件夹。接着将需要被播放的短促音效对应的音频文件musictest.ogg复制到raw文件夹下,如图2-3所示。


5a6ca0c6bfd51ded47291957e433acba388cd632


9a64a21d7dc15da7b6913cae2aa78ac826c8b49e

提示 一般在Android手机平台上使用的即时音效文件越小越好,这有助于提高游戏的整体性能。对于同一个音效文件,在不改变其时长的情况下,可以采用降低采样率(如降低到16Kbit/s)或由立体声改为单声道的方式来缩小体积。
(2)准备好声音资源后,接下来进行本案例中Sample2_1_Activity类的开发。该类中使用了声音池技术实现了即时音效的播放,其代码如下。

1 package com/bn/pp1;         //声明包
2 import java.util.HashMap;        //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.widget.Toast;       //引入相关类
5 public class Sample2_1_Activity extends Activity {
6  SoundPool sp;          // 声明SoundPool的引用
7  HashMap<Integer, Integer> hm;       // 声明HashMap来存放声音文件
8  int currStreamId;         // 当前正播放的streamId
9  @Override
10             // 重写onCreate方法
11  public void onCreate(Bundle savedInstanceState) {
12   super.onCreate(savedInstanceState);
13   setContentView(R.layout.main);    // 跳转到主界面
14   initSoundPool();        // 初始化声音池的方法
15   Button b1 = (Button) this.findViewById(R.id.Button01); // 获取播放按钮
16   b1.setOnClickListener       // 为播放按钮添加监听器
17   (new OnClickListener() {
18    @Override
19    public void onClick(View v) {
20     playSound(1, 0);      // 播放1号声音资源,且播放一次
21             // 提示播放即时音效
22     Toast.makeText(getBaseContext(), "播放即时音效", Toast.LENGTH_SHORT)
23       .show();
24    }
25   });
26   Button b2 = (Button) this.findViewById(R.id.Button02); // 获取停止按钮
27   b2.setOnClickListener       // 为停止按钮添加监听器
28   (new OnClickListener() {
29    @Override
30    public void onClick(View v) {
31     sp.stop(currStreamId);    // 停止正在播放的某个声音
32             // 提示停止播放
33     Toast.makeText(getBaseContext(), "停止播放即时音效", 
34       Toast.LENGTH_SHORT)
35       .show();
36    }
37   });}
38             // 初始化声音池的方法
39  public void initSoundPool() {
40   sp = new SoundPool(4, AudioManager.STREAM_MUSIC, 0); // 创建SoundPool对象
41   hm = new HashMap<Integer, Integer>();   // 创建HashMap对象
42         // 加载声音文件musictest并且设置为1号声音放入hm中
43   hm.put(1, sp.load(this, R.raw.musictest, 1)); 
44  }
45             // 播放声音的方法
46  public void playSound(int sound, int loop) { // 获取AudioManager引用
47   AudioManager am = (AudioManager) this
48     .getSystemService(Context.AUDIO_SERVICE);
49             // 获取当前音量
50   float streamVolumeCurrent = am
51     .getStreamVolume(AudioManager.STREAM_MUSIC);
52             // 获取系统最大音量
53   float streamVolumeMax = am
54     .getStreamMaxVolume(AudioManager.STREAM_MUSIC);
55             // 计算得到播放音量
56   float volume = streamVolumeCurrent / streamVolumeMax;
57          // 调用SoundPool的play方法来播放声音文件
58   currStreamId = sp.play(hm.get(sound), volume, volume, 1, loop, 1.0f);
59 }}

第6-8行为声明所用到的SoundPool和HashMap对象的引用,其中SoundPool用来加载、播放、停止音效;HashMap用来管理音效id。currStreamId为当前正播放的streamId,用于对正在播放的音效进行管理。
第14行调用initSoundPool方法来对声音池进行初始化。
第15-37行为播放和停止按钮添加监听器,并在单击操作时显示Toast进行提示。
第39-44行为初始化声音池的方法,其首先创建SoundPool对象,然后加载音效文件到声音池并将生成的音效id存储进HashMap中。
第46-59行为播放声音的方法,其中调用SoundPool的play方法来实现即时音效的播放。

提示 通过以上的案例,读者可以看出使用SoundPool播放即时音效是非常简单的。今后的游戏开发中,只要是游戏中的即时音效都应该用此方式来实现。

2.1.3 背景音乐播放技术
背景音乐也可以采用前一小节的声音池技术,在播放背景音乐的时候,只需要把loop播放次数参数设置成-1进行无限循环即可。但由于SoundPool只适合播放不大于7秒的音效文件,限制较大。而背景音乐对时延并不敏感,因此在实际的游戏开发中,时长较长的背景音乐一般采用媒体播放器(MediaPlayer)来进行播放。

要想很好地使用MediaPlayer进行音/视频文件的播放,首先必须要熟悉MediaPlayer的生命周期。这样不仅有利于开发人员开发出更加合理的代码,而且可以达到充分利用系统资源的目的。

1.MediaPlayer的生命周期
MediaPlayer的生命周期包括10种状态,每种状态下可以调用相应的方法来实现音/视频文件的管理或播放。其各个状态及状态间的关系可以用一个简单的流程图来表示,如图2-4所示。


df24291c41c20298a310875d557d9c53de01f6b1

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方法,这是不会改变状态的。

Stop 状态。
Started或Paused状态下均可调用stop方法停止播放并进入Stop状态,而处于Stop状态的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所列。


59a8e4eeb3c6f5de4a12efcdab312036b63ab9c3

提示 MediaPlayer类还可以对视频文件进行操作,由于本书只介绍与游戏音效相关的功能,因此不再对其进行介绍,有兴趣的读者可以自行查阅相关资料。

2.1.4 简易音乐播放器的实现
了解了MediaPlayer和AudioManager类的基本操作方法之后,就可以对游戏的背景音乐功能进行开发了。本小节将通过这两个类来实现一个简易的音乐播放器,其主要功能为对手机SD卡中的音乐文件进行播放,运行效果如图2-5所示。

提示 图2-5中从左到右分别为案例运行后,依次单击“播放音乐”按钮、“暂停播放”按钮、“增大音量”按钮后的效果图。停止播放音乐和减小音量的效果与图2-5中的已有效果类似,这里没有给出,请读者自行运行本案例进行查看。
了解了案例的具体运行效果后,接下来就介绍案例的具体开发步骤,具体如下所列。

(1)首先需要准备好要播放的音乐文件,本案例中使用的是著名的高山流水古曲,文件名为“gsls.mp3”。准备完音乐文件后,将该音乐文件通过DDMS导入到模拟器或真机的SD卡中,如图2-6所示。


d99bdd94dbce3c23f04ff54cfc7d08db0db94aba

提示 高山流水曲的音乐资源文件见随书光盘中源代码/第2章目录下的gsls.mp3。导入时直接用鼠标光标将文件拖曳到DDMS下“File Explorer”面板中的“sdcard”目录下即可。
(2)音乐资源在SD卡中放置完成后,接下来进行本案例中Sample2_2_Activity类的开发。该类使用了MediaPlayer实现了背景音乐的播放,具体代码如下。

1 package com.bn.pp2;         //声明包
2 import java.io.IOException;       //引入相关类
3 ……//此处省略了部分类的引入代码,读者可自行查看随书光盘的源代码
4 import android.widget.Toast;       //引入相关类
5 public class Sample2_2_Activity extends Activity {
6  MediaPlayer mp;          // 声明MediaPlayer的引用
7  AudioManager am;          // 声明AudioManager的引用
8  private int maxVolume;        // 最大音量值   
9  private int currVolume;        // 当前音量值   
10  private int stepVolume;       // 每次调整的音量幅度 
11  @Override
12  public void onCreate(Bundle savedInstanceState)  // 重写onCreate方法
13  {
14   super.onCreate(savedInstanceState);
15   setContentView(R.layout.main);    // 跳转到主界面
16   mp = new MediaPlayer();   // 创建MediaPlayer实例对象
17   try {
18    mp.setDataSource("/sdcard/gsls.mp3"); //为MediaPlayer设置要播放文件资源
19    mp.prepare();    // MediaPlayer进行缓冲准备
20   }
21   catch (Exception e) {
22    e.printStackTrace();
23   }
24          // 获取AudioManager对象引用
25   am = (AudioManager) this.getSystemService(Context.AUDIO_SERVICE); 
26          // 获取最大音乐音量   
27   maxVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);    
28          // 每次调整的音量大概为最大音量的1/6   
29   stepVolume = maxVolume / 6;  
30   Button bstart = (Button) this.findViewById(R.id.Button01); // 获取开始按钮
31   bstart.setOnClickListener   // 为开始按钮添加监听器
32   (new OnClickListener() {
33    @Override
34    public void onClick(View v) {
35     mp.start();    // 调用MediaPlayer的start方法来播放音乐
36     Toast.makeText(getBaseContext(),“开始播放‘高山流水曲’”,
37       Toast.LENGTH_LONG).show();
38    }
39   });
40   Button bpause = (Button) this.findViewById(R.id.Button02); // 获取暂停按钮
41   bpause.setOnClickListener   // 为暂停按钮添加监听器
42   (new OnClickListener() {
43    @Override
44    public void onClick(View v) {
45     mp.pause();    // 调用MediaPlayer的pause方法暂停播放音乐
46     Toast.makeText(getBaseContext(), "暂停播放'高山流水曲'",
47       Toast.LENGTH_LONG).show();
48    }
49   });
50   Button bstop = (Button) this.findViewById(R.id.Button03); // 获取停止按钮
51   bstop.setOnClickListener       // 为停止按钮添加监听器
52   (new OnClickListener() {
53    @Override
54    public void onClick(View v) {
55     mp.stop();    // 调用MediaPlayer的stop方法停止播放音乐
56     try {
57      mp.prepare();     //进入准备状态
58     } catch (IllegalStateException e) {  //捕获异常
59      e.printStackTrace();
60     } catch (IOException e) {    //捕获异常
61      e.printStackTrace();
62     }
63     Toast.makeText(getBaseContext(), "停止播放'高山流水曲'",
64       Toast.LENGTH_LONG).show();
65    }
66   });
67   Button bUp = (Button) this.findViewById(R.id.Button04); // 获取增大音量按钮
68   bUp.setOnClickListener      // 为增大音量按钮添加监听器
69   (new OnClickListener() {
70    @Override
71    public void onClick(View v) {
72             // 获取当前音量  
73     currVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);    
74             //增加音量,但不超过最大音量值
75     int tmpVolume = currVolume + stepVolume;  //临时音量
76     currVolume = tmpVolume < maxVolume ? tmpVolume:maxVolume;
77     am.setStreamVolume(AudioManager.STREAM_MUSIC, currVolume,
78       AudioManager.FLAG_PLAY_SOUND);
79     Toast.makeText(getBaseContext(), "增大音量",
80       Toast.LENGTH_SHORT).show();
81    }
82   });
83   Button bDown = (Button) this.findViewById(R.id.Button05); // 获取减小音量按钮
84   bDown.setOnClickListener       // 为减小音量按钮添加监听器
85   (new OnClickListener() {
86    @Override
87    public void onClick(View v) {
88              // 获取当前音量  
89     currVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC);    
90              //减小音量,但不小于0
91     int tmpVolume = currVolume - stepVolume;  //临时音量
92     currVolume = tmpVolume > 0 ? tmpVolume:0;  
93     am.setStreamVolume(AudioManager.STREAM_MUSIC, currVolume,
94       AudioManager.FLAG_PLAY_SOUND);
95     Toast.makeText(getBaseContext(), "减小音量",
96       Toast.LENGTH_SHORT).show();
97    }
98 });}}

第6-10行为声明需要用到的MediaPlayer和AudioManager对象的引用、最大音量值、当前音量值及每次调整的音量幅度等。
第16-23行获取了MediaPlayer类对象的引用,设置了音乐资源的路径,并调用prepare方法进入了准备状态。
第24-29行获取了AudioManager类对象的引用,同时获取了音乐播放的最大音量值,并设置每次调整的音量幅度为最大值的1/6。
第30-98行分别获取了开始按钮、暂停按钮、停止按钮以及增大和减小音量按钮对象的引用,并分别为这些按钮添加了监听器。这样在用户单击这些按钮时程序就能完成指定的功能了,例如,按下开始按钮开始播放音乐、按下减小音量按钮减小播放音量等。

相关文章
|
2月前
|
安全 Android开发 iOS开发
Android vs. iOS:构建生态差异与技术较量的深度剖析###
本文深入探讨了Android与iOS两大移动操作系统在构建生态系统上的差异,揭示了它们各自的技术优势及面临的挑战。通过对比分析两者的开放性、用户体验、安全性及市场策略,本文旨在揭示这些差异如何塑造了当今智能手机市场的竞争格局,为开发者和用户提供决策参考。 ###
|
2月前
|
安全 Android开发 iOS开发
安卓与iOS的较量:技术深度对比
【10月更文挑战第18天】 在智能手机操作系统领域,安卓和iOS无疑是两大巨头。本文将深入探讨这两种系统的技术特点、优势以及它们之间的主要差异,帮助读者更好地理解这两个平台的独特之处。
55 0
|
20天前
|
Java API 开发工具
Cocos游戏如何快速接入安卓优量汇广告变现?
本文介绍了如何在Cocos游戏项目中快速接入安卓优量汇广告,通过详细的步骤指导,包括前期准备、编辑gradle和清单文件、核心代码集成等,帮助开发者轻松实现广告功能,增加游戏的盈利渠道。文中还提供了示例工程下载链接,方便开发者直接上手实践。
Cocos游戏如何快速接入安卓优量汇广告变现?
|
1月前
|
安全 搜索推荐 Android开发
揭秘安卓与iOS系统的差异:技术深度对比
【10月更文挑战第27天】 本文深入探讨了安卓(Android)与iOS两大移动操作系统的技术特点和用户体验差异。通过对比两者的系统架构、应用生态、用户界面、安全性等方面,揭示了为何这两种系统能够在市场中各占一席之地,并为用户提供不同的选择。文章旨在为读者提供一个全面的视角,理解两种系统的优势与局限,从而更好地根据自己的需求做出选择。
80 2
|
1月前
|
安全 搜索推荐 Android开发
揭秘iOS与安卓系统的差异:一场技术与哲学的较量
在智能手机的世界里,iOS和Android无疑是两大巨头,它们不仅定义了操作系统的标准,也深刻影响了全球数亿用户的日常生活。本文旨在探讨这两个平台在设计理念、用户体验、生态系统及安全性等方面的本质区别,揭示它们背后的技术哲学和市场策略。通过对比分析,我们将发现,选择iOS或Android,不仅仅是选择一个操作系统,更是选择了一种生活方式和技术信仰。
|
2月前
|
安全 Android开发 iOS开发
iOS与安卓:技术生态的双雄争霸
在当今数字化时代,智能手机操作系统的竞争愈发激烈。iOS和安卓作为两大主流平台,各自拥有独特的技术优势和市场地位。本文将从技术架构、用户体验、安全性以及开发者支持四个方面,深入探讨iOS与安卓之间的差异,并分析它们如何塑造了今天的移动技术生态。无论是追求极致体验的苹果用户,还是享受开放自由的安卓粉丝,了解这两大系统的内在逻辑对于把握未来趋势至关重要。
|
2月前
|
安全 搜索推荐 Android开发
揭秘iOS与Android系统的差异:一场技术与哲学的较量
在当今数字化时代,智能手机操作系统的选择成为了用户个性化表达和技术偏好的重要标志。iOS和Android,作为市场上两大主流操作系统,它们之间的竞争不仅仅是技术的比拼,更是设计理念、用户体验和生态系统构建的全面较量。本文将深入探讨iOS与Android在系统架构、应用生态、用户界面及安全性等方面的本质区别,揭示这两种系统背后的哲学思想和市场策略,帮助读者更全面地理解两者的优劣,从而做出更适合自己的选择。
|
2月前
|
安全 Android开发 iOS开发
安卓vs iOS:探索两种操作系统的独特魅力与技术深度###
【10月更文挑战第16天】 本文旨在深入浅出地探讨安卓(Android)与iOS这两种主流移动操作系统的特色、优势及背后的技术理念。通过对比分析,揭示它们各自如何塑造了移动互联网的生态,并为用户提供丰富多彩的智能体验。无论您是科技爱好者还是普通用户,都能从这篇文章中感受到技术创新带来的无限可能。 ###
58 2
|
2月前
|
机器学习/深度学习 人工智能 Android开发
安卓与iOS:技术演进的双城记
【10月更文挑战第16天】 在移动操作系统的世界里,安卓和iOS无疑是两个最重要的玩家。它们各自代表了不同的技术理念和市场策略,塑造了全球数亿用户的移动体验。本文将深入探讨这两个平台的发展历程、技术特点以及它们如何影响了我们的数字生活,旨在为读者提供一个全面而深入的视角,理解这两个操作系统背后的哲学和未来趋势。
34 2
|
2月前
|
Java Android开发 Swift
掌握安卓与iOS应用开发:技术比较与选择指南
在移动应用开发领域,谷歌的安卓和苹果的iOS系统无疑是两大巨头。它们不仅塑造了智能手机市场,还影响了开发者的日常决策。本文深入探讨了安卓与iOS平台的技术差异、开发环境及工具、以及市场表现和用户基础。通过对比分析,旨在为开发者提供实用的指导,帮助他们根据项目需求、预算限制和性能要求,做出最合适的平台选择。无论是追求高度定制的用户体验,还是期望快速进入市场,本文都将为您的开发旅程提供有价值的见解。