Skip to content

Proposal: New ECS implementation + bundles and accessors #402

Open
@caelunshun

Description

@caelunshun

In implementing custom components for plugins, I realized that the ECS implementation needs to allocate custom plugin components within the plugin's linear memory. We thus have two options:

  • continue using a further modified fork of hecs; or
  • implement our own ECS.

I am inclined to proceed with the latter because a custom ECS designed to work with the plugin system will be more flexible. Also, I have some ideas to solve #397 that we can integrate into the new ECS.

New ECS design

This is my plan for the new ECS:

  • Like EnTT, it will be based on sparse sets, whereas hecs uses archetypes. Sparse sets are easier and safer to implement than archetypes, and they have better performance characteristics when inserting/deleting components at a high frequency. (Our component-based event system does a lot of insertions and deletions, which convinces me that sparse sets are the best choice for Feather.)
  • It will support more flexibility with the borrow checker. In particular, components will have a stable location in memory, even if new entities and components are created. As a result, it should be possible to add/remove components and spawn entities inside a query loop, which will be convenient for some use cases in Quill.
  • The Component trait will not be implemented for all T; each component type needs to implement the trait instead. While this is less convenient in some areas, it prevents querying for types that are not components, like bundles. (We'll provide a derive macro for Component to avoid too much boilerplate.)
  • It will have built-in support for bundles and bundle accessors as an alternative implementation to HList and Plucker for components #388 and High-Level Entity Methods (WIP) #397. My vision is this: each entity type has its own Bundle struct, which is the set of components it spawns with. The ECS crate provides a derive macro that generates an accessor struct for the bundle that can be queried. The bundle accessor is a wrapper for EntityId that provides convenience methods to access components, and it can also include methods that access multiple components, like item_in_main_hand(). In code, this looks like:
#[derive(Bundle)]
#[bundle(generate_accessor = "Player")] // generates the Player struct
pub struct PlayerBundle {
    /// another bundle containing default components for all living entities, like `Position` and `Health`
    /// (this emulates inheritance via composition)
    #[bundle(inherit)]
    base: LivingEntityBundle,
    inventory: Inventory,
    player_marker: PlayerMarker,
}

impl Player {
    pub fn item_in_main_hand(&self) -> &ItemStack { /* ... */ }
}

// To spawn a player:
game.spawn_entity(PlayerBundle {
    base: LivingEntityBundle {
        pos,
        ..Default::default()
    },
    ..Default::default()
});

// The accessor for the bundle can be queried using a new `query_as` function
// to distinguish accessors from real components.
for (entity_id, player): (EntityId, Player) in game.query_as::<Player, ()>() {
    // the accessor provides methods to conveniently get components
    println!("{:?}", player.position());
    // it can also provide convenience functions that access multiple components
    // (HeldItem and Inventory in this case)
    println!("Player is holding {:?}", player.item_in_main_hand());
}

// Accessors also work with abstract bundles like LivingEntity
for (entity_id, entity): (EntityId, LivingEntity) in game.query_as::<LivingEntity, ()>() {
    println!("{:?}", entity.position());
}

I opened this issue to collect feedback in the proposal, in the hope that we can resolve any issues with it before I implement something we don't want. So feedback is welcome from all.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions