#ecs #micro #projects #parallelism #parallel #cache #query

moecs

微ECS引擎。适用于Rust项目的轻量级ECS引擎。

1个不稳定版本

0.1.0 2024年1月20日

#470游戏开发

MIT 许可证

46KB
855

moecs

Build Status License

moecs微ECS)是用Rust编写的轻量级ECS库。

专为与轻量级基于Rust的游戏引擎配合使用而构建,如ggez

请参阅示例实现此处

功能

  • 简单用户界面API。
  • 实体查询缓存,以实现高效的重复查找。
  • 可配置的并行性(由rayon提供)
    • 实体查询并行执行(当未缓存时)。
    • 系统执行可以配置为并行运行。
    • 系统参数被包裹在Arc<RwLock>中,因此可以在System中轻松实现并行性。

文档

组件

组件

Component是高度可配置的数据包,可以将任意组合在一起以形成一个实体。例如,一个狗实体可能由位置组件(它在世界中的位置)和状态组件(它正在做什么)组成。在moecs中,这将像这样实现

#[derive(Component)]
struct PositionComponent {
    x: f32,
    y: f32,
}

#[derive(Component)]
struct DogStateComponent {
    state: DogState,
}
enum DogState {
    Sleeping,
    Playing,
    Barking,
}

请注意,必须为每个Component定义#[derive(Component)]属性。

组件包

可以使用ComponentBundle轻松地将组件打包在一起(这在创建新实体时非常有用,稍后将要讨论)。使用我们的狗示例,我们可以使用以下方法将PositionComponentDogStateComponent分组

let bundle = ComponentBundle::new()
    .add_component(PositionComponent {
        x: 0,
        y: 0,
    })
    .add_component(DogStateComponent {
        state: Sleeping,
    });

实体

实体管理器

如组件部分所述,实体仅仅是相关组件的集合。通过EntityManager执行实体操作。

注意:将EntityManager直接传递给定义的System。因此,所有与实体相关的更改都只能在System中发生。

EntityManager的职责包括

  • 创建新实体。

通过提供要关联到该实体的初始 ComponentComponentBundle 来创建实体(注意:可以提供空的 ComponentBundle)。将返回一个 u32,表示实体的唯一标识符,可以用它来稍后检索该实体的 Component

注意:严格的要求是,实体在任何给定时间只能注册一个特定类型的组件。如果违反此规则,moecs 将引发恐慌。

entity_manager.create_entity(
    ComponentBundle::new()
        .add_component(PositionComponent { x: 0, y: 0 })
);
  • 删除现有实体。

给定某个实体 ID,可以通过以下方式完全删除该实体(包括删除所有相关 Component):

entity_manager.delete_entity(entity_id);
  • 向现有实体添加组件。

可以通过以下方式向现有实体添加额外的 Component

entity_manager.add_components_to_entity(
    &entity_id,
    ComponentBundle::new()
        .add_component(VelocityComponent { x_vel: 0, y_vel: 0 })
);

注意:上述规则仍适用,即实体只能有一个特定类型的组件。如果违反此规则,moecs 将引发恐慌。

  • 从现有实体中删除组件。

同样,可以通过以下方式从现有实体中删除(删除)组件:

entity_manager.remove_component_from_entity::<PositionComponent>(&entity_id);
  • 查询具有(或不具有)指定组件的实体。

查询是通过使用 Query 结构来完成的,该结构具有两种指定过滤条件的方式:withwithout。这些用于遍历所有已注册实体的列表,以过滤出具有特定 Component 的实体,或者相应地过滤出没有其他组件的实体。

查询结果通过 QueryResult 结构返回,该结构包括过滤实体的实体 ID 以及相关的 Component

注意:查询结果会自动缓存。此外,查询处理是并行执行的(跨注册实体),以提高效率。

示例(简化)流程

entity_manager
    .filter(
        Query::new()
            .with::<SomeComponent>()
            .without::<SomeOtherComponent>(),
    )
    .iter()
    .for_each(|result: QueryResult| {
        let component = result.get_component::<SomeComponent>();
        println!(
            "Entity: {} has component {:?}.",
            result.entity_id(),
            component
        );
    });

系统

系统

System 是属于某些实体的 Component 发生变化并与彼此交互的地方。换句话说,System 包含了程序的逻辑。例如,一个基本的 PhysicsSystem 可以这样实现:

#[derive(System)]
struct PhysicsSystem;
impl System for PhysicsSystem {
    fn execute(entity_manager: Arc<RwLock<EntityManager>>, params: Arc<SystemParamAccessor>) {
        entity_manager
            .read()
            .unwrap()
            .filter(
                Query::new()
                    .with::<PositionComponent>()
                    .with::<VelocityComponent>(),
            )
            .iter()
            .for_each(|result| {
                let entity_id = result.entity_id();
                let position = result.get_component::<PositionComponent>().unwrap();
                let velocity = result.get_component::<VelocityComponent>().unwrap();

                position.write().unwrap().x += velocity.read().unwrap().x_vel;
                position.write().unwrap().y += velocity.read().unwrap().y_vel;
            });
    }
}

以下几点需要注意

  • 所有 System 必须使用 #[derive(System)] 属性。
  • 所有 System 必须类似地实现 System 特性,这基本上意味着实现 execute fn。这使您能够访问 EntityManager(如上所述),以及 SystemParamAccessor(如下所述)。

系统参数

将数据从 moecs 生态系统外部传递进来是有益的(例如,一个输入处理程序或渲染画布)。这些可以通过 SystemParam 来传递。以下是一个示例定义:

#[derive(SystemParam)]
struct CanvasParam<'a> {
    canvas: &'a mut Canvas,
}

SystemParam 通过 SystemParamAccessor 收集和访问,它基本上只是提供了一个方便的从 System 中查找 SystemParam 的方式。例如:

#[derive(System)]
struct RenderSystem;
impl System for RenderSystem {
    fn execute(entity_manager: Arc<RwLock<EntityManager>>, params: Arc<SystemParamAccessor>) {
        let canvas_param = params.get_param::<CanvasParam>().unwrap();
        let canvas_param = &mut canvas_param.write().unwrap();
        let canvas = &mut canvas_param.canvas;

        // etc.
    }
}

系统组

SystemGroup 是用户定义的类似 System 的分组。实际上,只能通过 SystemGroupSystem 交互。

SystemGroup 中的 System 可以根据用户的配置注册为顺序执行或并行执行。例如:

let group_1 = SystemGroup::new_sequential_group()
    .register::<PhysicsSystem>()
    .register::<CollisionSystem>();
let group_2 = SystemGroup::new_parallel_group().register::<RenderSystem>();

这里的并行是水平的。也就是说,System 本身是彼此并行运行的。一个 System 内部的并行是分开的(手动)进行的。

引擎

Engine 是外部进程(即某些中央游戏循环)如何与 moecs 交互的方式。它有以下职责:

  • 注册/注销 SystemGroup
  • 执行已注册的 SystemGroup

一般的流程如下:

  • 在程序启动时,使用 SystemGroup(如上所述)定义并分组相似类型的 System
  • 然后,在主循环中,以某种顺序执行 SystemGroup

一个基本示例

fn main() {
    let mut engine = Engine::new();

    let update_systems = engine.register_system_group(
        SystemGroup::new_sequential_group()
            .register::<PhysicsSystem>(),
    );
    let render_systems = engine.register_system_group(
        SystemGroup::new_sequential_group()
            .register::<DrawShapeSystem>(),
    );

    loop {
        engine.execute_group(update_systems, SystemParamAccessor::new());
        engine.execute_group(render_systems, SystemParamAccessor::new());
    }
}

依赖项

~1.4–2MB
~42K SLoC