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
10.05k stars 881 forks source link

Ideabox: get components with runtime identifiers #104

Closed Milerius closed 6 years ago

Milerius commented 6 years ago

Hello !

I advance well on my engine using EnTT and I am very satisfied! I'm at the scripting part and here's what I'm able to do:

what i'm not able to do:

All the examples of this issue are presented with lua and with the sol2 library, but could very well be done with python and pybind for example.

Here is an example in lua of how we use entt:

function test_create_entity()
    local id = shiva.entity_registry:create()
    assert(id == 0, "id should be 0 here")
    return id
end

function test_destroy_entity()
    local id = shiva.entity_registry:create()
    shiva.entity_registry:destroy(id)
    return true
end

function test_component()
    local entity_id = shiva.entity_registry:create()
    local component = shiva.entity_registry:add_layer_1_component(entity_id)
    local same_component = shiva.entity_registry:get_layer_1_component(entity_id)
    assert(shiva.entity_registry:has_layer_1_component(entity_id) == true, "should be true")
    shiva.entity_registry:remove_layer_1_component(entity_id)
    assert(shiva.entity_registry:has_layer_1_component(entity_id) == false, "should be false")
    return true
end

How do I register my functions in the system lua without doing template specialization as in the examples of modding of EnTT:

Register Components example C++:

        template <typename Component>
        void register_component()
        {
            using namespace std::string_literals;
            log_->info("register component: {}", Component::class_name());

            state_[entity_registry_.class_name()]["get_"s + Component::class_name() + "_component"s] = [](
                shiva::entt::entity_registry &self,
                shiva::entt::entity_registry::entity_type entity) {
                return std::ref(self.get<Component>(entity));
            };

            state_[entity_registry_.class_name()]["has_"s + Component::class_name() + "_component"s] = [](
                shiva::entt::entity_registry &self,
                shiva::entt::entity_registry::entity_type entity) {
                return self.has<Component>(entity);
            };

            state_[entity_registry_.class_name()]["remove_"s + Component::class_name() + "_component"s] = [](
                shiva::entt::entity_registry &self,
                shiva::entt::entity_registry::entity_type entity) {
                self.remove<Component>(entity);
            };

            if constexpr (std::is_default_constructible_v<Component>) {
                state_[entity_registry_.class_name()]["add_"s + Component::class_name() + "_component"s] = [](
                    shiva::entt::entity_registry &self,
                    shiva::entt::entity_registry::entity_type entity) {
                    return std::ref(self.assign<Component>(entity));
                };
            }
        }

        template <typename T>
        void register_type() noexcept
        {
               /* This tuple is magic, it contains both the name of the class 
                  we want to record by scripting but also the name of functions, 
                 their pointers to associated members, 
                 their variable name and pointer to variables associated member */
            const auto table = std::tuple_cat(
                std::make_tuple(T::class_name()),
                T::reflected_functions(),
                T::reflected_members()); 

            try {
                std::apply(
                    [this](auto &&...params) {
                        this->state_.new_usertype<T>(std::forward<decltype(params)>(params)...);
                    }, table);
            }
            catch (const std::exception &error) {
                log_->error("error: {}", error.what());
                return;
            }

            log_->info("successfully registering type: {}", T::class_name());
        }

        template <typename ... Types>
        void register_components(meta::type_list<Types...>)
        {
            (register_type<Types>(), ...);
            (register_component<Types>(), ...);
        }

What a component look like with compile-time reflection:

struct layer_1
    {
        reflect_class(layer_1);

        static constexpr auto reflected_functions() noexcept
        {
            return meta::makeMap();
        }

        static constexpr auto reflected_members() noexcept
        {
            return meta::makeMap();
        }
    };

Create a meta::type_list and register components:

using common_components = meta::type_list<layer_1,
        layer_2,
        layer_3,
        layer_4,
        layer_5,
        layer_6,
        layer_7,
        layer_8>;

 scripting_system.register_components(common_components{});

It's all, we are done with the registration of the components we will use in our scripting, and we are able to use them immediately without additional code or template specialization.

Ok, now important point of reflection, how to succeed to filter entities by their different components knowing that we can not use view<Cmp1, Cmp2, Cmp3>.

Ideally in scripting I wish I could do that:

function functor(current_entity)
   -- things...
end
shiva::entity_registry:for_each("component1", "component2", "component3", functor)

which is an equivalent of something like this

        template <typename Functor, typename ... RuntimeIdentifiers>
        void for_each(entt::DefaultRegistry &self, Functor&& func, RuntimeIdentifiers &&... ids) 
        {
            // hashed_string(id) and hashed_string(id2) to retrieve the id's associated to an entity
            self.view_runtime(std::forward<RuntimeIdentifiers>(ids)...).each(std::forward<Functor>(func));  
        }

auto ov = sol::overload(
                [](entt::DefaultRegistry &self, const std::string &component, sol::function functor) {
                    return for_each(self, functor, component);
                },
                [](entt::DefaultRegistry &self, const std::string &component, const std::string &component2, sol::function functor) {
                    return for_each(self, functor, component, component2);
                }, 
               [](entt::DefaultRegistry &self, const std::string &component, const std::string &component2, const std::string& component3, sol::function functor) {
                    return for_each(self, functor, component, component2, component3);
                });
           state_[entity_registry.class_name()]["for_each"s] = ov;
template<typename Component, typename... Args>
    Component & assign(const entity_type entity, Args &&... args) {
        assert(valid(entity));
        assure<Component>();
        pool<Component>().construct(entity, std::forward<Args>(args)...);
        std::get<1>(pools[component_family::type<Component>()]).publish(*this, entity);
        // what about
        if constexpr(has_reflectible_class_name_v<Component>) {
           auto magic_id = entt::HashedString(Component::class_name()); // I assume here hashed_string return unique id

           // store this id with the current entity to eventually retrieve it latter.
        }
        return pool<Component>().get(entity);
    }

So the function below will give in theory: I have thanks to the reflection pre-record all my creation of components via add_"component_name"_component. This in function will generates a unique hashed id that can potentially be used to retrieve entities that have the id of this component.

local entity_id = shiva.entity_registry:create()
local component = shiva.entity_registry:add_layer_1_component(entity_id)

With the approach of a possible arrival of reflection in EnTT, we can imagine that the first time we assign a component that would be reflective we could retrieve a unique id based on the name of its class and therefore potentially find it later. This would greatly advance the scripting approach with EnTT by mixing Metaprogramming, Reflection.

I would like to point out that with type_traits we can keep EnTT as it is currently for those who do not wish to have reflection / scripting through EnTT and as EnTT is moving to C++ 17 we can take advantage of if constexpr (is_reflectible_v<T>) on the desired template parameters and keep the users happy.

It's only an idea, but I think it's nice to suggest it and have your opinions maybe for a few days and push the discussion further. I would be totally available for the implementation of the reflection part if it interested users of EnTT

I also read the part https://github.com/skypjack/entt#runtime-identifiers but I did not really understand if it was possible to perform actions with these identifiers and if it was possible to do the things presented in the different examples of this issue.

indianakernick commented 6 years ago

I would be very interested in seeing a reflection system integrated with EnTT.

skypjack commented 6 years ago

have your opinions maybe for a few days

I'm out of home for the weekend. If you want my opinion, it should be longer than a few days. I'm sorry. :-)

Milerius commented 6 years ago

@Kerndog73 I can show a potential synopsis from my project:

namespace shiva::meta
{
    namespace details
    {
        template <template <typename...> typename MetaFunction, typename, typename ...Params>
        struct is_detected : std::false_type
        {
        };

        template <template <typename...> typename MetaFunction, typename ...Params>
        struct is_detected<MetaFunction, std::void_t<MetaFunction<Params...>>, Params...> : std::true_type
        {
        };
    }

    template <template <typename...> typename MetaFunction, typename ...Params>
    using is_detected = details::is_detected<MetaFunction, void, Params...>;

    template <typename T, typename U>
    using comparison_t = decltype(std::declval<T &>() == std::declval<U &>());

    template <typename T, typename U>
    using is_eq_comparable_with = is_detected<comparison_t, T, U>;

    template <typename T, typename U>
    static constexpr const bool is_eq_comparable_with_v = is_eq_comparable_with<T, U>::value;
};

^ https://en.cppreference.com/w/cpp/experimental/is_detected usefull for compile time feature detection

/** Convert a token to a string litteral */
#define pp_stringify(s)         #s

/** Get the length of a string litteral */
#define pp_strlen(s)            (sizeof(s"") - 1)

/** Convert a token to a string_view */
#define pp_stringviewify(s)     std::string_view(pp_stringify(s), pp_strlen(pp_stringify(s)))

^ Usefull macros to convert class name for example -> pp_stringify(foo) -> "foo"

namespace shiva::meta
{
    template <typename ...Ts>
    using Map = std::tuple<Ts...>;

    namespace details
    {
        template <typename ...Ts, typename KeyT, typename FuncT>
        constexpr bool find(const Map<Ts...> &, KeyT &&, FuncT &&, std::index_sequence<>) noexcept
        {
            return false;
        }

        template <typename ...Ts, typename KeyT, typename FuncT, size_t I, size_t I2, size_t ...Is>
        constexpr bool find(const Map<Ts...> &map, KeyT &&needle, FuncT &&f, std::index_sequence<I, I2, Is...>) noexcept
        {
            using T1 = std::decay_t<decltype(std::get<I>(map))>;
            using T2 = std::decay_t<decltype(needle)>;

            if constexpr (shiva::meta::is_eq_comparable_with_v<T1, T2>) {
                if (std::get<I>(map) == needle) {
                    f(std::get<I>(map), std::get<I2>(map));
                    return true;
                }
            }
            return find(map, std::forward<KeyT>(needle), std::forward<FuncT>(f), std::index_sequence<Is...>{});
        }

        template <typename ...Ts, typename FuncT>
        constexpr void for_each(const Map<Ts...> &, const FuncT &, std::index_sequence<>) noexcept
        {
        }

        template <typename ...Ts, typename FuncT, size_t I, size_t I2, size_t ...Is>
        constexpr void for_each(const Map<Ts...> &map, const FuncT &f, std::index_sequence<I, I2, Is...>) noexcept
        {
            f(std::get<I>(map), std::get<I2>(map));
            for_each(map, f, std::index_sequence<Is...>{});
        }
    }

    template <typename ...Ts, typename KeyT, typename FuncT>
    constexpr auto find(const Map<Ts...> &map, KeyT &&needle, FuncT &&f) noexcept
    {
        return details::find(map, std::forward<KeyT>(needle), std::forward<FuncT>(f), std::index_sequence_for<Ts...>{});
    }

    template <typename ...Ts, typename FuncT>
    constexpr void for_each(const Map<Ts...> &map, const FuncT &f) noexcept
    {
        details::for_each(map, f, std::index_sequence_for<Ts...>{});
    }

    template <typename ...Ts>
    constexpr auto makeMap(Ts &&...ts) noexcept
    {
        return Map<Ts...>(std::forward<Ts>(ts)...);
    }
}

^ Compile-Time Map, usefull for map of reflected_function or reflected_members

namespace shiva::refl
{
    template <typename T>
    using member_map_t = decltype(T::reflected_members());

    template <typename T>
    using function_map_t = decltype(T::reflected_functions());

    template <typename T>
    using class_name_t = decltype(T::class_name());

    template <typename T>
    using has_reflectible_members = meta::is_detected<member_map_t, T>;

    template <typename T>
    inline constexpr bool has_reflectible_members_v = has_reflectible_members<T>::value;

    template <typename T>
    using has_reflectible_functions = meta::is_detected<function_map_t, T>;

    template <typename T>
    inline constexpr bool has_reflectible_functions_v = has_reflectible_functions<T>::value;

    template <typename T>
    using has_reflectible_class_name = meta::is_detected<class_name_t, T>;

    template <typename T>
    inline constexpr bool has_reflectible_class_name_v = has_reflectible_class_name<T>::value;

    template <typename T>
    using is_reflectible = std::disjunction<has_reflectible_members<T>, has_reflectible_functions<T>>;

    template <typename T>
    inline constexpr bool is_reflectible_v = is_reflectible<T>::value;

    namespace details
    {
        constexpr std::string_view skipNamespaceName(const std::string_view v) noexcept
        {
            return (v[0] == ':' && v[1] == ':') ? std::string_view{v.data() + 2, v.length() - 2}
                                                : skipNamespaceName({v.data() + 1, v.length() - 1});
        }
    }

#define reflect_member(member)          shiva::refl::details::skipNamespaceName(pp_stringviewify(member)), member

#define reflect_function(func)          shiva::refl::details::skipNamespaceName(pp_stringviewify(func)), func

#define reflect_class(cls)                                                                 \
    static const std::string &class_name() noexcept                          \
    {                                                                                                       \
        static const std::string name = pp_stringify(cls);                       \
        return name;                                                                                 \
    }
}

^ Current reflection_module

Some usage:

namespace
{
    class i_dont_like_refl
    {
    };

    class i_like_refl
    {
    public:
        i_like_refl(int i, const std::string &s, double d) noexcept : _i(i), _s(s), _d(d)
        {
        }

    private:
        int _i;
        std::string _s;
        double _d;

    public:
        static constexpr auto reflected_members() noexcept
        {
            return shiva::meta::makeMap(
                reflect_member(&i_like_refl::_i),
                reflect_member(&i_like_refl::_s),
                reflect_member(&i_like_refl::_d)
            );
        }
    };

    struct i_have_a_reflectible_name
    {
        reflect_class(i_have_a_reflectible_name);
    };

    struct i_have_refl_member_functions
    {
        int func(int i)
        {
            return i;
        }

        static constexpr auto reflected_functions() noexcept
        {
            return shiva::meta::makeMap(reflect_function(&i_have_refl_member_functions::func));
        }
    };
}

TEST(reflection, traits)
{
    ASSERT_TRUE(shiva::refl::has_reflectible_members_v<i_like_refl>);
    ASSERT_FALSE(shiva::refl::has_reflectible_functions_v<i_like_refl>);

    ASSERT_TRUE(shiva::refl::has_reflectible_functions_v<i_have_refl_member_functions>);
    ASSERT_FALSE(shiva::refl::has_reflectible_members_v<i_have_refl_member_functions>);

    ASSERT_TRUE(shiva::refl::is_reflectible_v<i_like_refl>);
    ASSERT_TRUE(shiva::refl::is_reflectible_v<i_have_refl_member_functions>);
    ASSERT_FALSE(shiva::refl::is_reflectible_v<i_dont_like_refl>);

    ASSERT_TRUE(shiva::refl::has_reflectible_class_name_v<i_have_a_reflectible_name>);
    ASSERT_FALSE(shiva::refl::has_reflectible_class_name_v<i_like_refl>);
    ASSERT_FALSE(shiva::refl::has_reflectible_class_name_v<i_have_refl_member_functions>);
}

Every snippet of code of this comment compile under MSVC, Clang++ and GCC. But need a c++17 compiler. (MSVC 15.6) (clang++ 6.0) (GCC 7.1)

indianakernick commented 6 years ago

@Milerius Very interesting.

I've never seen that is_detected thing before. That's a neat way of implementing Concepts. I've always been fancinated by reflection in C++. I want to use it but my projects aren't really big enough to make all of the boiler plate worth it. With your system, you have to define reflected_functions and reflected_members for each class.

I've been doing this to get the names of classes:

#include <string_view>

namespace Utils {
  // there's a good reason why I'm using std::basic_string_view<char> instead of std::string_view
  // I just don't remember what it is!

  template <typename T>
  constexpr std::basic_string_view<char> typeName() {
    //clang   std::basic_string_view<char> Utils::typeName() [T = int]
    //gcc     constexpr std::basic_string_view<char> Utils::typeName() [with T = int]
    std::basic_string_view<char> name = __PRETTY_FUNCTION__;
    name.remove_prefix(name.find('='));
    //trimming "= "
    name.remove_prefix(2);
    //trimming "]"
    name.remove_suffix(1);
    return name;
  }

  namespace detail {
    class Dummy {};
  }

  static_assert(typeName<int>() == "int");
  static_assert(typeName<unsigned>() == "unsigned int");
  static_assert(typeName<detail::Dummy>() == "Utils::detail::Dummy");
}

It uses __PRETTY_FUNCTION__ which is non-standard but it means that I don't have to put the name of the class in the declaration of the class. Given this function, and a typelist of all of my components (generated by a python script), I can do this:

template <typename CompList>
void loadComponent(
  entt::DefaultPrototype &proto,
  const std::string_view name,
  const json &component
) {
  List::forEach<CompList>([&proto, name, &component] (auto t) {
    using Comp = LIST_TYPE(t);
    if (Utils::typeName<Comp>() == name) {
      if constexpr (hasFromjson<Comp>(0)) {
        proto.set<Comp>(component.get<Comp>());
      } else {
        proto.set<Comp>();
      }
    }
  });
}

template <typename CompList>
void loadProto(entt::DefaultPrototype &proto, const json &node) {
  const json::object_t &object = node.get_ref<const json::object_t &>();
  for (const auto &pair : object) {
    loadComponent<CompList>(proto, pair.first, pair.second);
  }
}

Then I define a flamethrower tower:

{
  "CommonTowerStats": {
    "range": 2,
    "damage": 3,
    "rof": 20,
    "armourPiercing": 0.2
  },
  "TowerGold": {
    "buy": 150,
    "sell": 100
  },
  "SplashTower": {
    "aoe": 2
  }
  // there are many more components here
}

Then when the player wants to place down a tower:

reg.get<BaseGold>().gold -= proto->get<TowerGold>().buy;
const uint32_t tower = proto->create();
reg.assign<Position>(tower, pos);

By the way, your game engine (Shiva) has an extremely similar name to my scripting language (Stela)! The same number of syllables (2), the same number of characters (5), same starting character (s), same ending character (a). Is Shiva an acronym for Scripting, Hardware, Input, Video, Audio? Because that would mean that both names are acronyms!

Milerius commented 6 years ago

@Kerndog73

Very interesting, but as you say __PRETTY_FUNCTION__ doesn't work on MSVC, and also you didn't skip the namespace as i do! but it's a great approach !

It's funny because i named my engine shiva only because i like the indoui god.

(With my reflection via class member functions, it serves precisely at the moment of registering via scripting!)

state_[entity_registry_.class_name()]["get_"s + Component::class_name() + "_component"s] = [](
                shiva::entt::entity_registry &self,
                shiva::entt::entity_registry::entity_type entity) {
                return std::ref(self.get<Component>(entity));
            };

the only reason why it is interesting to add the reflection directly in the class concerned is to be able to benefit from the type_traits and to know if the class is reflectible or not

Milerius commented 6 years ago

@Kerndog73 Maybe you have an idea on how to filter entities with runtime identifiers? All keeping a maximum of performance?

ArnCarveris commented 6 years ago

Registering type list manually is kind'a step backward, my opinion is to implement some kind of visitor pattern inside registry. There is example how rttr used with chaiscript.

Milerius commented 6 years ago

I prefer to use type_list, than typing in RTTI personally, especially that loop on all types of RTTR recorded without different behavior of different types is not very interesting. The example is functional, but does not push deep into every case.

25579198_10211610747964371_1091393437_o

http://ithare.com/

The main issue as the blogger of Ithare.com says is to be able to use modern C++ without all the time typing in the RTTI

I think maybe bad but it still seems to me that the creator of EnTT uses virtual only to make type erasure. And try to make the most of the templates, the inlining for maximum performance, it is sure that if EnTT had been designed as RTTR the performance would not have been the same. Maybe it will be harder, but I prefer to look more at compile-time reflection, after having what everyone thinks.

RTTR have also a problem, it's force user to use .cpp file for registering. But if i remember since few months RTTR have an option to disable RTTI

ArnCarveris commented 6 years ago

@Milerius

RTTR the performance would not have been the same

This not goal of library to have good runtime performance.

Maybe it will be harder, but I prefer to look more at compile-time reflection, after having what everyone thinks.

This right direction.

RTTR have also a problem, it's force user to use .cpp file for registering.

Actually this done as intended to reduce compile-time, that is very crucial for development.

But if i remember since few months RTTR have an option to disable RTTI

Yep, you right, there link.

skypjack commented 6 years ago

Wow, such an interesting discussion here. Sorry for being late with my first answer!
I read everything twice to be sure not to miss anything.

First of all, thank you all for your interest in this library.
It's time probably to push it around and attract even more users, because it's finally pretty mature for real world uses. Isn't it?

Below are my two cents on this topic.

While I like both the ideas presented above, I'm not that sure that we should develop one of them in terms of the other one.
I mean: we have reflection on one side and the need to iterate components at runtime somehow efficiently on the other side.
To solve the latter doesn't necessarily mean to use the former as a layer on top of which to evolve.

Apparently someone else already had the same problem and tried to solve it somehow (of course, it looks like Minecraft is using EnTT after all, who knows?).
I don't like much their solution because of several reasons that I won't discus here. Moreover, I don't want to use a proposal that hasn't been officially submitted upstream as a starting point for such a feature, but something along that line could solve the problem of iterating multiple components in a way that isn't tightly bound to the reflection system.

Of course, I agree on the fact that they are two parts of a bigger requirement. However it doesn't mean that we have to develop both of them as a whole.

For what it concerns, my thoughts before falling asleep:

Let's discuss this. It looks like we can go further with this discussion!!

Milerius commented 6 years ago

I totally agree with skypjack, the two ideas proposed at the end must be pushed because on the one hand we will not manage to push the plugins with EnTT without more possibility in runtime.

skypjack commented 6 years ago

I made some experiments with a sort of runtime view. Currently users can use it as:

registry.view(entt::runtime_t{}).each(first, last, [](const auto entity) {
    // ...
});

Where first and last are pointers to a range of types of components (see Registry::type).
This approach does no allocations, or at least it's demanded to user of the view to manage the memory for the list of types to use.
The view offers only the each member function, mainly because it has no types associated and therefore it cannot return a size or any other information.

The other way around is to create an empty view to which users can literally attach types one at a time. It could offer much more functionalities, with the drawback that it requires to allocate some space to work properly.
I've still to try it, so I cannot give any preview of the API.

I'm not sure about what's the best solution. I'm just experimenting right now.
In both cases, the view will return only the entities it contains. There is no way it can return also components as far as I can see.

Milerius commented 6 years ago

@skypjack

Hey ! Thank's

I understand registry.view(entt:runtime_t{}), but can you give some examples with fake component's for the each part ?

for example let's take a component Position and a component Speed, how you will loop through all entities which has theses components ?

skypjack commented 6 years ago

Pretty much as I shown above:

using component_type = typename entt::DefaultRegistry::component_type;

component_type types[2] = {
    registry.type<Position>(),
    registry.type<Speed>()
};

registry.view(entt::runtime_t{}).each(types, types+2, [&registry](const auto entity) {
    // ...
});

Obviously, in a runtime situation, types won't be a fixed size array but likely an std::vector or kind of. Btw, it's fine as long as you can pack types of components in a container to iterate.
Types are already available. It's just a matter of using them as identifiers. They are guaranteed to be unique because of how the registry works internally.

Milerius commented 6 years ago

@skypjack

I think after we can do something like this

// without overload
state["for_each_runtime"]([](shiva::entt::entity_registry &self, std::vector<comp_type> types, sol::function functor) {
                return self.view(entt::runtime_t{}).each(types.begin(), types.end(), functor);
            });
function fake_functor(current_entity)
   -- things...
end

local table_type = {shiva.entity_registry:layer_1_id(),
                               shiva.entity_registry:layer_2_id() }
shiva.entity_registry:for_each_runtime(table_type, fake_functor)
skypjack commented 6 years ago

The other way around could be something like this (not tested yet, not even sure I can implement it this way, but it should give you an idea):

auto view = registry.view(entt::runtime{});

view.set<Position>();
view.set<Speed>();

for(const auto entity: view) {
    // ...
}

It allocates internally to keep track somehow of the components of which to keep track. It won't offer an each member function.

Not sure what version is the best. The iterable view is more consistent with the current API. Usually each returns entities and components, so it could be misleading an each that does return only entities. On the other side, the second approach is probably a bit more verbose.

Comments?

Milerius commented 6 years ago

@skypjack

For sure the first approach ! We want a each functionnality !

skypjack commented 6 years ago

I tried the opposite approach. Nothing production ready yet, but I like much the resulting API:

auto view = registry.view(first, last);

for(const auto entity: view) {
    // ...
}

view.each([](const auto entity) {
    // ...
});

It's on the same line with the other views being iterable and by exposing the each member function at the same time. first and last are iterators to a range of types of components as in the previous example.
The sole drawback is that it allocates a vector having size (first - last) to store the pointers to the selected pools. Not a big problem and probably something definitely acceptable for a runtime tool.

As already mentioned, it's not that easy to return also the components during an each call. I'm not even sure that it's worth it actually for a runtime view.

Milerius commented 6 years ago

@skypjack

It's ok, since we have the entity we can easily get components in each lambda !

skypjack commented 6 years ago

Almost true. The other views make a lot of optimizations within the each member function to avoid indirections as much as possible and therefore are faster (much, much faster) than just iterating entities and getting components with a get. That being said, in this case it doesn't worth it, because the API would become complex and I'm not sure performance will benefit so much. It's a runtime tool after all, so one cannot even expect it to be as fast as all the other views, this is a fact.

skypjack commented 6 years ago

It looks like I found a way for the runtime views. All tests pass with no problems and the API is pretty much consistent with the ones of the other pools. They are iterable and offer the each member function, so I got the best from all the different ideas discussed above.

The sole differences with the other views are that:

I must admit that I'm pretty satisfied with the result. I already had a try a couple of months ago but I failed to find the right way or at least something that was good enough from my point of view.

Thank you all for this interesting discussion and all the ideas you put here. Really appreciated.

skypjack commented 6 years ago

Still to document it in the README file, but you can try the RuntimeView from the experimental branch. More during the next days. Feedback are appreciated.

--- EDIT

There are some bugs in this first draft. ie it could call front on an empty vector. BTW you can test it as long as you don't create views for no components (that doesn't make much sense actually).

Milerius commented 6 years ago

@skypjack Great ! when you will release it, could you make a new patch also, i will update vcpkg port at the same time !

skypjack commented 6 years ago

Sure. I'm a bit busy these days, so probably it will be early next week. BTW, not a problem to create a tag, it's time to do that anyway.

skypjack commented 6 years ago

Ready to test on experimental. Documentation updated. Waiting for a feedback.

Milerius commented 6 years ago

I will test it tommorow night it's ok? Can you release an experimental tag under experimental branch ?

skypjack commented 6 years ago

Actually it's ready, it's tested, there is no reason to wait. I can just merge it on master, tag it and create a patch release if some problems pop out from that. Right?

Milerius commented 6 years ago

alright !

skypjack commented 6 years ago

On master. Because you're dealing with a real world problem, can you confirm the API works fine for the most of the use cases? I like the iterator-way to define a range and it would work well with ie duktape, but this is the sole experience I have currently.

Milerius commented 6 years ago

I will, test it during the night !

Milerius commented 6 years ago

@skypjack as i say, i'm using EnTT through VCPKG which work's only with tag's i cannot test youre runtime view without a new tag!

skypjack commented 6 years ago

Yeah, sorry, I'm out of home until tomorrow. I'll create a tag in the evening when I'm back home.

DavidHamburg commented 6 years ago

@Milerius

@skypjack Great ! when you will release it, could you make a new patch also, i will update vcpkg port at the same time !

As far as I know they have a script to update entt automatically. Unfortunately this took several weeks last time.

https://github.com/Microsoft/vcpkg/pull/3287

Milerius commented 6 years ago

Yes but @skypjack need to create a tag atleast

Milerius commented 6 years ago

python scripting is now done in my engine, there is an example in constrast with lua for people who want reflection:

import shiva

def test_create_entity():
    result = shiva.ett_registry.create()
    assert result == 0, "result should be 0"
    assert shiva.ett_registry.nb_entities() == 1, "should be 1"
    return result

def test_destroy_entity():
    result = shiva.ett_registry.create()
    assert shiva.ett_registry.nb_entities() == 1, "should be 1"
    shiva.ett_registry.destroy(result)
    assert shiva.ett_registry.nb_entities() == 0, "should be 0"
    return True

def test_component():
    entity_id = shiva.ett_registry.create()
    component = shiva.ett_registry.add_layer_1_component(entity_id)
    same_component = shiva.ett_registry.get_layer_1_component(entity_id)
    assert shiva.ett_registry.layer_1_id() == 0, "should be 0"
    assert shiva.ett_registry.has_layer_1_component(entity_id), "should be true"
    shiva.ett_registry.remove_layer_1_component(entity_id)
    assert not shiva.ett_registry.has_layer_1_component(entity_id), "should be false"
    return True

def functor(entity_id):
    print(entity_id)
    shiva.ett_registry.destroy(entity_id)

def test_for_each():
    for i in range(1, 11):
        id = shiva.ett_registry.create()
        shiva.ett_registry.add_layer_1_component(id)
    assert shiva.ett_registry.nb_entities() == 10, "should be 10"
    shiva.ett_registry.for_each_entities_which_have_layer_1_component(functor)
    assert shiva.ett_registry.nb_entities() == 0, "should be 0"
skypjack commented 6 years ago

Does it use runtime views?

Milerius commented 6 years ago

no, i'm waiting for the patch to test runtime view ^^'

skypjack commented 6 years ago

Created tag v.2.7.0 - enjoy.

Milerius commented 6 years ago

god bless you <3

Milerius commented 6 years ago

No problem with runtilme view there is an example in lua which is working:

function simple_functor(entity_id)
    shiva.entity_registry:destroy(entity_id)
end

function test_for_each_runtime()
    for i = 1, 10
    do
        local id = shiva.entity_registry:create()
        if i == 4 then
            shiva.entity_registry:add_layer_3_component(id)
        else
            shiva.entity_registry:add_layer_1_component(id)
            shiva.entity_registry:add_layer_2_component(id)
        end
    end

    assert(shiva.entity_registry:nb_entities() == 10, "should be 10")

    local table_type = {
        shiva.entity_registry:layer_1_id(),
        shiva.entity_registry:layer_2_id()
    }
    shiva.entity_registry:for_each_runtime(table_type, simple_functor)
    assert(shiva.entity_registry:nb_entities() == 1, "should be 1")
    return true
end

cpp ->:

(*state_)[entity_registry_.class_name()]["for_each_runtime"] = [](shiva::entt::entity_registry &self,
                                                                              std::vector<comp_type> array,
                                                                              sol::function functor) {
                return self.view(std::cbegin(array), std::cend(array)).each([func = std::move(functor)](auto entity) {
                    func(entity);
                });
            };

bonus minibenchmark for_each_one_element (compile time + reflection) vs for_each_runtime:

capture d ecran 2018-07-01 a 22 12 44
skypjack commented 6 years ago

Good to know. EnTT is getting every day more flexible. Thank you for participating during the development, your help is so valueable!!