返回文章列表

ECS架構解析與Bevy引擎2D遊戲開發

本文探討實體元件系統(ECS)架構,並以 Bevy 引擎開發 2D 貓咪排球遊戲為例,示範如何載入與管理精靈圖、使用紋理地圖集最佳化遊戲資源,以及實作遊戲角色的移動控制。文章涵蓋 ECS 架構的核心概念、Bevy

遊戲開發 Rust

實體元件系統(ECS)架構在遊戲開發中被廣泛應用,其核心概念是將資料和邏輯分離,提高程式碼的可維護性和效能。本文以 Bevy 遊戲引擎為例,展示瞭如何使用 ECS 架構開發一個簡單的 2D 貓咪排球遊戲。首先,我們介紹了 ECS 架構中的元件和系統的概念,並用 Rust 程式碼示範瞭如何定義元件和系統。接著,我們逐步講解了如何使用 Bevy 引擎建立專案、設定攝影機、新增玩家角色,以及載入和管理精靈圖。為了進一步最佳化遊戲效能,我們還介紹瞭如何使用紋理地圖集,將多個紋理合併成一個,減少繪製呼叫次數。最後,我們示範瞭如何根據玩家輸入控制遊戲角色的移動,並限制角色在遊戲場景內的活動範圍,完成了遊戲的基本功能開發。

實體元件系統(Entity Component System)架構解析

在遊戲開發中,實體元件系統(Entity Component System, ECS)是一種常見的架構模式,用於管理和更新遊戲中的實體(entities)。這種架構將實體的資料和行為分離成不同的元件(components),並使用系統(systems)來更新和處理這些元件。

元件(Components)設計

元件是用於儲存實體狀態的資料結構。例如,在一個遊戲中,我們可能有以下元件:

  • Transform:儲存實體的位置、旋轉和縮放資訊
  • Collision:儲存實體的碰撞相關資訊,如碰撞框的大小和形狀
  • Health:儲存實體的生命值
  • Attack:儲存實體的攻擊力

這些元件可以被組合起來,形成不同的實體。例如:

  • 玩家(Player):Attack + Transform + Collision + Health
  • 怪物(Monster):Attack + Transform + Collision + Health
  • 樹(Tree):Transform + Collision

程式碼範例:定義元件

// 定義Transform元件
#[derive(Component)]
struct Transform {
    position: Vec3,
    rotation: Quat,
    scale: Vec3,
}

// 定義Collision元件
#[derive(Component)]
struct Collision {
    hitbox: Vec3,
}

// 定義Health元件
#[derive(Component)]
struct Health {
    value: u32,
}

// 定義Attack元件
#[derive(Component)]
struct Attack {
    power: u32,
}

內容解密:

  1. Transform 元件:此元件負責儲存實體的位置、旋轉和縮放資訊。這些資訊對於渲染和物理模擬至關重要。
  2. Collision 元件:此元件定義了實體的碰撞框,用於檢測實體之間的碰撞。
  3. Health 元件:此元件儲存實體的生命值,用於處理傷害和死亡邏輯。
  4. Attack 元件:此元件定義了實體的攻擊力,用於計算對其他實體造成的傷害。

系統(Systems)設計

系統負責更新和處理元件。例如:

  • Movement:更新實體的Transform元件,使其移動
  • Input:處理使用者輸入,更新玩家的Transform和Attack元件
  • Collision:檢測實體之間的碰撞,並更新相關元件(如Health)
  • Attack:處理攻擊邏輯,根據攻擊者的Attack元件更新受害者的Health元件

程式碼範例:實作系統

// 實作Movement系統
fn movement_system(mut query: Query<(&mut Transform, &Velocity)>) {
    for (mut transform, velocity) in query.iter_mut() {
        transform.position += velocity.value;
    }
}

// 實作Collision系統
fn collision_system(mut query: Query<(&Transform, &Collision, &mut Health)>) {
    for (transform1, collision1, mut health1) in query.iter_mut() {
        for (transform2, collision2, mut health2) in query.iter_mut() {
            if check_collision(transform1, collision1, transform2, collision2) {
                // 處理碰撞邏輯,例如減少生命值
                health1.value -= 1;
                health2.value -= 1;
            }
        }
    }
}

內容解密:

  1. Movement 系統:此係統負責更新實體的位置,使其根據速度移動。
  2. Collision 系統:此係統檢測實體之間的碰撞,並更新相關的Health元件。

ECS架構的優勢

ECS架構具有以下優勢:

  • 高效能:ECS架構將相同型別的元件儲存在連續的記憶體中,提高了快取命中率,從而提升了效能。
  • 靈活性:ECS架構允許開發者輕鬆地新增和刪除元件,從而實作不同的實體行為。
  • 可擴充套件性:ECS架構使得開發者可以輕鬆地新增新的系統和元件,從而擴充套件遊戲的功能。

建立Bevy專案

要使用Bevy引擎建立一個新的專案,首先需要安裝必要的系統函式庫。然後,可以使用Cargo建立一個新的專案,並新增Bevy作為相依性。

程式碼範例:Cargo.toml

[package]
name = "cat_volleyball"

[dependencies]
bevy = "0.10.1"

程式碼範例:main.rs

use bevy::prelude::*;

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

內容解密:

  1. 新增Bevy相依性:在Cargo.toml中新增Bevy作為相依性。
  2. 建立Bevy App:在main.rs中使用Bevy的App結構建立一個新的應用程式,並新增預設的外掛程式。

使用 Bevy 引擎建立 2D 遊戲:貓咪排球

簡介

本章節將介紹如何使用 Bevy 遊戲引擎建立一個簡單的 2D 遊戲。我們將從建立一個基本的 Bevy 專案開始,然後逐步新增遊戲元素,例如攝影機、玩家角色和遊戲邏輯。

建立 Bevy 專案

首先,我們需要建立一個新的 Bevy 專案。Bevy 提供了一個簡單的方式來建立新的專案,只需使用以下命令:

use bevy::prelude::*;

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

這個範例程式碼建立了一個新的 Bevy 應用程式,並新增了預設的外掛程式。DefaultPlugins 包含了許多重要的外掛程式,例如日誌記錄、時間追蹤、視窗管理和使用者輸入處理。

內容解密:

  • App::new():建立一個新的 Bevy 應用程式。
  • .add_plugins(DefaultPlugins):新增預設的外掛程式。
  • .run():執行 Bevy 應用程式。

設定攝影機

在 Bevy 中,攝影機是用來呈現遊戲畫面的重要元件。我們需要設定攝影機的位置和大小,以確保遊戲畫面正確顯示。

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()
    });
}

內容解密:

  • Camera2dBundle:用於建立 2D 攝影機的 bundle。
  • transform:設定攝影機的位置和旋轉。
  • Transform::from_xyz:建立一個新的 Transform 元件。

新增玩家角色

接下來,我們將新增玩家角色到遊戲中。首先,我們需要建立一個 Player 元件來代表玩家角色。

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

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

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()
        },
    ));
}

內容解密:

  • Player 元件:代表玩家角色,包含 side 屬性。
  • Side 列舉:代表玩家角色的所屬方。
  • initialize_player 函式:用於初始化玩家角色。

載入與管理遊戲角色精靈圖

在開發遊戲時,載入和管理遊戲角色的精靈圖(sprite)是至關重要的環節。本章節將介紹如何使用Bevy框架載入和管理精靈圖,並實作遊戲角色的顯示。

載入單一精靈圖

首先,我們需要載入單一的精靈圖。Bevy提供了AssetServer資源來管理遊戲的資源,包括精靈圖。我們可以透過AssetServer載入精靈圖,並將其用於初始化遊戲角色。

let cat_sprite = asset_server.load("textures/cat-sprite.png");

內容解密:

  • asset_server.load 方法用於載入指定的精靈圖。
  • "textures/cat-sprite.png" 是精靈圖的路徑。
  • cat_sprite 變數儲存了載入的精靈圖的控制程式碼(Handle)。

初始化遊戲角色

接下來,我們需要初始化遊戲角色。我們定義了一個Player元件,並編寫了一個initialize_player函式來初始化遊戲角色。

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()
        },
    ));
}

內容解密:

  • commands.spawn 方法用於生成新的實體(entity)。
  • Player { side } 初始化了一個Player元件,包含了玩家的方向資訊。
  • SpriteBundle 是一個包含精靈圖相關元件的包,包含了texturetransform等元件。
  • texture: cat_sprite 設定了精靈圖的紋理。
  • transform: Transform::from_xyz(x, y, 0.0) 設定了精靈圖的位置。

載入精靈圖集

為了提高遊戲的效能,我們通常會將多個精靈圖合併成一個精靈圖集(spritesheet)。這樣可以減少載入和渲染的開銷。

let spritesheet = asset_server.load("textures/spritesheet.png");
let mut sprite_atlas = TextureAtlas::new_empty(spritesheet, Vec2::new(58.0, 34.0));

內容解密:

  • asset_server.load 方法用於載入精靈圖集。
  • TextureAtlas::new_empty 方法建立了一個新的空的TextureAtlas,並指定了精靈圖的大小。

從精靈圖集中提取精靈圖

我們可以從精靈圖集中提取出需要的精靈圖,並將其用於初始化遊戲角色。

let left_cat_index = sprite_atlas.add_texture(Rect::from_corners(
    left_cat_corner,
    left_cat_corner + cat_size,
));

內容解密:

  • sprite_atlas.add_texture 方法將精靈圖新增到TextureAtlas中。
  • Rect::from_corners 方法建立了一個矩形區域,指定了精靈圖在精靈圖集中的位置和大小。

使用精靈圖集初始化遊戲角色

最後,我們可以使用精靈圖集來初始化遊戲角色。

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()
        },
    ));
}

內容解密:

  • SpriteSheetBundle 取代了之前的SpriteBundle,用於處理精靈圖集相關的元件。
  • TextureAtlasSprite::new(cat_sprite) 建立了一個新的TextureAtlasSprite,指定了要使用的精靈圖索引。
  • texture_atlas: atlas 設定了精靈圖集的控制程式碼。

使用紋理地圖集最佳化遊戲資源

在遊戲開發中,最佳化資源的使用是非常重要的。Bevy 提供了一個非常有用的功能叫做 TextureAtlas,它允許我們將多個紋理合併成一個大紋理圖集,從而減少 GPU 的呼叫次數,提高遊戲的效能。

建立紋理地圖集

首先,我們需要在 setup 函式中更新引數,提供一個可變的參考到 TextureAtlas 資源。TextureAtlas 是一個單一的位置,我們可以在整個程式中儲存所有的紋理地圖集。

// 載入精靈表
let spritesheet_handle = asset_server.load("spritesheet.png");
// 建立紋理地圖集
let texture_atlas = TextureAtlas::from_grid(
    spritesheet_handle,
    Vec2::new(32.0, 32.0),
    4,
    4,
);
// 將紋理地圖集加入到資源中
let texture_atlas_handle = texture_atlases.add(texture_atlas);

內容解密:

  1. 載入精靈表:使用 asset_server.load 方法載入我們的精靈表圖片。
  2. 建立紋理地圖集:使用 TextureAtlas::from_grid 方法根據精靈表的大小和格子數量建立紋理地圖集。
  3. 加入到資源中:將建立好的紋理地圖集加入到 texture_atlases 資源中,並取得其 handle。

使用紋理地圖集

在建立好紋理地圖集之後,我們就可以使用它來渲染我們的遊戲物件了。

// 使用紋理地圖集渲染遊戲物件
commands.spawn_bundle(SpriteSheetBundle {
    texture_atlas: texture_atlas_handle.clone(),
    sprite: TextureAtlasSprite::new(0),
    transform: Transform::from_translation(Vec3::new(100.0, 100.0, 0.0)),
    ..default()
});

內容解密:

  1. 複製 handle:複製 texture_atlas_handle 以便在多個地方使用。
  2. 建立精靈:使用 TextureAtlasSprite 建立一個新的精靈,並指定其在紋理地圖集中的索引。
  3. 設定變換:設定精靈的位置。

移動貓咪

現在我們的貓咪已經可以被渲染出來了,但是它們還是靜止的。我們需要讓它們能夠根據鍵盤輸入移動。

定義 Side enum 的方法

首先,我們需要在 Side enum 上實作一些方法,用於取得對應的鍵盤按鍵和移動範圍。

impl Side {
    fn go_left_key(&self) -> KeyCode {
        match self {
            Side::Left => KeyCode::A,
            Side::Right => KeyCode::Left,
        }
    }

    fn go_right_key(&self) -> KeyCode {
        match self {
            Side::Left => KeyCode::D,
            Side::Right => KeyCode::Right,
        }
    }

    fn range(&self) -> (f32, f32) {
        match self {
            Side::Left => (PLAYER_WIDTH / 2.0, ARENA_WIDTH / 2.0 - PLAYER_WIDTH / 2.0),
            Side::Right => (ARENA_WIDTH / 2.0 + PLAYER_WIDTH / 2.0, ARENA_WIDTH - PLAYER_WIDTH / 2.0),
        }
    }
}

內容解密:

  1. go_left_keygo_right_key 方法:根據 Side 的不同,傳回對應的向左或向右移動的鍵盤按鍵。
  2. range 方法:傳回貓咪在不同 Side 下的可移動範圍。

玩家系統

接下來,我們需要建立一個系統來控制貓咪的移動。

fn player(
    keyboard_input: Res<Input<KeyCode>>,
    time: Res<Time>,
    mut query: Query<(&Player, &mut Transform)>,
) {
    for (player, mut transform) in query.iter_mut() {
        let left = if keyboard_input.pressed(player.side.go_left_key()) { -1.0 } else { 0.0 };
        let right = if keyboard_input.pressed(player.side.go_right_key()) { 1.0 } else { 0.0 };
        let direction = left + right;
        let offset = direction * PLAYER_SPEED * time.raw_delta_seconds();
        transform.translation.x += offset;
        let (left_limit, right_limit) = player.side.range();
        transform.translation.x = transform.translation.x.clamp(left_limit, right_limit);
    }
}

內容解密:

  1. 取得鍵盤輸入和時間:使用 Res<Input<KeyCode>>Res<Time> 取得鍵盤輸入和時間資源。
  2. 遍歷玩家實體:使用 Query<(&Player, &mut Transform)> 遍歷所有具有 PlayerTransform 元件的實體。
  3. 計算移動方向和偏移:根據鍵盤輸入計算移動方向和偏移量。
  4. 更新變換:更新貓咪的位置,並限制在可移動範圍內。

透過以上步驟,我們成功地實作了貓咪的移動控制,並且使用了紋理地圖集最佳化了遊戲資源。