Closed Milerius closed 6 years ago
I would be very interested in seeing a reflection system integrated with EnTT.
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. :-)
@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)
@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!
@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
@Kerndog73 Maybe you have an idea on how to filter entities with runtime identifiers? All keeping a maximum of performance?
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.
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.
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
@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.
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:
Reflection can help to easily export functionalities between layers (ie from C++ to Lua). We can refine the idea proposed by @Milerius but the purpose is quite clear.
Runtime support in general can help to offer a performance wise approach to all users that want to integrate a plug-in system with EnTT
(or kind of). We can already do that with a bit of machinery. However, I agree on the fact that something built-in would be easier to use.
Let's discuss this. It looks like we can go further with this discussion!!
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.
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.
@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 ?
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, [®istry](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.
@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)
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?
@skypjack
For sure the first approach ! We want a each functionnality !
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.
@skypjack
It's ok, since we have the entity we can easily get components in each lambda !
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.
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:
each
, as expected (it won't return components any time soon, so this isn't something I want to discuss further - use a registry for 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.
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).
@skypjack Great ! when you will release it, could you make a new patch also, i will update vcpkg port at the same time !
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.
Ready to test on experimental
. Documentation updated. Waiting for a feedback.
I will test it tommorow night it's ok? Can you release an experimental tag under experimental branch ?
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?
alright !
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.
I will, test it during the night !
@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!
Yeah, sorry, I'm out of home until tomorrow. I'll create a tag in the evening when I'm back home.
@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.
Yes but @skypjack need to create a tag atleast
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"
Does it use runtime views?
no, i'm waiting for the patch to test runtime view ^^'
Created tag v.2.7.0 - enjoy.
god bless you <3
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:
Good to know. EnTT
is getting every day more flexible. Thank you for participating during the development, your help is so valueable!!
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 thesol2
library, but could very well be done withpython
andpybind
for example.Here is an example in lua of how we use entt:
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++:
What a component look like with compile-time reflection:
Create a meta::type_list and register 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:
which is an equivalent of something like this
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.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 withEnTT
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 throughEnTT
and asEnTT
is moving toC++ 17
we can take advantage ofif 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.