12 个版本 (7 个破坏性更新)

0.9.1 2023 年 8 月 10 日
0.9.0 2023 年 4 月 22 日
0.8.2 2023 年 4 月 3 日
0.7.0 2023 年 3 月 28 日
0.3.0 2022 年 10 月 29 日

#49 in 游戏开发

Download history 14/week @ 2024-07-01 68/week @ 2024-07-22

每月 82 次下载

MIT/Apache

1MB
21K SLoC

brood

GitHub Workflow Status codecov.io crates.io docs.rs MSRV License

快速灵活的 实体组件系统 库。

brood 从头开始构建,旨在使用户感觉舒适,同时速度尽可能快,甚至可能比其他流行的实体组件系统(通常缩写为 ECS)库更快。 brood 使用异构列表来允许任意数量的组件集合,这意味着您的实体大小或系统视图范围没有限制。 所有您期望从标准 ECS 库中获得的功能都存在,包括与 serderayon 库的互操作性,分别用于序列化和并行处理。

主要功能

  • 由任意数量的组件组成的实体。
  • 内置对 serde 的支持,提供轻松的 World 容器的序列化和反序列化。
  • 使用 rayon 实现的内部和外部并行。
  • 最小化样板代码。
  • no_std 兼容。

用法

使用 brood 有两个主要方面:存储实体和操作实体。

存储实体

在存储实体之前,应建立以下定义

  • 组件:单个数据块。在本文库中,它是指实现了 Any 特质的任何类型。
  • 实体:一组组件。这些是通过 entity!() 宏定义的。
  • 世界:实体的容器。

通过简单地定义其类型来定义组件。例如,以下 struct 是组件

struct Position {
    x: f32,
    y: f32,
}

struct Velocity {
    x: f32,
    y: f32,
}

为了在 World 容器中使用这些组件,它们需要被包含在一个 Registry 中,并在创建时提供给 World。可以使用 Registry!() 宏来创建一个 Registry

use brood::Registry;

type Registry = Registry!(Position, Velocity);

然后可以使用这个 Registry 创建一个 World,并将实体存储在其中。

use brood::{entity, World};

let mut world = World::<Registry>::new();

// Store an entity inside the newly created World.
let position = Position {
    x: 3.5,
    y: 6.2,
};
let velocity = Velocity {
    x: 1.0,
    y: 2.5,
};
world.insert(entity!(position, velocity));

请注意,上面存储在 world 中的实体可以由 Registry 的任何子集组成,并且可以以任何顺序提供。

操作实体

要操作存储在 World 中的实体,必须使用 SystemSystem 被定义为操作包含指定组件集的任何实体,读取和修改这些组件。以下是如何定义和运行一个示例系统的示例

use brood::{query::{filter, result, Views}, registry, system::System};

struct UpdatePosition;

impl System for UpdatePosition {
    type Filter: filter::None;
    type Views<'a>: Views!(&'a mut Position, &'a Velocity);
    type ResourceViews: Views!();
    type EntryViews: Views!();

    fn run<'a, R, S, I, E>(
        &mut self,
        query_results: Result<R, S, I, Self::ResourceViews<'a>, Self::EntryViews<'a>, E>,
    ) where
        R: registry::Registry,
        I: Iterator<Item = Self::Views<'a>>,
    {
        for result!(position, velocity) in query_results.iter {
            position.x += velocity.x;
            position.y += velocity.y;
        }
    }
}

world.run_system(&mut UpdatePosition);

此系统将操作包含 PositionVelocity 组件的每个实体(无论它们可能包含哪些其他组件),使用 Velocity 组件中的值就地更新 Position 组件。

对于更复杂的 System,有许多选项,包括可选组件、自定义过滤器以及后处理逻辑。请参阅文档以获取更多信息。

序列化/反序列化

brood 提供了对使用 serde 的序列化和反序列化的第一级支持。通过启用 serde 软件包功能,可以使用 serdeSerializerDeserializerWorld 容器和它们包含的实体进行序列化和反序列化。请注意,只要 WorldRegistry 中的每个组件都是可序列化和反序列化的,World 就是可序列化和反序列化的。

例如,可以将 World 序列化为 bincode(并从同一文件反序列化)如下所示

use brood::{entity, Registry, World};

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

#[derive(Deserialize, Serialize)]
struct Velocity {
    x: f32,
    y: f32,
}

type Registry = Registry!(Position, Velocity);

let mut world = World::<Registry>::new();

// Insert several entities made of different components.
world.insert(entity!(Position {
    x: 1.0,
    y: 1.1,    
});
world.insert(entity!(Velocity {
    x: 0.0,
    y: 5.0,
});
world.insert(entity!(Position {
    x: 4.2,
    y: 0.1,
}, Velocity {
    x: 1.1,
    y: 0.4,
});

let encoded = bincode::serialize(&world).unwrap();

let decoded_world = bincode::deserialize(&encoded).unwrap();

请注意,根据序列化器和解序列化器是否是 可读的,存在两种序列化模式。可读的序列化将按行序列化实体,这较慢但更容易被人阅读。不可读的序列化将按列序列化实体,这要快得多,但手动阅读要困难得多。

并行处理

brood 通过 rayon 支持并行处理。通过启用 rayon 软件包功能,可以并行化对 World 的操作。

并行操作实体

为了并行化系统对实体的操作(通常称为内部并行化),可以使用 ParSystem 而不是标准的 System。这将允许将 ParSystem 的操作分散到多个 CPU 上。例如,可以定义如下 ParSystem

use brood::{entity, query::{filter, result, Views}, Registry, registry, World, system::ParSystem};
use rayon::iter::ParallelIterator;

struct Position {
    x: f32,
    y: f32,
}

struct Velocity {
    x: f32,
    y: f32,
}

type Registry = Registry!(Position, Velocity);

let mut world = World::<Registry>::new();

// Store an entity inside the newly created World.
let position = Position {
    x: 3.5,
    y: 6.2,
};
let velocity = Velocity {
    x: 1.0,
    y: 2.5,
};
world.insert(entity!(position, velocity));

struct UpdatePosition;

impl ParSystem for UpdatePosition {
    type Filter: filter::None;
    type Views<'a>: Views!(&'a mut Position, &'a Velocity);
    type ResourceViews: Views!();
    type EntryViews: Views!();

    fn run<'a, R, S, I, E>(
        &mut self,
        query_results: Result<R, S, I, Self::ResourceViews<'a>, Self::EntryViews<'a>, E>,
    ) where
        R: registry::Registry,
        I: ParallelIterator<Item = Self::Views<'a>>,
    {
        query_results.iter.for_each(|result!(position, velocity)| {
            position.x += velocity.x;
            position.y += velocity.y;
        });
    }
}

world.run_par_system(&mut UpdatePosition);

定义 ParSystem 与定义 System 非常相似。请参阅文档以获取更多定义选项。

并行运行系统

通过定义一个 Schedule,可以并行运行多个 SystemParSystem。一个 Schedule 将自动将 System 划分为阶段,每个阶段可以同时运行。这些阶段旨在确保它们不会违反 Rust 的借用和可变性规则,并且完全安全使用。

定义并运行一个包含多个 SystemSchedule,如下所示

use brood::{entity, query::{filter, result, Views}, Registry, registry, World, system::{schedule, schedule::task, System}};

struct Position {
    x: f32,
    y: f32,
}

struct Velocity {
    x: f32,
    y: f32,
}

struct IsMoving(bool);

type Registry = Registry!(Position, Velocity, IsMoving);

let mut world = World::<Registry>::new();

// Store an entity inside the newly created World.
let position = Position {
    x: 3.5,
    y: 6.2,
};
let velocity = Velocity {
    x: 1.0,
    y: 2.5,
};
world.insert(entity!(position, velocity, IsMoving(false)));

struct UpdatePosition;

impl System for UpdatePosition {
    type Filter: filter::None;
    type Views<'a>: Views!(&'a mut Position, &'a Velocity);
    type ResourceViews: Views!();
    type EntryViews: Views!();

    fn run<'a, R, S, I, E>(
        &mut self,
        query_results: Result<R, S, I, Self::ResourceViews<'a>, Self::EntryViews<'a>, E>,
    ) where
        R: registry::Registry,
        I: Iterator<Item = Self::Views<'a>>,
    {
        for result!(position, velocity) in query_results.iter {
            position.x += velocity.x;
            position.y += velocity.y;
        }
    }
}

struct UpdateIsMoving;

impl System for UpdateIsMoving {
    type Filter: filter::None;
    type Views<'a>: Views!(&'a Velocity, &'a mut IsMoving);
    type ResourceViews: Views!();
    type EntryViews: Views!();

    fn run<'a, R, S, I, E>(
        &mut self,
        query_results: Result<R, S, I, Self::ResourceViews<'a>, Self::EntryViews<'a>, E>,
    ) where
        R: registry::Registry,
        I: Iterator<Item = Self::Views<'a>>,
    {
        for result!(velocity, is_moving) in query_results.iter {
            is_moving.0 = velocity.x != 0.0 || velocity.y != 0.0;
        }
    }
}

let mut schedule = schedule!(task::System(UpdatePosition), task::System(UpdateIsMoving));

world.run_schedule(&mut schedule);

请注意,阶段是由每个 SystemViews 决定的。那些 Views 不包含组件的冲突可变借用 System 被分组到单个阶段中。

最低支持的 Rust 版本

本软件包保证在稳定版本的 rustc 1.65.0 及更高版本上编译。

许可证

本项目许可协议为以下之一

任选其一。

贡献

除非您明确说明,否则根据 Apache-2.0 许可证定义的,您有意提交的任何贡献,包括在本工作中使用的内容,都将按上述方式双重许可,不附加任何额外条款或条件。

依赖关系

~0.5–1.1MB
~19K SLoC