Bevy 引擎作為 Rust 生態的新興遊戲引擎,以其資料導向的實體元件系統架構和 Rust 語言的效能優勢,吸引了眾多開發者的目光。本文將以一個簡化的 2D 貓咪排球遊戲為例,示範如何使用 Bevy 引擎構建遊戲場景、載入資源、設定攝影機以及初始化玩家角色。透過實作範例,讀者可以快速掌握 Bevy 引擎的核心概念,並學習如何運用 ECS 模式組織遊戲邏輯。文章將逐步講解精靈圖集的最佳化技巧,以及如何有效地管理遊戲實體和元件,為後續開發更複雜的遊戲機制奠定基礎。

第7章 遊戲開發

電子遊戲的發展歷程相當長遠,從早期的簡單實作到現在的複雜系統。曾經的NES上的Super Mario Bros.執行在8位元CPU上,時脈僅1.79 MHz,遊戲本身的大小約為31 kilobytes。如今,我們可以輕易地取得一台具備8核心中央處理器(CPU)、時脈達3-5 GHz的遊戲PC,以及大小達到50-70 gigabytes的遊戲。這代表著計算能力的數千倍提升和儲存空間的數百萬倍增長。遊戲的複雜度也隨之增加,使得遊戲程式設計師的工作變得比以往更加艱鉅。

Rust由於其底層記憶體安全保證和卓越的效能,成為了開發遊戲的理想候選語言。其低階記憶體管理能力和高效能表現使其非常適合用於建構強健且高效的遊戲引擎和遊戲。同時,其高階語法允許以乾淨和模組化的方式撰寫遊戲邏輯。

雖然Rust的遊戲開發生態系統仍相當年輕,但已經有一些優秀的遊戲採用Rust開發。同時,也有少數的遊戲引擎可供選擇,我們將在最後一節討論。本章中,我們將使用Bevy遊戲引擎來展示如何在Rust中開發遊戲。該專案仍在積極開發中,因此程式碼和檔案變化迅速。本章將使用目前的穩定版本0.9.1。

7.1 我們要開發什麼遊戲?

在過去的Flash遊戲時代,日本曾經有一款非常簡單但極具吸引力的遊戲叫做Pikachu Volleyball。該遊戲中有兩個皮卡丘(寶可夢角色)在沙灘上打排球。玩家可以與電腦對戰,也可以與其他人使用同一個鍵盤進行競爭。我們將透過重新創造(部分功能)這款遊戲來喚起懷舊之情。

本遊戲將具備以下功能:

  • 這將是一款2D遊戲,左側和右側各有一名玩家(一隻貓)。
  • 使用WASD鍵控制左側玩家的移動,使用方向鍵控制右側玩家的移動。
  • 球將從中間發出,每位玩家必須使用頭部和身體將球反彈給對手。
  • 球將在重力影響下反彈和下落。
  • 當球觸及對手一側的地面時,玩家將得分。
  • 將會有音樂和音效。

7.2 Bevy與實體元件系統模式

Bevy是一款根據實體元件系統(ECS)模式的遊戲引擎。ECS是一種在遊戲引擎設計中的架構模式。其核心思想是促進組合而非繼承,同時最佳化記憶體存取順序以提高效能。例如,假設有一款角色扮演遊戲(RPG),其中有玩家、怪物和可毀壞的樹木。玩家和怪物可以移動和攻擊,我們需要追蹤他們的位置和生命值。當怪物觸及玩家時,我們需要減少玩家的生命值,因此我們也需要追蹤碰撞。

首先,我們有實體。實體是遊戲中的物件,如玩家、怪物和樹木。如果將實體的所有方面都集中在同一段程式碼中,很快就會變得難以管理。相反,我們將每個方面分離成元件,並將元件附加到實體上,從而使用一系列元件組合出遊戲物件。例如,我們可以建立以下元件:

  • Attack:攻擊力和攻擊範圍
  • Transform:追蹤位置、方向和縮放比例
  • Collision:偵測碰撞
  • Health:追蹤生命值和死亡狀態

然後,我們的實體可以組合如下:

  • 玩家:Attack + Transform + Collision + Health
  • 怪物:Attack + Transform + Collision + Health
  • 樹木:Transform + Collision

最後,為了讓遊戲運作起來,我們需要實作系統來更新每個元件。一個系統負責遊戲的一個方面。例如,我們可以有以下系統:

  • Movement:移動實體並更新其Transform。例如,怪物會自行移動。
  • Input:接收使用者輸入並更新玩家的位置和執行攻擊。
  • Collision:檢查碰撞並阻止實體相互穿透;也可能造成傷害。
  • Attack:當攻擊發生時,根據攻擊者的攻擊力減少受害者的生命值。對於樹木,當被攻擊時將其摧毀。

系統可以根據單一元件,也可以更常見地處理元件之間的互動。例如,碰撞系統需要檢視實體的Transform元件以判斷何時發生碰撞,也可能需要存取每個實體的Health元件以處理碰撞造成的傷害。

使用這種架構,我們可以使程式碼更加乾淨和結構化,從而幫助我們建構非常複雜的遊戲。建立一個新的實體通常就像給它快速組合一些已經處理了所需行為的元件一樣簡單。如果需要新的行為,可以為實體建立一個新的元件,然後在未來其他新實體需要該行為時重複使用它。就像任何形式的程式設計一樣,這需要決定你的元件應該有多專門或多通用。對於簡單的專案,專門化的元件和一些重複是可以接受的,但隨著專案的成長,使用共通元件並簡單地改變它們的組合方式會更容易。

// 這是一個簡單的範例,用於展示如何使用Bevy的ECS模式
use bevy::prelude::*;

struct Player;
struct Enemy;

#[derive(Component)]
struct Health(f32);

#[derive(Component)]
struct Attack(f32);

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

fn setup(mut commands: Commands) {
    commands.spawn((Player, Health(100.0), Attack(10.0)));
    commands.spawn((Enemy, Health(50.0), Attack(5.0)));
}

內容解密:

這段程式碼展示瞭如何使用Bevy的遊戲引擎建立一個簡單的遊戲場景。首先,我們定義了兩個標記(marker)PlayerEnemy,用於標識不同的實體。然後,我們定義了兩個元件(component)HealthAttack,分別用於表示實體的生命值和攻擊力。在main函式中,我們建立了一個Bevy應用,並新增了一個啟動系統(startup system)setup。在setup函式中,我們使用commands.spawn方法建立了兩個實體,分別是玩家和敵人,並為它們附加了相應的元件。

這個範例展示了Bevy的ECS模式的基本用法,包括定義元件、建立實體和附加元件等。透過這種方式,我們可以輕鬆地建立和管理複雜的遊戲物件。

遊戲開發中的實體元件系統與 Bevy 引擎應用

在遊戲開發中,如何有效地管理遊戲實體及其屬性是一個重要的課題。傳統的物件導向程式設計方法雖然能夠封裝實體的狀態和行為,但隨著遊戲複雜度的增加,這種方法可能會導致效能問題。實體元件系統(Entity Component System, ECS)提供了一種更高效的架構,能夠改善遊戲的效能和可維護性。

實體元件系統的優勢

實體元件系統的核心思想是將遊戲實體的資料和行為分離。實體(Entity)僅僅是一個容器,而元件(Component)則用於儲存實體的各種屬性和狀態,系統(System)則負責對特定的元件進行操作。這種設計使得遊戲開發者能夠更靈活地組合和擴充套件遊戲實體的功能。

記憶體存取效率的提升

在傳統的物件導向設計中,每個遊戲實體都是一個完整的物件,包含了所有的屬性和狀態。這意味著當需要對某一類別屬性進行批次處理時(如碰撞檢測),處理器需要跳躍存取記憶體中的不同位置,這會嚴重影響效能。實體元件系統透過將相同型別的元件儲存在連續的記憶體空間中(如 Vec<Transform>Vec<Hitbox>),大大提高了記憶體存取的效率。

Bevy 引擎專案建立與基本設定

Bevy 是一個現代化的 Rust 遊戲引擎,採用了實體元件系統的架構。要開始使用 Bevy,首先需要安裝必要的系統函式庫。以 Ubuntu 為例,可以透過以下命令安裝所需的依賴:

$ sudo apt install g++ pkg-config libx11-dev libasound2-dev libudev-dev

接著,建立一個新的 Rust 專案並加入 Bevy 依賴:

$ cargo new cat_volleyball
$ cd cat_volleyball
$ cargo add bevy

Cargo.toml 中確認 Bevy 的版本:

[dependencies]
bevy = "0.10.1"

然後,在 src/main.rs 中建立一個基本的 Bevy 專案框架:

use bevy::prelude::*;

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

執行 cargo run 後,你應該會看到一個空白的視窗出現。這是 Bevy 專案的基本骨架,包含了多個預設的外掛程式,如日誌記錄、時間追蹤、視窗管理等。

#### 內容解密:

  • App::new():建立一個新的 Bevy 應使用案例項。
  • .add_plugins(DefaultPlugins):加入預設的外掛程式集合,這些外掛程式提供了視窗建立、輸入處理等基本功能。
  • .run():啟動 Bevy 應用,開始事件迴圈和渲染。

設定遊戲攝影機

在 Bevy 中,攝影機負責決定遊戲世界中的哪個部分應該被顯示在螢幕上。對於一個 2D 遊戲,我們需要設定攝影機的位置和視窗大小。假設我們的遊戲區域大小為 200x200 畫素,我們可以相應地調整攝影機的設定。

#### 內容解密:

  • 設定遊戲座標系統:在這個例子中,遊戲的座標原點位於左下角,x 和 y 軸的最大值分別為 200。
  • 設定攝影機:由於這是一個 2D 遊戲,攝影機的預設朝向是合適的,我們只需要決定攝影機的位置和視窗大小。

在Bevy中建立遊戲基礎:攝影機與玩家的初始化

本章節將探討如何在Bevy遊戲引擎中建立遊戲的基礎,包括攝影機的設定和玩家的初始化。我們將詳細介紹如何使用Bevy的API來建立一個2D遊戲,並新增基本的遊戲元素。

設定攝影機

首先,我們需要設定攝影機以顯示遊戲場景。攝影機將位於競技場的中心,高度設為單位高度(1.0)。我們使用Camera2dBundle來建立攝影機,並透過Transform元件設定其位置。

use bevy::prelude::*;

const ARENA_WIDTH: f32 = 200.0;
const ARENA_HEIGHT: f32 = 200.0;

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle {
        transform: Transform::from_xyz(ARENA_WIDTH / 2.0, ARENA_HEIGHT / 2.0, 1.0),
        ..default()
    });
}

內容解密:

  1. Camera2dBundle是用於建立2D攝影機的元件包,內含多個必要元件。
  2. Transform::from_xyz用於設定攝影機的位置,使其位於競技場中心上方。
  3. ..default()語法用於套用預設值給未明確指定的元件。

初始化遊戲

接下來,我們需要在main函式中設定Bevy應用程式,並新增啟動系統。

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "Cat Volleyball".into(),
                resolution: (ARENA_WIDTH, ARENA_HEIGHT).into(),
                ..default()
            }),
            ..default()
        }))
        .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
        .add_startup_system(setup)
        .run();
}

內容解密:

  1. DefaultPlugins提供了一組預設的外掛程式,用於設定基本的Bevy應用程式。
  2. WindowPlugin用於設定視窗的屬性,如標題和解析度。
  3. ClearColor資源用於設定視窗的背景顏色。
  4. add_startup_system(setup)setup函式註冊為啟動系統,在應用程式啟動時執行。

新增玩家

現在,我們將新增玩家到遊戲中。首先,建立一個名為assets的資料夾,並在其中建立一個textures子資料夾,用於存放玩家的精靈圖。

const PLAYER_HEIGHT: f32 = 32.0;
const PLAYER_WIDTH: f32 = 22.0;

#[derive(Copy, Clone)]
enum Side {
    Left,
    Right,
}

#[derive(Component)]
struct Player {
    side: Side,
}

fn initialize_player(
    commands: &mut Commands,
    cat_sprite: Handle<Image>,
    side: Side,
    x: f32,
    y: f32,
) {
    commands.spawn((
        Player { side },
        SpriteBundle {
            texture: cat_sprite,
            transform: Transform::from_xyz(x, y, 0.0),
            ..default()
        },
    ));
}

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    let cat_sprite = asset_server.load("textures/cat-sprite.png");
    initialize_player(&mut commands, cat_sprite.clone(), Side::Left, PLAYER_WIDTH / 2.0, PLAYER_HEIGHT / 2.0);
    initialize_player(&mut commands, cat_sprite.clone(), Side::Right, ARENA_WIDTH - PLAYER_WIDTH / 2.0, PLAYER_HEIGHT / 2.0);
}

內容解密:

  1. Player元件用於區分不同的玩家,並包含一個Side列舉欄位。
  2. initialize_player函式用於初始化一個玩家,生成一個包含Player元件和SpriteBundle的實體。
  3. SpriteBundle用於顯示玩家的精靈圖,並設定其初始位置。
  4. 玩家的初始位置根據競技場的大小和玩家的尺寸計算,以確保玩家完全顯示在畫面上。

載入精靈圖集(Spritesheet)最佳化遊戲效能

在遊戲開發中,為每個畫面元素使用單獨的圖片通常效率不高,因為需要將圖片(紋理)載入到圖形處理單元(GPU)中,這會帶來很高的開銷。通常,我們會將所有圖片(或相關的圖片)聚合成一張大圖,稱為「精靈圖集」(Spritesheet)。然後,為每個元素「剪下」大圖中的一小部分。這樣,可以減少整體載入時間,並允許GPU更有效地處理圖片。

為什麼使用精靈圖集?

  • 減少載入時間:將多個圖片合併成一個,可以減少GPU的載入次數。
  • 提高GPU效率:GPU可以更高效地處理合併後的圖片。

載入精靈圖集的程式碼實作

以下程式碼展示瞭如何在Bevy遊戲引擎中載入和使用精靈圖集:

fn initialize_player(
    commands: &mut Commands,
    atlas: Handle<TextureAtlas>,
    cat_sprite: usize,
    side: Side,
    x: f32,
    y: f32,
) {
    commands.spawn((
        Player { side },
        SpriteSheetBundle {
            sprite: TextureAtlasSprite::new(cat_sprite),
            texture_atlas: atlas,
            transform: Transform::from_xyz(x, y, 0.0),
            ..default()
        },
    ));
}

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut texture_atlases: ResMut<Assets<TextureAtlas>>,
) {
    let spritesheet = asset_server.load("textures/spritesheet.png");
    let mut sprite_atlas = TextureAtlas::new_empty(spritesheet, Vec2::new(58.0, 34.0));

    // 定義每個精靈在圖集中的位置和大小
    let left_cat_corner = Vec2::new(11.0, 1.0);
    let right_cat_corner = Vec2::new(35.0, 1.0);
    let cat_size = Vec2::new(22.0, 32.0);

    // 將精靈加入圖集並取得索引
    let left_cat_index = sprite_atlas.add_texture(Rect::from_corners(left_cat_corner, left_cat_corner + cat_size));
    let right_cat_index = sprite_atlas.add_texture(Rect::from_corners(right_cat_corner, right_cat_corner + cat_size));

    // 將圖集加入到 Bevy 的資源中
    let texture_atlas_handle = texture_atlases.add(sprite_atlas);

    // 初始化玩家實體
    initialize_player(&mut commands, texture_atlas_handle.clone(), left_cat_index, Side::Left, PLAYER_WIDTH / 2.0, PLAYER_HEIGHT / 2.0);
    initialize_player(&mut commands, texture_atlas_handle, right_cat_index, Side::Right, ARENA_WIDTH - PLAYER_WIDTH / 2.0, PLAYER_HEIGHT / 2.0);
}

程式碼解析:

  1. initialize_player函式:用於初始化玩家實體,接受TextureAtlas的控制程式碼和精靈索引,生成對應的SpriteSheetBundle

    • SpriteSheetBundle包含TextureAtlasSpritetexture_atlas欄位,前者指定使用的精靈索引,後者指定使用的圖集。
    • transform定義了實體的位置。
  2. setup函式:負責載入精靈圖集並建立TextureAtlas

    • 載入精靈圖集圖片。
    • 建立一個空的TextureAtlas,並定義每個精靈在圖集中的矩形區域。
    • 將定義好的精靈加入到TextureAtlas中,並獲得對應的索引。
    • 將建立好的TextureAtlas加入到Bevy的資源管理器中,獲得一個控制程式碼。
    • 使用控制程式碼初始化玩家實體。

使用精靈圖集的優勢

  • 提高效能:減少了GPU載入紋理的次數,特別是在大型遊戲中優勢明顯。
  • 靈活性:展示了實體元件系統(ECS)的靈活性,透過更換不同的Bundle即可改變實體的顯示內容,而無需修改其他遊戲邏輯。