30个版本

0.14.1 2024年8月2日
0.14.0 2024年7月4日
0.14.0-rc.42024年6月27日
0.13.1 2024年3月18日
0.3.0 2020年11月3日

#12 in 游戏开发

Download history 15796/week @ 2024-05-05 15404/week @ 2024-05-12 15167/week @ 2024-05-19 16671/week @ 2024-05-26 18179/week @ 2024-06-02 13085/week @ 2024-06-09 17391/week @ 2024-06-16 15273/week @ 2024-06-23 15404/week @ 2024-06-30 18344/week @ 2024-07-07 20868/week @ 2024-07-14 22199/week @ 2024-07-21 24704/week @ 2024-07-28 19998/week @ 2024-08-04 28621/week @ 2024-08-11 20571/week @ 2024-08-18

96,287 每月下载量
用于 1,280 个包 (252 直接)

MIT/Apache

2.5MB
47K SLoC

Bevy ECS

License Crates.io Downloads Docs Discord

什么是Bevy ECS?

Bevy ECS是为Bevy游戏引擎量身定制的实体组件系统。它旨在简单易用、人体工程学设计、快速、高度并行、有意见且功能丰富。它是为Bevy的需求而创建的,但也可以轻松地作为其他项目的独立包使用。

ECS

Bevy中所有应用程序逻辑都使用实体组件系统(ECS)范式,通常简称为ECS。ECS是一种软件模式,涉及将程序分解为实体、组件和系统。实体是唯一的事物,分配了一组组件,然后使用系统进行处理。

例如,一个实体可能有一个PositionVelocity组件,而另一个实体可能有一个PositionUI组件。您可能有一个在所有具有位置和速度组件的实体上运行的移动系统。

ECS模式通过迫使您将应用程序数据和逻辑分解为其核心组件,鼓励清洁、解耦的设计。它还通过优化内存访问模式并使并行化更容易来帮助使您的代码更快。

概念

Bevy ECS是Bevy对ECS模式的实现。与其他需要复杂生命周期、特质、构建器模式或宏的Rust ECS实现不同,Bevy ECS使用正常的Rust数据类型来表示所有这些概念。

组件

组件是正常的Rust结构体。它们存储在World中,特定组件的实例与实体相关联。

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }

世界

实体、组件和资源存储在World中。与世界类似,std::collectionsHashSetVec公开了插入、读取、写入和删除它们存储的数据的操作。

use bevy_ecs::world::World;

let world = World::default();

实体

实体是唯一标识符,与零个或多个组件相关联。

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Velocity { x: f32, y: f32 }

let mut world = World::new();

let entity = world
    .spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 1.0, y: 0.0 }))
    .id();

let entity_ref = world.entity(entity);
let position = entity_ref.get::<Position>().unwrap();
let velocity = entity_ref.get::<Velocity>().unwrap();

系统

系统是普通的Rust函数。得益于Rust的类型系统,Bevy ECS可以使用函数参数类型来确定需要发送到系统的数据。它还使用这种“数据访问”信息来确定哪些系统可以相互并行运行。

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }

fn print_position(query: Query<(Entity, &Position)>) {
    for (entity, position) in &query {
        println!("Entity {:?} is at position: x {}, y {}", entity, position.x, position.y);
    }
}

资源

应用程序通常需要独特的资源,例如资产集合、渲染器、音频服务器、时间等。Bevy ECS将这种模式提升为一等公民。《code>Resource是一种特殊的组件,不属于任何实体。相反,它通过其类型唯一标识。

use bevy_ecs::prelude::*;

#[derive(Resource, Default)]
struct Time {
    seconds: f32,
}

let mut world = World::new();

world.insert_resource(Time::default());

let time = world.get_resource::<Time>().unwrap();

// You can also access resources from Systems
fn print_time(time: Res<Time>) {
    println!("{}", time.seconds);
}

resources.rs示例中说明了如何从系统中读取和写入Counter资源。

调度

调度根据某种执行策略运行一组系统。可以将系统添加到任意数量的系统集中,这些系统集用于控制它们的调度元数据。

内置的“并行执行器”考虑系统之间的依赖关系,并默认尽可能并行运行尽可能多的系统。这最大限度地提高了性能,同时保持了系统执行的安全性。要控制系统顺序,请定义系统及其集之间的显式依赖关系。

使用Bevy ECS

对于熟悉Rust语法的开发者来说,Bevy ECS应该感觉非常自然。

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Velocity { x: f32, y: f32 }

// This system moves each entity with a Position and Velocity component
fn movement(mut query: Query<(&mut Position, &Velocity)>) {
    for (mut position, velocity) in &mut query {
        position.x += velocity.x;
        position.y += velocity.y;
    }
}

fn main() {
    // Create a new empty World to hold our Entities and Components
    let mut world = World::new();

    // Spawn an entity with Position and Velocity components
    world.spawn((
        Position { x: 0.0, y: 0.0 },
        Velocity { x: 1.0, y: 0.0 },
    ));

    // Create a new Schedule, which defines an execution strategy for Systems
    let mut schedule = Schedule::default();

    // Add our system to the schedule
    schedule.add_systems(movement);

    // Run the schedule once. If your app has a "loop", you would run this once per loop
    schedule.run(&mut world);
}

功能

查询过滤器

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Alive;

// Gets the Position component of all Entities with Player component and without the Alive
// component. 
fn system(query: Query<&Position, (With<Player>, Without<Alive>)>) {
    for position in &query {
    }
}

变更检测

Bevy ECS跟踪组件和资源的所有更改。

查询可以过滤更改的组件

use bevy_ecs::prelude::*;

#[derive(Component)]
struct Position { x: f32, y: f32 }
#[derive(Component)]
struct Velocity { x: f32, y: f32 }

// Gets the Position component of all Entities whose Velocity has changed since the last run of the System
fn system_changed(query: Query<&Position, Changed<Velocity>>) {
    for position in &query {
    }
}

// Gets the Position component of all Entities that had a Velocity component added since the last run of the System
fn system_added(query: Query<&Position, Added<Velocity>>) {
    for position in &query {
    }
}

资源也公开更改状态

use bevy_ecs::prelude::*;

#[derive(Resource)]
struct Time(f32);

// Prints "time changed!" if the Time resource has changed since the last run of the System
fn system(time: Res<Time>) {
    if time.is_changed() {
        println!("time changed!");
    }
}

change_detection.rs示例中展示了如何查询仅更新的实体并针对资源中的更改做出反应。

组件存储

Bevy ECS支持多种组件存储类型。

组件可以存储在

  • 表格:快速且缓存友好的迭代,但添加和删除组件较慢。这是默认存储类型。
  • 稀疏集:快速添加和删除组件,但迭代较慢。

组件存储类型是可配置的,如果未手动定义存储,则默认为表格存储。

use bevy_ecs::prelude::*;

#[derive(Component)]
struct TableStoredComponent;

#[derive(Component)]
#[component(storage = "SparseSet")]
struct SparseStoredComponent;

组件包

定义应一起添加的组件集合。

use bevy_ecs::prelude::*;

#[derive(Default, Component)]
struct Player;
#[derive(Default, Component)]
struct Position { x: f32, y: f32 }
#[derive(Default, Component)]
struct Velocity { x: f32, y: f32 }

#[derive(Bundle, Default)]
struct PlayerBundle {
    player: Player,
    position: Position,
    velocity: Velocity,
}

let mut world = World::new();

// Spawn a new entity and insert the default PlayerBundle
world.spawn(PlayerBundle::default());

// Bundles play well with Rust's struct update syntax
world.spawn(PlayerBundle {
    position: Position { x: 1.0, y: 1.0 },
    ..Default::default()
});

事件

事件在系统之间提供通信渠道。可以使用系统参数EventWriter发送事件,并用EventReader接收事件。

use bevy_ecs::prelude::*;

#[derive(Event)]
struct MyEvent {
    message: String,
}

fn writer(mut writer: EventWriter<MyEvent>) {
    writer.send(MyEvent {
        message: "hello!".to_string(),
    });
}

fn reader(mut reader: EventReader<MyEvent>) {
    for event in reader.read() {
    }
}

events.rs中可以看到使用事件的最小配置。

观察者

观察者是监听特定Event“触发”的系统。

use bevy_ecs::prelude::*;

#[derive(Event)]
struct MyEvent {
    message: String
}

let mut world = World::new();

world.observe(|trigger: Trigger<MyEvent>| {
    println!("{}", trigger.event().message);
});

world.flush();

world.trigger(MyEvent {
    message: "hello!".to_string(),
});

它们与EventReaderEventWriter不同,因为它们是“反应性的”。它们不是在调度中的特定点发生,而是在触发时立即发生。触发可以触发其他触发,所有触发都会同时评估!

事件也可以被触发以针对特定实体

use bevy_ecs::prelude::*;

#[derive(Event)]
struct Explode;

let mut world = World::new();
let entity = world.spawn_empty().id();

world.observe(|trigger: Trigger<Explode>, mut commands: Commands| {
    println!("Entity {:?} goes BOOM!", trigger.entity());
    commands.entity(trigger.entity()).despawn();
});

world.flush();

world.trigger_targets(Explode, entity);

依赖关系

~6–8.5MB
~147K SLoC