Как работает Unity GetComponent()?

Я экспериментировал с созданием системы на основе компонентов, похожей на Unity, но на С++. Мне интересно, как метод GetComponent(), который Unity реализует, работает. Это очень мощная функция. В частности, я хочу знать, какой контейнер он использует для хранения своих компонентов.

Два критерия, которые мне нужны в моем клоне этой функции, следующие. 1. Мне нужны любые унаследованные компоненты, которые нужно вернуть. Например, если SphereCollider наследует коллайдер, GetComponent <Collider> () вернет SphereCollider, прикрепленный к GameObject, но GetComponent <SphereCollider> () не вернет прикрепленный коллайдер. 2. Мне нужно, чтобы функция была быстрой. Предпочтительно, он использовал бы какую-то хэш-функцию.

Для критериев один, я знаю, что я мог бы использовать что-то похожее на следующую реализацию

std::vector<Component*> components
template <typename T>
T* GetComponent()
{
    for each (Component* c in components)
        if (dynamic_cast<T>(*c))
            return (T*)c;
    return nullptr;
}

Но это не соответствует второму критерию бытия. Для этого я знаю, что могу сделать что-то вроде этого.

std::unordered_map<type_index, Component*> components
template <typename T>
T* GetComponent()
{
    return (T*)components[typeid(T)];
}

Но опять же, это не соответствует первым критериям.

Если кто-нибудь знает какой-то способ объединить эти две функции, даже если он будет немного медленнее второго примера, я бы хотел пожертвовать немного. Спасибо!

Ответ 1

Поскольку я пишу свой собственный игровой движок и включаю тот же дизайн, я решил поделиться своими результатами.

Обзор

Я написал собственный RTTI для классов, которые я хотел использовать как Components для моих экземпляров GameObject. Количество ввода сокращается на #define с помощью двух макросов: CLASS_DECLARATION и CLASS_DEFINITION

CLASS_DECLARATION объявляет уникальный static const std::size_t, который будет использоваться для идентификации функции class (Type) и virtual, которая позволяет объектам перемещаться по иерархии class, вызывая их родительские- функция класса с тем же именем (IsClassType).

CLASS_DEFINITION определяет эти две вещи. А именно, Type инициализируется хешем строковой версии имени class (используя TO_STRING(x) #x), так что сравнения Type - это просто сравнение int, а не сравнение строк.

std::hash<std::string> - используемая хэш-функция, которая гарантирует равные входы, дает равные выходы, а число столкновений почти нулевое.


Помимо низкого риска хеш-коллизий, эта реализация имеет дополнительное преимущество, позволяя пользователям создавать свои собственные классы Component, используя эти макросы, не имея при этом необходимости ссылаться на расширение главного файла include enum class s или используйте typeid (который предоставляет только тип времени выполнения, а не родительские классы).


AddComponent

Этот настраиваемый RTTI упрощает синтаксис вызова для Add|Get|RemoveComponent только для указания типа template, как и Unity.

Метод AddComponent совершенный - переводит универсальный эталонный пакет переменных параметров в конструктор пользователя. Так, например, пользовательский Component -derived class CollisionModel может иметь конструктор:

CollisionModel( GameObject * owner, const Vec3 & size, const Vec3 & offset, bool active );

а затем пользователь просто вызывает:

myGameObject.AddComponent<CollisionModel>(this, Vec3( 10, 10, 10 ), Vec3( 0, 0, 0 ), true );

Обратите внимание на явное построение Vec3, потому что безупречная пересылка может не связываться, если использовать выведенный синтаксис списка инициализаторов, например { 10, 10, 10 }, независимо от объявлений конструктора Vec3.


Этот настраиваемый RTTI также разрешает 3 проблемы с решением std::unordered_map<std::typeindex,...>:

  • Даже при обходе иерархии с использованием std::tr2::direct_bases конечный результат по-прежнему является дубликатом того же указателя на карте.
  • Пользователь не может добавить несколько компонентов эквивалентного типа, если не используется карта, которая разрешает/разрешает коллизии без перезаписывания, что еще больше замедляет работу кода.
  • Не требуется неопределенный и медленный dynamic_cast, просто прямой static_cast.

GetComponent

GetComponent просто использует static const std::size_t Type типа template как аргумент метода virtual bool IsClassType и выполняет итерацию над std::vector< std::unique_ptr< Component > > в поисках первого совпадения.

Я также внедрил метод GetComponents, который может получить все компоненты запрошенного типа, опять же, включая получение из родительского класса.

Обратите внимание, что к элементу static Type можно обращаться как с экземпляром класса, так и без него.

Также обратите внимание, что Type - public, объявленный для каждого класса Component -derived,... и заглавный, чтобы подчеркнуть его гибкое использование, несмотря на то, что он является членом POD.


RemoveComponent

Наконец, RemoveComponent использует C++14 init-capture, чтобы передать тот же самый static const std::size_t Type типа template в лямбда, чтобы он мог в основном совершать один и тот же обход вектора, на этот раз получив iterator первый элемент соответствия.


В коде есть несколько комментариев о идеях для более гибкой реализации, не говоря уже о const версиях всех этих функций, которые также могут быть легко реализованы.


Код

Classes.h

#ifndef TEST_CLASSES_H
#define TEST_CLASSES_H

#include <string>
#include <functional>
#include <vector>
#include <memory>
#include <algorithm>

#define TO_STRING( x ) #x

//****************
// CLASS_DECLARATION
//
// This macro must be included in the declaration of any subclass of Component.
// It declares variables used in type checking.
//****************
#define CLASS_DECLARATION( classname )                                                      \
public:                                                                                     \
    static const std::size_t Type;                                                          \
    virtual bool IsClassType( const std::size_t classType ) const override;                 \

//****************
// CLASS_DEFINITION
// 
// This macro must be included in the class definition to properly initialize 
// variables used in type checking. Take special care to ensure that the 
// proper parentclass is indicated or the run-time type information will be
// incorrect. Only works on single-inheritance RTTI.
//****************
#define CLASS_DEFINITION( parentclass, childclass )                                         \
const std::size_t childclass::Type = std::hash< std::string >()( TO_STRING( childclass ) ); \
bool childclass::IsClassType( const std::size_t classType ) const {                         \
        if ( classType == childclass::Type )                                                \
            return true;                                                                    \
        return parentclass::IsClassType( classType );                                       \
}                                                                                           \

namespace rtti {

//***************
// Component
// base class
//***************
class Component {
public:         

static const std::size_t                    Type;
virtual bool                                IsClassType( const std::size_t classType ) const { 
                                                return classType == Type; 
                                            }

public:

    virtual                                ~Component() = default;
                                            Component( std::string && initialValue ) 
                                                : value( initialValue ) { 
                                            }

public:

    std::string                             value = "uninitialized";
};

//***************
// Collider
//***************
class Collider : public Component {

    CLASS_DECLARATION( Collider )

public:

                                            Collider( std::string && initialValue ) 
                                                : Component( std::move( initialValue ) ) { 
                                            }
};

//***************
// BoxCollider
//***************
class BoxCollider : public Collider {

    CLASS_DECLARATION( BoxCollider )

public:

                                            BoxCollider( std::string && initialValue ) 
                                                : Collider( std::move( initialValue ) ) { 
                                            }
};

//***************
// RenderImage
//***************
class RenderImage : public Component {

    CLASS_DECLARATION( RenderImage )

public:

                                            RenderImage( std::string && initialValue ) 
                                                : Component( std::move( initialValue ) ) { 
                                            }
};

//***************
// GameObject
//***************
class GameObject {
public:

    std::vector< std::unique_ptr< Component > > components;

public:

    template< class ComponentType, typename... Args >
    void                                    AddComponent( Args&&... params );

    template< class ComponentType >
    ComponentType &                         GetComponent();

    template< class ComponentType >
    bool                                    RemoveComponent();

    template< class ComponentType >
    std::vector< ComponentType * >          GetComponents();

    template< class ComponentType >
    int                                     RemoveComponents();
};

//***************
// GameObject::AddComponent
// perfect-forwards all params to the ComponentType constructor with the matching parameter list
// DEBUG: be sure to compare the arguments of this fn to the desired constructor to avoid perfect-forwarding failure cases
// EG: deduced initializer lists, decl-only static const int members, 0|NULL instead of nullptr, overloaded fn names, and bitfields
//***************
template< class ComponentType, typename... Args >
void GameObject::AddComponent( Args&&... params ) {
    components.emplace_back( std::make_unique< ComponentType >( std::forward< Args >( params )... ) );
}

//***************
// GameObject::GetComponent
// returns the first component that matches the template type
// or that is derived from the template type
// EG: if the template type is Component, and components[0] type is BoxCollider
// then components[0] will be returned because it derives from Component
//***************
template< class ComponentType >
ComponentType & GameObject::GetComponent() {
    for ( auto && component : components ) {
        if ( component->IsClassType( ComponentType::Type ) )
            return *static_cast< ComponentType * >( component.get() );
    }

    return *std::unique_ptr< ComponentType >( nullptr );
}

//***************
// GameObject::RemoveComponent
// returns true on successful removal
// returns false if components is empty, or no such component exists
//***************
template< class ComponentType >
bool GameObject::RemoveComponent() {
    if ( components.empty() )
        return false;

    auto & index = std::find_if( components.begin(), 
                                    components.end(), 
                                    [ classType = ComponentType::Type ]( auto & component ) { 
                                    return component->IsClassType( classType ); 
                                    } );

    bool success = index != components.end();

    if ( success )
        components.erase( index );

    return success;
}

//***************
// GameObject::GetComponents
// returns a vector of pointers to the the requested component template type following the same match criteria as GetComponent
// NOTE: the compiler has the option to copy-elide or move-construct componentsOfType into the return value here
// TODO: pass in the number of elements desired (eg: up to 7, or only the first 2) which would allow a std::array return value,
// except there'd need to be a separate fn for getting them *all* if the user doesn't know how many such Components the GameObject has
// TODO: define a GetComponentAt<ComponentType, int>() that can directly grab up to the the n-th component of the requested type
//***************
template< class ComponentType >
std::vector< ComponentType * > GameObject::GetComponents() {
    std::vector< ComponentType * > componentsOfType;

    for ( auto && component : components ) {
        if ( component->IsClassType( ComponentType::Type ) )
            componentsOfType.emplace_back( static_cast< ComponentType * >( component.get() ) );
    }

    return componentsOfType;
}

//***************
// GameObject::RemoveComponents
// returns the number of successful removals, or 0 if none are removed
//***************
template< class ComponentType >
int GameObject::RemoveComponents() {
    if ( components.empty() )
        return 0;

    int numRemoved = 0;
    bool success = false;

    do {
        auto & index = std::find_if( components.begin(), 
                                        components.end(), 
                                        [ classType = ComponentType::Type ]( auto & component ) { 
                                        return component->IsClassType( classType ); 
                                        } );

        success = index != components.end();

        if ( success ) {
            components.erase( index );
            ++numRemoved;
        }
    } while ( success );

    return numRemoved;
}

}      /* rtti */
#endif /* TEST_CLASSES_H */

Classes.cpp

#include "Classes.h"

using namespace rtti;

const std::size_t Component::Type = std::hash<std::string>()(TO_STRING(Component));

CLASS_DEFINITION(Component, Collider)
CLASS_DEFINITION(Collider, BoxCollider)
CLASS_DEFINITION(Component, RenderImage)

main.cpp

#include <iostream>
#include "Classes.h"

#define MORE_CODE 0

int main( int argc, const char * argv ) {

    using namespace rtti;

    GameObject test;

    // AddComponent test
    test.AddComponent< Component >( "Component" );
    test.AddComponent< Collider >( "Collider" );
    test.AddComponent< BoxCollider >( "BoxCollider_A" );
    test.AddComponent< BoxCollider >( "BoxCollider_B" );

#if MORE_CODE
    test.AddComponent< RenderImage >( "RenderImage" );
#endif

    std::cout << "Added:\n------\nComponent\t(1)\nCollider\t(1)\nBoxCollider\t(2)\nRenderImage\t(0)\n\n";

    // GetComponent test
    auto & componentRef     = test.GetComponent< Component >();
    auto & colliderRef      = test.GetComponent< Collider >();
    auto & boxColliderRef1  = test.GetComponent< BoxCollider >();
    auto & boxColliderRef2  = test.GetComponent< BoxCollider >();       // boxColliderB == boxColliderA here because GetComponent only gets the first match in the class hierarchy
    auto & renderImageRef   = test.GetComponent< RenderImage >();       // gets &nullptr with MORE_CODE 0

    std::cout << "Values:\n-------\ncomponentRef:\t\t"  << componentRef.value
              << "\ncolliderRef:\t\t"                   << colliderRef.value    
              << "\nboxColliderRef1:\t"                 << boxColliderRef1.value
              << "\nboxColliderRef2:\t"                 << boxColliderRef2.value
              << "\nrenderImageRef:\t\t"                << ( &renderImageRef != nullptr ? renderImageRef.value : "nullptr" );

    // GetComponents test
    auto allColliders = test.GetComponents< Collider >();
    std::cout << "\n\nThere are (" << allColliders.size() << ") collider components attached to the test GameObject:\n";
    for ( auto && c : allColliders ) {
        std::cout << c->value << '\n';
    }

    // RemoveComponent test
    test.RemoveComponent< BoxCollider >();                              // removes boxColliderA
    auto & boxColliderRef3      = test.GetComponent< BoxCollider >();   // now this is the second BoxCollider "BoxCollider_B"

    std::cout << "\n\nFirst BoxCollider instance removed\nboxColliderRef3:\t" << boxColliderRef3.value << '\n';

#if MORE_CODE
    // RemoveComponent return test
    int removed = 0;
    while ( test.RemoveComponent< Component >() ) {
        ++removed;
    }
#else
    // RemoveComponents test
    int removed = test.RemoveComponents< Component >();
#endif

    std::cout << "\nSuccessfully removed (" << removed << ") components from the test GameObject\n";

    system( "PAUSE" );
    return 0;
}

Выход

    Added:
    ------
    Component       (1)
    Collider        (1)
    BoxCollider     (2)
    RenderImage     (0)

    Values:
    -------
    componentRef:           Component
    colliderRef:            Collider
    boxColliderRef1:        BoxCollider_A
    boxColliderRef2:        BoxCollider_A
    renderImageRef:         nullptr

    There are (3) collider components attached to the test GameObject:
    Collider
    BoxCollider_A
    BoxCollider_B


    First BoxCollider instance removed
    boxColliderRef3:        BoxCollider_B

    Successfully removed (3) components from the test GameObject

Боковое примечание: предоставлено Unity использует Destroy(object), а не RemoveComponent, но моя версия теперь соответствует моим потребностям.

Ответ 2

Извините, если это не то, что вы ищете, но у меня возникла идея использовать неупорядоченную карту с индексом типа, и с помощью некоторых метапрограммирования и TR2 поместите несколько указателей на компонент на карту, включая его базовые классы в качестве дополнительных ключей. Таким образом, getComponent<SphereCollider>() и getComponent<Collider>() вместе с down-cast будут иметь одинаковый указатель.

#include <tr2/type_traits>
#include <tuple>
#include <typeindex>
#include <unordered_map>
#include <iostream>

class Component {
public:
  virtual ~Component() {}
};

class GameObject {
public:
  template <typename T>
  void addComponent(T *component);

  template <typename T>
  T *getComponent();

  std::unordered_map<std::typeindex, Component *> components;
};

template <typename>
struct direct_bases_as_tuple {};

template <typename... Types>
struct direct_bases_as_tuple<std::tr2::__reflection_typelist<Types...>> {
  typedef std::tuple<Types...> type;
};

template <std::size_t N, typename ComponentBases, typename ComponentType>
struct AddComponent {
  GameObject *owner;

  explicit AddComponent(GameObject *owner) : owner(owner) {}

  void operator()(ComponentType *component) {
    AddComponent<N-1, ComponentBases, ComponentType>{owner}(component);

    using BaseType = std::tuple_element<N-1, ComponentBases>::type;

    owner->components[typeid(BaseType)] = component;
  }
};

template <typename ComponentBases, typename ComponentType>
struct AddComponent<0u, ComponentBases, ComponentType> {
  GameObject *owner;

  explicit AddComponent(GameObject *owner) : owner(owner) {}

  void operator()(ComponentType *component) {
    return;
  }
};

template <typename T>
void GameObject::addComponent(T *component) {
  using ComponentBases = direct_bases_as_tuple<std::tr2::direct_bases<ComponentType>::type>::type;

  constexpr classCount = std::tuple_size<ComponentBases>::value;

  AddComponent<classCount, ComponentBases, T>{this}(component);

  components[typeid(T)] = component;
}

template <typename T>
T * GameObject::getComponent() {
  auto iter = components.find(typeid(T));

  if (iter != std::end(components)) {
    return dynamic_cast<T *>(iter->second);
  }

  return nullptr;
}

class Collider : public Component {};
class SphereCollider : public Collider {};

int main() {
  GameObject gameObject;
  gameObject.addComponent(new SphereCollider);

  //get by derived class
  SphereCollider *sphereColliderA = gameObject.getComponent<SphereCollider>();

  //get by subclass
  SphereCollider *sphereColliderB = dynamic_cast<SphereCollider *>(
    gameObject.getComponent<Collider>()
  );

  if (sphereColliderA == sphereColliderB) {
    std::cout << "good" << std::endl;
  }
}

Я создал структуру AddComponent для рекурсии через базовые классы компонентов во время компиляции и вставляет указатель (значение) с соответствующим классом (ключом) на каждую итерацию. Вспомогательная структура direct_bases_as_tuple была вдохновлена ​​ответом Andy Prowl, чтобы изменить прямые базы в кортеж. Я скомпилировал это с использованием GCC 4.9.2 с использованием возможностей С++ 11.