#ecs #build-script #component #abstraction #entities #systems #zero-ecs

zero_ecs_build

为 ZeroECS:一个使用零成本抽象的实体组件系统(ECS)编写构建脚本

23 个版本

0.2.22 2024年5月13日
0.2.21 2024年4月14日
0.2.18 2024年3月12日
0.1.3-beta.22024年3月5日

#606游戏开发

Download history 329/week @ 2024-04-13 7/week @ 2024-04-20 417/week @ 2024-04-27 9/week @ 2024-05-04 171/week @ 2024-05-11 10/week @ 2024-05-18 1/week @ 2024-05-25 231/week @ 2024-07-27

231 每月下载量

MIT/Apache

63KB
1.5K SLoC

Zero ECS

Zero ECS 是一个实体组件系统,编写时有 4 个目标

  1. 仅使用零成本抽象 - 不使用 dyn 和 Box 等东西 零成本抽象
  2. 不使用 unsafe rust 代码。
  3. 非常用户友好。用户应该尽可能少地编写样板代码。
  4. 非常快

它通过在编译时生成所有代码,结合宏和构建脚本来实现这一点。

说明

创建新项目

cargo new zero_ecs_example
cd zero_ecs_example

添加依赖

cargo add zero_ecs
cargo add zero_ecs_build --build

您的 Cargo.toml 应该看起来像这样

[dependencies]
zero_ecs = "*"

[build-dependencies]
zero_ecs_build = "*"

创建 build.rs

touch build.rs

编辑 build.rs 以调用 zero_ecs 的构建生成代码。

use zero_ecs_build::*;
fn main() {
    generate_ecs("src/main.rs"); // look for components, entities and systems in main.rs
}

这将根据 main.rs 中的组件、实体和系统生成实体组件系统。它接受通配符,因此您可以使用通配符。

use zero_ecs_build::*;
fn main() {
    generate_ecs("src/**/*.rs"); // look in all *.rs files in src. 
}

使用 ECS

包含 ECS

main.rs 中按如下方式包含 ECS

include!(concat!(env!("OUT_DIR"), "/zero_ecs.rs"));

组件

定义一些组件

位置和速度有 x 和 y

#[component]
struct Position(f32, f32);

#[component]
struct Velocity(f32, f32);

在 ECS 中,通常使用组件来“标记”实体,以便在系统中单独识别这些实体。

#[component]
struct EnemyComponent;

#[component]
struct PlayerComponent;

实体

实体是一组组件的集合,它们也可能被称为原型、捆绑或游戏对象。注意,一旦“在” ECS 中。实体只是一个可以复制的 ID。

在我们的示例中,我们定义了一个敌人和一个玩家,他们都有位置和速度,但可以通过他们的“标记”组件来区分。

#[entity]
struct Enemy {
    position: Position,
    velocity: Velocity,
    enemy_component: EnemyComponent,
}

#[entity]
struct Player {
    position: Position,
    velocity: Velocity,
    player_component: PlayerComponent,
}

系统

系统运行应用程序的逻辑。它们可以接受引用、可变引用和查询。

在我们的示例中,我们可以创建一个系统,它简单地打印所有实体的位置

#[system]
fn print_positions(world: &World, query: Query<&Position>) {
    world.with_query(query).iter().for_each(|position| {
        println!("Position: {:?}", position);
    });
}

解释

  • world: &World - 由于系统不修改任何内容,它可以是不可变引用
  • query: Query<&Position> - 我们想查询世界中的所有位置
  • world.with_query(query).iter() - 创建所有位置组件的迭代器

创建实体和调用系统

在我们的 fn main 中,将其改为创建 10 个敌人和 10 个玩家,并添加 systems_main(&world); 来调用所有系统。

fn main() {
    let mut world = World::default();

    for i in 0..10 {
        world.create(Enemy {
            position: Position(i as f32, 5.0),
            velocity: Velocity(0.0, 1.0),
            ..Default::default()
        });

        world.create(Player {
            position: Position(5.0, i as f32),
            velocity: Velocity(1.0, 0.0),
            ..Default::default()
        });
    }

    systems_main(&world);
}

现在运行程序,将打印实体的位置。

更高级

继续我们的示例

修改系统

大多数系统会修改世界状态,并需要额外的资源,如纹理管理器、时间管理器、输入管理器等。一种良好的做法是将它们组合在一个 Resources 结构体中。(但不是必需的)

struct Resources {
    delta_time: f32,
}

#[system]
fn apply_velocity(
    world: &mut World, // world mut be mutable
    resources: &Resources, // we need the delta time
    query: Query<(&mut Position, &Velocity)>, // position should be mutable, velocity not.
) {
    world
        .with_query_mut(query) // we call with_query_mut because it's a mutable query
        .iter_mut() // iterating mutable
        .for_each(|(position, velocity)| {
            position.0 += velocity.0 * resources.delta_time;
            position.1 += velocity.1 * resources.delta_time;
        });
}

我们还必须更改主函数以包含资源调用。

let resources = Resources { delta_time: 0.1 };

systems_main(&resources, &mut world);

销毁实体

假设我们想创建一条规则,如果玩家和敌人之间的距离小于 3 个单位,他们都应该被销毁。这是我们的实现方式

#[system]
fn collide_enemy_and_players(
    world: &mut World, // we are destorying entities so it needs to be mutable
    players: Query<(&Entity, &Position, &PlayerComponent)>, // include the Entity to be able to identify entities
    enemies: Query<(&Entity, &Position, &EnemyComponent)>, // same but for enemies
) {
    let mut entities_to_destroy: Vec<Entity> = vec![]; // we can't (for obvious reasons) destroy entities from within an iteration.

    world
        .with_query(players)
        .iter()
        .for_each(|(player_entity, player_position, _)| {
            world
                .with_query(enemies)
                .iter()
                .for_each(|(enemy_entity, enemy_position, _)| {
                    if (player_position.0 - enemy_position.0).abs() < 3.0
                        && (player_position.1 - enemy_position.1).abs() < 3.0
                    {
                        entities_to_destroy.push(*player_entity);
                        entities_to_destroy.push(*enemy_entity);
                    }
                });
        });

    for entity in entities_to_destroy {
        world.destroy(entity);
    }
}

获取 & At 实体

Get 与 query 相同,但需要一个实体。At 与 query 相同,但需要一个索引。

假设你想要一个跟随玩家的实体。这是你如何实现它的

定义一个伴随组件

#[component]
struct CompanionComponent {
    target_entity: Option<Entity>,
}

定义伴随实体。它有一个位置和一个伴随组件

#[entity]
struct Companion {
    position: Position,
    companion_component: CompanionComponent,
}

现在我们需要编写伴随系统。对于每个伴随,我们需要检查它是否有目标。如果有目标,我们需要检查目标是否存在(它可能已被删除)。如果目标存在,我们获取目标的 位置值 并使用该值设置伴随的位置。

我们需要查询伴随及其位置,作为可变查询。我们还需要查询具有位置的每个实体。这意味着伴随理论上可以跟随它自己。

#[system]
fn companion_follow(
    world: &mut World,
    companions: Query<(&mut Position, &CompanionComponent)>,
    positions: Query<&Position>,
) {

实现:我们不能简单地遍历伴随,获取目标位置并更新位置,因为我们只能有一个可变借用(除非我们使用 unsafe 代码)。

我们可以像销毁实体那样做,但这会很慢。

解决方案是使用索引迭代,只短暂借用所需的内容

#[system]
fn companion_follow(
    world: &mut World,
    companions: Query<(&mut Position, &CompanionComponent)>,
    positions: Query<&Position>,
) {
    for companion_idx in 0..world.with_query_mut(companions).len() {
        // iterate the count of companions
        if let Some(target_position) = world
            .with_query_mut(companions)
            .at_mut(companion_idx) // get the companion at index companion_idx
            .and_then(|(_, companion)| companion.target_entity) // then get the target entity, if it is not none
            .and_then(|companion_target_entity| {
                // then get the VALUE of target position (meaning we don't use a reference to the position)
                world
                    .with_query(positions)
                    .get(companion_target_entity) // get the position for the companion_target_entity
                    .map(|p| (p.0, p.1)) // map to get the VALUE
            })
        {
            if let Some((companion_position, _)) =
                world.with_query_mut(companions).at_mut(companion_idx)
            // Then simply get the companion position
            {
                // and update it to the target's position
                companion_position.0 = target_position.0;
                companion_position.1 = target_position.1;
            }
        }
    }
}

待办事项

  • 重新使用已删除实体的 ID

依赖关系

~0.7–1.2MB
~27K SLoC