- OpenGL ES 3.x游戲開發(上卷)
- 吳亞峰
- 4614字
- 2019-01-05 00:53:37
2.1 游戲中的音效
一款好游戲,除了具備優質的畫面和較高的可玩性之外,還應該有出色的音效。音效一般指的是游戲中發生特定行為或進行特定操作時播放的效果音樂或為了渲染整體氣氛播放的背景音樂等,如遠處隆隆的炮聲、由遠而近的腳步聲等。
開發人員通過精心準備的聲音特效,并結合游戲酷炫的場景,可以渲染出一種緊張刺激的氛圍,使玩家產生身臨其境的感覺。這就像電影中的聲音特效一樣,假如沒有了合適的音效,那么游戲和電影一樣,真實感會大打折扣。
提示
按照作用的不同,可以將音效劃分為即時音效和背景音樂。兩種音效在Android中的實現技術不同,本節將向讀者詳細介紹兩種音效在Android中的具體實現。
2.1.1 游戲中的即時音效
游戲中有時需要根據情況播放即時音效,如槍炮聲、碰撞聲等。即時音效的特點是短暫、可以重復、可以同時播放的。由于Android提供的MediaPlayer(媒體播放器)會占用大量的系統資源,而且播放時還需要進行緩沖,有較大的時延,因此使用MediaPlayer無法實現即時音效。
Android系統的設計者也考慮到了這個問題,為即時音效的實現提供了一個專門的類—SoundPool。SoundPool類用于管理和播放應用程序中的聲音資源,使用該類時首先需要通過該類將聲音資源加載到內存中,然后在需要即時音效的地方播放即可,幾乎沒有時延,可以滿足游戲實時性的需要。
提示
由于SoundPool設計的初衷是用于無時延地播放游戲中的短促音效,因此實際開發中應該只將長度小于7s的聲音資源放進SoundPool,否則可能會加載失敗或內存占用過大。
SoundPool類的構造器及常用方法如表2-1所列。
表2-1 SoundPool類的構造器以及方法

2.1.2 即時音效的一個案例
了解了SoundPool類的基本操作方法之后,接下來就可以開發游戲中用到的即時音效了。本小節將向讀者展示一個播放和停止即時音效的簡單案例,其主要功能為,通過SoundPool聲音池技術來實現一個即時音效的播放和停止,運行效果如圖2-1和圖2-2所示。

▲圖2-1 播放即時音效

▲圖2-2 停止播放音效
了解了本案例的運行效果后,接下來對其具體開發步驟進行介紹,具體如下所列。
(1)首先在Eclipse中創建名稱為Sample2_1的項目,然后在項目目錄下的res文件夾下創建raw文件夾。接著將需要被播放的短促音效對應的音頻文件musictest.ogg復制到raw文件夾下,如圖2-3所示。

▲圖2-3 導入音效文件
提示
一般在Android手機平臺上使用的即時音效文件越小越好,這有助于提高游戲的整體性能。對于同一個音效文件,在不改變其時長的情況下,可以采用降低采樣率(如降低到16kbit/s)或由立體聲改為單聲道的方式來縮小體積。
(2)準備好聲音資源后,接下來進行本案例中Sample2_1_Activity類的開發。該類中使用了聲音池技術實現了即時音效的播放,其代碼如下。
代碼位置:見隨書中源代碼/第2章/Sample2_1/src/com/bn/pp1目錄下的Sample2_1_Activity.java。
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所示。

▲圖2-4 MediaPlayer的生命周期圖
? 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所列。
表2-2 AudioManager類中的常用方法

提示
MediaPlayer類還可以對視頻文件進行操作,由于本書只介紹其與游戲音效相關的功能,因此不再對其視頻文件的播放功能進行介紹,有興趣的讀者可以自行查閱相關資料。
2.1.4 簡易音樂播放器的實現
了解了MediaPlayer和AudioManager類的基本操作方法之后,就可以對游戲的背景音樂功能進行開發了。本小節將通過這兩個類來實現一個簡易的音樂播放器,其主要功能為對手機SD卡中的音樂文件進行播放,運行效果如圖2-5所示。

▲圖2-5 案例運行效果圖
提示
圖2-5中從左到右分別為案例運行后,依次單擊“播放音樂”按鈕、“暫停播放”按鈕、“增大音量”按鈕后的效果圖。停止播放音樂和減小音量的效果與圖2-5中的已有效果類似,這里沒有給出,請讀者自行運行本案例進行查看。
了解了本案例的具體運行效果后,接下來就介紹本案例的具體開發步驟,具體如下所列。
(1)首先需要準備好要播放的音樂文件,本案例中使用的是著名的高山流水古曲,文件名為“gsls.mp3”。準備完音樂文件后,將該音樂文件通過DDMS導入到模擬器或真機的SD卡中,如圖2-6所示。

▲圖2-6 導入音樂文件
提示
高山流水曲的音樂資源文件見隨書中源代碼/第2章目錄下的gsls.mp3。導入時直接用鼠標光標將文件拖曳到DDMS下“File Explorer”面板中的“sdcard”目錄下即可。
(2)音樂資源在SD卡中放置完成后,接下來進行本案例中Sample2_2_Activity類的開發。該類使用了MediaPlayer實現了背景音樂的播放,具體代碼如下。
代碼位置:見隨書中源代碼/第2章/Sample2_2/src/com/bn/pp2目錄下的Sample2_2_Activity.java。
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行分別獲取了開始按鈕、暫停按鈕、停止按鈕以及增大和減小音量按鈕對象的引用,并分別為這些按鈕添加了監聽器。這樣在用戶單擊這些按鈕時程序就能完成指定的功能了,例如,按下開始按鈕開始播放音樂、按下減小音量按鈕減小播放音量等。