Как работает GetComponent() Unity?
я экспериментировал с созданием системы на основе компонентов, подобной Unity, но на C++. Мне интересно, как работает метод GetComponent (), который реализует Unity. Это очень мощная функция. В частности, я хочу знать, какой контейнер он использует для хранения своих компонентов.
два критерия, которые мне нужны в моем клоне этой функции, следующие. 1. Мне также нужно вернуть все унаследованные компоненты. Например, если SphereCollider наследует Collider, GetComponent () вернет SphereCollider, прикрепленный к GameObject, но GetComponent () не вернет прикрепленный коллайдер. 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)];
}
но опять же, это не соответствует первым критериям.
Если кто-нибудь знает, как объединить эти две функции, даже если это немного медленнее, чем второй пример, я был бы готов пожертвовать немного. Спасибо!
2 ответов
поскольку я пишу свой собственный игровой движок и включаю тот же дизайн, я подумал, что поделюсь своими результатами.
обзор
я написал свой собственный RTTI для классов, которые я хотел использовать как Components
моего GameObject
экземпляров. Количество набранных текста уменьшается на #define
ing два макроса:CLASS_DECLARATION
и CLASS_DEFINITION
CLASS_DECLARATION
объявляет уникальную static const std::size_t
, который будет использоваться для идентификации class
типа (Type
), и a virtual
функция, которая позволяет объекты для прохождения их class
иерархия, вызывая их функцию родительского класса с тем же именем (IsClassType
).
CLASS_DEFINITION
определяет эти две вещи. А именно:Type
инициализируется хэшем Строковой версии class
имя (используя TO_STRING(x) #x
), так что Type
сравнения-это просто сравнение int, а не сравнение строк.
std::hash<std::string>
используется хэш-функция, которая гарантирует равные входы, равные выходы и количество столкновений близок к нулю.
Помимо низкого риска хеширования, эта реализация имеет дополнительное преимущество, позволяя пользователям создавать свои собственные Component
классы, использующие эти макросы без необходимости ссылаться на / расширить некоторые master на enum class
S, или использовать typeid
(который предоставляет только тип времени выполнения, а не родительские классы).
AddComponent
этот пользовательский RTTI упрощает синтаксис вызова для Add|Get|RemoveComponent
просто указание template
тип, как и Unity.
на AddComponent
метод perfect-пересылает универсальный ссылочный пакет параметров variadic в конструктор пользователя. Так, например, определяемый пользователем Component
-производное 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
s, потому что perfect-forwarding может не связать, если использовать выведенный синтаксис инициализатора-списка, например { 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
is public
, объявлена для каждого Component
-производный класс ...и с большой буквы, чтобы подчеркнуть его гибкое использование, несмотря на то, что он является членом POD.
RemoveComponent
и наконец, RemoveComponent
использует C++14
' s init-capture to пройти то же самое static const std::size_t Type
на template
введите в лямбду, чтобы он мог в основном делать тот же обход вектора, на этот раз получая iterator
к первому соответствующему элементу.
в коде есть несколько комментариев об идеях для более гибкой реализации, не говоря уже о const
версии всего этого также могут быть легко реализованы.
в Код
классы.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 */
классы.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
, но мой вариант мне подходит сейчас.
извиняюсь, если это не то, что вы ищете, но у меня была идея использовать неупорядоченную карту с индексом типа и с помощью некоторого метапрограммирования и TR2 поместить несколько указателей на компонент в карту, включая его прямые базовые классы в качестве дополнительных ключей. Так что getComponent<SphereCollider>()
и getComponent<Collider>()
вместе с down-cast будет иметь тот же pointee.
#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
struct для рекурсии через базовые классы компонентов во время компиляции и вставки указатель (значение) с соответствующим классом (ключом) на каждой итерации. Вспомогательная структура direct_bases_as_tuple
был вдохновлен ответ Энди Проула чтобы изменить прямые базы в кортеж. Я скомпилировал это с помощью GCC 4.9.2, используя функции C++11.