1 unstable release

Uses new Rust 2024

new 0.1.0 Apr 28, 2025

#472 in Rust patterns

MIT-0 license

54KB
1K SLoC

Ecsilarant

Welcome to Ecsilarant, an ECS framework for the future!

Goals

The following are the goals of ecsilarant, in order of priority.

Safety

If something compiles, then it should not produce errors at runtime.

Expressiveness

Users should be empowered to express the behaviors they care about.

Introspection and decoupling induction

The framework should induce the user to think about the meaningful relationship between newly inserted behaviors and pre-existing behaviors, but incremental insertions of new behaviors should minimize the amount of pre-existing code that needs to be changed or understood.

Speed & Parallelism

Applications written on to of ecsilarant should run fast, and they should exploit all parallelism available in the target platform.

Intuitiveness & Predictability

Users should have intuitive ways of expressing the semantics they care about, and the results of what they write should be unsurprising.

Core API

Component

A component is just any rust type. A single world cannot have two components of the same type, but this constraint can be circumvented by using the newtype pattern (i.e.: by wrapping a type in different structs with a single field of the desired type).

Entity

Entities are a specific assignment of values for a set of components.

Archetype

An archetype represents a set of components. Each entity at a particular point in time conforms to a specific archetype.

World

A world is a collection of Archetypes and entities with conform to exactly one Archetype.

Realm

A realm represent a temporary claim over some part of the world. It is expressed a set of immutable or mutable references of components, where each component is only represented once.

trait Realm {}

impl<Component, RealmTail : Realm> Realm for Cons<& Component, RealmTail> 
where RealmTail : DoesNotContain<& Component> // TODO: Can this be expressed?
{}

impl<Component, RealmTail : Realm> Realm for Cons<&mut Component, RealmTail>
where RealmTail : DoesNotContain<&mut Component> // TODO: Can this be expressed?
{}

Subjects

The concept of Subjects is also important for a few more advanced use cases. Subjects represents the set of Archetypes that are matched by a particular Realm.

EntityId

Represents an identifier for an entity which contains a particular set of components.

EntityId can only be used when a pertinent index exists (See IndexedQuery) and they can be converted into Ids for any larger ComponentSet.

struct EntityId<ComponentSet> {
    persistent_id: usize,
}

impl<ComponentSet, OtherComponentSet> Into<EntityId<OtherComponentSet>> for EntityId<ComponentSet>
where ComponentSet: IsSubset<OtherComponentSet> { /*...*/ }

Component Visitors

Component visitors allows you to read, modify, insert or remove a specific component on a set of entities.

trait ComponentVisitor {
    type RealmAtom;
    /*...*/
}

Immutable references

Immutable references are component visitors. They give you access to a read only view on a particular component.

impl<Component> ComponentVisitor for &Component {
    type RealmAtom = &Component;
    /*...*/
}

Mutable references

Mutable references are component visitors. They give you access to a mutable view on a particular component.

impl<Component> ComponentVisitor for &mut Component {
    type RealmAtom = &mut Component;
    /*...*/
}

Inserter

Inserter is a component visitor which allows inserting a component in an entity which doesn't have that component yet.

impl<Component> ComponentVisitor for Inserter<Component> {
    type RealmAtom = &mut Component;
    /*...*/
}

struct Inserter<Component>{}

impl Inserter<Component> {
    fn insert(value: Component) {/*...*/}
}

Remover

Remover is a component visitor which allows removing a component from an entity which has that component.

impl<Component> ComponentVisitor<Component> for Remover<Component> {
    type RealmAtom = &mut Component;
    /*...*/
}

struct Remover<Component>{}

impl Remover<Component> {
    fn remove() {/*...*/}
}

With

Component visitor used to enforce that the queried entity has a specific component. Under the hood, this is a convenience method which requests an immutable reference without exposing it to the querying system. Useful to signal to the reader of the system that the component is merely used as a marker.

impl<Component> ComponentVisitor for With<Component> {
    type RealmAtom = &Component;
    /*...*/
}

Without

Component visitor used to enforce that the queried entity does not have a specific component. This is a convenience method which creates a dummy component which is enforced to be exclusive with another component.

impl<Component> ComponentVisitor for Without<Component> {
    type RealmAtom = &Without<Component>;
    /*...*/
}

Immutable Option (is this a good idea?)

Component visitor used to match archetypes with or without a specific component, and provide an immutable reference is the component is present.

impl<Component> ComponentVisitor for Option<& Component> {
    type RealmAtom = & Component;
    /*...*/
}

Mutable Option (is this a good idea?)

Component visitor used to match archetypes with or without a specific component, and provide a mutable reference is the component is present.

impl<Component> ComponentVisitor for Option<&mut Component> {
    type RealmAtom = &mut Component;
    /*...*/
}

ArchetypeSlice

An ArchetypeSlice represents a set of compatible component visitors.

The rule for compatibility between component visitors is simple:

  • The same component can't be referenced more than once.

The Realm of an ArchetypeSlice is the set of the RealmAtoms of each component visitor.


type ArchetypeSliceCons<HeadComponentVisitor, ArchetypeSliceTail> = 
    Cons<HeadComponentVisitor, ArchetypeSliceTail> ;

trait ArchetypeSlice {
    type Realm;
}

impl<HeadComponentVisitor: ComponentVisitor, ArchetypeSliceTail: ArchetypeSlice> ArchetypeSlice 
for ArchetypeSliceCons<HeadComponentVisitor, ArchetypeSliceTail>  
where HeadComponentVisitor: IsCompatibleWithArchetypeSlice<ArchetypeSliceTail> {/*...*/}

impl ArchetypeSlice for Nil {/*...*/}

Entity visitors

Entity visitors allows you to read, modify, create or destroy a set of entities.

Each Entity Visitor has an associated Realm.

trait EntityVisitor {/*...*/}

Query

A Query is a view of the entities which conform to a particular ArchetypeSlice.

The Realm of a query, is the same as the Realm of the associated ArchetypeSlice.

A query allows you to iterate over the matched entities, potentially in parallel, or to access an ArchetypeSlice for a specific index.

trait QueryEntityVisitor: EntityVisitor {
    type ArchetypeSlice;
    type ComponentSet; // Components touched by this Query
    type Iter : Iterator<Item=ArchetypeSlice>;
    type ParIter: ParallelIterator<Item:ArchetypeSlice>;

    // Get an entity from an entity_id if the entity still exists and if the entity still matches this Query. 
    fn get<ComponentSet>(&mut self, entity_id: EntityId<ComponentSet>) -> Option<Self::ArchetypeSlice>
    where Self::ComponentSet : IsSubSet<ComponentSet>; //Can this be implemented?

    // Apply some function to the ArchetypeSlice matching this query. 
    fn for_each<F : Fn(ArchetypeSlice)>(&mut self, f: F);

    // Apply some function to the ArchetypeSlice matching this query in parallel. 
    fn par_for_each<F : Fn(ArchetypeSlice)>(&mut self, f: F);
    
    // Iterate over each ArchetypeSlices matching this query.
    fn iter(&mut self) -> Self::Iter;

    // Iterate over each ArchetypeSlices matching this query in parallel.
    fn par_iter(&mut self) -> Self::ParallelIterator;
    
    // Count the amount of entities matching the query.
    // Runtime complexity is O(A) where A is the number of Archetype matching the query.
    fn count() -> usize;
}

struct Query<QueriedArchetypeSlice: ArchetypeSlice, IsIndexed> { /*...*/ }

impl<QueriedArchetypeSlice> EntityVisitor for Query<QueriedArchetypeSlice> { /*...*/ }
impl<QueriedArchetypeSlice> QueryEntityVisitor for Query<QueriedArchetypeSlice> {
    type ArchetypeSlice = QueriedArchetypeSlice;
    /*...*/ 
}

IndexedQuery

An indexed query is very similar to a query, but additionally offers access to a persistent EntityId for each entity matched.

trait IndexedQueryEntityVisitor: QueryEntityVisitor {
    type IndexedIter : Iterator<(EntityId<Self::ComponentSet>, ArchetypeSlice)>;
    type IndexedParIter: ParallelIterator<(EntityId<Self::ComponentSet>, ArchetypeSlice)>;
    
    // Apply some function to the ArchetypeSlice matching this query. 
    fn enumerate_for_each<F : Fn((EntityId<Self::ComponentSet>, ArchetypeSlice))>(&mut self, f: F);

    // Apply some function to the ArchetypeSlice matching this query in parallel. 
    fn par_enumerate_for_each<F : Fn((EntityId<Self::ComponentSet>, ArchetypeSlice))>(&mut self, f: F);
    
    // Iterate over each ArchetypeSlices matching this query.
    fn enumerate(&mut self) -> Self::IndexedIter;

    // Iterate over each ArchetypeSlices matching this query in parallel.
    fn par_enumerate(&mut self) -> Self::IndexedParIter;
}

struct IndexedQuery<QueriedArchetypeSlice: ArchetypeSlice> { /*...*/ }

impl<QueriedArchetypeSlice> EntityVisitor for IndexedQuery<QueriedArchetypeSlice> { /*...*/ }
impl<QueriedArchetypeSlice> QueryEntityVisitor for IndexedQuery<QueriedArchetypeSlice> {
    type ArchetypeSlice = QueriedArchetypeSlice;
    /*...*/ 
}

QueryAndDestroyer

QueryAndDeferredDestroyer

Creator

A creator allows the creation of entities fulfilling a particular Archetype. The entities are created immediately after the end of the associated system execution, and before any dependent system. The realm of a Creator, is the set of mutable references for each component in the Archetype.

struct EntityCreator<Archetype> { /*...*/ }

impl EntityCreator {
    fn create(component_values: Archetype) { /*...*/ }
}
//TODO: Decide if you actually need a trait for EntityVisitors.

IndexedCreator

An IndexedCreator is very similar to a Creator, but it allows you to specify one or more indexes which will be returned when an entity is created.

struct IndexedEntityCreator<Indexes, Archetype> { /*...*/ }

impl<Indexes, Archetype> EntityCreator<Indexes, Archetype> {
    fn create(component_values: Archetype) -> Indexes { /*...*/ }
}
//TODO: Decide if you actually need a trait for EntityVisitors.

Destroyer

A destroyer allows the destruction of entities matching a set of components. The destroyer has a really large realm. The Realm of the destroyer corresponds to the set of mutable references for all components

DeferredCreator

DeferredIndexedCreator

DeferredDestroyer

MultiEntityVisitor

Rules for

Implementation details

Storage layout

Ecsilarant uses an archetype based storage, where all components pertinent to entities conforming to an individual archetypes are stored in one place.

World Layout

The WorldStorageconsist of a recursive Cons structure where each Head represent a single ArchetypeStorage.

Archetype Layout

Each ArchetypeStorage is itself a Cons where each Head represents a single ComponentStorage.

Component Layout

Each ComponentStorage is an abstraction that allows iterations or indexed accesses over all component values of a particular type within a single Archetype. Within a single ArchetypeStorage, component values for a single entity will be consistently indexed across the component storages.

Persistent indexes?

How to maintain across add/remove of entities or component?

World creation

Parallel scheduler

Recursive async structure

World view splitting / merging

Precedence rule

DAG validation

Signal missing dependencies

Signal useless (harmful) dependencies

Removal of redundant edges

Same-component parallelism?

Supported Query language

Query

Insert

Add component (how to forward to follow-up query?)

Remove component (how to forward to follow-up query?)

Filter by Without?

Filter by With? (Or just use immutable ref)

MaybeComponent query?

#Query by index? how to expose. Use type erased fn.

Crust with macro transmongrification

No runtime deps