Game System Registry With Integrated Simple Unit Testing Framework

This system I will share today is roughly adopted from Guillaume Blanc’s Registrer(Registry) system[1]. This Registry system is designed to keep track all registered classes and objects in an easy, global, and manageable manner. This system can easily send events such as initialization, shutdown, unit testing, or anything else that you can imagine and each registered systems/objects can react to a specified event. If this sounds very familiar to you, chances are you’ve already heard this in your Design Patterns class. Yes, this is the dispatcher-listener design pattern. An object or a system (aka listener) registers to the Registry(aka dispatcher) and when the Registry signals a message, it sends a message to each and every registered listeners.

There are two types the Registry accepts. One is a class (static classes) and the second is an instantiated object. An interface class will play a big role on these two types in order for them to be accepted by the Registry. Code listing 1 shows the interface class.

Code Listing 1
class IRegistryObject : private NonCopyable
{
public:
    // Initialize object priority
    IRegistryObject( ERegistryPrimaryPriority::Type primaryPriority, ERegistrySecondaryPriority::Type secondaryPriority, BOOL bRegisterObject = TRUE );
    virtual ~IRegistryObject ( void );

protected:
    friend Registry;

    // Registry object operation handling
    virtual BOOL DoRegistryOperation( ERegistryOperation::Type op ) = 0;
    // Less than registry object comparison
    static BOOL IsLessPriority( const IRegistryObject *plhs, const IRegistryObject *prhs );

private:

    ERegistryPrimaryPriority::Type m_primaryPriority;
    ERegistrySecondaryPriority::Type m_secondaryPriority;
};

As you can see in Code Listing 1, the abstract base class needs the derived classes to define DoRegistryOperation(). DoRegistryOperation() method is the entry point for all the events sent by the Registry.

You may also have noticed that the constructor accepts two parameters named ERegistryPrimaryPriority and ERegistrySecondaryPriority. These enums defines the priority of the Registry Object from lowest to highest priority. These priorities are very important as they control the order of the registered objects to call. What you can do is probably set very important systems or classes having the highest priority without dependencies. Less important systems or systems that requires dependencies would obviously need to be set in a lower priority than the dependency it requires.

For registering an instantiated object, one would simply inherit the class with IRegistryObject then calling IRegistryObject’s constructor to automatically register it in the Registry.

For classes, it takes a little bit more of explaining to do. The problem with this is that, there are no actual objects to register in the Registry. So what we would do is create a class that encapsulates or connect the class in question in some way. Thus, we devised a macro to do exactly like that. Take a look at Code Listing 2.

Code Listing 2
#define PSX_START_REGISTRY_OBJECT( _class_, primaryPriority, secondaryPriority )    \\par     class RegistryObject##_class_ : public IRegistryObject    \\par     {    \\par     public:    \\par         RegistryObject##_class_##( void ) : IRegistryObject( primaryPriority, secondaryPriority ) { } \\par         virtual BOOL DoRegistryOperation( ERegistryOperation::Type op );    \\par     };    \\par     static RegistryObject##_class_ gRegistryObject##_class_##;    \\par     \\par     BOOL RegistryObject##_class_##::DoRegistryOperation( ERegistryOperation::Type op )    \\par     {    \\par         switch( op ) \\par         {

#define PSX_END_REGISTRY_OBJECT( )    \\par         default:    \\par         break; \\par         }    \\par         return TRUE;    \\par     }

#define PSX_REGISTRY_ON_INITIALIZE()    break; case ERegistryOperation::INITIALIZE:
#define PSX_REGISTRY_ON_SHUTDOWN()        break; case ERegistryOperation::SHUTDOWN:
#define PSX_REGISTRY_ON_TEST()            break; case ERegistryOperation::TEST:

Quite scary, no? Well, don’t be. These are just predefined preprocessors that basically replaces everything in code that finds the same name as the defined name. What these macro does is that it declares a new class, derived from IRegistryObject , and defines the DoRegistryOperation() method. The DoRegistryOperation() is actually empty and you’ll be the one to write code into it let be initialization, shutdown, or test code using the PSX_REGISTRY_ON…() macro declarations. Code Listing 3 shows a sample on how the macros are used.

Code Listing 3
PSX_START_REGISTRY_OBJECT( String, ERegistryPrimaryPriority::Normal, ERegistrySecondaryPriority::Normal )
    PSX_REGISTRY_ON_TEST()
    {
        String str1 = "Hello World.";
        PSX_Assert( str1.GetLength() == 12, "Length should be 12." );

        SIZE_T index = str1.FindFirstOf( "l" );
        PSX_Assert( index == 2, "Index should be 2." );

        index = str1.FindFirstNotOf( "H" );
        PSX_Assert( index == 1, "index should be 1." );

        String str2 = str1.SubString( index );
        PSX_Assert( PSX_StrCmp( str2.GetCString(), PSX_String("ello World.") ) == 0, "Substring method failed." );

        return TRUE;
    }
PSX_END_REGISTRY_OBJECT();

Code Listing 3 shows registering a String class to the Registry and defining a Unit Test code for code testing.[2] Please study Code Listing 2 and Code Listing 3 to fully grasp and understand what it is trying to do.

And lastly, I present to you the Registry class shown in Code Listing 4.

Code Listing 4
class Registry
{
public:

    static BOOL ExecuteOperation( ERegistryOperation::Type op );

private:

    static void RegisterObject( IRegistryObject *pObj );

    static void UnregisterObject( IRegistryObject *pObj );

private:

    friend IRegistryObject;
    friend RegisterClassRAII;
    friend class TestRAII;

    // Custom compare trait for sorting registry objects based on their priority
    template < typename T >
    class RegistryObjectLessThan
    {
    public:
        BOOL operator () ( const T &lhs, const T &rhs ) const
        {
            return IRegistryObject::IsLessPriority( lhs, rhs );
        }
    };

    typedef SortedLList< IRegistryObject *, RegistryObjectLessThan<IRegistryObject *> > ObjectList;

    static ObjectList *m_pObjectList;

};

There’s nothing much really to discuss here. Registry objects simply register themselves by calling Registry::RegisterObject and they will be registered in the Registry. UnregisterObject removes them from the Registry.

Last Thoughts and Further Improvements

You may have noticed that the Registry class uses a SortedList (SortedLList). It is basically a sorted linked-list. That being said, it won’t be a good idea to register lots and lots of instantiated objects. This system is designed to handle application/game systems at best. Such systems would be a memory manager, Sound System, Input System, Grpahics System, etc… If you want to make it handle a great amount of instantiated objects then you may have to change a more capable container such as a vector, hash-table or a map for example.

Implementation

Full implementation of the Registry system/class can be found here[3].

Bibliography

  1. Guillaume Blanc, Article source code, Registrer, Cross platform rendering thread, ShaderX7, 2009
  2. Pulse::String class, http://codaset.com/codesushi/pulse-tec/source/master/blob/Source/Engine/Include/String.h, http://codaset.com/codesushi/pulse-tec/source/master/blob/Source/Engine/Source/String.cpp
  3. Pulse::Registry class, http://codaset.com/codesushi/pulse-tec/source/master/blob/Source/Engine/Include/Registry.h, http://codaset.com/codesushi/pulse-tec/source/master/blob/Source/Engine/Source/Registry.cpp

Don’t forget a good dose of Mr. Louis Armstrong song!

What a Wonderful WOrld–Louis Armstrong

 

– MrCodeSushi

Advertisements