Can I gain access to a “component” by type?

2019-05-15 12:32发布

I have a class like this:

class Component1 {...};
class Component2 {...};
class Component3 {...};

class Entity
{
  Component1 c1;
  Component2 c2;
  Component3 c3;
public:
  Component1& get_c1() { return c1;}
  Component2& get_c2() { return c2;}
  Component3& get_c3() { return c3;}
};

Basically the Entity is a container of all possible types of components (with other stuff, too). My problem is that I have more than 15 different components and I don't like to copy&paste lines this way. I'm looking for something like:

myEntity.get<Component1>();

to obtain the component I need. I took a look at boost::tuple which is cool but it allows access using an integer as key. I could use a public static const integer in each Component* class and gain access like this:

myEntity.get<Component1::id>();

but then I have to make sure to use different ids for each component and that's bad for mantainance.

Is there a way to "map" a type to a value of that type using magic (i.e. templates), so that myEntity.get<Component1>() works as expected?

I'd also like to have O(1) access to a component since the myEntity::get<T> is used very often (not that with 15-20 components makes sense talking about complexity anyway) but that's not mandatory.

8条回答
Bombasti
2楼-- · 2019-05-15 13:01

It might be possible to use a CRTP-based solution.

template<typename Component> struct comp_internal {
    template<typename T> T& GetComponent();
};

template<typename Component> struct comp : public comp_internal {
    Component component;
public:
    Component& GetComponent<Component>() {
        return component;
    }
};

class Entity : public comp<Component1>, public comp<Component2> {
};

Note that I aven't actually tried this, but I think it should work. However, spamming get() functions like this typically shows that, well, your class design is really kind of poor.

查看更多
Anthone
3楼-- · 2019-05-15 13:01

In many situation, this haves drawbacks, but in you case, may be a "selective decay" can suffice:

class Entity
{
  Component1 c1;
  Component2 c2;
  Component3 c3;
public:
  operator Component1*() { return &c1;}
  operator Component2*() { return &c2;}
  operator Component3*() { return &c3;}
  template<class X> operator X*() { return 0; }
};

right now, you can use * as a "component selector" as

Entity* pe = ... //whatever gets you access to an Entity;
Component1* p1 = *pe; //will use operator Component1*()
Component4* p4 = *pe; //will use operator X*()
if(p1) { /* component1 exist */ }
if(p4) { /* component 4 exist */ }
查看更多
我想做一个坏孩纸
4楼-- · 2019-05-15 13:03

If you make your components available to everyone and their dog anyways, why not simply make them public? Mission accomplished without copy&paste.

查看更多
闹够了就滚
5楼-- · 2019-05-15 13:13

I very nearly asked the same question: for me boost::fusion::map or boost::fusion::set are overkill and I really don't like the extra-long template parameter lists and having to set a macro if I have more than 10 in my container. I went with something like this:

template <class T>
struct Holder
{
    T t;
};

struct A {};
struct B {};

struct Aggregate
    :
    Holder<A>, 
    Holder<B>  // add as many more as you need here
{
    template <class T>
    T &get()
    {
        return this->Holder<T>::t;
    }
};

Aggregate a;
a.get<A>();
a.get<B>();
查看更多
Root(大扎)
6楼-- · 2019-05-15 13:13

Look at using typeindex and typeid. You could add components to a map by template type with its typeid being the map key. You can then get components from the map by type.

#include <unordered_map>
#include <memory>
#include <typeindex>
#include "component.h"
class GameObject
{
public:
virtual ~GameObject(){}

template < typename T >
std::shared_ptr< T > GetComponent( void )
{
    auto it = m_component.find( typeid( T ) );

    if( it != m_component.end() )
        return std::dynamic_pointer_cast< T >( it->second );

    return nullptr;
}

protected:
template< typename T >
void AddComponent( void )
{
    static_assert( std::is_base_of< Component, T >::value, "Non-component class cannot be added!" );
    m_component[ typeid( T ) ] = std::static_pointer_cast< Component >( std::make_shared< T >() );
}

private:
std::unordered_map< std::type_index, std::shared_ptr< Component >>  m_component;
};
查看更多
smile是对你的礼貌
7楼-- · 2019-05-15 13:14

You could do it like this:

class Entity {
public:
    template<typename Component>
    Component&
    get();

private:
    // convenience typedef since you mention 15+ components
    typedef boost::tuple<Component1, Component2, Component3> tuple_type;
    tuple_type tuple; // store components in a tuple

    template<typename Tuple, typename Key>
    struct lookup;
};

template<typename Tuple, typename Key>
struct Entity::lookup {
    /*
     * is_same is from the Boost TypeTraits library
     */
    static const int value =
        boost::is_same<typename Tuple::head_type, Key>::value ?
            0 :
            1 + lookup<typename Tuple::tail_type, Key>::value;

};

/*
 * still need an explicit specialization to end the recursion because the above
 * will eagerly instantiate lookup<boost::tuples::null_type, Key> even when
 * the key is found
 */
template<typename Key>
struct Entity::lookup<boost::tuples::null_type, Key> {
    static const int value = 0;
};

template<typename Component>
Component&
Entitiy::get()
{
    return boost::get<lookup<tuple_type, Component>::value>(tuple);
}

This does a linear lookup but that's O(n) in compile-time (actually in terms of template instantiations) only; it's O(1) in runtime so perhaps that's acceptable to you. Note that some compilers have O(n) template lookup so you may end up in O(n^2) compile-time; I believe C++11 will require that compilers do constant-time template lookup. You can also avoid some instantiations by not eagerly instantiating the recursion, e.g. using Boost.MPL. I avoided this for brevity and clarity.

The above relies on advanced features of Boost Tuple which aren't available for std::tuple (C++11). However I believe it wouldn't be too hard to implement lookup in C++11 using variadic templates (left as an exercise to the reader ;). You'd avoid the eager instantiation without using Boost.MPL, too.

Other remarks:

  • This requires that each component be of a different type.
  • Inside your member functions you'll lose easy access to each component since you can't name them directly but have to resort to calling get. I suppose you could still use them as individual members, and use a tie-tuple inside Entity::get to return the proper reference. This would come at a small cost to maintenance (change Entity::get everytime you add/remove a component). This also left as an exercise to the reader (don't forget to take into account that the new keys will be of the form Component&!).
查看更多
登录 后发表回答