ECS

Bevy 中的所有应用程序逻辑都使用实体组件系统范式,通常缩写为 ECS。ECS 是一种软件模式,它涉及将您的程序分解为**实体**、**组件**和**系统**。**实体**是分配给**组件**组的唯一“事物”,然后使用**系统**进行处理。

例如,一个实体可能具有PositionVelocity组件,而另一个实体可能具有PositionUI组件。系统是对特定组件类型集运行的逻辑。您可能有一个movement系统,它对所有具有PositionVelocity组件的实体运行。

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

Bevy ECS #

Bevy ECS 是 Bevy 对 ECS 模式的实现。与其他 Rust ECS 实现不同,其他 Rust ECS 实现通常需要复杂的生存期、特征、构建器模式或宏,Bevy ECS 对所有这些概念使用普通的 Rust 数据类型。

  • **组件**:实现Component特征的 Rust 结构体
#[derive(Component)]
struct Position {
    x: f32,
    y: f32,
}
  • **系统**:普通的 Rust 函数
fn print_position_system(query: Query<&Position>) {
    for position in &query {
        println!("position: {} {}", position.x, position.y);
    }
}
  • **实体**:包含唯一整数的简单类型
struct Entity(u64);

现在让我们看看它在实践中是如何工作的!

您的第一个系统 #

将以下函数粘贴到您的main.rs文件中

fn hello_world() {
    println!("hello world!");
}

这将是我们的第一个系统。剩下的唯一步骤是将它添加到我们的App中!

fn main() {
    App::new().add_systems(Update, hello_world).run();
}

add_systems函数将系统添加到您的 App 的UpdateSchedule中,但我们将在后面详细介绍。

现在使用cargo run再次运行您的应用程序。您应该在终端中看到一次打印的hello world!

您的第一个组件 #

问候全世界很棒,但如果我们想问候特定的人呢?在 ECS 中,您通常会将人建模为具有定义他们的组件集的实体。让我们从一个简单的Person组件开始。

将此结构体添加到您的main.rs文件中

#[derive(Component)]
struct Person;

但如果我们想让人们有名字呢?在更传统的模型中,我们可能只会将name: String字段添加到Person中。但是其他实体也可能拥有名字!例如,狗也应该有一个名字。将数据类型分解成小片段来鼓励代码重用通常是有意义的。因此,让我们将Name设为一个独立的组件

#[derive(Component)]
struct Name(String);

然后,我们可以使用“启动系统”将人员添加到我们的World中。启动系统与普通系统非常相似,但它们只运行一次,在所有其他系统之前,就在我们的应用程序启动时运行。让我们使用Commands将一些实体生成到我们的World

fn add_people(mut commands: Commands) {
    commands.spawn((Person, Name("Elaina Proctor".to_string())));
    commands.spawn((Person, Name("Renzo Hume".to_string())));
    commands.spawn((Person, Name("Zayna Nieves".to_string())));
}

现在像这样注册启动系统

fn main() {
    App::new()
        .add_systems(Startup, add_people)
        .add_systems(Update, hello_world)
        .run();
}

您的第一个查询 #

我们现在可以运行它,add_people系统将首先运行,然后是hello_world。但是我们新的人还没有什么事可做!让我们创建一个系统来正确地问候我们World的新公民

fn greet_people(query: Query<&Name, With<Person>>) {
    for name in &query {
        println!("hello {}!", name.0);
    }
}

我们传递给“系统函数”的参数定义了系统运行的数据。在这种情况下,greet_people将在所有具有PersonName组件的实体上运行。

您可以将上面的Query解释为:“遍历所有具有Person组件的实体的每个Name组件”。

现在我们只需要在我们的App中注册该系统。请注意,您可以通过使用元组将多个系统传递给add_systems调用!

fn main() {
    App::new()
        .add_systems(Startup, add_people)
        .add_systems(Update, (hello_world, greet_people))
        .run();
}

运行我们的应用程序将产生以下输出

**快速说明**:“hello world!”可能显示的顺序与下面不同。这是因为系统默认情况下尽可能并行运行。

hello world!
hello Elaina Proctor!
hello Renzo Hume!
hello Zayna Nieves!

太棒了!

您的第一个可变查询 #

如果我们想更改一些人的名字(也许他们结婚了!),例如,我们可以使用可变查询来做到这一点

fn update_people(mut query: Query<&mut Name, With<Person>>) {
    for mut name in &mut query {
        if name.0 == "Elaina Proctor" {
            name.0 = "Elaina Hume".to_string();
            break; // We don't need to change any other names.
        }
    }
}

我们需要使query可变,并使用可变引用(&mut)到我们想要更改的组件。

不要忘记将系统添加到Update计划中

fn main() {
    App::new()
        .add_systems(Startup, add_people)
        .add_systems(Update, (hello_world, (update_people, greet_people).chain()))
        .run();
}

请注意,我们在两个系统上使用了.chain()。这是因为我们希望它们都按代码中列出的顺序运行:update_peoplegreet_people之前发生。如果它们没有,则名称可能会在我们问候人们之后发生变化。

但是我们没有将hello_world系统添加到链中,因为它运行的时机并不重要。这样,Bevy 可以在其他系统运行时并行运行hello_world