Crates.io | moecs |
lib.rs | moecs |
version | 0.1.0 |
source | src |
created_at | 2024-01-20 16:50:21.668391 |
updated_at | 2024-01-20 16:50:21.668391 |
description | Micro ECS engine. A small and lightweight ECS engine for Rust projects. |
homepage | |
repository | https://github.com/mccloskeybr/moecs |
max_upload_size | |
id | 1106609 |
size | 46,650 |
moecs
(micro ECS) is a small
ECS
library written in Rust.
Built to be used with lightweight Rust-based game engines, like ggez.
See example implementations here.
Arc<RwLock>
, so parallelism can
easily be achieved within a System
as well.Component
s are highly configurable bundles of data that can be arbitrarily
grouped together to form an Entity. For example, a Dog Entity may be
comprised of a position component (where it is in the world), and a state
component (what it's getting up to). In moecs
, this would be implemented like
so:
#[derive(Component)]
struct PositionComponent {
x: f32,
y: f32,
}
#[derive(Component)]
struct DogStateComponent {
state: DogState,
}
enum DogState {
Sleeping,
Playing,
Barking,
}
Note that the #[derive(Component)]
attribute must be defined for each
Component
.
Components can be easily bundled together using a ComponentBundle
(this is most useful when creating a new Entity, as explored later). Using our
Dog example, we can group the PositionComponent
and DogStateComponent
s
using the following:
let bundle = ComponentBundle::new()
.add_component(PositionComponent {
x: 0,
y: 0,
})
.add_component(DogStateComponent {
state: Sleeping,
});
As discussed in the Component section, Entities are simply bundles of relevant
Components. Operations on Entities are done via the EntityManager
.
Note: The EntityManager
is passed directly to defined System
s. Therefore,
all Entity-related mutations can only occur in a System
.
Responsibilities of the EntityManager
include:
Entities are created by providing a ComponentBundle
of initial Component
s
to be associated with that Entity (Note: an empty ComponentBundle
may be
provided). A u32
will be returned denoting that Entity's unique identifier,
which can be used to retrieve that Entity's Component
s later.
Note: A strict requirement is that an Entity can only have one Component of
a given type registered at a given time. moecs
will panic
if this rule is
broken.
entity_manager.create_entity(
ComponentBundle::new()
.add_component(PositionComponent { x: 0, y: 0 })
);
Given some Entity id, that Entity can be removed wholesale (incl. deleting all
relevant Component
s) via:
entity_manager.delete_entity(entity_id);
Additional Component
s can be added to an existing Entity via:
entity_manager.add_components_to_entity(
&entity_id,
ComponentBundle::new()
.add_component(VelocityComponent { x_vel: 0, y_vel: 0 })
);
Note: The above stated rule that an Entity may only have one Component of a
given type still applies here. moecs
will panic
if this rule is broken.
Similarly, Components can be removed (deleted) from an existing Entity via:
entity_manager.remove_component_from_entity::<PositionComponent>(&entity_id);
Querying is done using the Query
struct, which has 2 mechanisms of specifying
filter criteria: with
, without
. These are used to iterate over the list of
all registered Entities in order to filter out Entities with a certain
Component
, or similarly without other components as applicable.
Query results are returned via a QueryResult
struct, which includes the
Entity id of the filtered Entity, as well as the relevant Component
s.
Note: Query results are automatically cached. Additionally, query processing is performed in parallel (across registered Entities) to improve efficiency.
Example (simplified) flow:
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
s are where Component
s belonging to certain Entities change and
interact with each other. In other words, System
s contain the logic of your
program. For example, a rudimentary PhysicsSystem
could be implemented like
so:
#[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;
});
}
}
A couple things to note:
System
s must use the #[derive(System)]
attribute.System
s must similarly implement the System
trait, which
essentially means implementing the execute
fn
. This gives you access to
the EntityManager
(discussed above), as well as the SystemParamAccessor
(discussed below).It's often adventageous to pass data from outside the moecs
ecosystem in (for
example, an input handler, or a rendering canvas). These can be passed via.
a SystemParam
. An example definition:
#[derive(SystemParam)]
struct CanvasParam<'a> {
canvas: &'a mut Canvas,
}
SystemParam
s are collected and accessed by a SystemParamAccessor
, which
essentially just provides a convenient means of looking up a SystemParam
from within the System
. For example:
#[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.
}
}
A SystemGroup
is a user-defined grouping of like-System
s. Practically,
a System
can only be interacted with via. a SystemGroup
.
System
s in a SystemGroup
can be registered to execute in sequence or in
parallel depending on configuration by the user. For example:
let group_1 = SystemGroup::new_sequential_group()
.register::<PhysicsSystem>()
.register::<CollisionSystem>();
let group_2 = SystemGroup::new_parallel_group().register::<RenderSystem>();
Parallelism here is horizontal. That is, the System
s themselves are run in
parallel with each other. Parallelism within a System
is done separatetely
(manually).
The Engine
is how moecs
is interacted with by some outside process (i.e.
some central game loop). It has the following responsibilities:
SystemGroup
s.SystemGroup
.The general flow is as follows:
System
s of similar type using
SystemGroup
s (discussed above).SystemGroup
s in some sequential
manner.A basic example:
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());
}
}