Bevy 0.5

由 Carter Anderson 於 2021 年 4 月 6 日發布 ( 一個長著貓耳朵,揮舞著觸手的身影剪影,或 Octocat:GitHub 的吉祥物和標誌 @cart 一隻灰色鳥飛翔的向量藝術;X(以前稱為 Twitter)的舊標誌 @cart_cart 圓角矩形中指向右的三角形;Youtube 的標誌 cartdev )

Screenshot of Ante: a voxel builder game being developed in Bevy by @TheNeikos
Ante 的螢幕截圖:由 @TheNeikos 在 Bevy 中開發的體素建造遊戲

感謝 88 位貢獻者、283 個拉取請求,以及我們慷慨的贊助商,我很榮幸宣布 Bevy 0.5 版本在crates.io 上發布!

對於那些不了解的人,Bevy 是一個以 Rust 构建的、令人耳目一新的簡單數據驅動遊戲引擎。您可以查看快速入門指南以開始使用。Bevy 也是永久免費且開源的!您可以在 GitHub 上取得完整的原始程式碼。查看Awesome Bevy 以取得社群開發的插件、遊戲和學習資源列表。

Bevy 0.5 比我們過去的幾個版本大得多(並且花費了更長的時間),因為我們進行了許多基礎性的變更。如果您打算將您的 App 或插件更新到 Bevy 0.5,請查看我們的0.4 到 0.5 遷移指南

以下是此版本的一些重點

基於物理的渲染 (PBR) #

作者:@StarArawn、@mtsr、@mockersf、@IngmarBitter、@Josh015、@norgate、@cart

Bevy 現在在渲染時使用 PBR 着色器。PBR 是一種半標準的渲染方法,它嘗試使用對現實世界「基於物理」的光照和材質屬性的近似值。我們主要使用來自Filament PBR 實作的技術,但我們也納入了UnrealDisney 的一些想法。

Bevy 的StandardMaterial 現在具有 base_colorroughnessmetallicreflectionemissive 屬性。它現在也支援 base_colornormal_mapmetallic_roughnessemissiveocclusion 屬性的紋理。

新的 PBR 範例有助於視覺化這些新的材質屬性

pbr

GLTF 改善 #

PBR 紋理 #

作者:@mtsr、@mockersf

GLTF 加載器現在支援法線貼圖、金屬/粗糙度、遮蔽和發射紋理。我們的「飛行頭盔」gltf 範例利用了新的 PBR 紋理支援,因此看起來好得多

頂級 GLTF 資產 #

作者:@mockersf

以前很難與 GLTF 資產互動,因為場景/網格/紋理/和材質僅作為「子資產」加載。由於新的頂級 Gltf 資產類型,現在可以導覽 GLTF 資產的內容

// load GLTF asset on startup
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
    let handle = assets.load("flight_helmet.gltf");
    commands.insert_resource(handle);
}

// access GLTF asset at some later point in time
fn system(handle: Res<Handle<Gltf>>, gltfs: Res<Assets<Gltf>>, materials: Res<Assets<StandardMaterial>>) {
    let gltf = gltfs.get(&handle).unwrap();
    let material_handle = gltf.named_materials.get("MetalPartsMat").unwrap();
    let material = materials.get(material_handle).unwrap();
}

Bevy ECS V2 #

此版本標誌著 Bevy 的 ECS 向前邁出了一大步。它對 Bevy 應用程式的組成方式及其效能有重大影響

  • ECS 核心的完全重寫
    • 全面大幅提升效能
    • 「混合」元件儲存
    • 用於更快原型變更的「原型圖」
    • 跨執行快取結果的有狀態查詢
  • 全新的平行系統執行器
    • 支援明確的系統排序
    • 系統標籤
    • 系統集合
    • 改進的系統「執行條件」
    • 增加的系統平行性
  • 「可靠」的變更偵測
    • 系統現在將始終偵測到元件變更,即使跨影格也是如此
  • 狀態系統的重寫
    • 更自然的「基於堆疊的狀態機」模型
    • 與新排程器的直接整合
    • 改進的「狀態生命週期」事件

繼續閱讀以了解詳細資訊!

ECS 核心重寫 #

作者:@cart

到目前為止,Bevy 對於我們的 ECS 核心使用了 hecs 的高度分支版本。自 Bevy 的第一個版本以來,我們對 Bevy 的 ECS 需求了解了很多。我們還與其他 ECS 專案負責人合作,例如 Sander Mertensflecs 的主要開發人員)和 Gijs-Jan Roelofs(Xenonauts ECS 框架開發人員)。作為一個「ECS 社群」,我們已經開始將焦點集中在 ECS 的未來可能發展的方向。

Bevy ECS v2 是我們邁向未來的​​第一步。這也表示 Bevy ECS 不再是「hecs 分支」。我們正在走自己的路!

元件儲存(問題) #

多年來,兩種 ECS 儲存範例獲得了很多關注

  • 原型 ECS:
    • 將元件儲存在具有靜態架構的「表格」中。每個「欄」儲存給定類型的元件。每個「列」都是一個實體。
    • 每個「原型」都有自己的表格。新增/移除實體的元件會變更原型。
    • 由於其快取友好的資料佈局,可以實現超快速的查詢迭代
    • 代價是實體的元件新增/移除操作成本較高,因為所有元件都需要複製到新原型的「表格」
    • 平行性友善:實體一次僅存在於一個原型中,因此存取相同元件但在不同原型中的系統可以並行執行
    • 框架:舊的 Bevy ECS、hecs、legion、flecs、Unity DOTS
  • 稀疏集合 ECS:
    • 將相同類型的元件儲存在密集封裝的陣列中,這些陣列由密集封裝的無符號整數(實體 ID)稀疏索引
    • 查詢迭代比原型 ECS 慢(預設),因為每個實體的元件都可能位於稀疏集合中的任何位置。這種「隨機存取」模式對快取不友善。此外,還有一層額外的間接層,因為您必須先將實體 ID 對應到元件陣列中的索引。
    • 新增/移除元件是廉價的、常數時間的操作
    • 「元件包」用於最佳化逐個案例的迭代效能(但包之間會發生衝突)
    • 較不平行友善:系統需要鎖定整個元件儲存(不精細)或個別實體(成本高昂)
    • 框架:Shipyard、EnTT

選擇 ECS 框架的開發人員陷入了艱難的選擇。選擇「到處都有快速迭代」的「原型」框架,但無法廉價地新增/移除元件,或選擇「稀疏集合」框架以廉價地新增/移除元件,但迭代效能較慢或手動(且衝突)包最佳化。

混合元件儲存(解決方案) #

在 Bevy ECS V2 中,我們可以兩全其美。它現在具有上述兩種元件儲存類型(如果需要,以後還可以新增更多類型)

  • 表格(在其他框架中又稱為「原型」儲存)
    • 預設儲存。如果您沒有組態任何項目,這就是您得到的結果
    • 預設快速迭代
    • 新增/移除操作較慢
  • 稀疏集合
    • 選擇加入
    • 迭代較慢
    • 新增/移除操作較快

這些儲存類型可以完美地互補。預設情況下,查詢迭代速度很快。如果開發人員知道他們想要以高頻率新增/移除元件,他們可以將儲存設定為「稀疏集合」

app.register_component(
    ComponentDescriptor::new::<MyComponent>(StorageType::SparseSet)
);

元件新增/移除基準測試(以毫秒為單位,越少越好) #

此基準測試說明從具有 5 個其他 4x4 矩陣元件的實體中新增和移除單個 4x4 矩陣元件 10,000 次。「其他」元件包含在內,以幫助說明「表格儲存」(由 Bevy 0.4、Bevy 0.5 (表格) 和 Legion 使用)的成本,這需要將「其他」元件移動到新的表格。

component add/remove

您可能已經注意到,Bevy 0.5 (表格) 也比 Bevy 0.4得多,即使它們都使用「表格儲存」。這主要是新原型圖 的結果,它大大降低了原型變更的成本。

有狀態的查詢和系統參數 #

World 查詢(和其他系統參數)現在是有狀態的。這讓我們能夠

  1. 快取原型(和表格)匹配
    • 這解決了(天真)原型 ECS 的另一個問題:隨著原型數量的增加(並且發生片段化),查詢效能會變差。
  2. 快取查詢提取和篩選狀態
    • 獲取/篩選操作中昂貴的部分(例如雜湊 TypeId 來尋找 ComponentId)現在只會在第一次建構 Query 時發生一次
  3. 增量建立狀態
    • 當新增新的原型時,我們只處理新的原型(不需要為舊的原型重建狀態)

因此,直接的 World 查詢 API 現在看起來像這樣

let mut query = world.query::<(&A, &mut B)>();
for (a, mut b) in query.iter_mut(&mut world) {
}

然而,對於系統而言,這是一個非破壞性的變更。查詢狀態管理由相關的 SystemParam 在內部完成。

由於新的 [Query] 系統,我們取得了一些相當顯著的效能提升。

「稀疏」碎片化迭代器基準測試(以奈秒為單位,越少越好) #

此基準測試執行一個查詢,該查詢在單個原型中匹配 5 個實體,並且匹配其他 100 個原型。這是一個合理的「真實世界」遊戲查詢測試,這些查詢通常有許多不同的實體「類型」,其中大多數與給定的查詢匹配。此測試全面使用「表格儲存」。

sparse_frag_iter

Bevy 0.5 在這種情況下標誌著巨大的改進,這要歸功於新的「有狀態查詢」。Bevy 0.4 每次執行迭代器時都需要檢查每個原型,而 Bevy 0.5 將該成本攤銷為零。

碎片化迭代器基準測試(以毫秒為單位,越少越好) #

這是 ecs_bench_suitefrag_iter 基準測試。它在 27 個原型上執行查詢,每個原型有 20 個實體。然而,與「稀疏碎片化迭代器基準測試」不同,這裡沒有「不匹配」的原型。此測試全面使用「表格儲存」。

frag_iter

與上一個基準測試相比,這裡的增益較小,因為沒有不匹配的原型。但是,由於更好的迭代器/查詢實作、將匹配原型的成本攤銷為零以及 for_each 迭代器,Bevy 0.5 仍然獲得了不錯的提升。

超快的「for_each」查詢迭代器 #

開發人員現在可以選擇使用快速的 Query::for_each 迭代器,這為「碎片化迭代」帶來約 1.5-3 倍的迭代速度提升,而為非碎片化迭代帶來約 1.2 倍的迭代速度提升。

fn system(query: Query<(&A, &mut B)>) {
    // you now have the option to do this for a speed boost
    query.for_each_mut(|(a, mut b)| {
    });

    // however normal iterators are still available
    for (a, mut b) in query.iter_mut() {
    }
}

我們將繼續鼓勵使用「普通」迭代器,因為它們更靈活且更符合「Rust 慣用語」。但是,當需要額外的「力量」時,for_each 會在那裡...等著你 :)

新的平行系統執行器 #

作者:@Ratysz

Bevy 舊的平行執行器有一些基本限制

  1. 明確定義系統順序的唯一方法是建立新的階段。這既繁瑣又阻止了平行處理(因為階段依序「一個接一個」執行)。我們注意到系統排序是一個常見的要求,而階段根本無法滿足。
  2. 當系統存取衝突資源時,它們具有「隱含」的排序。這些排序很難理解。
  3. 「隱含排序」產生的執行策略通常會留下許多潛在的平行處理空間。

幸運的是,@Ratysz 在這方面做了大量的 研究,並自願貢獻一個新的執行器。新的執行器解決了上述所有問題,並增加了一些新的可用性改進。「排序」規則現在非常簡單

  1. 系統預設以平行方式執行
  2. 具有明確定義排序的系統將遵守這些排序

明確的系統依賴性和系統標籤 #

作者:@Ratysz、@TheRawMeatball

現在可以為系統指派一個或多個 SystemLabels。然後,其他系統(在一個階段內)可以參考這些標籤,以便在具有該標籤的系統之前或之後執行

app
    .add_system(update_velocity.system().label("velocity"))
    // The "movement" system will run after "update_velocity" 
    .add_system(movement.system().after("velocity"))

這會產生等效的排序,但它使用 before() 來代替。

app
    // The "update_velocity" system will run before "movement" 
    .add_system(update_velocity.system().before("movement"))
    .add_system(movement.system().label("movement"));

可以使用任何實作 SystemLabel 特性的類型。在大多數情況下,我們建議定義自訂類型並為它們衍生 SystemLabel。這樣可以防止拼寫錯誤,允許封裝(在需要時),並允許 IDE 自動完成標籤

#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum PhysicsSystem {
    UpdateVelocity,
    Movement,
}

app
    .add_system(update_velocity.system().label(PhysicsSystem::UpdateVelocity))
    .add_system(movement.system()
        .label(PhysicsSystem::Movement)
        .after(PhysicsSystem::UpdateVelocity)
    );

多對多系統標籤 #

多對多標籤是一個強大的概念,可以輕鬆地依賴於許多產生特定行為/結果的系統。例如,如果您有一個系統需要在所有「物理」完成更新後執行(請參閱上面的範例),您可以將所有「物理系統」標記為相同的 Physics 標籤

#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub struct Physics;

#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)]
pub enum PhysicsSystem {
    UpdateVelocity,
    Movement,
}

app
    .add_system(update_velocity.system()
        .label(PhysicsSystem::UpdateVelocity)
        .label(Physics)
    )
    .add_system(movement.system()
        .label(PhysicsSystem::Movement)
        .label(Physics)
        .after(PhysicsSystem::UpdateVelocity)
    )
    .add_system(runs_after_physics.system().after(Physics));

Bevy 外掛程式作者應該在其公共 API 中匯出這樣的標籤,以使其使用者能夠在外掛程式提供的邏輯之前/之後插入系統。

系統集合 #

SystemSets 是一種新的方法,可以將相同的配置套用到一組系統,這大大減少了重複程式碼。上面的「物理」範例可以像這樣重新措辭

app
    .add_system_set(SystemSet::new()
        // this label is added to all systems in the set
        .label(Physics)
        .with_system(update_velocity.system().label(PhysicsSystem::UpdateVelocity))
        .with_system(movement.system()
            .label(PhysicsSystem::Movement)
            .after(PhysicsSystem::UpdateVelocity)
        )
    )

SystemSets 也可以使用 before(Label)after(Label) 來讓集合中的所有系統在給定標籤之前/之後執行。

這對於需要使用相同 RunCriteria 執行的一組系統也非常有用。

app
    // all systems in this set will run once every two seconds
    .add_system_set(SystemSet::new()
        .with_run_criteria(FixedTimestep::step(2.0))
        .with_system(foo.system())
        .with_system(bar.system())
    )

改進的執行條件 #

執行條件現在與系統解耦,並且會在可能的情況下重複使用。例如,上面範例中的 FixedTimestep 條件每個階段執行只會執行一次。執行器將為 foobar 系統重複使用條件的結果。

現在也可以標記執行條件並由其他系統參考

fn every_other_time(mut has_ran: Local<bool>) -> ShouldRun {
    *has_ran = !*has_ran;
    if *has_ran {
        ShouldRun::Yes
    } else {
        ShouldRun::No
    }
}

app.add_stage(SystemStage::parallel()
   .with_system_run_criteria(every_other_time.system().label("every_other_time")))
   .add_system(foo.system().with_run_criteria("every_other_time"))

執行條件的結果也可以「管道化」到其他條件中,這可以實現有趣的組合行為

fn once_in_a_blue_moon(In(input): In<ShouldRun>, moon: Res<Moon>) -> ShouldRun {
    if moon.is_blue() {
        input
    } else {
        ShouldRun::No
    }
}

app
    .add_system(foo.with_run_criteria(
        "every_other_time".pipe(once_in_a_blue_moon.system())
    )

歧義檢測和解析 #

雖然新的執行器現在更容易理解,但它確實引入了一種新的錯誤類型:「系統順序歧義」。當兩個系統與相同的資料互動,但沒有定義明確的排序時,它們產生的輸出是不確定的(而且通常不是作者預期的)。

考慮以下應用程式

fn increment_counter(mut counter: ResMut<usize>) {
    *counter += 1;
}

fn print_every_other_time(counter: Res<usize>) {
    if *counter % 2 == 0 {
        println!("ran");
    }
}

app
    .add_system(increment_counter.system())
    .add_system(print_every_other_time.system())

作者顯然打算讓 print_every_other_time 每隔一次更新執行。然而,由於這些系統沒有定義順序,它們可能會在每次更新時以不同的順序執行,並產生在兩次更新過程中沒有任何輸出列印的情況

UPDATE
- increment_counter (counter now equals 1)
- print_every_other_time (nothing printed)
UPDATE
- print_every_other_time (nothing printed)
- increment_counter (counter now equals 2)

舊的執行器會隱含地強制 increment_counter 先執行,因為它與 print_every_other_time 衝突,並且它是第一個插入的。但是新的執行器要求您在這裡明確(我們認為這是一件好事)。

為了協助檢測這類錯誤,我們建立了一個選擇加入工具,可以檢測這些歧義並記錄它們

// add this resource to your App to enable ambiguity detection
app.insert_resource(ReportExecutionOrderAmbiguities)

然後,當我們執行我們的應用程式時,我們將會在終端機上看到以下訊息

Execution order ambiguities detected, you might want to add an explicit dependency relation between some of these systems:
 * Parallel systems:
 -- "&app::increment_counter" and "&app::print_every_other_time"
    conflicts: ["usize"]

歧義檢測器發現衝突,並提到新增明確的依賴性將可以解決衝突

app
    .add_system(increment_counter.system().label("increment"))
    .add_system(print_every_other_time.system().after("increment"))

在某些情況下,歧義不是錯誤,例如對無序集合(如 Assets)的操作。這就是我們預設不啟用偵測器的原因。您可以自由地忽略這些歧義,但如果您想抑制偵測器中的訊息(而不定義依賴性),您可以將您的系統新增到「歧義集合」中

app
    .add_system(a.system().in_ambiguity_set("foo"))
    .add_system(b.system().in_ambiguity_set("foo"))

我想強調這完全是可選的。Bevy 程式碼應該符合人體工學且「有趣」地編寫。如果到處撒上歧義集合不是您的菜,那就別擔心!

我們也在積極尋求對新執行器的回饋。我們相信新的實作更容易理解,並鼓勵自我記錄程式碼。改進的平行處理也很好!但我們希望聽取使用者(包括剛開始的新使用者和將程式碼庫移植到新執行器的舊使用者)的意見。這個領域都與設計權衡有關,回饋將有助於我們確保我們做出了正確的決定。

可靠的變更偵測 #

作者:@Davier、@bjorn3、@alice-i-cecile、@cart

全域變更偵測,在任何 ECS 元件或資源的變更/新增狀態上執行查詢的能力,剛剛獲得了重大的可用性提升:現在可以跨框架/更新偵測變更

// This is still the same change detection API we all know and love,
// the only difference is that it "just works" in every situation.
fn system(query: Query<Entity, Changed<A>>) {
    // iterates all entities whose A component has changed since
    // the last run of this system 
    for e in query.iter() {
    }
}

全域變更偵測已經是一個使 Bevy 與其他 ECS 框架區分開來的功能,但現在它完全是「萬無一失」的。無論系統排序、階段成員資格或系統執行條件如何,它都能按預期運作。

舊的行為是「系統偵測到此框架中在它們之前執行的系統中發生的變更」。這是因為我們使用一個 bool 來追蹤每個元件/資源何時新增/修改。此旗標會在框架結束時為每個元件清除。因此,使用者必須非常小心操作順序,並且如果系統在給定的更新中沒有執行,則使用「系統執行條件」等功能可能會導致遺漏變更。

我們現在使用一個巧妙的「世界刻度」設計,允許系統偵測自上次執行以來在任何時間點發生的變更。

狀態 V2 #

作者:@TheRawMeatball

上一個 Bevy 版本 新增了狀態,使開發人員能夠根據 State<T> 資源的值來執行一組 ECS 系統。可以根據「狀態生命週期事件」來執行系統,例如 on_enter、on_update 和 on_exit。狀態使諸如單獨的「載入畫面」和「遊戲中」邏輯之類的事情更容易在 Bevy ECS 中編碼。

舊的實作基本上可以運作,但它有一些怪癖和限制。首先,它需要新增一個新的 StateStage,這減少了平行處理,增加了重複程式碼,並強制了不需要的排序。此外,某些生命週期事件的行為並不總是如預期。

新的 State 實作是建立在新的平行執行器的 SystemSet 和 RunCriteria 功能之上的,提供了一個更自然、靈活和並行的 API,該 API 建基於現有的概念而不是建立新的概念

#[derive(Debug, Clone, Eq, PartialEq, Hash)]
enum AppState {
    Menu,
    InGame,
}

fn main() {
    App::build()
        .add_state(AppState::Menu)
        .add_system_set(SystemSet::on_enter(AppState::Menu).with_system(setup_menu.system()))
        .add_system_set(SystemSet::on_update(AppState::Menu).with_system(menu_logic.system()))
        .add_system_set(SystemSet::on_exit(AppState::Menu).with_system(cleanup_menu.system()))
        .add_system_set(SystemSet::on_enter(AppState::InGame).with_system(setup_game.system()))
        .add_system_set(
            SystemSet::on_update(AppState::InGame)
                .with_system(game_logic.system())
                .with_system(more_game_logic.system())
        )
        .run();
}

狀態現在使用「基於堆疊的狀態機」模型。這為狀態轉換開啟了許多選項

fn system(mut state: ResMut<State<AppState>>) {
    // Queues up a state change that pushes a new state on to the
    // stack (preserving previous states)
    state.push(AppState::InGame).unwrap();

    // Queues up a state change that removes the current state on
    // the stack and reverts to the previous state
    state.pop().unwrap();

    // Queues up a state change that overwrites the current state at
    // the "top" of the stack
    state.set(AppState::InGame).unwrap();

    // Queues up a state change that replaces the entire stack of states
    state.replace(AppState::InGame).unwrap();
}

就像舊的實作一樣,狀態變更會在同一個框架中套用。這表示可以從狀態 A->B->C 轉換並執行相關的狀態生命週期事件,而不會跳過框架。這是建立在「循環執行條件」之上的,我們也將其用於我們的「固定時間步長」實作(您也可以將其用於您自己的執行條件邏輯)。

事件人體工學 #

作者:@TheRawMeatball

現在事件具有一流的簡寫語法,以便更容易使用

// Old Bevy 0.4 syntax
fn system(mut reader: Local<EventReader<SomeEvent>>, events: Res<Events<SomeEvent>>) {
    for event in reader.iter(&events) {
    }
}

// New Bevy 0.5 syntax
fn system(mut reader: EventReader<SomeEvent>) {
    for event in reader.iter() {
    }
}

現在還有一個對稱的 EventWriter API

fn system(mut writer: EventWriter<SomeEvent>) {
    writer.send(SomeEvent { ... })
}

仍然可以使用 ManualEventReader 實現舊的「手動」方法

fn system(mut reader: Local<ManualEventReader<SomeEvent>>, events: Res<Events<SomeEvent>>) {
    for event in reader.iter(&events) {
    }
}

富文本 #

作者:@tigregalis

現在文字可以有「區段」,每個區段都有自己的樣式/格式。這使文字更加靈活,同時仍然遵守文字佈局規則

rich_text

這是透過新的「文字區段」API 完成的

commands
    .spawn_bundle(TextBundle {
        text: Text {
            sections: vec![
                TextSection {
                    value: "FPS: ".to_string(),
                    style: TextStyle {
                        font: asset_server.load("FiraSans-Bold.ttf"),
                        font_size: 90.0,
                        color: Color::WHITE,
                    },
                },
                TextSection {
                    value: "60.03".to_string(),
                    style: TextStyle {
                        font: asset_server.load("FiraMono-Medium.ttf"),
                        font_size: 90.0,
                        color: Color::GOLD,
                    },
                },
            ],
            ..Default::default()
        },
        ..Default::default()
    })

HIDPI 文字 #

作者:@blunted2night

現在會根據目前螢幕的縮放比例來呈現文字。這可以在任何解析度下提供清晰銳利的文字。

hidpi_text

在 2D 世界空間中渲染文字 #

作者:@CleanCut, @blunted2night

現在可以使用新的 Text2dBundle 在 2D 場景中生成文字。這使得「在玩家上方繪製名稱」之類的操作更容易實現。

世界座標到螢幕座標的轉換 #

作者:@aevyrie

現在可以使用新的 Camera::world_to_screen() 函數將世界座標轉換為指定攝影機的螢幕座標。以下範例展示了如何使用此功能將 UI 元素定位在移動的 3D 物件之上。

3D 正交攝影機 #

作者:@jamadazi

現在可以在 3D 中使用正交攝影機!這對於 CAD 應用程式和等距視角遊戲等應用非常有用。

ortho_3d

正交攝影機縮放模式 #

作者:@jamadazi

Bevy 0.5 之前,Bevy 的正交攝影機只有一種模式:「視窗縮放」。它會根據視窗的垂直和水平尺寸調整投影。這適用於某些遊戲風格,但其他遊戲需要任意的獨立於視窗的比例因子,或者由水平或垂直視窗尺寸定義的比例因子。

Bevy 0.5OrthographicCamera 中新增了一個新的 ScalingMode 選項,使開發人員可以自訂投影的計算方式。

它還新增了使用 OrthographicProjection::scale「縮放」攝影機的能力。

彈性的攝影機綁定 #

作者:@cart

Bevy 過去會為每個 RenderGraph PassNode「硬寫」攝影機綁定。當只有一種綁定類型(組合的 ViewProj 矩陣)時,這還行得通,但許多著色器需要其他攝影機屬性,例如世界空間位置。

在 Bevy 0.5 中,我們移除了「硬寫」方式,轉而使用其他地方使用的 RenderResourceBindings 系統。這使著色器能夠綁定任意攝影機資料(使用任何集合或綁定索引),並且只提取它們需要的資料。

新的 PBR 著色器利用了此功能,但自訂著色器也可以使用它。

layout(set = 0, binding = 0) uniform CameraViewProj {
    mat4 ViewProj;
};
layout(set = 0, binding = 1) uniform CameraPosition {
    vec3 CameraPos;
};

渲染圖層 #

作者:@schell

有時您不希望攝影機繪製場景中的所有內容,或者您希望暫時隱藏場景中的一組內容。Bevy 0.5 新增了一個 RenderLayer 系統,使開發人員能夠透過新增 RenderLayers 組件將實體新增到圖層。

攝影機也可以具有 RenderLayers 組件,該組件決定它們可以看到哪些圖層。

// spawn a sprite on layer 0
commands
    .spawn_bundle(SpriteBundle {
        material: materials.add(Color::rgb(1.0, 0.5, 0.5).into()),
        transform: Transform::from_xyz(0.0, -50.0, 1.0),
        sprite: Sprite::new(Vec2::new(30.0, 30.0)),
    })
    .insert(RenderLayers::layer(0));
// spawn a sprite on layer 1
commands
    .spawn_bundle(SpriteBundle {
        material: materials.add(Color::rgb(1.0, 0.5, 0.5).into()),
        transform: Transform::from_xyz(0.0, -50.0, 1.0),
        sprite: Sprite::new(Vec2::new(30.0, 30.0)),
    })
    .insert(RenderLayers::layer(1));
// spawn a camera that only draws the sprite on layer 1
commands
    .spawn_bundle(OrthographicCameraBundle::new_2d());
    .insert(RenderLayers::layer(1));

精靈翻轉 #

作者:@zicklag

現在可以輕鬆(且有效率地)沿著 x 或 y 軸翻轉精靈

sprite_flipping

commands.spawn_bundle(SpriteBundle {
    material: material.clone(),
    transform: Transform::from_xyz(150.0, 0.0, 0.0),
    ..Default::default()
});
commands.spawn_bundle(SpriteBundle {
    material,
    transform: Transform::from_xyz(-150.0, 0.0, 0.0),
    sprite: Sprite {
        // Flip the logo to the left
        flip_x: true,
        // And don't flip it upside-down ( the default )
        flip_y: false,
        ..Default::default()
    },
    ..Default::default()
});

色彩空間 #

作者:@mockersf

Color 現在在內部表示為枚舉,可以實現無損(且正確的)色彩表示。這是對先前實作的重大改進,先前實作會在內部將所有色彩轉換為線性 sRGB(這可能會導致精確度問題)。現在只有在將色彩傳送到 GPU 時才會將色彩轉換為線性 sRGB。我們也藉此機會修復了一些在錯誤色彩空間中定義的不正確的色彩常數。

pub enum Color {
    /// sRGBA color
    Rgba {
        /// Red component. [0.0, 1.0]
        red: f32,
        /// Green component. [0.0, 1.0]
        green: f32,
        /// Blue component. [0.0, 1.0]
        blue: f32,
        /// Alpha component. [0.0, 1.0]
        alpha: f32,
    },
    /// RGBA color in the Linear sRGB colorspace (often colloquially referred to as "linear", "RGB", or "linear RGB").
    RgbaLinear {
        /// Red component. [0.0, 1.0]
        red: f32,
        /// Green component. [0.0, 1.0]
        green: f32,
        /// Blue component. [0.0, 1.0]
        blue: f32,
        /// Alpha component. [0.0, 1.0]
        alpha: f32,
    },
    /// HSL (hue, saturation, lightness) color with an alpha channel
    Hsla {
        /// Hue component. [0.0, 360.0]
        hue: f32,
        /// Saturation component. [0.0, 1.0]
        saturation: f32,
        /// Lightness component. [0.0, 1.0]
        lightness: f32,
        /// Alpha component. [0.0, 1.0]
        alpha: f32,
    },
}

線框 #

作者:@Neo-Zhixing

Bevy 現在可以使用選擇加入的 WireframePlugin 繪製線框

wireframe

可以透過新增新的 Wireframe 組件來全域啟用或每個實體啟用這些線框。

簡單的 3D 遊戲範例:外星蛋糕癮 #

作者:@mockersf

此範例可作為在 Bevy 中建置 3D 遊戲的快速入門。它展示了如何產生場景、回應輸入、實作遊戲邏輯以及處理狀態轉換。盡可能收集更多的蛋糕!

alien_cake_addict

計時器改進 #

作者:@kokounet

Timer 結構現在在內部使用 Duration 而不是使用秒的 f32 表示法。這既提高了精確度,又使 API 看起來更美觀。

fn system(mut timer: ResMut<Timer>, time: Res<Time>) {
    if timer.tick(time.delta()).just_finished() {
        println!("timer just finished");
    }
}

資源改進 #

作者:@willcrichton, @zicklag, @mockersf, @Archina

Bevy 的資源系統在這個版本中做了一些小的改進

  • Bevy 不再在載入資源時發生錯誤而 panic
  • 現在可以正確處理具有多個點的資源路徑
  • 提高了資源載入器產生的「已標籤資源」的類型安全性
  • 使資源路徑載入不區分大小寫

WGPU 配置選項 #

作者:@Neo-Zhixing

現在可以透過在 WgpuOptions 資源中設定來啟用/停用 wgpu 功能(例如 WgpuFeature::PushConstantsWgpuFeature::NonFillPolygonMode

app
    .insert_resource(WgpuOptions {
        features: WgpuFeatures {
            features: vec![WgpuFeature::NonFillPolygonMode],
        },
        ..Default::default()
    })

wgpu 限制(例如 WgpuLimits::max_bind_groups)現在也可以在 WgpuOptions 資源中進行配置。

場景實例實體迭代 #

作者:@mockersf

現在可以迭代已產生的場景實例中的所有實體。這使得可以在載入場景後對其執行後處理。

struct MySceneInstance(InstanceId);

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut scene_spawner: ResMut<SceneSpawner>,
) {
    // Spawn a scene and keep its `instance_id`
    let instance_id = scene_spawner.spawn(asset_server.load("model.gltf#Scene0"));
    commands.insert_resource(MySceneInstance(instance_id));
}

fn print_scene_entities(
    scene_spawner: Res<SceneSpawner>,
    scene_instance: Res<MySceneInstance>,
) {
    if let Some(entity_iter) = scene_spawner.iter_instance_entities(scene_instance.0) {
        for entity in entity_iter {
            println!("Found scene entity {:?}", entity);
        }
    }
}

視窗調整大小約束 #

作者:@digital7-code

視窗現在可以具有「調整大小約束」。視窗不能調整大小超過這些約束

app
    .insert_resource(WindowDescriptor {
        resize_constraints: WindowResizeConstraints {
            min_height: 200.0,
            max_height: 800.0,
            ..Default::default()
        },
        ..Default::default()
    })

!Send 工作 #

作者:@alec-deason

Bevy 的非同步工作系統現在支援 !Send 工作。某些工作無法在其他執行緒上傳送/執行(例如即將推出的 Distill 資源外掛程式建立的工作)。現在可以在 Bevy TaskPools 中產生「執行緒區域」工作,如下所示

let pool = TaskPool::default();
pool.scope(|scope| {
    scope.spawn_local(async move {
        println!("I am a local task");
    });
});

更多 ECS V2 變更 #

EntityRef / EntityMut #

作者:@cart

Bevy 0.4 中,世界實體操作需要使用者將 entity id 傳遞給每個操作

let entity = world.spawn((A, )); // create a new entity with A
world.get::<A>(entity);
world.insert(entity, (B, C));
world.insert_one(entity, D);

這表示每個操作都需要查詢實體位置/驗證其有效性。初始產生操作還需要一個 Bundle 作為輸入。當不需要任何元件(或只需要一個元件)時,這可能會很麻煩。

這些操作已由 EntityRefEntityMut 取代,它們是圍繞世界的「建構器樣式」包裝器,可對單個預先驗證的實體提供讀取和讀取/寫入操作

// spawn now takes no inputs and returns an EntityMut
let entity = world.spawn()
    .insert(A) // insert a single component into the entity
    .insert_bundle((B, C)) // insert a bundle of components into the entity
    .id() // id returns the Entity id

// Returns EntityMut (or panics if the entity does not exist)
world.entity_mut(entity)
    .insert(D)
    .insert_bundle(SomeBundle::default());

// The `get_X` variants return Options, in case you want to check existence instead of panicking 
world.get_entity_mut(entity)
    .unwrap()
    .insert(E);

if let Some(entity_ref) = world.get_entity(entity) {
    let d = entity_ref.get::<D>().unwrap();
}

Commands 也已更新為使用此新模式

let entity = commands.spawn()
    .insert(A)
    .insert_bundle((B, C))
    .insert_bundle(SomeBundle::default())
    .id();

Commands 也仍然支援使用 Bundle 產生,這應該可以簡化從 Bevy 0.4 的遷移。它還減少了某些情況下的樣板程式碼

commands.spawn_bundle(SomeBundle::default());

請注意,這些 Command 方法使用「類型狀態」模式,這表示此類鏈接不再可能

// Spawns two entities, each with the components in SomeBundle and the A component
// Valid in Bevy 0.4, but invalid in Bevy 0.5
commands
    .spawn(SomeBundle::default())
    .insert(A)
    .spawn(SomeBundle::default())
    .insert(A);

相反,您應該執行此操作

commands
    .spawn_bundle(SomeBundle::default())
    .insert(A);
commands
    .spawn_bundle(SomeBundle::default())
    .insert(A);

這使我們可以使「實體 id 擷取」之類的操作變得無錯誤,並為將來的 API 改進開啟了大門。

Query::single #

作者:@TheRawMeatball

現在,如果正好有一個符合的實體,查詢將具有 Query::singleQuery::single_mut 方法,它們會傳回單個查詢結果

fn system(query: Query<&Player>) {
    // only returns Ok if there is exactly one Player
    if let Ok(player) = query.single() {
    }
}

已移除 ChangedRes #

作者:@TheRawMeatball

我們已移除 ChangedRes<A>,取而代之的是以下內容

fn system(a: Res<A>) {
    if a.is_changed() {
        // do something
    }
}

可選資源查詢 #

作者:@jamadazi

現在系統可以透過 Option 查詢檢查資源是否存在

fn system(a: Option<Res<A>>) {
    if let Some(a) = a {
        // do something
    }
}

新的 Bundle 命名慣例 #

元件 Bundle 先前使用 XComponents 命名慣例(例如:SpriteComponentsTextComponents 等)。我們決定改用 XBundle 命名慣例(例如:SpriteBundleTextBundle 等),以更明確地說明這些類型是什麼,並協助防止新使用者混淆 Bundle 和元件。

世界中繼資料改進 #

作者:@cart

World 現在具有可查詢的 ComponentsArchetypesBundlesEntities 集合

// you can access these new collections from normal systems, just like any other SystemParam
fn system(archetypes: &Archetypes, components: &Components, bundles: &Bundles, entities: &Entities) {
}

這使開發人員可以從其系統存取內部 ECS 中繼資料。

可配置的 SystemParams #

作者:@cart, @DJMcNab

使用者現在可以為系統參數提供一些初始配置/值(如果可能)。大多數 SystemParams 沒有配置(配置類型為 ()),但 Local<T> 參數現在支援使用者提供的參數

fn foo(value: Local<usize>) {    
}

app.add_system(foo.system().config(|c| c.0 = Some(10)));

準備支援腳本 #

作者:@cart

Bevy ECS 元件現在與 Rust 類型解耦。新的 Components 集合儲存中繼資料,例如記憶體配置和解構子。元件也不再需要 Rust TypeId。

可以使用 world.register_component() 隨時新增新的元件中繼資料。

所有元件儲存類型(目前為 Table 和 Sparse Set)都是「blob 儲存」。它們可以儲存任何具有給定記憶體配置的值。這使得可以儲存和存取來自其他來源(例如:Python 資料類型)的資料,與 Rust 資料類型的方式相同。

我們尚未完全啟用腳本功能(而且很可能永遠不會正式支援非 Rust 腳本),但這是朝向啟用社群支援的腳本語言邁出的重要一步。

將資源合併到 World 中 #

作者:@cart

資源現在只是一種特殊的元件。這使我們能夠透過重複使用現有的 Bevy ECS 內部元件來保持程式碼大小小巧。它也使我們能夠最佳化平行執行器存取控制,並且應該可以簡化未來對腳本語言的整合。

world.insert_resource(1);
world.insert_resource(2.0);
let a = world.get_resource::<i32>().unwrap();
let mut b = world.get_resource_mut::<f64>().unwrap();
*b = 3.0;

// Resources are still accessed the same way in Systems
fn system(foo: Res<f64>, bar: ResMut<i32>) {
}

但是,這種合併確實為直接與 World 互動的人員帶來了問題。如果您需要同時對多個資源進行可變存取會發生什麼?world.get_resource_mut() 可變地借用 World,這會阻止多次可變存取!我們使用 WorldCell 解決了這個問題。

WorldCell #

作者:@cart

WorldCell 將系統使用的「存取控制」概念應用於直接的世界存取

let world_cell = world.cell();
let a = world_cell.get_resource_mut::<i32>().unwrap();
let b = world_cell.get_resource_mut::<f64>().unwrap();

這增加了廉價的執行期檢查,以確保 World 的存取不會彼此衝突。

我們將其設為一個獨立的 API,讓使用者可以決定他們想要的權衡。直接存取 World 的生命週期較嚴格,但效率更高,並且在編譯時進行存取控制。WorldCell 的生命週期較寬鬆,但因此會產生些微的執行期效能損失。

此 API 目前僅限於資源存取,但未來將擴展至查詢/實體組件存取。

資源作用域 #

作者:@cart

WorldCell 尚未支援組件查詢,即使未來支援,有時也會有正當理由需要可變的 World 參考可變的資源參考(例如:bevy_render 和 bevy_scene 都需要)。在這種情況下,我們始終可以退回到不安全的 world.get_resource_unchecked_mut(),但這並非理想的作法!

開發人員可以使用「資源作用域」來替代。

world.resource_scope(|world: &mut World, mut a: Mut<A>| {
})

這會暫時從 World 中移除 A 資源,提供兩者的可變指標,並在完成時將 A 重新新增回 World。由於改用 ComponentIds/稀疏集合,這是一個低成本的操作。

如果需要多個資源,則可以巢狀使用作用域。如果這種模式變得常見且樣板程式碼變得令人厭煩,我們也可以考慮在 API 中新增「資源元組」。

查詢衝突使用 ComponentId 而非 ArchetypeComponentId #

作者:@cart

為了安全起見,系統不能包含彼此衝突的查詢,除非將它們包裝在 QuerySet 中。在 Bevy 0.4 中,我們使用 ArchetypeComponentIds 來判斷衝突。這很好,因為它可以考慮篩選器

// these queries will never conflict due to their filters
fn filter_system(a: Query<&mut A, With<B>>, b: Query<&mut B, Without<B>>) {
}

但它也有一個顯著的缺點

// these queries will not conflict _until_ an entity with A, B, and C is spawned
fn maybe_conflicts_system(a: Query<(&mut A, &C)>, b: Query<(&mut A, &B)>) {
}

如果生成具有 A、B 和 C 的實體,上面的系統會在執行期恐慌。這使得很難信任您的遊戲邏輯將在不崩潰的情況下執行。

Bevy 0.5 中,我們改為使用 ComponentId 而非 ArchetypeComponentId。這確實更具約束性。maybe_conflicts_system 現在總是會失敗,但它會在啟動時一致地執行此操作。

簡單來說,它也會禁止 filter_system,這將會大幅降低可用性。Bevy 有許多內部系統依賴於不相交的查詢,我們預期這將是使用者空間中的常見模式。為了解決這個問題,我們新增了一個新的內部 FilteredAccess<T> 類型,它會包裝 Access<T> 並新增 with/without 篩選器。如果兩個 FilteredAccess 的 with/without 值證明它們是不相交的,它們將不再衝突。

這表示 filter_systemBevy 0.5 中仍然完全有效。我們獲得了舊實作的大部分優點,但在應用程式啟動時強制執行一致且可預測的規則。

Bevy 的下一步? #

我們仍然有很長的路要走,但 Bevy 開發人員社群正在快速成長,而且我們已經為未來制定了宏偉的計畫。預計很快就會在以下領域看到進展

  • 「管線化」的渲染和其他渲染器最佳化
  • Bevy UI 重新設計
  • 動畫:組件動畫和 3D 骨骼動畫
  • ECS:關係/索引、非同步系統、原型不變量、「無階段」系統排程
  • 3D 光照功能:陰影、更多光照類型
  • 更多 Bevy 場景功能和可用性改進

我們也計畫在最終確定 Bevy UI 設計後,立即開始開發 Bevy 編輯器。

支援 Bevy #

贊助有助於讓 Bevy 的全職工作能夠持續下去。如果您相信 Bevy 的使命,請考慮贊助 @cart ... 點滴都將有所幫助!

捐款 heart icon

貢獻者 #

非常感謝 88 位貢獻者 促成此版本的發布(以及相關文件)!

  • mockersf
  • CAD97
  • willcrichton
  • Toniman20
  • ElArtista
  • lassade
  • Divoolej
  • msklywenn
  • cart
  • maxwellodri
  • schell
  • payload
  • guimcaballero
  • themilkybit
  • Davier
  • TheRawMeatball
  • alexschrod
  • Ixentus
  • undinococo
  • zicklag
  • lambdagolem
  • reidbhuntley
  • enfipy
  • CleanCut
  • LukeDowell
  • IngmarBitter
  • MinerSebas
  • ColonisationCaptain
  • tigregalis
  • siler
  • Lythenas
  • Restioson
  • kokounet
  • ryanleecode
  • adam-bates
  • Neo-Zhixing
  • bgourlie
  • Telzhaak
  • rkr35
  • jamadazi
  • bjorn3
  • VasanthakumarV
  • turboMaCk
  • YohDeadfall
  • rmsc
  • szunami
  • mnmaita
  • WilliamTCarroll
  • Ratysz
  • OptimisticPeach
  • mtsr
  • AngelicosPhosphoros
  • Adamaq01
  • Moxinilian
  • tomekr
  • jakobhellermann
  • sdfgeoff
  • Byteron
  • aevyrie
  • verzuz
  • ndarilek
  • huhlig
  • zaszi
  • Puciek
  • DJMcNab
  • sburris0
  • rparrett
  • smokku
  • TehPers
  • alec-deason
  • Fishrock123
  • woubuc
  • Newbytee
  • Archina
  • StarArawn
  • JCapucho
  • M2WZ
  • TotalKrill
  • refnil
  • bitshifter
  • NiklasEi
  • alice-i-cecile
  • joshuajbouw
  • DivineGod
  • ShadowMitia
  • memoryruins
  • blunted2night
  • RedlineTriad

變更記錄 #

新增 #