返回文章列表

Bevy遊戲計分系統與音效實作

本文介紹如何在 Bevy 遊戲引擎中實作計分系統,包含計分邏輯、計分板顯示與更新,並進一步示範如何整合音效和背景音樂,提升遊戲體驗。文章涵蓋 Bevy 資源管理、UI 元素操作、音訊播放等核心概念,提供程式碼範例與詳細解說,適合 Bevy 遊戲開發新手學習。

遊戲開發 Rust

在遊戲開發中,計分系統和音效是提升遊戲體驗的關鍵要素。本文將逐步講解如何在 Bevy 引擎中實作這些功能。首先,我們會建立一個 Score 資源來儲存遊戲分數,並利用 Bevy 的 ECS 系統,根據遊戲邏輯更新分數。接著,我們會建立計分板 UI 元素,並將其與 Score 資源繫結,實作即時分數顯示。最後,我們將介紹如何載入和播放音效及背景音樂,為遊戲增添更多互動性。程式碼範例中會使用 AudioSource 控制程式碼管理音效資源,並透過 Audio 資源進行播放控制,讓音效與遊戲事件同步。

use bevy::prelude::*;

#[derive(Resource)]
struct Score {
    left: usize,
    right: usize,
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        // ...
        .run();
}

實作遊戲計分系統與顯示

在遊戲開發中,計分系統是不可或缺的一部分。本章節將介紹如何在Bevy遊戲引擎中實作一個基本的計分系統,並將分數顯示在遊戲介面上。

計分邏輯實作

首先,我們需要定義一個資源(Resource)來儲存遊戲的分數狀態。我們建立一個名為Score的結構體,並使用#[derive(Resource)]屬性標記它為Bevy的資源。

#[derive(Resource)]
struct Score {
    left: usize,
    right: usize,
}

接下來,我們需要在遊戲邏輯中更新分數。當球觸地時,根據球的位置判斷哪一方得分,並更新Score資源。

fn scoring(
    mut query: Query<(&mut Ball, &mut Transform)>,
    mut score: ResMut<Score>,
) {
    for (mut ball, mut transform) in query.iter_mut() {
        let ball_x = transform.translation.x;
        let ball_y = transform.translation.y;
        if ball_y < ball.radius {
            // 觸地邏輯
            if ball_x <= ARENA_WIDTH / 2.0 {
                score.right += 1;
                ball.velocity.x = ball.velocity.x.abs();
            } else {
                score.left += 1;
                ball.velocity.x = -ball.velocity.x.abs();
            }
            // 重置球的位置和速度
            transform.translation.x = ARENA_WIDTH / 2.0;
            transform.translation.y = ARENA_HEIGHT / 2.0;
            ball.velocity.y = 0.0;
        }
    }
}

#### 內容解密:
1. `ResMut<Score>`允許系統存取和修改全域性的`Score`資源。
2. 根據球的x座標判斷哪一方得分,並更新對應的分數。
3. 更新球的速度方向,以模擬發球權轉移。
4. 重置球的位置到場地中央,並將y軸速度設為0,模擬自由落體。

### 計分板實體初始化

為了在遊戲介面上顯示分數,我們需要建立計分板實體。首先,定義一個`ScoreBoard`元件來標記計分板實體。

```rust
#[derive(Component)]
struct ScoreBoard {
    side: Side,
}

然後,實作一個函式來初始化計分板實體。

fn initialize_scoreboard(
    commands: &mut Commands,
    asset_server: &Res<AssetServer>,
    side: Side,
    x: f32,
) {
    commands.spawn((
        ScoreBoard { side },
        TextBundle::from_sections([
            TextSection::from_style(TextStyle {
                font_size: SCORE_FONT_SIZE,
                color: Color::WHITE,
                font: asset_server.load("fonts/square.ttf"),
            }),
        ])
        .with_style(Style {
            position_type: PositionType::Absolute,
            position: UiRect {
                top: Val::Px(25.0),
                left: Val::Px(x),
                ..default()
            },
            ..default()
        })
        .with_text_alignment(match side {
            Side::Left => TextAlignment::Left,
            Side::Right => TextAlignment::Right,
        }),
    ));
}

#### 內容解密:
1. 使用`TextBundle`建立包含文字的UI實體
2. 透過`asset_server.load`載入字型資源。
3. 設定文字樣式,包括大小、顏色和對齊方式。
4. 使用絕對定位將計分板放置在指定位置。

### 更新計分板顯示

最後,我們需要一個系統來根據`Score`資源的變化更新計分板的顯示。

```rust
fn score_display(
    score: Res<Score>,
    mut query: Query<(&mut Text, &ScoreBoard)>,
) {
    for (mut text, scoreboard) in query.iter_mut() {
        text.sections[0].value = match scoreboard.side {
            Side::Left => score.left.to_string(),
            Side::Right => score.right.to_string(),
        };
    }
}

#### 內容解密:
1. 查詢所有包含`Text`和`ScoreBoard`元件的實體。
2. 根據`ScoreBoard`的`side`欄位,更新對應的文字內容為當前分數。
3. 將分數轉換為字串以顯示在UI上

### 系統整合

將上述系統新增到Bevy應用程式中,以實作完整的計分功能。

```rust
fn main() {
    App::new()
        // ...
        .insert_resource(Score { left: 0, right: 0 })
        .add_system(scoring)
        .add_system(score_display)
        // ...
        .run();
}

透過以上步驟,我們成功地在Bevy遊戲中實作了計分系統和計分板的顯示。這不僅增強了遊戲的互動性,也為遊戲的玩法提供了基礎。

為遊戲增添音效與音樂

現在我們的遊戲已經具備完整的功能,但若要讓遊戲體驗更豐富,還需要新增音效和背景音樂。要在遊戲中播放音樂,我們首先需要新增音訊資源,並更新程式碼中的幾個部分以載入所需的音效檔案(清單 7-16)。Bevy 的音訊系統非常簡單,但足以滿足我們的基本遊戲需求。

載入音訊資源

#[derive(Component)]
pub struct Ball {
    pub velocity: Vec2,
    pub radius: f32,
    pub bounce: Handle<AudioSource>, // 碰撞音效的音訊來源
    pub score: Handle<AudioSource>, // 得分音效的音訊來源
}

// ...

fn initialize_ball(
    // ...
) {
    let bounce_audio = asset_server.load("audio/bounce.ogg");
    let score_audio = asset_server.load("audio/score.ogg");
    commands.spawn((
        Ball {
            velocity: Vec2::new(BALL_VELOCITY_X, BALL_VELOCITY_Y),
            radius: BALL_RADIUS,
            bounce: bounce_audio,
            score: score_audio,
        },
        // ...
    ));
}

// ...

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
    audio: Res<Audio>, // 新增音訊子系統
) {
    audio.play_with_settings(
        asset_server.load("audio/Computer_Music_All-Stars_-_Albatross_v2.ogg"),
        PlaybackSettings::LOOP.with_volume(0.25),
    );
    // ...
}

內容解密:

  1. 我們為 Ball 結構體增加了兩個新的欄位:bouncescore,這兩個欄位都是 AudioSource 的控制程式碼,用於儲存碰撞和得分的音效。
  2. initialize_ball 函式中,我們使用 asset_server.load 方法載入音效檔案,並將傳回的控制程式碼指定給 bouncescore 欄位。
  3. setup 函式中,我們新增了 Audio 資源,並使用它來播放背景音樂。背景音樂檔案同樣透過 asset_server.load 載入,並設定為迴圈播放,音量設為 0.25。

在遊戲中播放音效

fn bounce(
    mut ball_query: Query<(&mut Ball, &Transform)>,
    player_query: Query<(&Player, &Transform)>,
    audio: Res<Audio>, // 新增音訊資源
) {
    for (mut ball, ball_trans) in ball_query.iter_mut() {
        let ball_x = ball_trans.translation.x;
        let ball_y = ball_trans.translation.y;
        if ball_y >= (ARENA_HEIGHT - ball.radius) && ball.velocity.y > 0.0 {
            audio.play(ball.bounce.clone()); // 播放碰撞音效
            ball.velocity.y = -ball.velocity.y;
        } else if ball_x <= ball.radius && ball.velocity.x < 0.0 {
            audio.play(ball.bounce.clone()); // 播放碰撞音效
            ball.velocity.x = -ball.velocity.x;
        } else if ball_x >= (ARENA_WIDTH - ball.radius) && ball.velocity.x > 0.0 {
            audio.play(ball.bounce.clone()); // 播放碰撞音效
            ball.velocity.x = -ball.velocity.x;
        }
        for (player, player_trans) in player_query.iter() {
            let player_x = player_trans.translation.x;
            let player_y = player_trans.translation.y;
            if point_in_rect(
                // ...
            ) {
                if ball.velocity.y < 0.0 {
                    audio.play(ball.bounce.clone());
                    // ... 其餘處理球碰撞玩家的程式碼 ...
                }
            }
        }
    }
}

fn scoring(
    mut query: Query<(&mut Ball, &mut Transform)>,
    mut score: ResMut<Score>,
    audio: Res<Audio>, // 新增音訊資源
) {
    for (mut ball, mut transform) in query.iter_mut() {
        let ball_x = transform.translation.x;
        let ball_y = transform.translation.y;
        if ball_y < ball.radius {
            audio.play(ball.score.clone()); // 播放得分音效
            // ... 其餘處理球觸地得分的程式碼 ...
        }
    }
}

內容解密:

  1. bounce 系統中,我們新增了 Audio 資源,並在球碰撞到場地的邊界或玩家時播放碰撞音效。
  2. scoring 系統中,我們同樣新增了 Audio 資源,並在球觸地(即玩家得分)時播放得分音效。
  3. 這些音效的控制程式碼是在初始化球時載入並儲存在 Ball 結構體中的。

使用Rust進行實體運算

到目前為止,您所編寫的程式都只存在於虛擬世界中。然而,我們生活的物理世界中有很大一部分是由軟體控制的。交通燈、自動駕駛汽車、飛機,甚至火箭和衛星都是例子。其中很多軟體需要在與通常的桌面環境截然不同的環境中構建和執行。它們通常需要在相對較弱的CPU上執行,記憶體也較少。有時它們甚至需要在沒有作業系統的情況下執行,或是在專門的嵌入式作業系統上執行。

傳統上,這些應用程式是用C或C++編寫的,以獲得最大的效能和對記憶體的低階控制。許多嵌入式平台都非常有限,以至於垃圾回收是不可行的。但這正是Rust的優勢所在。Rust可以提供像C或C++一樣的效能和低階控制,但同時也能保證更高的安全性。Rust程式可以被編譯以在許多不同的中央處理器(CPU)架構上執行,如Intel、ARM、RISC-V和MIPS。它也支援各種主流作業系統,甚至可以在沒有作業系統的情況下執行。

8.1 正在構建什麼?

在本章中,您將專注於在Raspberry Pi上使用Rust。Raspberry Pi是一種價格低廉、信用卡大小的電腦,旨在使電腦教育更加普及。它有一些重要的功能,可以幫助我們在本章中闡述要點:

  • 它具有通用輸入/輸出(GPIO)針腳,可以用來與物理電路(如LED和按鈕)互動。
  • 它足夠強大,可以執行完整的Debian-based作業系統(Raspberry Pi OS),因此您可以學習實體運算和交叉編譯,而無需深入到裸機程式設計。但是,如果您覺得自己很冒險,也可以嘗試為它編寫自己的迷你Rust作業系統。
  • 它具有ARM CPU,可以演示如何為ARM平台編譯和交叉編譯程式碼。

首先,您將在Raspberry Pi上安裝完整的作業系統。然後,您將在其上安裝完整的Rust工具鏈。您將在麵包板上構建兩個電路,一個用於輸出,一個用於輸入,並使用Rust與它們互動,如下所示:

  • 輸出:第一個電路將允許我們向物理世界生成輸出。您將建立一個簡單的LED電路,連線到GPIO輸出針腳。您可以編寫一個Rust程式來開啟和關閉LED,並以固定的間隔使其閃爍。
  • 輸入:您也可以從物理世界取得輸入。您將在電路中新增一個按鈕。Rust程式可以檢測按鈕點選,然後切換LED的開關。

這兩個例子將幫助我們瞭解Rust程式碼如何與物理世界互動。然而,您是在Raspberry Pi本身上編譯它們。在許多嵌入式應用程式中,目標平台(即Raspberry Pi或類別似板)不夠強大,無法編譯程式碼。您可以改為在另一台更強大的電腦上編譯,但它可能具有與目標平台不同的CPU架構和OS。這種編譯方法稱為交叉編譯。您將設定交叉編譯工具鏈,並使用它交叉編譯前面的例子。

8.2 Raspberry Pi上的實體運算

對於那些習慣於網頁或桌面專案的人來說,實體運算可能是一個很大的變化。人們花費更多的精力來設定測試環境和工具鏈,並確保您不會因為不知道是程式碼還是硬體有問題而發瘋。我們將首先花一些時間設定Raspberry Pi 4。

使用Plantuml圖表展示GPIO針腳組態

@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle

title Bevy遊戲計分系統與音效實作

package "系統架構" {
    package "前端層" {
        component [使用者介面] as ui
        component [API 客戶端] as client
    }

    package "後端層" {
        component [API 服務] as api
        component [業務邏輯] as logic
        component [資料存取] as dao
    }

    package "資料層" {
        database [主資料庫] as db
        database [快取] as cache
    }
}

ui --> client : 使用者操作
client --> api : HTTP 請求
api --> logic : 處理邏輯
logic --> dao : 資料操作
dao --> db : 持久化
dao --> cache : 快取

note right of api
  RESTful API
  或 GraphQL
end note

@enduml

此圖示展示了Raspberry Pi如何透過GPIO針腳與LED電路和按鈕電路互動。

實體運算設定與挑戰

實體運算涉及多個挑戰,包括設定測試環境、選擇合適的硬體和軟體,以及確保程式碼的正確性和穩定性。在本章中,您將學習如何在Raspberry Pi上使用Rust進行實體運算,包括設定GPIO針腳、控制LED和檢測按鈕點選。

內容解密:

本章節將探討如何在Raspberry Pi上使用Rust進行實體運算。首先,您需要了解Raspberry Pi的硬體架構和GPIO針腳的功能。然後,您將學習如何使用Rust程式碼控制GPIO針腳,以實作對LED的控制和對按鈕點選的檢測。

// 使用Rust控制GPIO針腳的範例程式碼
use rppal::gpio::Gpio;

fn main() {
    let gpio = Gpio::new().unwrap();
    let mut pin = gpio.get(17).unwrap().into_output();
    loop {
        pin.set_high();
        std::thread::sleep(std::time::Duration::from_millis(500));
        pin.set_low();
        std::thread::sleep(std::time::Duration::from_millis(500));
    }
}

內容解密:

上述程式碼展示瞭如何使用rppal crate控制Raspberry Pi的GPIO針腳。在這個例子中,我們使用了第17號針腳作為輸出,並使其以500毫秒的間隔閃爍。Gpio::new().unwrap()用於初始化GPIO介面,gpio.get(17).unwrap().into_output()用於取得第17號針腳並將其設定為輸出模式。pin.set_high()pin.set_low()分別用於設定針腳的高電平和低電平,從而控制LED的開關。