返回文章列表

Amethyst遊戲引擎音效系統實作與整合

本文介紹如何在 Amethyst 遊戲引擎中實作音效系統,包含音樂迴圈播放、碰撞與得分音效處理。使用 `AudioSink` 控制音訊播放,`DjSystem` 實作音樂迴圈,並在 `BounceSystem` 和 `WinnerSystem` 中分別觸發碰撞和得分音效。文章同時涵蓋了 Rust

遊戲開發 Rust

在 Amethyst 引擎中,音效系統的建構仰賴 AudioSinkDjSystem 等核心元件。AudioSink 提供了基礎的音訊控制介面,例如播放、暫停、停止及音量調整等功能。DjSystem 則負責管理音樂迴圈播放,當一首曲目結束時,它會自動載入下一首。碰撞和得分音效的實作則分別整合在 BounceSystemWinnerSystem 中,透過呼叫 play_bounce()play_score() 函式觸發對應的音效。這些函式會檢查音訊輸出裝置,並從資源函式庫載入指定的音效檔案進行播放。

音效系統的實作與整合

在遊戲開發中,音效的加入能夠大幅提升遊戲的沉浸感與真實感。本章節將詳細介紹如何在Amethyst遊戲引擎中實作音效系統,包括音樂迴圈播放、碰撞音效以及得分音效的處理。

音效系統的初始化

首先,我們需要建立一個initialize_audio()函式來初始化音效系統。在這個函式中,我們會建立一個AudioSink物件,它類別似於一個音樂播放器,提供了諸如.play(), .pause(), .stop(), 和.set_volume()等控制函式。接著,我們使用load_audio_track()輔助函式來載入指定的音訊檔案。

// src/audio.rs
use amethyst::audio::{AudioSink, Source};

fn initialize_audio(world: &mut World) {
    let mut audio_sink = world.write_resource::<AudioSink>();
    // 載入音訊檔案
    let music = load_audio_track("path/to/music.ogg");
    audio_sink.play(music);
}

內容解密:

  1. AudioSink是Amethyst提供的音訊控制介面,可以用來播放、暫停或停止音訊。
  2. load_audio_track()函式負責載入音訊檔案並傳回一個Source物件。
  3. initialize_audio()中,我們將AudioSink加入到世界資源中,並載入音樂檔案。

音樂迴圈播放的實作

為了實作音樂的迴圈播放,我們使用了DjSystem。當AudioSink中的音樂播放完畢後,DjSystem會呼叫一個使用者定義的閉包來取得下一首音樂並播放。我們將音樂軌跡存放在一個Cycle迭代器中,這樣可以實作無限迴圈播放。

// src/main.rs
use amethyst::audio::{AudioBundle, DjSystemDesc};

fn main() -> amethyst::Result<()> {
    // ...
    let game_data = GameDataBuilder::default()
        // ...
        .with_bundle(AudioBundle::default())?
        .with(
            DjSystemDesc::new(|music: &mut Music| music.music.next()),
            "dj_system",
            &[],
        )
        // ...
}

內容解密:

  1. DjSystemDesc用於建立一個DjSystem,它負責在音樂播放完畢後自動播放下一首。
  2. 我們透過閉包|music: &mut Music| music.music.next()來取得下一首音樂。
  3. DjSystem加入到遊戲資料中,並命名為"dj_system"

碰撞音效的新增

當球體碰撞到牆壁或球拍時,我們需要播放碰撞音效。首先,我們在src/audio.rs中定義了兩個函式:play_bounce()play_score(),分別用於播放碰撞音效和得分音效。

// src/audio.rs
pub fn play_bounce(sounds: &Sounds, storage: &AssetStorage<Source>, output: Option<&Output>) {
    if let Some(ref output) = output.as_ref() {
        if let Some(sound) = storage.get(&sounds.bounce_sfx) {
            output.play_once(sound, 1.0);
        }
    }
}

內容解密:

  1. play_bounce()函式檢查是否有有效的音訊輸出裝置。
  2. 如果有,它會從資產儲存中取得碰撞音效並以1.0的音量播放一次。

在BounceSystem中播放碰撞音效

我們修改了BounceSystem,使其在球體碰撞到牆壁或球拍時播放碰撞音效。

// src/systems/bounce.rs
use crate::audio::{play_bounce, Sounds};

#[derive(SystemDesc)]
pub struct BounceSystem;

impl<'s> System<'s> for BounceSystem {
    type SystemData = (
        WriteStorage<'s, Ball>,
        ReadStorage<'s, Player>,
        ReadStorage<'s, Transform>,
        Read<'s, AssetStorage<Source>>,
        ReadExpect<'s, Sounds>,
        Option<Read<'s, Output>>,
    );

    fn run(&mut self, (mut balls, players, transforms, storage, sounds, audio_output): Self::SystemData) {
        for (ball, transform) in (&mut balls, &transforms).join() {
            // 碰撞邏輯...
            play_bounce(&*sounds, &storage, audio_output.as_ref().map(|o| o.deref()));
        }
    }
}

內容解密:

  1. 我們在BounceSystem中加入了必要的系統資料,包括AssetStorage<Source>SoundsOutput
  2. 當球體發生碰撞時,我們呼叫play_bounce()函式來播放碰撞音效。

在WinnerSystem中播放得分音效

同樣地,我們在WinnerSystem中加入了播放得分音效的邏輯。

// src/systems/winner.rs
use crate::audio::{play_score, Sounds};

#[derive(SystemDesc)]
pub struct WinnerSystem;

impl<'s> System<'s> for WinnerSystem {
    type SystemData = (
        WriteStorage<'s, Ball>,
        WriteStorage<'s, Transform>,
        WriteStorage<'s, UiText>,
        Write<'s, ScoreBoard>,
        ReadExpect<'s, ScoreText>,
        Read<'s, AssetStorage<Source>>,
        ReadExpect<'s, Sounds>,
        Option<Read<'s, Output>>,
    );

    fn run(&mut self, (mut balls, mut locals, mut ui_text, mut scores, score_text, storage, sounds, audio_output): Self::SystemData) {
        for (ball, transform) in (&mut balls, &mut locals).join() {
            // 得分邏輯...
            play_score(&*sounds, &storage, audio_output.as_ref().map(|o| o.deref()));
        }
    }
}

內容解密:

  1. 我們在WinnerSystem中加入了播放得分音效所需的系統資料。
  2. 當球體觸地得分時,我們呼叫play_score()函式來播放得分音效。

使用Rust進行實體運算

在前面的章節中,我們所撰寫的程式都僅存在於虛擬世界。然而,現實世界中有很大一部分是由軟體控制的。交通燈、自駕車、飛機,甚至火箭和衛星等,都是由軟體控制的例子。這些軟體通常需要在與常見的Linux、Windows或MacOS桌面或筆記型電腦截然不同的環境中編譯和執行。它們通常需要在相對較弱的CPU和較少的記憶體下執行,有時甚至需要在沒有作業系統的情況下執行,或是在專為嵌入式系統設計的特殊作業系統上執行。

為何選擇Rust

傳統上,這些應用程式都是使用C或C++撰寫,以達到最佳的效能和對記憶體的低階控制。許多嵌入式平台由於資源有限,無法使用垃圾回收機制。然而,這正是Rust的強項所在。Rust不僅能提供與C或C++相同的效能和低階控制,還能保證更高的安全性。Rust程式可以被編譯以執行在多種不同的CPU架構上,如Intel、ARM和MIPS,並且支援多種主流作業系統,甚至是無作業系統環境。

在Raspberry Pi上使用Rust

本章節將重點放在使用Rust在Raspberry Pi上進行實體運算。Raspberry Pi是一款信用卡大小的廉價電腦,旨在使電腦教育更加普及。它具有幾個重要的特點,能夠展示本章節的重點:

  • 它採用ARM CPU,能夠學習如何為ARM平台編譯和交叉編譯程式碼。
  • 它具有GPIO針腳,可以用來控制實體電路,如LED燈和按鈕。
  • 它足夠強大,可以執行完整的根據Debian的作業系統(Raspbian),因此可以在不深入裸機程式設計的情況下學習實體運算和交叉編譯。

首先,我們將在Raspberry Pi上安裝完整的作業系統。然後,我們將安裝完整的Rust工具鏈。接著,我們將構建兩個電路,一個用於輸出,一個用於輸入,並使用Rust與它們互動:

  • 輸出:第一個電路將使我們能夠透過LED燈向實體世界輸出訊號。我們將建立一個簡單的LED電路,連線到GPIO輸出針腳。然後,我們可以撰寫Rust程式來開啟和關閉LED燈,並使其規律地閃爍。
  • 輸入:我們也可以從實體世界接收輸入訊號。我們將在電路中新增一個按鈕。Rust程式可以檢測到按鈕的點選,並據此切換LED燈的開關狀態。

電路搭建與程式設計

// 使用Rust控制LED燈的範例程式碼
use std::thread;
use std::time::Duration;

fn main() {
    // 初始化GPIO針腳
    let led_pin = init_gpio_pin();

    loop {
        // 開啟LED燈
        led_pin.set_high();
        thread::sleep(Duration::from_millis(500));

        // 關閉LED燈
        led_pin.set_low();
        thread::sleep(Duration::from_millis(500));
    }
}

#### 內容解密:
1. `init_gpio_pin()` 函式用於初始化GPIO針腳,將其設定為輸出模式。
2. 在無窮迴圈中,我們交替開啟和關閉LED燈,每次持續500毫秒。
3. `thread::sleep(Duration::from_millis(500))` 用於使執行緒暫停執行,以達到閃爍的效果。

這些範例將幫助我們瞭解Rust程式碼如何與實體世界互動。然而,我們將直接在Raspberry Pi上編譯這些程式碼。在許多嵌入式應用中,目標平台(如Raspberry Pi或類別似的板子)可能不夠強大來編譯程式碼。因此,我們將把編譯工作移到另一台更強大的電腦上,這台電腦具有不同的CPU和作業系統。這種編譯方式被稱為交叉編譯。

交叉編譯

我們將設定交叉編譯工具鏈,並在該工具鏈上交叉編譯之前的範例。最後,為了讓大家提前瞭解GPIO針腳的工作原理,我們將使用底層API來控制它。這將使我們能夠瞭解高階GPIO函式庫的工作原理。

交叉編譯設定

  1. 安裝交叉編譯工具鏈。
  2. 設定Rust編譯目標為ARM架構。
  3. 編譯Rust程式碼以在Raspberry Pi上執行。
# 安裝ARM架構的交叉編譯工具鏈
rustup target add arm-unknown-linux-gnueabihf

# 編譯Rust程式碼
cargo build --target=arm-unknown-linux-gnueabihf

內容解密:

  1. rustup target add arm-unknown-linux-gnueabihf 命令用於安裝ARM架構的交叉編譯工具鏈。
  2. cargo build --target=arm-unknown-linux-gnueabihf 命令用於編譯Rust程式碼,以在ARM架構的Raspberry Pi上執行。

透過本章節的學習,我們將能夠瞭解如何使用Rust進行實體運算,包括如何控制GPIO針腳、如何進行交叉編譯等。這將為我們進一步探索嵌入式系統和物聯網領域打下堅實的基礎。

樹莓派(Raspberry Pi)基礎與Rust安裝

樹莓派是一種類別似迷你電腦的開發板,具備電腦的基本元件,如CPU、記憶體、WiFi、藍牙、HDMI輸出、USB等。與傳統桌上型或筆記型電腦不同的是,樹莓派採用ARM架構的CPU,而非常見的Intel x86/x86_64架構。由於Rust是一種編譯成機器碼的語言,CPU架構決定了最終輸出的形式。

樹莓派的硬體特性

樹莓派具備多種周邊裝置,如SD卡讀卡器、micro-USB電源輸入、HDMI輸出、USB介面等。此外,樹莓派還有兩排金屬針腳(GPIO),用於與外部電路(如LED燈和按鈕)互動。

透過NOOBS安裝Raspbian作業系統

Raspbian是樹莓派的官方作業系統,根據Debian開發,具有友善的桌面環境和多種實用軟體套件。安裝Raspbian最簡單的方法是使用NOOBS(New Out Of the Box Software)安裝程式。

安裝步驟:

  1. 前往NOOBS下載頁面,下載離線和網路安裝的ZIP檔案。
  2. 準備至少8GB的SD卡,並格式化為FAT格式。
  3. 將NOOBS ZIP檔案解壓縮,並將所有檔案複製到SD卡中。
  4. 將SD卡插入樹莓派。
  5. 連線鍵盤、滑鼠和HDMI顯示器到樹莓派。
  6. 使用micro-USB電源開啟樹莓派。
  7. NOOBS啟動後,選擇Raspbian選項進行安裝。
  8. 安裝完成後,重啟樹莓派,即可進入Raspbian作業系統。

安裝Rust工具鏈

在Raspbian上安裝Rust編譯器和Cargo,可以使用rustup工具。執行以下命令:

curl https://sh.rustup.rs -sSf | sh

此命令會在樹莓派上安裝完整的Rust工具鏈。rustup會偵測到ARM CPU,並建議安裝對應的目標架構(armv7-unknown-linux-gnueabihf)。安裝完成後,記得將Cargo資料夾新增至PATH環境變數中,以便使用Cargo命令。

安裝過程中的注意事項:

  • rustup會根據樹莓派的ARM CPU建議合適的目標架構。
  • 將Cargo資料夾新增至PATH環境變數,以確保Cargo命令可用。

控制GPIO針腳

GPIO(General-Purpose Input/Output)針腳用於與外部世界溝通。當針腳作為輸出時,可以透過軟體控制輸出3.3V或0V。當針腳作為輸入時,可以偵測針腳電壓是高(3.3V)還是低(0V)。

GPIO針腳型別:

  • 5V:5V電源供應
  • 3V3:3.3V電源供應
  • GND:接地
  • 編號:GPIO針腳的BCM編號,用於在程式碼中參照針腳

GPIO針腳由硬體暫存器控制,暫存器就像電腦記憶體一樣,可以讀寫位元。透過寫入特定的位元模式到暫存器,可以設定針腳的模式(輸入、輸出或特殊協定)。

// 示例程式碼:控制GPIO針腳
use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom, Write};

fn main() {
    // 開啟GPIO記憶體對映檔
    let mut gpio_mem = OpenOptions::new()
        .read(true)
        .write(true)
        .open("/dev/gpiomem")
        .unwrap();

    // 設定GPIO針腳模式
    gpio_mem.seek(SeekFrom::Start(0)).unwrap();
    gpio_mem.write(&[0b00100000]).unwrap(); // 設定針腳為輸出模式

    // 控制GPIO針腳輸出
    gpio_mem.seek(SeekFrom::Start(4)).unwrap();
    gpio_mem.write(&[0b00000001]).unwrap(); // 設定針腳輸出高電平
}

內容解密:

  1. 開啟/dev/gpiomem檔案,該檔案代表GPIO記憶體對映。
  2. 使用seek方法將檔案指標移到指定位置,準備寫入資料。
  3. 寫入特定的位元模式到暫存器,設定GPIO針腳的模式為輸出。
  4. 再次使用seek方法將檔案指標移到另一個位置,準備寫入輸出資料。
  5. 寫入資料到暫存器,控制GPIO針腳輸出高電平。