Entt: Getting component data without knowing its type in compile time

Created on 5 May 2020  路  15Comments  路  Source: skypjack/entt

registry::visit is a handy function that allows us to iterate over component types that are attached to an entity. Is there any way to get the component data without knowing its type in compile time though? I imagine something like registry::get(entt::type_id, entt::entity) would be possible but I don't currently see any function resembling it and the only way I can get a component is by providing a template parameter.

question

All 15 comments

I'm about to cut a new release and the next iteration will be almost entirely dedicated to support this kind of runtime functionalities. Many things are possible already but they aren't well documented and therefore questions like this one pop out time by time.
I'm sorry for this. The lockdown slowed everything down a bit and visit is half way between the fully static model and a runtime-aware solution. More will come, it's already on its way!

However, EnTT is designed in such a way that you pay only for what you use/need. This case isn't different.
Supporting an opaque get like the one you proposed would make all users pay the price of an erased function or similar, even when they aren't interested in it. Moreover, I don't like much to mix all functionalities in the same tool/API.

What you can do today is to use the runtime identifier (the one returned by visit) to lookup a meta type or a data structure where you put the erased getters.
The plan is to extend the possibility of hooking into the type system of EnTT so as to have all these _functionalities_ implicitly generated on first use and therefore always available for who's interested in them. Again, this is already possible but a little tricky at the moment (well, 10 lines of code or so probably, not that tricky at the end of the day).

I don't know if you're already using the meta module or want to avoid it for any reason.
So, before to provide an example that works today and may change a little tomorrow, I'll wait for a feedback. :+1:

No need to be sorry! EnTT is a phenomenal library and I deeply appreciate you spending your time to create and maintain it. Thanks!

I was just wondering how I could improve my code and if it's even possible at the moment. I'm by no means well versed in the C++ magic that's going on in EnTT, so I was not sure about that. It's great to hear that more related features are in the works.

Now, to elaborate on my case. I'm in the process of writing a property inspector for entities that obviously needs to iterate over all the components attached to the selected entity and provide an interface to modify the data. Reflection is currently handled by RTTR in my project, since I knew about it before I learned about meta, but it might change at some point if I deem it justified.

I have my components fully reflected and even can store their ENTT_ID_TYPE in the metadata. Still, I have to specify the component types in compile time while getting them which forces me to update the code shown below each time I add or remove a component type in my project.

registry.visit(entity, [this](ENTT_ID_TYPE _id)
    {
        if (_id == entt::type_info<TransformComponent>::id())
            DrawComponent(registry.get<TransformComponent>(entity));
        else if (_id == entt::type_info<TextureComponent>::id())
            DrawComponent(registry.get<TextureComponent>(entity));
        // ...
        else
            DrawUnknownComponent(_id);
    });

DrawComponent function then erases the type by using rttr::instance, so it'd be beneficial to also get a type erased component from the registry, like in the exemplary code below.

registry.visit(entity, [this](ENTT_ID_TYPE _id)
    {
        DrawComponent(registry.get(entity, _id));
    });

I feel it would be tidier and also wouldn't require constant maintenance.

I'm certainly interested in the type erased getters you mentioned. How'd I go about implementing it?

I know about RTTR but I've never used it, so forgive me if I provide you with an example that uses the meta module from EnTT.
Is something like this what you're looking for?

#include <https://raw.githubusercontent.com/skypjack/entt/master/single_include/entt/entt.hpp>
#include <iostream>

template<typename Type>
Type & get(entt::registry &registry, entt::entity entity) {
    return registry.template get<Type>(entity);
}

struct transform { int value; };
struct texture { int value; };

void draw(const entt::meta_any &) { std::cout << "drawing..." << std::endl; }

int main() {
    entt::meta<transform>().func<&get<transform>, entt::as_alias_t>("get"_hs);
    entt::meta<texture>().func<&get<texture>, entt::as_alias_t>("get"_hs);

    entt::registry registry;
    const auto entity = registry.create();

    registry.emplace<transform>(entity);
    registry.emplace<texture>(entity);

    registry.visit(entity, [&registry, entity](auto component) {
        const auto type = entt::resolve_if([component](auto type) { return type.type_id() == component; });
        const auto any = type.func("get"_hs).invoke({}, std::ref(registry), entity);
        draw(any);
    });
}

That is, since you're already defining your meta types, you can lookup them by type id and invoke an opaque getter.
In EnTT, as_alias_t (renamed to as_ref_t on master) makes it work without copies too. The return type is wrapped in a meta_any that acts as a reference.
Moreover, consider that the line that uses resolve_if is replaced on master by this one:

const auto type = entt::resolve_type(component);

The reason for which the opaque getter isn't built-in in the registry boils down to the overall design of EnTT.

As a side note, consider these two lines:

entt::meta<transform>().func<&get<transform>, entt::as_alias_t>("get"_hs);
entt::meta<texture>().func<&get<texture>, entt::as_alias_t>("get"_hs);

It may be annoying if you had for example 100 types, right?
The next iteration will make it possible to _hook_ into the type system of EnTT and therefore to have some functions invoked on _first use_.
It means that you can just define this:

template<typename Type>
void extend_meta_type() {
    entt::meta<Type>().func<&get<Type>, entt::as_alias_t>("get"_hs);
}

Then have it executed for every type, no matter what. Of course, meta here is a details. You aren't forced to use it and hooking into the type system isn't meant only to extend meta types but this should give you a grasp of what I meant before.
Let me know if you've any question.


This issue made me realize that a template cast operator would be useful so as to define the following function and pass them directly any:

void draw(const transform &) { std::cout << "drawing transform..." << std::endl; }
void draw(const texture &) { std::cout << "drawing texture..." << std::endl; }

There is nothing that forbids an implicit cast like that (put aside the fact that I never defined the function to do that).
Please, don't close the issue. This will help me to remember that I want to add such a function to meta_any.

Thanks! It certainly is an interesting direction but doesn't really lift the need of constant maintenance since I still have to explicitly register the getters. I can make it a part of writing the reflection code though so that there's no need to modify the property inspector code when I add or remove a component type anymore. So there's a profit there.

About the next iteration -- how would the _executing for every type_ part work? Does it mean iterating over each type registered in meta and extending the reflection without knowing the compile time type?

It means that you can define _setup functions_ on a per-type basis and have them invoked on first use.
This way you don't have to explicitly call them somewhere in your code.

This can help here since the setup functions can be generic. You can define an opaque get once (because I guess all get-ters are equal) and have them generated automatically and attached to your meta types every time you add a new type to your software. No need to extend all meta types manually.
So, roughly speaking, for the common feature you can have a generic (as in template based) extend_meta_type function and have it invoked automatically by the library at the right time.

Not sure you get what I mean. Feel free to ask if it's not clear.

Thank skypjack for elegant solution

template<typename T>
void Get(entt::registry* ecs, entt::entity entity, sol::state* lua)
{
    (*lua)["Get" + entt::resolve<T>().prop("name"_hs)
                        .value().cast<std::string>()] = ecs->template get<T>(entity);
}

void GetCom(entt::registry* ecs, sol::state* lua, entt::entity entity, entt::id_type component)
{
    entt::resolve_type(component)
        .func("get"_hs).invoke({}, ecs, entity, lua);
}



md5-ed31422d87891c60c6bc8618067f9fb9



template<typename T>
void extend_meta_type(std::string name) {
    entt::meta<T>()
        .type(entt::type_info<T>::id())
        .prop("name"_hs, std::move(name))
        .func<&Get<T>, entt::as_void_t>("get"_hs);
}



md5-ed31422d87891c60c6bc8618067f9fb9



struct transform { int value; };
struct texture { int value; };



md5-ed31422d87891c60c6bc8618067f9fb9



int main()
{
    extend_meta_type<transform>("transform");
    extend_meta_type<texture>("texture");
    entt::registry ecs;
    sol::state lua;

    lua.open_libraries();
    lua["ECS"] = &ecs;
    lua["lua"] = &lua;
    lua["Get"] = &GetCom;
    lua["transformID"] = entt::type_info<transform>::id();
    lua.new_usertype<transform>("transform",
        "value", &transform::value
        );
    const auto entity = ecs.create();
    ecs.emplace<transform>(entity, 1);
    ecs.emplace<texture>(entity, 2);

    lua["entity"] = entity;


    const auto& code = R"(  
        Get(ECS, lua, entity, transformID)
        print(Gettransform.value)
    )";
    lua.script(code);
}

@UxiBeo two comments:

  • Even though it doesn't cause problems, as_void_t isn't necessary here since the return type is void already. It's to be used when you want to _suppress_ a non-void return type in general.

  • You could hook into type_index and invoke extend_meta_type on first use for each type. This way you won't have to explicitly invoke it for every type.

  • You could hook into type_index and invoke extend_meta_type on first use for each type. This way you won't have to explicitly invoke it for every type.

How can I achieve this "magic". Can you give me some code snippets

@skypjack, getters are indeed the same but I only need them for component types, not all types I register. Is this not a problem in this approach? I also might not understand correctly though.

How can I achieve this "magic". Can you give me some code snippets

@UxiBeo this is what I want to achieve during the next iteration.
Currently, the library _supports_ this somehow but the way you do it must be polished.
This is an example of an opaque stamping function. As you can see, I never _announce_ my types:

int main() {
    entt::registry registry;
    const auto entity = registry.create();

    registry.emplace<transform>(entity, 2);
    registry.emplace<texture>(entity, 3);

    const auto other = registry.create();

    registry.visit(entity, [&](const auto component) {
        const auto type = entt::resolve_type(component);
        const auto any = type.func("get"_hs).invoke({}, std::ref(registry), entity);
        type.func("set"_hs).invoke({}, std::ref(registry), other, any);
    });

    assert(registry.has<transform>(other));
    assert(registry.has<texture>(other));

    assert(registry.get<transform>(other).value == 2);
    assert(registry.get<texture>(other).value == 3);
}

Thanks to entt::as_ref_t (see the link for more details), you've also zero copies here. All meta_any objects behave as tiny wrappers around references.

getters are indeed the same but I only need them for component types, not all types I register. Is this not a problem in this approach? I also might not understand correctly though.

@Vennor you have these functions invoked implicitly for all types for which you use type_index, that is, for all your components. I cannot really _intercept_ an user type unless it's passed somehow to the library.

@skypjack, but doesn't that mean that the functions will also be invoked for other types I use type_index on that aren't components? I guess that's not something we'd want.

@Vennor yes and no. The example is minimal but in fact entt::type_index is sfinae friendly. You can specialize it on a per-traits basis. Or you can just use an if constexpr for the most trivial cases. Here it is a not production ready example. :)
That is, you can _intercept_ the components somehow if you want, then invoke the initialization function only for them.

Oh, that's a nice trick. Thank you very much for the explanation and help! I'll leave the issue opened as you suggested.

I'm glad to help @Vennor

Today I tried to add the conversion operator to meta_any as mentioned above but:

  • It's not that useful unless I make it not explicit, that is something I'd rather avoid instead.
  • It doesn't solve the problem I thought it would solve.

So, all in all, we can live without it. :)

I'm closing the issue because it seems to me you've received a proper answer.
Feel free to reopen it if I'm wrong and this isn't the case. Thanks.

How can I achieve this "magic". Can you give me some code snippets

@UxiBeo this is what I want to achieve during the next iteration.
Currently, the library _supports_ this somehow but the way you do it must be polished.
This is an example of an opaque stamping function. As you can see, I never _announce_ my types:

int main() {
    entt::registry registry;
    const auto entity = registry.create();

    registry.emplace<transform>(entity, 2);
    registry.emplace<texture>(entity, 3);

    const auto other = registry.create();

    registry.visit(entity, [&](const auto component) {
        const auto type = entt::resolve_type(component);
        const auto any = type.func("get"_hs).invoke({}, std::ref(registry), entity);
        type.func("set"_hs).invoke({}, std::ref(registry), other, any);
    });

    assert(registry.has<transform>(other));
    assert(registry.has<texture>(other));

    assert(registry.get<transform>(other).value == 2);
    assert(registry.get<texture>(other).value == 3);
}

Thanks to entt::as_ref_t (see the link for more details), you've also zero copies here. All meta_any objects behave as tiny wrappers around references.

getters are indeed the same but I only need them for component types, not all types I register. Is this not a problem in this approach? I also might not understand correctly though.

@Vennor you have these functions invoked implicitly for all types for which you use type_index, that is, for all your components. I cannot really _intercept_ an user type unless it's passed somehow to the library.

Here is a fixed version of the snippet. The original version was missing the meta-types registration with a name so they were not resolvable by id.

Was this page helpful?
0 / 5 - 0 ratings