Generic Object Management

Michael Ramsey

miker@masterempire.com

 

Overview

 

Todays games have huge AIs, being worked on by multiple programmers. Unless a new technique is introduced when the project begins, it becomes difficult to add any new type of methodology to the framework. This comes from the concern of breaking a currently implemented system or the real world fact that the new technique is just too complex. What the Genericized Object Manager (GOM) allows for is a simple way to register multiple objects through a parameterized functor [Alexandrescu02], which can then be easily accessed at runtime through one central core routine.[0]

 

 

A benefit of GOM is that the implementation can fit into almost any preexisting framework, so your game can have the immediate gains without refitting your framework to a particular solution. The GOM technique allows for setting up a specific AI, such as a particular Field Manager (see Designing a Multi-Tiered AI Framework), input managers, state machines that need to deal with multiple behaviors, or just a central system that is needed because the programming team is large. GOM also serves as a good technique while refactoring a large codebase.

 

Genericized Object Management

 

The layout of the Genericized Object Manager (GOM) is shown in figure x.x.1. The base interface used by all objects is defined by the cObjectFunct class. cObjectFunc has a function called Update() which is used as the entry point into a registered object.  The game object implementation can be coded in any manner, just as long as one of the

member classes serves as any entry point. The RegisterObjectsEntryPoint object is a parameterized class that inherits from cObjectFunc. This allows any object that inherits from cObjectFunc to be used as an interface to RegisterObjectsEntryPoint.

 


 


To create a new object that is of the type RegisterObjectsEntryPoint, you simply have a piece code like the following:

 

RegisterObjectsEntryPoint<cSmartGeneral>  SmartGeneralObject(&cSmartAI, &cSmartGeneral::Evaluate);

 

The SmartGeneralObject is the new object that we'll use. The parameters that initialize SmartGeneralObject are the object types and the entry point for that function, respectively. The entry point for the class is the function that will called when Update() is invoked elsewhere in the game. This allows you to specify multiple entry points for a single object, by just registering different members in your state machine. 

 

To setup a simple state machine, we first must allocate a simple state object that is of type cObjectFunctions. This will allow us to interface, with the registered game objects. The sample code is:

 

int iObjectSize=sizeof(cObjectFunctions)*MaxStates;

cObjectFunctions **pPSM;

pPSM= (cObjectFunctions**)malloc(iObjectSize);

 

The array of objects pPSM, will be our interface to our SmartGeneralObject created above. To actually assign an object to the interface, we simply do the following:

 

pPSM[iCurrentState] = &SmartGeneralObject;

 

This maps a reference to the pPSM interface.

 

To execute an object, we simply call the interfaces Update function with the optional data packet. This begins the execution of the function that serves as the entry point into the registered class.

 

pPSM[iStateLoop]->

Update(DataPacketToProcess[iStateLoop]);

 

In this code snippet, iStateLoop is an index into the current list of states that are being executed. This could also be a direct mapping into a state transition matrix, that has been set up for an entire subsystem. The DataPacketToProcess array is a potential list of

data that is required for the object to operate.  This allows the registered object to also operate with limited autonomy. This is ideal when you want to keep your game specific objects small or when they are broken up across the team.

 

In the code listing x.x.1, we have the complete listing of the base interface class as well as its registration derivation.

 

 

 

//==============================

//cobjectfunction

//desc:basic object container that the RegisterObjectsEntryPoint() inherits from.

//     Also serves as the interface to any derived object

//==============================

class cObjectFunctions

{

private:

     int iObjectType;

 

public:

     //base functions that all objects need to have

     //

     virtual void operator()(cDataPack cData)=0;

     virtual void Update(cDataPack cData)=0;

     int GetObjectType(){return iObjectType;}

     void SetObjectType(int iOType){iObjectType=iOType;};

};

 

//==============================

//RegisterObjectsEntryPoint

//desc: derived template class for object registration

//==============================

template <class T> class RegisterObjectsEntryPoint :  public cObjectFunctions

{

private:

     T *pObject;                  

     void (T::*pFunction)(cDataPack cData);

    

public:

     RegisterObjectsEntryPoint(T* pNewObject, void(T::*pNewFunction)(cDataPack cData))

     {

          pObject  = pNewObject; 

          pFunction= pNewFunction;

     };

 

     T& GetRef(){return(this);}

 

     void operator()(cDataPack cData)

     {

          ((*pObject).*pFunction)(cData);

     };            

     void Update(cDataPack cData)

     {

          ((*pObject).*pFunction)(cData);

     };

};

 

 

 

Object Usage in a State Machine

 

The parameterized functor is ideal for use with a state machine. A state machine is a simple and effective method to deal with object registration, because it can maintain a list of state transitions in a local table. This allows you to map out the state transitions, encode them into your transition matrix, and then concentrate on writing the code to perform the actions. This is an ideal solution when having multiple programmers working on objects that are related to the same subsystem. Once the state transition matrix has been laid out, each programmer gets their own specific object to implement. This allows them to layout the basic object, integrate it into the project, establish the hooks and then they can go code away. Because once the hooks are in, the state machine will process the object as if it where there.

 

Another benefit of using the GOM technique is that is allows your state machine to be kept relatively clutter free of game logic. Once the state transition matrix is setup, you write all of your game logic inside the objects. This keeps a nice, clean delineation between the game decision logic and the game code that it executes. 

 

Demo

 

Enclosed on the CD is a demo, showing the basic framework. Two simple game objects are registered, and then an example of how the objects are called and executed.

 

Conclusion[0]

 

I have [0]used Game Object Management solution a number of times over the years. GOM has been used in a online game[0], Lost Continents and Master of the Empire. Its also been used when working with large codebases that I personally [0]did not write. It allowed me to identify behaviours that I wanted to occur, which where then factored into a state transition matrix. This transition matrix then became my management structure for the implemented game logic. GOM is a perfect refactoring tool, it allows you to insert a layer of control that you understand, which in turn can simplify working on someone elses code. With so many situations occuring in day to day practice, where a mass rewrite of a system is not feasible or too time consuming, we have the ability to interject a new way to manage objects. This method allows you to simplify and break out the game logic from the decision making process. That is definite win.

 

 

 

References[0]

 

[Alexandrescu02] Alexandrescu, Andrei, Modern C++ Design, Addison Wesley, 2002

[Coplien92] Coplien, James, Advanced C++, Addison Wesley, 1992

[Noble01] Noble, James, Weir, Charles, Small Memory Software, Addison Wesley, 2001

[Ramsey02] Ramsey, Michael, “Simple Techniques for Complex Systems” available online at http://www.masterempire.com/OpenKimono.html, October 15, 2002.