c++ - 在 Unity 中用 C++ 实现组件系统

标签 c++ unity3d data-structures

我一直在尝试制作一个类似于 Unity 的基于组件的系统,但使用 C++。我想知道如何 GetComponent() Unity 实现的方法有效。这是一个非常强大的功能。具体来说,我想知道它使用什么样的容器来存储其组件。
我在此函数的克隆中需要的两个标准如下。 1. 我还需要返回任何继承的组件。例如,如果 SphereCollider继承对撞机,GetComponent<Collider>()将返回 SphereCollider附在GameObject ,但是 GetComponent<SphereCollider>()不会返回任何 Collider随附的。 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)];
}
但同样,这不符合第一个标准。
如果有人知道结合这两个功能的某种方法,即使它比第二个示例慢一点,我也愿意牺牲一点。谢谢!

最佳答案

由于我正在编写自己的游戏引擎并采用相同的设计,因此我想我会分享我的结果。
概述
我为我想用作 Components 的类编写了自己的 RTTI。我的 GameObject实例。打字量减少#define两个宏:CLASS_DECLARATIONCLASS_DEFINITIONCLASS_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 (仅提供运行时类型,不提供父类)。

添加组件
此自定义 RTTI 简化了 Add|Get|RemoveComponent 的调用语法仅指定 template类型,就像 Unity 一样。AddComponent方法完美地将通用引用可变参数包转发到用户的构造函数。因此,例如,用户定义的 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 因为如果使用推导的初始化列表语法(如 { 10, 10, 10 }),完美转发可能无法链接不管 Vec3的构造函数声明。

此自定义 RTTI 还解决了 std::unordered_map<std::typeindex,...> 的 3 个问题解决方案:
  • 即使使用 std::tr2::direct_bases 进行层次结构遍历最终结果仍然是 map 中相同指针的重复项。
  • 用户不能添加多个等效类型的组件,除非使用允许/解决冲突而不覆盖的映射,这会进一步减慢代码速度。
  • 没有不确定和缓慢dynamic_cast需要,直接static_cast .

  • 获取组件GetComponent只使用 static const std::size_t Typetemplate type 作为 virtual bool IsClassType 的参数方法并迭代 std::vector< std::unique_ptr< Component > >寻找第一场比赛。
    我还实现了一个 GetComponents可以获取请求类型的所有组件的方法,同样包括从父类获取。
    请注意 static成员(member)Type可以在有和没有类的实例的情况下访问。
    另请注意 Typepublic , 为每个 Component 声明- 派生类,...并大写以强调其灵活使用,尽管它是 POD 成员。

    移除组件
    最后,RemoveComponent用途 C++14的 init-capture 传递相同的 static const std::size_t Typetemplate输入一个 lambda,所以它基本上可以进行相同的 vector 遍历,这次得到一个 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)
    

    主程序
    #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 ,但我的版本现在适合我的需求。

    关于c++ - 在 Unity 中用 C++ 实现组件系统,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/44105058/

    相关文章:

    c++ - 在原生c++中查找直接或间接使用特定方法的所有方法

    c# - Unity 180 旋转 Texture2D,或者翻转两者

    java - 自动提示 : Substring matching

    c++ - fstream 的问题读取正常但不写入用户输入字符串

    c++ - SWIG:仅使用 header 和共享库为 Perl 包装 C++,无法定位可加载对象错误

    c++ - 为什么我的堆栈大小会根据构建版本是 Release 还是 Debug 而有所不同?

    c++ - C++应用程序中的非永久巨大外部数据存储

    function - 在 CG Shader 中为 unity3d 的两个 channel 共享一个函数

    ios - 如何在 Unity 导出的 iOS App 中使用 Swift Pod

    c++ - 为什么我不能为指向 null 的指针设置数据值