An alternative to ECS. Here is a video summary.
Requires the nightly compiler and specialization feature - it's only used in a sound way.
rustup override set nightly # enable nightly compiler
cargo add entity-trait-system # get lib// explicitly opt in to language feature
#![allow(incomplete_features)]
#![feature(specialization)]
// declare world, entities, and traits which the entities could have
entity_trait_system::world!(
MyWorld, Enemy, Player; TestTrait, SecondTestTrait);
let mut world = MyWorld::default();
// directly access arena member
let player_id = world.player.insert(Player { id: 1 });
// compile time type accessor of arena member (similar)
world.arena_mut::<Enemy>().insert(Enemy { hp: 10 });
// visit all arenas with types that implement trait
// (likely static dispatch)
#[cfg(feature = "rayon")]
world.par_visit_test_trait(|e| e.do_something());
#[cfg(not(feature = "rayon"))]
world.visit_test_trait(|e| e.do_something());
// runtime type API - access type-erased (Any) arena
let arena_id = MyWorld::arena_id::<Player>();
let player_arena = world.any_arena_mut(arena_id);
// unwrap: I know that this is a player
// and that the reference is valid
let player = player_arena
.get_mut(player_id).unwrap()
.downcast_mut::<Player>().unwrap();
player.do_something_else();visit_<trait>- Iter over implementing entitiesvisit_mut_<trait>- Mutable iter over implementing entitiesvisit_key_<trait>- Iter(Key, &Value)tuplesvisit_key_mut_<trait>- Mutable iter(Key, &mut Value)tuplesretain_<trait>- Keep entities matching predicatediff_<trait>- Gather diff vector from immutable view, to apply laterdiff_mut_<trait>- Same as previous, but from mutable viewdiff_apply_<trait>- Apply diff vectorclear_<trait>- Clear all arenas implementing traitlen_<trait>- Count entities implementing traitany_arenas_<trait>- Array of type-erased arenas implementing traitany_arenas_mut_<trait>- Mutable version of above
arena<T>()/arena_mut<T>()- Compile-time typed arena accessany_arena()/any_arena_mut()- Runtime type-erased accessarena_id<T>()- Get stable arena identifier - serializes to type name.clear()- Clear all arenaslen()- Total entity count across all arenas
Parallel operations exposed via par_* variants.
Both map (serde_json) and seq (bincode) style ser/deserialization.
Here's a benchmark test suite comparing ETS to other popular libraries.
ETS performed the best. It's the same speed as the underlying dense slot map insertion.
The results for this benchmark form two distinct groups. The fastest group, including hecs and legion, arrange the components of the data as a structure of arrays. The second group (including ETS) iterate through an array of structures. A structure of arrays performs better since only the relevant data is loaded into the cache.
Since ETS doesn't use components, it is inside the second group.
ETS arrived in second place, just behind shipyard.
ETS performed the best. But, disjoint systems (outer parallelism) must be stated explicitly.
ETS overlapped with the other libraries - not much of a difference.
ETS is omitted from this benchmark since it doesn't use components; it's not applicable. For sparse components, an auxiliary structure can store entity keys. For dense components, a trait in the ETS can implement the desire behaviour.
Only three libraries implemented serialization. ETS arrived in second.
This can be found in the implementation of visit_*:
fn v_if_applicable<F>(
arena: &DenseSlotMap<DefaultKey, T>,
mut handler: F) where F: FnMut(&dyn $trait_name)
{
arena.values_as_slice().iter()
.for_each(|entity| {
// implicit type erase T -> &dyn $trait_name
handler(entity)
});
}The handler is typically inlined and devirtualized to erase dynamic dispatch, since the type is known at compile time and is type erased just before use. This means that static dispatch is reliant on compiler optimization; likely but not guaranteed.
License: MIT