I'm trying to create a nice modify function of an entity's components in my WIP small "game framework". However I'm stuck at creating the function when trying to modify more than one component (using packed parameters)
This is my function for a single component, which works fine and behaves as I like
template <typename C>
void mod_comp(Entity ent, std::function<void(C&)> cb) {
auto& c = get_comp<C>(ent);
return cb(c);
}
// Called like this
mng.mod_comp<Velocity>(ent, [](auto& vel) {
// Modify velocity force value
vel.f += 5;
});
When trying to create the function for multiple components I've found 2 problems that I cant figure out how to solve.
template <typename... C>
void mod_comp(Entity ent, std::function<void(C...)> cb) {
return cb(get_comp<C>(ent)...);
}
// Supposed to be called like this
mng.mod_comp<Velocity, Position>(ent, [](Velocity vel, Position pos) {
pos.x += vel.f;
});
But this gives a "no match error" even tho the packed parameter ...C
(Velocity, Position
) matches the parameters in the lambda (Velocity, Position
). I can't figure out of to fix this.
The main problem however is if the lambda parameters can be simplified like the single mod function like this([](auto& vel, auto& pos)
and also forward as references. I feel like this should be possible as nothing is unknown to the compiler but my C++ is limited.
You can do some sneaky tricks to get the signature of the lambda and use it to get the components.
template <typename Class, typename... Params>
void mod_comp_helper(Entity ent, Class *obj, void (Class::*fun)(Params...) const) {
(obj->*fun)(get_comp<std::decay_t<Params>>(ent)...);
}
// optional overload for mutable lambdas
template <typename Class, typename... Params>
void mod_comp_helper(Entity ent, Class *obj, void (Class::*fun)(Params...)) {
(obj->*fun)(get_comp<std::decay_t<Params>>(ent)...);
}
template <typename Functor>
void mod_comp(Entity ent, Functor &&fun) {
mod_comp_helper(ent, &fun, &std::decay_t<Functor>::operator());
}
// optional overload for function pointers
template <typename... Params>
void mod_comp(Entity ent, void(*fun)(Params...)) {
fun(get_comp<std::decay_t<Params>(ent)>...);
}
int main() {
mod_comp(ent, [](Velocity &vel, Position &pos) {
// modify components
});
// you can use std::function if you want
// although you probably don't need to
std::function<void(Velocity &, Position &)> fun = [](Velocity &vel, Position &pos) {
// modify components
};
mod_comp(ent, fun);
// this calls the function pointer overload
mod_comp(ent, +[](Velocity &vel, Position &pos) {
// modify components
});
}
A lambda expression is actually just syntax sugar for constructing a functor (an object with a call operator).
struct __anonymous_compiler_generated_class__ {
void operator()(int i) const {
// ...
}
};
int main() {
auto lambda = [](int i) {
// ...
};
// above is sugar for this:
auto functor = __anonymous_compiler_generated_class__{};
}
A lambda expression constructs a closure object. This object has an operator()
. We can take the address of the call operator and deduce its signature. Then we just std::decay_t
the parameter types to remove references and const-ness.
Another neat trick with lambdas is converting them to function pointers (which I showed in the first example). Non-capturing lambdas can be implicitly converted to function pointers. You can use a unary +
or a static_cast
to force this conversion. Here are a few more examples of that:
int main() {
auto lambda = [](int i) {};
void (*fnptr0)(int) = lambda;
auto fnptr1 = +lambda;
auto fnptr2 = static_cast<void(*)(int)>(lambda);
int capture;
auto capturing_lambda = [capture](int i) {};
// compiler says no
// auto fnptr3 = +capturing_lambda;
}
If you don't need capturing lambdas and can tolerate the unary +
then you could just use the function pointer overload of mod_comp
but for this, you probably want capturing lambdas.