Skip to content

Commit

Permalink
0.2 (#8)
Browse files Browse the repository at this point in the history
0.2.0
* Adds support for archetypes with `Vec`-like dynamic storage capacity. This can be used by setting the keyword `dyn` as the capacity value in the `ecs_archetype!` declaration. Worlds generated by `ecs_world!` now have a `with_capacity` function with arguments for each dynamic archetype to pre-allocate storage. 
    * Fixed-capacity archetypes won't have arguments in `with_capacity` -- they are always allocated to their full fixed capacity on world creation.
    * Resolves #3 
* Adds an `Entity<_>` pseudo-parameter to queries. This will match any archetype like `EntityAny`, but will yield a typed `Entity<A>` for the currently-matched archetype in the query body.
* Adds a new `32_components` crate feature to go up to 32 maximum components in a single archetype. Enabling this feature may impact compile times.
  • Loading branch information
recatek authored Jun 10, 2023
1 parent 3f1516a commit ac95267
Show file tree
Hide file tree
Showing 21 changed files with 1,779 additions and 378 deletions.
37 changes: 15 additions & 22 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gecs"
version = "0.1.2"
version = "0.2.0"
authors = ["recatek"]
description = "A generated entity component system."
edition = "2021"
Expand All @@ -10,7 +10,11 @@ readme = "README.md"
keywords = ["ecs", "entity"]
categories = ["data-structures", "game-engines"]

[features]
default = []
32_components = []

[dependencies]
gecs_macros = { version = "0.1.2", path = "macros" }
gecs_macros = { version = "0.2.0", path = "macros" }

paste = { version = "1.0" }
seq-macro = { version = "0.3" } # For building "variadic" storage
70 changes: 62 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@ or execution. However, this comes at the cost of requiring all archetypes to be
and declared at compile-time, so that adding or removing components from entities at
runtime isn't currently possible -- hybrid approaches could solve this in the future.

Archetypes in gecs can be set to contain a fixed capacity of entities. If all of the
archetypes in your ECS world declaration are configured in this way, gecs will perform
zero allocations after startup. This guarantees that your ECS world will adhere to a
known and predictable memory overhead for constrained environments (e.g. servers on
cloud instances). Attempting to add an entity to a full archetype can either report
failure or panic depending on the method you call to do so. Support for dynamically-
sized archetypes with `Vec`-like storage behavior is planned for support at a later
date but is not currently implemented.
Archetypes in gecs can be set to contain a fixed or dynamic capacity of entities. If
all of the archetypes in your ECS world declaration are set to a fixed capacity, gecs
will perform zero allocations after startup. This guarantees that your ECS world will
adhere to a known and predictable memory overhead for constrained environments (e.g.
servers on cloud instances). Attempting to add an entity to a full archetype can
either report failure or panic depending on the method you call to do so.

The goals for gecs are (in descending priority order):
- Fast iteration and find queries
Expand All @@ -35,6 +33,62 @@ use `#[deny(unsafe_code)]` in their own crates. Note that gecs does use unsafe c
internally to allow for compiler optimizations around known invariants. It is not a
goal of this library to be written entirely in safe Rust.

# Getting Started

See the `ecs_world!`, `ecs_find!`, and `ecs_iter!` macros for more information.
The following example creates a world with three components and two archetypes:

```rust
use gecs::prelude::*;

// Components -- these must be pub because the world is exported as pub as well.
pub struct CompA(pub u32);
pub struct CompB(pub u32);
pub struct CompC(pub u32);

ecs_world! {
// Declare two archetypes, ArchFoo and ArchBar.
ecs_archetype!(ArchFoo, 100, CompA, CompB); // Fixed capacity of 100 entities.
ecs_archetype!(ArchBar, dyn, CompA, CompC); // Dynamic (dyn) entity capacity.
}

fn main() {
let mut world = World::default(); // Initialize an empty new ECS world.

// Add entities to the world by pushing their components and receiving a handle.
let entity_a = world.push::<ArchFoo>((CompA(1), CompB(20)));
let entity_b = world.push::<ArchBar>((CompA(3), CompC(40)));

// Each archetype now has one entity.
assert_eq!(world.len::<ArchFoo>(), 1);
assert_eq!(world.len::<ArchBar>(), 1);

// Look up each entity and check its CompB or CompC value.
assert!(ecs_find!(world, entity_a, |c: &CompB| assert_eq!(c.0, 20)));
assert!(ecs_find!(world, entity_b, |c: &CompC| assert_eq!(c.0, 40)));

// Add to entity_a's CompA value.
ecs_find!(world, entity_a, |c: &mut CompA| { c.0 += 1; });

// Sum both entities' CompA values with one iter despite being different archetypes.
let mut sum = 0;
ecs_iter!(world, |c: &CompA| { sum += c.0 });
assert_eq!(sum, 5); // Adding 2 + 3 -- recall that we added 1 to entity_a's CompA.

// Collect both entities that have a CompA component.
let mut found = Vec::new();
ecs_iter!(world, |entity: &EntityAny, _: &CompA| { found.push(*entity); });
assert!(found == vec![entity_a.into(), entity_b.into()]);

// Remove both entities -- this will return an Option containing their components.
assert!(world.remove(entity_a).is_some());
assert!(world.remove(entity_b).is_some());

// Try to look up a stale entity handle -- this will return false.
assert_eq!(ecs_find!(world, entity_a, |_: &Entity<ArchFoo>| { panic!() }), false);
}
```

License
-------

Expand Down
3 changes: 1 addition & 2 deletions macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "gecs_macros"
version = "0.1.2"
version = "0.2.0"
authors = ["recatek"]
description = "Procedural macros for the gecs crate."
edition = "2021"
Expand All @@ -12,7 +12,6 @@ proc-macro = true

[dependencies]
convert_case = { version = "0.6" }
lazy_static = { version = "1.4" }
proc-macro2 = { version = "1.0" }
quote = { version = "1.0" }
syn = { version = "2.0", features = ["full", "extra-traits"] }
Expand Down
4 changes: 2 additions & 2 deletions macros/src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub struct DataArchetypeBuildOnly {

#[derive(Debug)]
pub enum DataCapacity {
Expression(Expr),
Fixed(Expr),
Dynamic,
}

Expand Down Expand Up @@ -128,7 +128,7 @@ impl DataArchetype {

fn convert_capacity(capacity: ParseCapacity) -> DataCapacity {
match capacity {
ParseCapacity::Expression(expr) => DataCapacity::Expression(expr),
ParseCapacity::Fixed(expr) => DataCapacity::Fixed(expr),
ParseCapacity::Dynamic => DataCapacity::Dynamic,
}
}
Expand Down
19 changes: 16 additions & 3 deletions macros/src/generate/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ pub fn generate_query_find(mode: FetchMode, query: ParseQueryFind) -> syn::Resul
if let Some(bound_params) = bound_params.get(&archetype.name) {
// Types and traits
let Archetype = format_ident!("{}", archetype.name);
let Type = bound_params.iter().map(to_type).collect::<Vec<_>>(); // Bind-dependent!
let Type = bound_params
.iter()
.map(|p| to_type(p, &archetype))
.collect::<Vec<_>>(); // Bind-dependent!

// Variables and fields
let slice = bound_params.iter().map(to_slice).collect::<Vec<_>>(); // Bind-dependent!
Expand Down Expand Up @@ -122,7 +125,10 @@ pub fn generate_query_iter(mode: FetchMode, query: ParseQueryIter) -> syn::Resul
if let Some(bound_params) = bound_params.get(&archetype.name) {
// Types and traits
let Archetype = format_ident!("{}", archetype.name);
let Type = bound_params.iter().map(to_type).collect::<Vec<_>>(); // Bind-dependent!
let Type = bound_params
.iter()
.map(|p| to_type(p, &archetype))
.collect::<Vec<_>>(); // Bind-dependent!

// Variables and fields
let slice = bound_params.iter().map(to_slice).collect::<Vec<_>>(); // Bind-dependent!
Expand Down Expand Up @@ -177,11 +183,13 @@ fn to_name(param: &ParseQueryParam) -> TokenStream {
quote!(#name)
}

fn to_type(param: &ParseQueryParam) -> TokenStream {
fn to_type(param: &ParseQueryParam, archetype: &DataArchetype) -> TokenStream {
let archetype_name = format_ident!("{}", archetype.name);
match &param.param_type {
ParseQueryParamType::Component(ident) => quote!(#ident),
ParseQueryParamType::Entity(ident) => quote!(Entity<#ident>),
ParseQueryParamType::EntityAny => quote!(EntityAny),
ParseQueryParamType::EntityWild => quote!(Entity<#archetype_name>),
ParseQueryParamType::OneOf(_) => panic!("must unpack OneOf first"),
}
}
Expand All @@ -191,6 +199,7 @@ fn to_slice(param: &ParseQueryParam) -> TokenStream {
ParseQueryParamType::Component(ident) => to_token_stream(&to_snake_ident(ident)),
ParseQueryParamType::Entity(_) => quote!(entities),
ParseQueryParamType::EntityAny => quote!(entities),
ParseQueryParamType::EntityWild => quote!(entities),
ParseQueryParamType::OneOf(_) => panic!("must unpack OneOf first"),
}
}
Expand All @@ -201,6 +210,7 @@ fn to_borrow(param: &ParseQueryParam) -> TokenStream {
(true, ParseQueryParamType::Component(ident)) => quote!(borrow_slice_mut::<#ident>()),
(_, ParseQueryParamType::Entity(_)) => quote!(get_slice_entities()),
(_, ParseQueryParamType::EntityAny) => quote!(get_slice_entities()),
(_, ParseQueryParamType::EntityWild) => quote!(get_slice_entities()),
(_, ParseQueryParamType::OneOf(_)) => panic!("must unpack OneOf first"),
}
}
Expand Down Expand Up @@ -246,6 +256,9 @@ fn bind_query_params(
ParseQueryParamType::EntityAny => {
bound.push(param.clone()); // Always matches
}
ParseQueryParamType::EntityWild => {
bound.push(param.clone()); // Always matches
}
ParseQueryParamType::Component(name) => {
if archetype.contains_component(name) {
bound.push(param.clone());
Expand Down
Loading

0 comments on commit ac95267

Please sign in to comment.