ECS

Bevy中的所有应用程序逻辑都使用实体(Entity)组件(Component)系统(System)范式, 通常简称为ECS.
ECS是一种软件模式, 包括将程序分解为实体(Entity), 组件(Component)系统(System).
实体(Entity) 是指向一群 组件(Component) 的唯一 事物(things), 然后使用 系统(System) 处理其过程.

例如, 一个实体可能有位置(Position)速度(Velocity)组件, 而另一个实体可能有位置(Position)UI组件. 系统是在一组特定组件上运行的逻辑. 你可能有一个运行在所有带有位置(Position)速度(Velocity)组件的实体上的移动系统.

ECS模式鼓励清晰, (解耦)分离的设计, 强制使你将数据和逻辑分解为核心组件. 通过优化内存访问模式并简化并行性, 有助于使你的代码更快.

Bevy ECS

Bevy ECS是Bevy对ECS模式的实现. 不像其他需要复杂的生存期, 特征, 构建器模式或者宏的Rust ECS的实现, Bevy ECS对所有这些概念使用常规的Rust数据类型:

  • 组件(Components): 普通的Rust结构体
    struct Position { x: f32, y: f32 }
    
  • 系统(Systems): 普通的Rust函数
    fn print_position_system(query: Query<&Transform>) {
        for transform in query.iter() {
            println!("position: {:?}", transform.translation);
        }
    }
    
  • 实体(Entities): 包含唯一整数的简单类型
    struct Entity(u64);
    

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

你的第一个系统(System)

拷贝下面的函数到你的main.rs文件中:

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

这将是我们第一个系统. 剩下的步骤就是把它加到我们的App中!

fn main() {
    App::build()
        .add_system(hello_world.system())
        .run();
}

注意hello_world.system()函数的调用. 这是一种特征扩展方法, 可将hello_world函数转变成System 类型.

调用add_system() 方法把系统加到App的Schedule , 这个我们稍后再介绍.

现在我们再一次执行cargo run. 你将在命令行中看见hello world!的输出.

你的第一个组件(Components)

能向整个世界打招呼当然很好, 但是如果我们只想问候特定的人呢? 在ESC中, 通常会将人建模成实体, 并使用一些组件来定义它们. 让我们从一个Person的组件开始.

将这个结构体添加到main.rs:

struct Person;

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

struct Name(String);

我们可以通过startup systemPeople加到我们的世界(World ).
startup system就像普通的系统(System)一样, 但它们只运行一次, 而且是在其他系统之前被调用.
当我们的应用启动时, 让我们通过Commands 生产大量的实体到我们的世界(World )中.

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

像这样注册启动系统(startup system):

fn main() {
    App::build()
        .add_startup_system(add_people.system())
        .add_system(hello_world.system())
        .run();
}

我们现在可以运行这个应用, add_people系统将会最先执行, 然后是hello_world. 但是我们新的people仍然无事可做! 让我们创建一个系统, 让我们的新公民向我们的世界(World )打招呼:

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

我们传递给system function的参数定了系统运行的数据. 在本例中, greet_people将在所有带有PersonName组件的实体上运行.

你可以将上面的查询解释为: "遍历每一个拥有Person组件的Name组件"

在我们的App中注册它:

fn main() {
    App::build()
        .add_startup_system(add_people.system())
        .add_system(hello_world.system())
        .add_system(greet_people.system())
        .run();
}

现在我们运行我们的App, 将会得到以下结果:

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

棒棒哒!

备注: hello world!可能出现的顺序和上面显示的不同. 这是因为系统在没有共享依赖关系的情况下, 默认是并行的.