理解 ECS

Bevy 是基于 ECS 的游戏引擎。

ECS 即表示 Entity, Component, System。

Componnet 是定义游戏世界各种概念的基本单元,比如代表玩家的 Player, 代表位置的 Position, 代表计时器的 Timer,代表窗口的 Window 等;

Entity 是游戏运行时一组相关 Component 的集合,比如描述一个玩家精灵时,需要 (Player, Position) 两个 Component 来标识;

System 定义游戏运行的规则,即 Entity 间如何交互。

下面通过一个例子来直观理解下:

use bevy::prelude::*;

// Player 是一个 Component
struct Player {
    name: String,
}

// Position 是一个 Component
struct Position {
    pub x: f32,
    pub y: f32,
}

// setup_system 是一个 System
fn setup_system(
    mut commands: Commands
) {
    // 生成一个 Entity
    commands.spawn()
        .insert(Player {name: "player1".to_string()})
        .insert(Position {x: 0.0, y: 0.0});

    // 又生成一个 Entity
    commands.spawn()
        .insert(Player {name: "player2".to_string()})
        .insert(Position {x: 0.0, y: 0.0});
}

// debug_system 是一个 System
fn debug_system(
    mut user_query: Query<(&Player, &mut Position)>,
) {
    println!("debug_system 开始");
    for (player, mut position) in user_query.iter_mut() {
        println!("查询到 {}, 当前位置: {},{}", player.name, position.x, position.y);
        println!("更新 {}", player.name);
        position.x += 1.0;
        position.y += 1.0;
    }
    println!("debug_system 结束\n\n");
}

fn main() {
    App::build()
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup_system.system())
        .add_system(debug_system.system())
        .run();
}

运行结果:

debug_system 开始
查询到 player1, 当前位置: 0,0
更新 player1
查询到 player2, 当前位置: 0,0
更新 player2
debug_system 结束


debug_system 开始
查询到 player1, 当前位置: 1,1
更新 player1
查询到 player2, 当前位置: 1,1
更新 player2
debug_system 结束


debug_system 开始
查询到 player1, 当前位置: 2,2
更新 player1
查询到 player2, 当前位置: 2,2
更新 player2
debug_system 结束


...

在 Bevy 中,Component 可以是任意 struct 或 enum;Entity 可以通过 Commands::spawn 生成;System 则是一些具有特殊参数的函数。

上面的例子中,Player 和 Position 都是 Component,在 setup_system 中,声明 mut commands: Commands 通过 commands 生成两个 Entity,这两个 Entity 都各自包含了 Player 和 Position 两个 Component 实例。

在 debug_system 中,我们声明了 Query 参数,Query 赋予了我们查询符合条件的 Entity 的能力,比如 Query<(&Player, &mut Position) 就表示查询同时包含 Player 和 Position 这两个 Component 的 Entity。

最后,我们使用 add_startup_system 添加 setup_system 是因为 setup_system 只需要运行一次,而通 add_system 添加 debug_system 是因为 debug_system 在游戏运行的每个物理帧都要执行。

简单来说我们可以根据下图理解 ECS:

ecs

假设我们已经有了 Player, Position, Robot 等 Component,然后通过 commands.spawn().insert(Player).insert(Position)commands.spawn().insert(Player).insert(Position).insert(Robot) 生成两个 Entity。

在 System 中,我们通过 Query 可以查询不同的 Entity:

  • Query<&Player> 查询所有带有 Player 的 Entity (entity1,entity2)
  • Query<(&Player, &Robot)> 查询所有同时带有 Player 和 Robot 的 Entity (entity2)
  • Query<&Player, With<Robot>> 查询所有同时带有 Player 和 Robot 的 Entity (entity2),和上面的区别是这个只能读取 Player,不能读取 Robot,相对的查询效率更高效
  • Query<&Player, Without<Robot>> 查询所有不带有 Robot 的 Player 的 Entity (entity1)
  • Query<(&Player, Option<&Robot>)> 查询所有带有 Player 的 Entity,并且尝试获取该 Entity 的 Robot Component (entity1, entity2)

Query 中的 Component 前面加上 & 表示只读,加上 &mut 表示可写。