skypjack / entt

Gaming meets modern C++ - a fast and reliable entity component system (ECS) and much more
https://github.com/skypjack/entt/wiki
MIT License
9.54k stars 840 forks source link

Acecss components of an entity, without giving full registry access #1140

Closed jjcasmar closed 1 week ago

jjcasmar commented 2 weeks ago

I have a function to initialize an entity with a bunch of components

My first attempt has been something like receiving only an entity, but that doesn't work. I have no way to create new components for that entity

template <>
void initialize<SomeObjectType>(entt::entity entity) {
    // Initialize components for this entity
    // but really can initialize anything, I can't store the components in the registry
}

So I add the registry to the function signature

template <>
void initialize<SomeObjectType>(entt:registry reg, entt::entity entity) {
    // Initialize components for this entity
    // This works fine, but it has too much access
}

This works from a usability perspective, but it gives the control full access to the whole registry, so now all the "encapsulation" is gone. Someone implementing that function might make something wrong with the registry, so I will have to double check every commited code...

Is there anyway to allow to create new components or modify existing components but only for the particular entity I passed to the function? For modifying it might be possible to receive a view (althought getting the type right might be tricky), but how can I initialize the components if I only receive the entity?

skypjack commented 2 weeks ago

Something like the handle class maybe? It has a method to get the registry though. However, you can use it as an example to implement a custom solution. Does it work for you?

jjcasmar commented 2 weeks ago

I looked into the handle, and see that it had access to the entity and to the registry, so it doesn't really solves my issue. I guess I could inherit from handle and disable the registry access. Would you accept a contribute for this?

skypjack commented 2 weeks ago

It would be a breaking change, so no. I don't plan to make the registry method private in that class. I'm sorry. However, you can inherit or take inspiration from it and do whatever you want that fits your design. 👍

jjcasmar commented 2 weeks ago

I wasnt thinking about breaking the current design of the class, that would be terrible.

jjcasmar commented 2 weeks ago

I was thinking on adding a new template parameter to the handle class, and instantiate the entity() and registry() methods depending on that template parameter. But I have checked the code and the basic_handle already has a ...Scope template parameter, which makes it impossible to add a new parameter between the Registry type and the Scope without making a breaking change.

Innokentiy-Alaytsev commented 2 weeks ago

I guess Michele suggests doing whatever you want/need in your code base. Adding a new type that is essentially handle, but not quite, or complicating the handle itself is not a great plan maintenance-wise.

jjcasmar commented 2 weeks ago

Yeah, I think so. I still think the functionality will be a nice addition to EnTT, I have found my self several times trying to restrict access to the registry. But if there is not enough traction for adding it, we can just close the issue.

Thanks for the answers!

skypjack commented 2 weeks ago

I have found my self several times trying to restrict access to the registry

What you're trying to do isn't restricting access to the registry though. Instead, you're trying to emulate the EC model of Unity or the like, where the entity is the key actor and you can do everything by means of its API. ECS models are slightly different. Systems are meant to operate on a whole set of entities and components rather than on a single entity. Whit this in mind, you can already restrict a system's scope by passing storage classes or views to it. That is why there isn't much traction probably. This is a pretty uncommon request. 🙂

jjcasmar commented 2 weeks ago

The issue is not when operating on the entities, for that I use views. The issue comes when creating entities. In my use case, I am trying to create an entity with the needed components to simulate a garment. I need to add several components (Mesh, Collider...).

Right now I have a function which is in charge of creating the components for the entity, but to do so, I need to pass the whole registry, so inside that function, which should only be allowed to create the components I want, now I can modify all the registry. That is why I was thinking on a way of being able to add components to a single entity, without giving full access to the registry.

If there is a better way to initialize the entities with its components, I am all ears

Green-Sky commented 2 weeks ago

Passing in the "whole" registry is not an issue in your case.

However I see 2 options for you that can enforce this form of separation.

  1. Like @skypjack said, instead of passing in the registry to the creation function, you can only pass in the entity and all the storages (component pools) that you require.
  2. You already found the Scope... template on the handle :), while this still would allow to access the registry field of a handle, it would allow you to restrict which component types (storages) can be accessed through it's interface. This feels more like the solution you want. You can also declare an alias for the special restricted handle type.
jjcasmar commented 2 weeks ago

Why do you say its not an issue in my case? I am thinking on a teammate doing something wrong like

void initialize_garment(entt::registry &reg, entt::entity garment) {
    registry.emplace<Mesh>(garment);
    registry.emplace<Collider>(garment);
    registry.emplace<Material>(garment);
    ...
    auto meshes = reg.view<Mesh>(); // Wrong, we are also accessing already existing meshes
}

I agree the programmer of that code is doing something really bad, and that code will never pass code review. But restricting what the programmer can use in the first place will solve the issue.

How would you use the component pools in this case? I have never done something like that.

The option of restricting the component types of the handle doesn't totally solve the issue (although it definitely improves it). I can still access entities which have one of the restricted types.

Maybe there is a third solution. Create entities with a "to be built" component and pass a view to those entities.

skypjack commented 2 weeks ago

I see your point. However, as you correctly pointed out, the handle class cannot be changed easily in this sense. That said, if all you want is to restrict access when creating new entities, a small utility with a minimal interface is bettter suited probably. Something like this (completely out of my mind and just to give you a grasp of what I mean):

struct my_utility {
    my_utility(entt::registry &ref, entt::entity target)
        : reg{&ref}, entity{target}
    {}

    template<typename Type>
    decltype(auto) emplace() { return reg->emplace<Type>(entity); }

private:
    entt::registry *reg;
    entt::entity entity;

That's all. You can pass it as a function argument and use it in the body as:

Mesh &mesh = util.emplace<Mesh>();
// fill the mesh as needed and proceed with other components

I mean, for a grand total of 9 loc you've a fully encapsulated emplace function, which is what you're looking for if I get it correctly? If this is the case, it sounds like a good compromise to me. 🤷‍♂️

How would you use the component pools in this case?

It wouldn't fit your requirements anyway. If you pass a pool to a function, your teammate can still remove random elements from it or iterate all the instances in it and do something crazy with them.

Create entities with a "to be built" component and pass a view to those entities.

I personally use something similar actually. Sort of setup this entity as concept C component that is handled by a specific system at the right point in the loop (for some definitions of right ofc 😅). This doesn't solve your issue though. My system still receives the whole registry to setup things.

jjcasmar commented 2 weeks ago

I like that small utility class, and it toally solves the issue. Probably it also need a variadic emplace() method to create the elements, but yeah, something like totally works.

I was probably overthinking, since its only for creation, I only need the emplace functionality.

skypjack commented 2 weeks ago

Yeah, it was just an example to give you a grasp. I don't even know if it compiles fine. 🙂 I'm glad it helped btw. Let me know if we can consider the issue closed. Thanks. 👍