void ExampleFunc( void )
{
object *GO = new SomeGameObjectType( );
// Who owns this object?
// What headers do I need to include?
}
The above code does work, although it creates a giant mess of file dependencies. Suddenly one would require a header file for all the various object definitions in order to use the new operator everywhere. Furthermore, handling of attaching the object to some sort of owner will have to be done everywhere that the new keyword is used.
A factory should be used to create game objects in a unified way. Here's an example of the simplest form of an object factory:
#include "RedBlock.h"
#include "BlueEnemy.h"
#include "FireBall.h"
// ID is an enumeration
object *CreateObject( ID type )
{
switch( ID )
{
case REDBLOCK:
return new RedBlock( );
case BLUENEMY:
return new BlueEnemy( );
case FIREBALL:
return new FireBall( );
}
}
However there are some problems with this type of factory. One is that the factory will still have annoying file dependencies requiring the user to make another include, and modify the switch statement each time a new game object is designed. Examine the following:
// Map strings to object types
void ExampleFunc( void )
{
object *GO = CreateObject( "RedBlock" );
}
The above example shows the usage of the object factory when strings are mapped to types of objects. This setup is the ideal interface one would desire, as a single include to the ObjectFactory header is all that is required to create any type of object. The previous examples were mapping integers of an enumeration type to object types, which would require modification to the switch statement run in the enum, as well as manual updating to the enumeration itself.
So now that we've identified the ideal interface for creating game objects, we still need to make sure that the game objects created have a clear owner. Mapping of strings to game object types will need to be implemented, and cutting down on file dependencies of the the GameObjectFactory file itself is still something to be solved as well. By using a map (perhaps std::map) or hash table we can use a string identifier to fetch some value. Here's what we want our factory to look like:
object *CreateObject( char *type )
{
return map[type]->Create( );
}
By mapping a string to a creator function of the type of object desired, all file dependencies except that of those required to utilize the table (map in the above example) are eliminated. Furthermore, whenever a new game object type is designed, all that is needed to be done to ensure compatibility with our factory is a single registration call.
There are some details about the Create function that should be discussed. The Create function is specific to each type of game object, and the interface for each Create function needs to be identical. To ensure this, each type of game object should have a single class called the creator. The creator object for each class derives from a base Creator class to ensure a clear and consistent interface.
// Base class
class Creator
{
virtual object *Create( void );
virtual ~Creator( void );
}
// In another file...
// Specific derived creator for a single object type
class RedBlockCreator : public Creator
{
virtual object *Create( void )
{
return new RedBlock( );
}
}
// In another file...
// Register all game object creators in a single place in a unified way!
void RegisterCreators( void )
{
map->register( "RedBlock", new RedBlockCreator( ) );
// Or use a macro like I did:
REGISTER_CREATOR( RedBlock );
}
What we've now created is called a distributed factory. All file dependencies are hidden by registration in using a hash table data structure to provide the mapping of typeless strings to actual types of game objects. The creation of objects has a unified interface, and there's a single access point (the object factory) that allows the creation of all types of game objects that are registered within its data table of types. There's one last small problem to solve: who owns the created objects? This answer can of course vary, but luckily there can be a single point of object management, like so:
object *CreateObject( char *type )
{
object *newObject = NULL;
newObject = map[type]->Create( );
// Single point of management
... any other code necessary, like init, counting, etc.
ObjectList->Insert( newObject );
return newObject;
}
And there we have it! A properly structured object factory. To further the awesome might of the factory, one could easily add in deserialization to the factory, since the factory is already working with abstract strings as type identifiers. Serialization, however, is for a future post!
Factories like this also interface very well with scripting languages :)
Additional reading:
C++ for Game Programmers: Noel Llopis - Chapter 13
This is also called the pluggable factory pattern. But you've left out the important bit: how to avoid the "static initialization order fiasco" (http://www.parashift.com/c++-faq/static-init-order.html)
ReplyDeleteIsn't this avoided by making sure construction of the creators happens on function call?
DeleteI don't think this is relevant because the Object factory is dynamically creating objects on the heap, whereas the fiasco linked to seems to apply to objects constructed on the stack that are created when the program begins.
ReplyDelete