Web 遊戲音訊

音訊是任何遊戲的重要組成部分;它增加反饋和氛圍。基於網路的音訊正在迅速成熟,但仍有許多瀏覽器差異需要解決。我們通常需要決定哪些音訊部分對我們的遊戲體驗至關重要,哪些是可有可無但並非必不可少的部分,並相應地制定策略。本文提供了實現 Web 遊戲音訊的詳細指南,著眼於目前在儘可能廣泛的平臺上的工作方式。

移動音訊注意事項

到目前為止,提供 Web 音訊支援最困難的平臺是移動平臺。不幸的是,這些也是人們經常用來玩遊戲的平臺。桌面瀏覽器和移動瀏覽器之間存在一些差異,這可能導致瀏覽器供應商做出一些選擇,從而使 Web 音訊難以供遊戲開發者使用。現在讓我們來看看這些。

自動播放

瀏覽器自動播放策略現在影響桌面*和*移動瀏覽器。有關它的更多資訊,請參閱Google Developers 網站上的此處

值得注意的是,如果滿足以下條件,則允許自動播放聲音:

許多瀏覽器將忽略您的遊戲發出的任何自動播放音訊的請求;相反,音訊播放需要由使用者發起的事件(例如點選或輕觸)來啟動。這意味著您必須構建音訊播放以考慮這一點。通常透過提前載入音訊並在使用者發起的事件上進行預載入來緩解這種情況。

對於更被動的音訊自動播放,例如遊戲載入時立即開始的背景音樂,一個技巧是檢測*任何*使用者發起的事件並開始播放。對於遊戲中使用的其他更主動的聲音,我們可以考慮在按下“開始”按鈕之類的操作時立即預載入它們。

要像這樣預載入音訊,我們希望播放它的一部分;因此,在您的音訊樣本末尾包含一段靜音很有用。跳到、播放然後暫停該靜音意味著我們現在可以使用 JavaScript 在任意點播放該檔案。您可以在此處找到有關自動播放策略最佳實踐的更多資訊

注意:如果瀏覽器允許您更改音量(請參見下文),則以零音量播放檔案的一部分也可能有效。另請注意,播放並立即暫停音訊並不能保證不會播放一小段音訊。

注意:將 Web 應用程式新增到移動裝置主螢幕可能會改變其功能。在 iOS 上自動播放的情況下,目前似乎就是這樣。如果可能,您應該在多個裝置和平臺上嘗試您的程式碼,以檢視其工作方式。

有關自動播放支援,請參閱<audio>

音量

移動瀏覽器中可能會停用程式化音量控制。通常給出的理由是使用者應該在作業系統級別控制音量,並且不應覆蓋此設定。

有關音量控制的支援,請參閱HTMLMediaElement.volume

緩衝和預載入

可能是為了緩解行動網路資料使用失控,我們還經常發現,在播放開始之前,緩衝已停用。緩衝是瀏覽器提前下載媒體的過程,我們通常需要這樣做以確保流暢播放。

HTMLMediaElement 介面提供了許多屬性,可幫助確定軌道是否處於可播放狀態。

注意:在許多方面,緩衝的概念已經過時。只要接受位元組範圍請求(這是預設行為),我們就可以跳轉到音訊中的特定點,而無需下載前面的內容。然而,預載入仍然有用——如果沒有它,在播放開始之前總是需要一些客戶端-伺服器通訊。

此處提供了移動和桌面 HTMLMediaElement 支援的完整相容性圖表

移動端變通方案

儘管移動瀏覽器可能會帶來問題,但仍有辦法解決上述問題。

音訊精靈

音訊精靈的名字來源於CSS 精靈,這是一種使用 CSS 和單個圖形資源將其分解為一系列精靈的視覺技術。我們可以將相同的原理應用於音訊,這樣我們就不必擁有大量載入和播放耗時的小音訊檔案,而是擁有一個包含所有所需小音訊片段的較大音訊檔案。要從檔案中播放特定聲音,我們只需使用每個音訊精靈的已知開始和停止時間。

優點是我們可以預載入一段音訊並讓我們的精靈準備就緒。為此,我們只需播放並立即暫停較大的音訊。您還將減少伺服器請求的數量並節省頻寬。

js
const myAudio = document.createElement("audio");
myAudio.src = "my-sprite.mp3";
myAudio.play();
myAudio.pause();

您需要取樣當前時間才能知道何時停止。如果您的單個聲音間隔至少 500 毫秒,那麼使用 timeUpdate 事件(每 250 毫秒觸發一次)就足夠了。您的檔案可能比實際需要的稍長,但靜音壓縮效果很好。

這是一個音訊精靈播放器示例——首先讓我們在 HTML 中設定使用者介面

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 來使其工作

js
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 APIXMLHttpRequest 獨立載入單獨的曲目和迴圈,這意味著您可以同步或並行載入它們。同步載入可能意味著您的音樂部分會更早準備好,您可以在其他部分載入時開始播放它們。

無論哪種方式,您都可能希望同步曲目或迴圈。Web Audio API 包含內部時鐘的概念,該時鐘在您建立音訊上下文的那一刻開始計時。您需要考慮建立音訊上下文與第一個音軌開始播放之間的時間。記錄此偏移量並查詢正在播放的音軌的當前時間,可以為您提供足夠的資訊來同步單獨的音訊片段。

為了檢視其作用,讓我們佈置一些單獨的曲目

html
<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 功能來完成此操作。

一旦它們可以播放,我們需要確保它們在其他音軌可能正在播放的正確點開始,以便它們同步。

讓我們建立我們的音訊上下文

js
const audioCtx = new AudioContext();

現在讓我們選擇所有<li> 元素;稍後我們可以利用這些元素來訪問軌道檔案路徑和每個單獨的播放按鈕。

js
const trackEls = document.querySelectorAll("li");

我們希望確保每個檔案在使用前都已載入並解碼為緩衝區,因此讓我們建立一個 async 函式來允許我們執行此操作

js
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 模式,以便在每個檔案載入和緩衝後執行進一步的操作

js
async function loadFile(filePath) {
  const track = await getFile(filePath);
  return track;
}

我們還建立一個 playTrack() 函式,一旦檔案被獲取,我們就可以呼叫它。這裡我們需要一個偏移量,所以如果我們已經開始播放一個檔案,我們有一個記錄,記錄要從哪裡開始播放另一個檔案。

start() 接受兩個可選引數。第一個是何時開始播放,第二個是何處,這是我們的偏移量。

js
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> 元素,為每個元素獲取正確的檔案,然後透過隱藏“正在載入”文字並顯示播放按鈕來允許播放

js
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() 函式的第一個引數,該引數接受播放應該開始的絕對時間。請注意第二個引數(在新曲目中從哪裡開始播放)是相對的

js
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 使音訊能夠與物件和視點繫結。

另見