bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.25k stars 3.58k forks source link

Kinded entities #1634

Open alice-i-cecile opened 3 years ago

alice-i-cecile commented 3 years ago

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

alice-i-cecile commented 3 years ago

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)

alice-i-cecile commented 3 years ago

In effect, this proposal extends the following metaphor:

to include:

tigregalis commented 3 years ago

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).

alice-i-cecile commented 3 years ago

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.

Weibye commented 2 years ago

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?

alice-i-cecile commented 2 years ago

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.

Weibye commented 2 years ago

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 πŸ˜„

Zeenobit commented 2 years ago

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()
Zeenobit commented 2 years ago

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

Zeenobit commented 2 years ago

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.

JoJoJet commented 2 years ago

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>>,
}
alice-i-cecile commented 2 years ago

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

Zeenobit commented 2 years ago

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.

Zeenobit commented 2 years ago

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.

alice-i-cecile commented 2 years ago

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

Zeenobit commented 2 years ago

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>>);
Zeenobit commented 2 years ago

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.

Zeenobit commented 2 years ago

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:

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>)>) { ... }
Zeenobit commented 2 years ago

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. πŸ˜…

Zeenobit commented 1 year ago

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.

CraigBuilds commented 1 day ago

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.