Tuesday, August 1, 2017

Game Engine Design 101: The Factory Design Pattern

Hey all,

Wow, it's been a while since I made a tutorial / learning series.

But I'm back, and I'm taking a 15 minute break from coding.

While coding up something, have you ever noticed that it would be nice to be able to create objects, but not worry about the specifics of the object you are handling? Maybe you would like to create an object "for now", but it may change later, and you don't want to rewrite all of the sections that use your object? Do you want to centralize all of the logic of creating objects of a specific type?

Good news! There's a design pattern for you!

Known as the Factory Design Pattern, this pattern allows you to specify the way in which you create objects, and let you not worry about the specifics of creating an object nor the specifics of what kind of object you are handling, so long as the objects derive from the same base. Another benefit is that all the logic to create derived types is in one place, rather than strewn about your code base.

All examples pulled from: JFramework

An example: Let's say you want to create a material (for chemistry stuff, think gold, steel, cloth, etc.), but you may create different kinds of materials based on certain parameters.

We start with a basic ChemistryMaterial class. This class can be derived from, or used as is.

#ifndef __JFramework__ChemistryMaterial__
#define __JFramework__ChemistryMaterial__

#include "Component.h"
#include "Common.h"
#include "ChemistryManager.h"

class ChemistryMaterial : public Component
{
private:
  // Members

public:
  ChemistryMaterial(ChemistryManager* aManager);
  virtual ~ChemistryMaterial();
  
  // Virtuals derived from Component
  virtual void Update();
  virtual void SendMessage(Message const& aMessage);
  virtual void ReceiveMessage(Message const& aMessage);
  virtual void Serialize(Parser& aParser);
  virtual void Deserialize(Parser& aParser);
};

#endif // __JFramework__ChemistryMaterial__
If you want different behaviors for a ChemistryMaterial, just derive from this class.

Next, we create a Factory class as an abstract class, which means in order to use these methods you need to derive from this class and implement them yourself, there's a reason for this.

What if you want different kinds of Factories to have different behaviors, but still be usable in the same places in the code base? That's why.

In JFramework's case, I can't possibly account for the numerous types of Factories, with distinct behaviors, that a user may want to make, and purposefully leave it open ended by allowing the user to create, implement, and change Factories within the engine.

#ifndef __JFramework_ChemicalFactory_h_
#define __JFramework_ChemicalFactory_h_

#include "ChemistryMaterial.h"
#include "ChemistryManager.h"

class ChemicalFactory
{
public:
  ChemicalFactory() {}
  virtual ~ChemicalFactory() {}
  
  virtual ChemistryMaterial* CreateMaterial(ChemistryManager *aManager, HashString const &aName) = 0;
};

#endif // __JFramework_ChemicalFactory_h_

Now, we derive a Factory.

#ifndef __JFramework_DefaultChemicalFactory_h_
#define __JFramework_DefaultChemicalFactory_h_

#include "ChemicalFactory.h"

class DefaultChemicalFactory : public ChemicalFactory
{
public:
  DefaultChemicalFactory();
  virtual ~DefaultChemicalFactory();

  virtual ChemistryMaterial* CreateMaterial(ChemistryManager *aManager, HashString const &aName);
};

#endif // __JFramework_DefaultChemicalFactory_h_

#include "DefaultChemicalFactory.h"
#include "ChemistryManager.h"

DefaultChemicalFactory::DefaultChemicalFactory() : ChemicalFactory()
{
}

DefaultChemicalFactory::~DefaultChemicalFactory()
{
}

/**
 * @brief Create new material.
 * @param aManager Manager associated with creation.
 * @param aName Name of material.
 * @return New material.
 */
ChemistryMaterial* DefaultChemicalFactory::CreateMaterial(ChemistryManager *aManager, HashString const &aName)
{
 ChemistryMaterial *material = new ChemistryMaterial(aManager);
  return material;
}

Making a Factory is simple now.

ChemicalFactory *mFactory = new DefaultChemicalFactory();

You can now create ChemistryMaterials with your Factory and do whatever you'd like with them like so:

/**
 * @brief Create new material.
 * @param aName Name of material type.
 * @return New material.
 */
ChemistryMaterial* ChemistryManager::CreateMaterial(HashString const &aName)
{
  ChemistryMaterial* material = mFactory->CreateMaterial(this, aName);
  AddMaterial(material);
  return material;
}

At this point, you can do whatever you want with your ChemistryMaterial that you could do with any other ChemistryMaterial.

To make things interesting, let's make our Factory return a variety of ChemistryMaterial types.

Assume that all materials referenced here derive from the ChemistryMaterial class.

#include "DefaultChemicalFactory.h"
#include "ChemistryManager.h"

DefaultChemicalFactory::DefaultChemicalFactory() : ChemicalFactory()
{
}

DefaultChemicalFactory::~DefaultChemicalFactory()
{
}

/**
 * @brief Create new material.
 * @param aManager Manager associated with creation.
 * @param aName Name of material.
 * @return New material.
 */
ChemistryMaterial* DefaultChemicalFactory::CreateMaterial(ChemistryManager *aManager, HashString const &aName)
{
  if(aName == "Steel")
    return new SteelMaterial(aManager);
  else if(aName == "Cloth")
    return new ClothMaterial(aManager);
  return new ChemistryMaterial(aManager);
}

If you were to do this, all the code that you wrote previously around the Factory would still be valid! You don't need to handle the particulars of the different ChemistryMaterial types yourself, and if you want to create a new ChemistryMaterial type, just change the CreateMaterial function in the DefaultChemicalFactory.

As you can (hopefully) see, the factory design pattern is great when you want to "set and forget" a particular implementation of an object. It's great when you need to make a variety of different types that derive from the same object, and you don't want to worry about the particulars of the derived object itself. It's also great when you want to centralize the creation of objects that share the same base.

With my time up, I'm gonna get back to work.

tl:dr of this whole article:
  • Make a base object that created objects will derive from.
  • Make a Factory object that has a method that creates objects of base object that.
    • Make that method take whatever parameters it needs to make objects.
  • Replace all uses of new on base object type, replace with you Factory creation method.

Jimmy