Skip to content

Kinded entities: typed entities that are guaranteed to have a component #1634

Open

Description

@alice-i-cecile

What problem does this solve or what need does it fill?

Defining relationships / connections between entities by adding an Entity component (or some thin wrapper) to one or both of them is very common, but often error-prone and obscures the nature of what's being done. For example:

#[derive(Bundle)]
pub struct SpringBundle {
    pub spring: Spring,
    pub transform: Transform,
    pub spring_strength: SpringStrength,
    pub connected: (Entity, Entity),
}

In this case, the Entity in the connected field must always have a Mass component and several other related fields in order for the game to function as intended. This is not at all clear from reading the code though, and is not enforced. We could attempt to connect these springs to UI widgets for example and nothing would happen: it would just silently no-op.

This pattern comes up in much more complex ways when discussing entity groups (#1592), or when using the entities as events pattern.

What solution would you like?

Allow users to define and work with type-like structures for their entities, permitting them to clearly define the required components that the entities that they are pointing to must have.

The idea here is to use archetype invariants (#1481) on a per entity-basis to enforce a) that an entity pointed to in this way has the required components at the time the b) that the entity never loses the required components. This could be extended to ensure that it never gains conflicting components as well, if that feature is either needed or convenient.

Following from the example above, we'd instead write:

#[derive(Bundle)]
pub struct SpringBundle {
    pub spring: Spring,
    pub transform: Transform,
    pub spring_strength: SpringStrength,
    pub connected: (KindedEntity<MassBundle>, KindedEntity<MassBundle>),
}

This would enforce that those specific entities always have each of the components defined in the MassBundle we already created. This would be done using archetype invariants, tracking which entities are registered as a KindedEntity on any component in any entity.

I expect that the simplest way to do this, under the hood, would be to insert a EntityKind<K> marker component on each entity registered in this way, and remove it upon deregistration. Then, have a blanket archetype invariant:

"If an Entity has an EntityKind<K: Bundle> component, it must always also have every component within that bundle."

This uses the existing always_with rule, and requires no special checking.

(thanks @BoxyUwU for the discussion to help come up with and workshop this idea)

What alternative(s) have you considered?

Pretend we're using Python and write an ever-expanding test suite to ensure type-safety-like behavior.

Use commands to define all of these components-that-point-to-entities, allowing us to check and ensure the behavior of the target at that time. This forces us to wait for commands to process (see #1613), adds more boilerplate, doesn't clarify the type signatures and doesn't stop the invariant from being broken later.

Additional context

It is likely clearer to call these "kinded entities" than "typed entities", in order to be clear that they don't actually use Rust's type system directly.

According to @BoxyUwU, this would be useful for engine-internal code when defining relationships (see #1627, #1527) to clean up the type signatures. This would let us replace:

struct RelationshipInfo {
  id: RelationshipId, // this is just used to find this struct in the vec it's stored it
  kind: RelationshipKindId, // this is just a newtype wrapper around a u64
  target: EntityOrDummyId,
  data_layout: ComponentDescriptor,
}

with

struct RelationshipInfo {
  id: RelationshipId,
  kind: Entity,
  target: Entity,
  data_layout: ComponentDescriptor,
}

and then finally with

struct RelationshipInfo {
  id: RelationshipId,
  kind: KindedEntity<RelationshipKind>,
  target: KindedEntity<RelationshipTarget>,
  data_layout:  ComponentDescriptor,
}

A similar design is mentioned in https://users.rust-lang.org/t/design-proposal-dynec-a-new-ecs-framework/71413

Activity

  1. added
    A-ECSEntities, components, systems, and events
    C-FeatureA new feature, making something new possible
    C-UsabilityA targeted quality-of-life change that makes Bevy easier to use
    on Mar 12, 2021
  2. alice-i-cecile commented on Mar 12, 2021

    @alice-i-cecile
    MemberAuthor

    To expand this feature's usefulness, we really want to be able to convert kinded entities into less restrictive / differently ordered kinded entities. This has some serious difficulties with Rust's type system however; see discussion.

    E.g. for a bundle (T1, T2, T3), we should be able to convert it into:
    (T1)
    (T2)
    (T3)
    (T1, T2)
    (T2, T1)
    (T1, T3)
    (T3, T1)
    (T2, T3)
    (T3, T2)

  3. alice-i-cecile commented on Mar 13, 2021

    @alice-i-cecile
    MemberAuthor

    In effect, this proposal extends the following metaphor:

    • archetypes are types
    • components are traits
    • entities are instances of the type of their archetype
    • systems are methods that are automatically run
    • archetype invariants define relationships between traits, like a stronger version of subtraits

    to include:

    • kinded entities are trait objects
  4. tigregalis commented on Mar 18, 2021

    @tigregalis
    Contributor

    I'm not sure I like that Kind is used as it is often used in type theory (e.g. "higher-kinded types") and is kind of obscure. So, a naming idea that simplifies things a little and makes it more obvious: SimilarEntity or LikeEntity or AlikeEntity - these are entities with similar components or are "like" each other (but particularly, not necessarily identical).

  5. alice-i-cecile commented on Mar 23, 2021

    @alice-i-cecile
    MemberAuthor

    I'm not sure I like that Kind is used as it is often used in type theory (e.g. "higher-kinded types")

    This is a deliberate choice on my part, FWIW. In effect, these KindedEntities are abstracting over the concept of which types the entity's components have: creating a higher level idea of the "type" of an entity that's distinct from the literal Entity type :)

    That said, it may be too academic; I'm not convinced by any of those suggestions, but I'm open to other ideas.

  6. added
    S-Needs-Design-DocThis issue or PR is particularly complex, and needs an approved design doc before it can be merged
    on Apr 23, 2021
  7. changed the title [-]"Typed" / "Kinded" Entities[/-] [+]Kinded entities[/+] on Feb 6, 2022
  8. Weibye commented on Aug 8, 2022

    @Weibye
    Contributor

    That said, it may be too academic; I'm not convinced by any of those suggestions, but I'm open to other ideas.

    As an end user I would more easily understand what this feature does if it was named something like RequiredComponent<T> or even EntityWithComponent<T> as then I see that I'm referencing an entity that needs to have a certain component.

    But it might be that "RequiredComponent" doesn't capture the nuance of the feature?

  9. alice-i-cecile commented on Aug 8, 2022

    @alice-i-cecile
    MemberAuthor

    There are two nuances that the RequiredComponent name doesn't capture.

    First, T can be a bundle. I'm even considering extending it to any WorldQuery type, to enable without filters.

    Second, KindedEntity is a drop-in replacement for Entity: it stores the same data and is used in the same way, just with stronger guarantees.

  10. Weibye commented on Aug 8, 2022

    @Weibye
    Contributor

    Yeah, makes sense!

    I'm even considering extending it to any WorldQuery type, to enable without filters.

    That would be really powerful and composable in that case, hoping this works out 😄

  11. Zeenobit commented on Sep 18, 2022

    @Zeenobit
    Contributor

    I've come across this a lot in my project, and I've managed to implement a really minimalistic version of this. I don't think it satisfies all the requirements of the original issue, but it might be a step forward.

    use std::marker::PhantomData;
    
    use bevy::ecs::prelude::*;
    use bevy::ecs::query::WorldQuery;
    use bevy::ecs::system::EntityCommands;
    
    /// Some type which can store an [`Entity`].
    pub trait EntityKind {
        type Bundle: Bundle;
    
        /// # Safety
        /// Assumes `entity` has a [`Kind<Self>`] component.
        unsafe fn from_entity_unchecked(entity: Entity) -> Self;
    
        fn entity(&self) -> Entity;
    }
    
    /// A [`Component`] which stores the [`EntityKind`] of the [`Entity`] it is attached to.
    #[derive(Component)]
    struct Kind<T: EntityKind>(PhantomData<T>);
    
    impl<T: EntityKind> Default for Kind<T> {
        fn default() -> Self {
            Self(PhantomData)
        }
    }
    
    /// A [`WorldQuery`] which may be used to query an [`Entity`] for the given [`EntityKind`].
    #[derive(WorldQuery)]
    pub struct EntityWithKind<T: EntityKind>
    where
        T: 'static + Send + Sync,
    {
        entity: Entity,
        kind: With<Kind<T>>,
    }
    
    impl<T: EntityKind> EntityWithKindItem<'_, T>
    where
        T: 'static + Send + Sync,
    {
        pub fn entity(&self) -> Entity {
            self.entity
        }
    
        pub fn instance(&self) -> T {
            // SAFE: Query has `With<Kind<T>>`
            unsafe { T::from_entity_unchecked(self.entity()) }
        }
    
        pub fn commands<'w, 's, 'a>(
            &self,
            commands: &'a mut Commands<'w, 's>,
        ) -> EntityKindCommands<'w, 's, 'a, T> {
            let entity = commands.entity(self.entity());
            // SAFE: Query has `With<Kind<T>>`
            unsafe { EntityKindCommands::from_entity_unchecked(entity) }
        }
    }
    
    /// A wrapper for [`EntityCommands`] which guarantees that the referenced entity has [`EntityKind`] of `T`.
    /// This type can be extended to provide commands for specific kinds of entities.
    pub struct EntityKindCommands<'w, 's, 'a, T: EntityKind>(
        PhantomData<T>,
        EntityCommands<'w, 's, 'a>,
    );
    
    impl<'w, 's, 'a, T: EntityKind> EntityKindCommands<'w, 's, 'a, T> {
        unsafe fn from_entity_unchecked(entity: EntityCommands<'w, 's, 'a>) -> Self {
            Self(PhantomData, entity)
        }
    
        pub fn commands(&mut self) -> &mut Commands<'w, 's> {
            self.entity_mut().commands()
        }
    
        pub fn entity(&self) -> &EntityCommands<'w, 's, 'a> {
            &self.1
        }
    
        pub fn entity_mut(&mut self) -> &mut EntityCommands<'w, 's, 'a> {
            &mut self.1
        }
    }
    
    /// Interface for spawning an [`Entity`] with a [`Kind`].
    pub trait SpawnWithKind<'w, 's, 'a> {
        fn spawn_with_kind<T: EntityKind>(self, bundle: T::Bundle) -> EntityKindCommands<'w, 's, 'a, T>
        where
            T: 'static + Send + Sync;
    }
    
    /// Implements [`SpawnWithKind`] for [`Commands`].
    impl<'w, 's, 'a> SpawnWithKind<'w, 's, 'a> for &'a mut Commands<'w, 's> {
        fn spawn_with_kind<T: EntityKind>(self, bundle: T::Bundle) -> EntityKindCommands<'w, 's, 'a, T>
        where
            T: 'static + Send + Sync,
        {
            let mut entity = self.spawn();
            let kind = Kind::<T>::default();
            entity.insert(kind).insert_bundle(bundle);
            // SAFE: Entity has just spawned with its kind
            unsafe { EntityKindCommands::from_entity_unchecked(entity) }
        }
    }

    To make this work, the user would have to define an entity kind, as such:

    struct Dummy(Entity);
    
    #[derive(Bundle)]
    struct DummyBundle {
        // Some components all dummies should have
    }
    
    impl EntityKind for Dummy {
        type Bundle = DummyBundle;
    
        unsafe fn from_entity_unchecked(entity: Entity) -> Self {
            Self(entity)
        }
    
        fn entity(&self) -> Entity {
            self.0
        }
    }
    
    trait DummyCommands {
        // Some command functions only dummies can invoke
    }
    
    impl DummyCommands for &mut EntityKindCommands<'_, '_, '_, Dummy> {
        // Implement commands using EntityKindCommands
    }

    You can spawn an entity with a specific kind using commands.spawn_with_kind::<Dummy>(DummyBundle). This returns an EntityKindCommands<'_, '_, '_, Dummy>, which the user can extend using DummyCommands.

    You can also query dummy entities using EntityWithKind<Dummy> in any query.

    The type Dummy itself can be used to reference entities of its kind. This implementation has no requirements on Copy/Clone for the entity kind, so whether or not the user can copy Dummy around is in user's control.
    You can use EntityWithKind to safely get a new Dummy.

    The big limitation of this system (that I know of, so far), is that each entity may only have one kind.


    Edit: Casting from an EntityRef would also work like this:

    pub trait EntityCast {
        fn cast<T: EntityKind>(self) -> Option<T>;
    }
    
    impl EntityCast for EntityRef<'_> {
        fn cast<T: EntityKind>(self) -> Option<T> {
            self.contains::<Kind<T>>()
                // SAFE: `contains::<Kind<T>>` is `true`.
                .then(|| unsafe { T::from_entity_unchecked(self.id()) })
        }
    }

    Usage:

    world.entity(entity).cast::<Dummy>().unwrap()
  12. Zeenobit commented on Sep 19, 2022

    @Zeenobit
    Contributor

    I've been cleaning up the snippet I posted above:
    https://gist.github.com/Zeenobit/ac9a2c889f038024bba1414b61092f76

  13. Zeenobit commented on Oct 5, 2022

    @Zeenobit
    Contributor

    I've been working more on my implementation. I've released it as a separate crate:
    https://github.com/Zeenobit/bevy_kindly

    I've also solved the issue of entities with multiple kinds. I'm basically using a KindBundle to solve the problem, and entities can have as many kind bundles as they need.

    I'm still working on the documentation/examples, but there is a working example of a Container/Containable implementation using this system, which should explain how it all works.

  14. joseph-gio commented on Oct 11, 2022

    @joseph-gio
    Member

    Could the "kind" be a parameter on the Entity struct itself?

    pub struct Entity<Kind: ReadOnlyWorldQuery = ()> {
        ...
    }
    
    #[derive(Component)]
    pub struct Parent {
        id: Entity<With<Children>>,
    }
  15. alice-i-cecile commented on Oct 11, 2022

    @alice-i-cecile
    MemberAuthor

    Yep, that generic strategy is the one that Cart and I currently prefer :)

  16. Zeenobit commented on Oct 23, 2022

    @Zeenobit
    Contributor

    I've been thinking some more about this issue based on the comments above, and also my experience with Bevy Kindly so far. There are 2 main points I've learned from Bevy Kindly as my project scaled up:

    1. I rarely (if ever) need to reference more than one component from an entity.
    2. Using explicit "Kind" types gets quite messy the same way multiple inheritance gets messy in OOP as the project grows.

    The simplified solution proposed by @JoJoJet ,

    pub struct Entity<Kind: ReadOnlyWorldQuery = ()> {
        ...
    }

    would solve the second problem, since there would be no requirement to tag entities with explicit kinds. The big challenge with this implementation is being able to automatically "upcast" from Entity<(With<A>, With<B>)> to Entity<With<A>> and ultimately to Entity<()>. The other issue I have with it is the verbosity, especially since using anything other than With or Without for its terms wouldn't make much sense, and would expose the user to misuse of the feature.

    So I've been thinking of an entirely different approach, using a concept of "component links":

    /// Acts as a weak reference to a specific component of an entity.
    pub struct Link<T: Component>(Entity, PhantomData<T>);
    
    impl<T: Component> Link<T> {
        unsafe fn from_entity_unchecked(entity: Entity) -> Self {
            Self(entity, PhantomData)
        }
    
        fn entity(&self) -> Entity {
            self.0
        }
    
        fn get<'a>(&self, world: &'a World) -> &'a T {
            world.get(self.entity()).unwrap()
        }
    
        fn get_mut<'a>(&self, world: &'a mut World) -> Mut<'a, T> {
            world.get_mut(self.entity()).unwrap()
        }
    }
    
    pub trait IntoLink<T: Component> {
        fn into_link(self) -> Link<T>;
    }
    
    impl<T: Component> IntoLink<T> for Link<T> {
        fn into_link(self) -> Link<T> {
            self
        }
    }
    
    #[derive(WorldQuery)]
    pub struct ComponentLink<T: Component> {
        entity: Entity,
        filter: With<T>,
    }
    
    impl<T: Component> IntoLink<T> for ComponentLinkItem<'_, T> {
        fn into_link(self) -> Link<T> {
            // SAFE: Query has `With<T>`
            unsafe { Link::from_entity_unchecked(self.entity) }
        }
    }

    This would allow users to reference components like so:

    #[derive(Component, Default)]
    pub struct Container(Vec<Link<Containable>>);
    
    #[derive(Component)]
    pub enum Containable {
        Uncontained,
        Contained(Link<Container>),
    }

    And similar to EntityKindCommands from Bevy Kindly, we can have ComponentCommands:

    pub trait ContainableCommands {
        fn insert_into(self, container: impl IntoLink<Container>);
    }
    
    impl ContainableCommands for &mut ComponentCommands<'_, '_, '_, Containable> {
        fn insert_into(self, container: impl IntoLink<Container>) {
            let containable = self.into_link();
            let container = container.into_link();
            self.add(move |world: &mut World| {
                container.get_mut(world).0.push(containable);
                *containable.get_mut(world) = Containable::Contained(container);
            });
        }
    }

    This solves a lot of problems addressed in this issue, without the need of a marker, and less verbose syntax (compared to both Bevy Kindly and Entity<Filter>). The only big limitation I can think of is being able to link to a single component.

    I'm still trying to work the details, especially how to resolve a component link from a query, as opposed to from world. But I'm curious what everyone thinks about this as an alternative solution to the same problem domain.

  17. Zeenobit commented on Oct 24, 2022

    @Zeenobit
    Contributor

    I've been playing around with the concept of component links some more, and managed to refactor my project from using Bevy Kindly to using links to test if component links solve the same problems.

    The implementation of component links is a bit more complicated:
    https://gist.github.com/Zeenobit/c57e238aebcd4ec3c4c9b537e858e0df

    But the end result is a much cleaner interface:

    let mut world = World::new();
    
    // Spawn a container and get a link to it
    let container: Link<Container> = world.spawn().insert_get_link(Container::default());
    
    // Spawn a containable and get a link to it
    let containable: Link<Containable> = world.spawn().insert_get_link(Containable::default());
    
    // Insert containable into container
    {
        let mut queue = CommandQueue::from_world(&mut world);
        let mut commands = Commands::new(&mut queue, &world);
    
        commands.get(&containable).insert_into(container);
    
        queue.apply(&mut world);
    }
    
    // Ensure containable is contained by conatainer
    assert!(world.component(&container).contains(&containable));
    assert!(world.component(&containable).is_contained_by(&container));

    Links can also be used as a WorldQuery:

    fn system(query: Query<Link<Container>>) {
        let container: Link<Container> = query.single();
    }

    This approach is a lot more scalable than Bevy kindly because there is no need for an explicit Kind<T> component. Also less verbose than the filtered entities (i.e. Entity<(With<A>, With<B>)>).

    The biggest limitation with this approach is inability to filter for multiple components. But I haven't come up with a real use case for that yet.

    I considered releasing this as a separate crate, but I think it could be made even more ergonomic if it was integrated into Bevy. So I'm curious to hear thoughts on this. For example, it could be used for existing components, like Parent and Children:

    struct Parent(Link<Children>);
    
    struct Children(Vec<Link<Parent>>);

    I've also debated renaming Link to Com (short for Component, similar to Res, and analogous to ComPtr from .NET, or "component pointer", because that's semantically what it is). I'm not sure if that's less or more readable.

  18. alice-i-cecile commented on Oct 24, 2022

    @alice-i-cecile
    MemberAuthor

    How would you envision links fitting into a world with relations? (#3742)

  19. Zeenobit commented on Oct 24, 2022

    @Zeenobit
    Contributor

    How would you envision links fitting into a world with relations? (#3742)

    I imagine it may be possible to implement entity relations in terms of links. For example:

    #[derive(Component);
    struct Person;
    
    #[derive(Component)]
    struct Likes(Link<Person>); // <-- This describes the relation
    
    fn system(query: Query<(Link<Person>, &Likes), With<Person>>) {
      for (person, Likes(other_person)) in &query {
        let person_entity: Entity = person.entity();
        // ...
      }
    }

    The syntax isn't as pretty as Flecs, but... it's a baby step towards it! 😁

    Edit: Alternatively, links could also be stored in maps, as local resources for example:

    #[derive(Component)]
    struct Person;
    
    struct FriendMap(HashMap<Link<Person>, Link<Person>>);
  20. Zeenobit commented on Oct 25, 2022

    @Zeenobit
    Contributor

    Thinking about it more, relations, kinded entities, and these component links are really all solving the same problem.

    In the gist I posted above, there is a test at the bottom which demonstrates usage of links to describe a relation between Containers and Containables:

    let mut world = World::new();
    
    // Spawn a container and get a link to it
    let container: Link<Container> = world.spawn().insert_get_link(Container::default());
    
    // Spawn a containable and get a link to it
    let containable: Link<Containable> = world.spawn().insert_get_link(Containable::default());
    
    // Insert containable into container
    {
        let mut queue = CommandQueue::from_world(&mut world);
        let mut commands = Commands::new(&mut queue, &world);
    
        commands.get(&containable).insert_into(container);
    
        queue.apply(&mut world);
    }
    
    // Ensure containable is contained by conatainer
    assert!(world.component(&container).contains(&containable));
    assert!(world.component(&containable).is_contained_by(&container));

    In short, some entities are Containers, which can only store entities that are Containable.

    In the example, I'm essentially creating a two way "ContainedBy/Contains" relation between the container and the containable using links. One way relations, such as a "ContainedBy" can be defined as a ContainedBy(Link<Container>), while "Contains" can be implemented as Contains(Vec<Link<Containable>>). And so:

    #[derive(Component)]
    struct Container;
    
    #[derive(Component)]
    struct Containable;
    
    struct Contains(Vec<Link<Containable>>);
    struct ContainedBy(Link<Container>);
    
    struct ContainsMap(HashMap<Link<Container>, Contains>);
    struct ContainedByMap(HashMap<Link<Containable>, ContainedBy>);

    In my mind, where and how these relations are stored shouldn't be a concern of Bevy (although Bevy could provide tools to deal with links/relations more ergonomically). This would allow the user to store relations either as a map within a local/world resource, or store relations as components on entities, depending on their needs.

  21. Zeenobit commented on Oct 25, 2022

    @Zeenobit
    Contributor

    Additionally, a big motivation for integrating this into Bevy for me is having an IntoEntity trait, and refactoring most functions that take an Entity to instead take an impl IntoEntity. Then IntoEntity could be implemented for Link<T>, which would turn something like this:

    let link: Link<A> = ...;
    world.entity(link.entity()).contains::<A>();
    
    let query: Query<...> = ...;
    query.get(link.entity());

    Into this:

    let link: Link<A> = ...;
    world.entity(link).contains::<A>()
    
    let query: Query<...> = ...;
    query.get(link);

    Another reason is being able to generate a Link<T> from any Query<Q, F> where Q contains Entity and:

    • Q contains &T or &mut T), or
    • F contains With<T> or Added<T> or Changed<T>

    But I'm not sure how to solve this problem yet.

    I've worked around it in my implementation by implementing WorldQuery for Link<T>. It's... ok, but a little annoying, especially if you need to access the component and get a link to it:

    fn system(query: Query<(&Container, Link<Container>)>) { ... }
  22. Zeenobit commented on Oct 31, 2022

    @Zeenobit
    Contributor

    Managed to implement a more elegant way to express entity relations directly, using Component Links.

    In summary, it'd allow us to describe a one-to-many relationship as such:

    relation! { 1 Container => * Containable as Contained }

    In English:
    A Container is relatable to many Containables, which become Contained after connection is made.
    Container, Containable, and Contained are all components that can be queried and iterated.

    I've included the full gist here:
    https://gist.github.com/Zeenobit/ce1f24f5230258d0cf5663dc64dc8f2b

    There is a test at the bottom which demonstrates how to define and use the relationship.

    As is, the macro is very inflexible, and I've only managed to get one-to-many relationships working. But I don't see one-to-one relationships or other variations of the macro to be impossible. I imagine it's just more macro voodoo, which I'm still relatively new to. 😅

  23. Zeenobit commented on Nov 1, 2023

    @Zeenobit
    Contributor

    I've released a new crate to tackle this issue:
    https://github.com/Zeenobit/moonshine_kind

    This is much more lightweight than the bevy_kindly, and more aligned with the problems outlined in this issue.

  24. CraigBuilds commented on Nov 14, 2024

    @CraigBuilds

    Just spitballing ideas:

    Now that we have required components we might be able to enforce these "Kinded" invariants using that system.

    So

    #[derive(Bundle)]
    pub struct SpringBundle {
        pub spring: Spring,
        pub transform: Transform,
        pub spring_strength: SpringStrength,
        pub connected: (Entity, Entity),
    }

    First becomes

    #[derive(Component)]
    #[require(
        Transform,
        SpringStrength,
        (Entity, Entity)
    )]
    pub struct Spring;

    My first thought was to add a custom constructor to enforce the invariants that the entities have mass. However this had an issue.

    #[derive(Component)]
    #[require(
        Transform,
        SpringStrength,
        (Entity, Entity)(entities_with_mass)
    )]
    pub struct Spring;
    
    fn entities_with_mass() -> (Entity, Entity) {
       //We can't spawn entities and add components here as we do not have access to world. Also, after the initial spawn they could be replaced with Entities that do not have mass. 
    }

    However, isn't this what the construct abstraction is for?

    #[derive(Component)]
    pub struct SpringConnections(Entity, Entity);
    
    #[derive(Component)]
    #[require(
        Transform,
        SpringStrength,
        construct(SpringConnections)
    )]
    pub struct Spring;
    
    pub impl Construct for SpringConnections {
    //construct the Entity pair and insert a Mass component for each
    }

    Spawning Spring will also spawn all of the required components, including the connected entities. They are guaranteed to have Mass components.

    Because SpringConnections is not Default, and the members are not pub, it needs to be Constructed using Construct, and hence the SpringConnections entities will always have mass.

    This idea is only half thought through, however I thought it was worth mentioning.

  25. added
    D-ComplexQuite challenging from either a design or technical perspective. Ask for help!
    on Nov 14, 2024
  26. changed the title [-]Kinded entities[/-] [+]Kinded entities: typed entities that are guaranteed to have a component[/+] on Nov 19, 2025
  27. Zeenobit commented on May 28, 2026

    @Zeenobit
    Contributor

    See below for an exploration into Kind-Safe relationships, related to #21481

    This relies on the Expect<T> component, which I've been relying on heavily to enforce kind safety. It also requires an InstanceVec<T> type because RelationshipSourceCollection is not implemented for Vec<T: ContainsEntity>.

    But overall, I thought this idea is interesting for enforcing relationship constraints, so I wanted to share it.

    The problem is that I have Person entities and Potato entities, and a Friends/FriendsOf relationship. Any Person can be friends with any other Person, but people can't be friends with potatoes. That's silly!

    So, the idea is to use Expect<T> and Component Requirements (my workaround to Archetype Constraints) to ensure Friends and/or FriendsOf can only exist on People. This means inserting FriendOf(person) onto a Potato would panic (as FriendOf expects Person), and inserting FriendOf(potato) onto a Person would also panic (as Bevy tries to insert Friends into a Potato).

    Of course, it'd be great if this could all be enforced at compile time, but that adds a lot more complexity than I'm willing to introduce.

    Full solution:
    https://github.com/Zeenobit/moonshine_kind/blob/dc5e5b06168c4e57b7c8799d8fdb3e6de0d04e06/src/instance.rs#L1154

    // In a world, where the relationship:
    //      Friends(Vec<Instance<Person>>) <-> FriendOf(Instance<Person>)
    // Cannot exist due to Bevy derive requirements and Rust trait implementation restrictions...
    // This is a workaround to sort of guarantee kind safety:
    
    use bevy::prelude::*;
    use moonshine_util::expect::Expect; // <--- Required for this pattern to work.
    
    use crate::prelude::*;
    
    // Marker to "guarantee" kind safety across related components:
    #[derive(Component)]
    struct Person;
    
    // A potato is not a person. It doesn't have friends. :(
    #[derive(Component)]
    struct Potato;
    
    #[derive(Component, Deref)]
    #[require(Expect<Person>)] // <--- Only People can have Friends
    #[relationship_target(relationship = FriendOf)]
    struct Friends(
        // Wrapper around `Vec<Instance<Person>>` to implement `RelationshipSourceCollection`
        InstanceVec<Person>,
    );
    
    #[derive(Component)]
    #[require(Expect<Person>)] // <--- Only People become Friends
    #[relationship(relationship_target = Friends)]
    struct FriendOf(
        // This entity can point to anything, but if it points to anything other
        // than a Person, it would cause a panic because both `Friends` and `FriendOf` expect `Person`.
        pub Entity,
    );
    
    #[test]
    fn test_instance_vec_relationship() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        let w = app.world_mut();
        let p0 = w.spawn_instance(Person).instance();
        let p1 = w.spawn_instance(Person).instance();
        w.entity_mut(*p1).insert(FriendOf(*p0));
    
        let fs = w.get::<Friends>(*p0).expect("Person 0 must have Friends");
        let f: Instance<Person> = *fs.first().expect("Person 0 must have at least 1 friend");
        assert_eq!(f, p1, "Person 1 must be a friend of Person 0");
    }
    
    #[test]
    #[should_panic]
    fn test_instance_vec_relationship_panic_1() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        let w = app.world_mut();
        let potato = w.spawn_instance(Potato).instance();
        let person = w.spawn_instance(Person).instance();
    
        // PANIC: Person cannot be friends with Potato. :(
        w.entity_mut(*person).insert(FriendOf(*potato));
    }
    
    #[test]
    #[should_panic]
    fn test_instance_vec_relationship_panic_2() {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins);
        let w = app.world_mut();
        let potato = w.spawn_instance(Potato).instance();
        let person = w.spawn_instance(Person).instance();
    
        // PANIC: Potato cannot be friends with Person. :(
        w.entity_mut(*potato).insert(FriendOf(*person));
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-ECSEntities, components, systems, and eventsC-FeatureA new feature, making something new possibleC-UsabilityA targeted quality-of-life change that makes Bevy easier to useD-ComplexQuite challenging from either a design or technical perspective. Ask for help!S-Needs-Design-DocThis issue or PR is particularly complex, and needs an approved design doc before it can be merged

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions