FPG LogoEarly in the development process of Canyon of Desolation we decided to add an achievement system. Many games, from the PC to smart phones, have an achievement system and they seemed like a good way to keep people coming back to play a game. As I designed the system I did a number of very stupid things, some without even realizing it. These are some of my failed attempts and lessons learned while implementing an achievement system.

Inheritance FTF! (for the fail)

My first thought was to make an Achievement class (in C++) that could be inherited by every class that needs to throw an achievement event. But I didn’t get to far before this thought crept into my head, “is a tree a banana or does a tree have a banana?” DOH! The plan to use inheritance was stupid! Stupid because inheritance models an “is-a” relationship, such as ‘a pine tree is-a plant’, ‘a dog is-an animal’, ‘RAM is-a semiconductor’, etc. I was erroneously trying to say that ‘a mouse handler is-an achievement event’, ‘a pine cone is-a plant’, ‘floppy ears is-an animal’, and ‘epoxy encapsulant is a semiconductor’. So I scraped that idea and hung my head in shame.

Composition FTF!

So it seemed natural that I’d make the Achievement class a component (a member) of  each class that needed to throw an achievement event. But there was a problem, I had already coded most of Canyon of Desolation by this time and without considering the impact of an achievement system. When we added achievements to the design document I, without much thought, considered it just another class, just another component of the game.

Canyon of Desolation has projectiles that are spawned by the underlying game engine. The projectiles are autonomous objects that fly through the air on a predefined spline path. Once they reach the end of the spline path they fired a hardcoded callback into the game engine to damage the enemy (also spawned by the game engine) and then recycle the projectile for later use. However, the game engine had no visibility of the Achievement class instance and there was no built-in way to hook into the ‘damage event’ without modifying the game engine. Plus, the hardcoded callback made me squirm in my chair a little.

Taking A Step Back

At this point I took a step back and looked at the overall architecture. I began sketching out the relationships between Canyon of Desolation and the underlying game engine, searching for a simple and clean way to insert achievement events. That word “event” kept rattling inside my head and it suddenly dawned me, there is no event messaging system in the game engine. There was an EventSystem in the game engine designed for spawning entities and other in-game events, but it was overkill for what I needed. So I decided to add a simple messaging system to the game engine. However, I had never coded a messaging system and I had no clue if it was simple or not.

Thwonk! Message For You Sir!

I needed a message to be delivered from point A to point B (the Achievement system). But thinking of it in those terms had me stumped. A message could originate from anywhere, even deep inside the game engine where the Achievement system is well out of scope:  the sender has no idea who to send the message to. Damn! I was back to  the same problem as before.

Thinking through it some more, I realized the message system needed to be sender/receiver agnostic. In other words the system needed to be like a broadcast repeater where many sources can feed information into the system and then broadcast the information to anyone who’s listening. As I thought about it more and sketched it out on paper it appeared like a neat many-to-many paradigm made up of many message transmitters and many message receivers all going through a central transmitter, like a radio transmitter.

The next problem was C++ itself.  C++ is an abject pain in the rear when it comes to callbacks. I could do c-style callbacks with static member functions, but I wasn’t about to go down that ugly path. There are also fast delegates but I wasn’t comfortable using them because of potential compiler incompatibilities.  std::tr-1::function<> was also possible, but alas! my compiler is old and doesn’t support it. I settled on the library that some folks love to hate: Boost.

It’s not clear why some people dislike Boost so much. It seems to work great and has an army of really smart people supporting it. The only things I don’t like about boost are the documentation, that can be esoteric at times, and reading the source makes my eyes bleed. Other than that it’s awesome.

In the boost libraries there exists boost::function that makes it relatively easy to create a callback in C++.

So I wrote out this header:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  
typedef boost::function<void (const String &arg1, const String& arg2)> VoidCallbackStringString;
 
   
 
  class MessageCenter
 
  {
 
  public:
 
      MessageCenter();
 
      ~MessageCenter();
 
   
 
      void addMessageListener(VoidCallbackStringString msgCallback, void *callingClass, const String &messageType);
 
      void removeMessageListener(void *callingClass, const String &messageType);
 
      void emitMessage(const String &messageType, const String &messageData);
 
   
 
  private:
 
      unsigned mCallCounter;
 
      std::map <String, std::map<void*, VoidCallbackStringString> > mMsgListeners;
 
  };

I had to make a few more design decisions:

  1. Would the transmitters send known enum types for the MessageType?
  2. Similarly, would the message data be an enum or arbitrary data?
    Since the message system was going into the game engine I opted to allow arbitrary data for both parameters via strings. Of course the issue with strings is that the receiver needs to know how to decode the data that’s in the string, but that’s the receivers problem.
  3. Do all listeners hear all messages?
    I opted to require a listener to specify the type of message it wanted to listen for.

Next I filled in the source and ended up with this:

1
 
  2
 
  3
 
  4
 
  5
 
  6
 
  7
 
  8
 
  9
 
  10
 
  11
 
  12
 
  13
 
  14
 
  15
 
  16
 
  17
 
  18
 
  19
 
  20
 
  21
 
  22
 
  23
 
  24
 
  25
 
  26
 
  27
 
  28
 
  29
 
  30
 
  31
 
  32
 
  33
 
  34
 
  35
 
  36
 
  37
 
  38
 
  39
 
  40
 
  41
 
  42
 
  43
 
  44
 
  45
 
  46
 
  47
 
  48
 
  
MessageCenter::MessageCenter() : mCallCounter(0)
 
  {
 
  }
 
   
 
  MessageCenter::~MessageCenter()
 
  {
 
  }
 
   
 
  void MessageCenter::addMessageListener(VoidCallbackStringString msgCallback, void *callingClass, const String &messageType)
 
  {
 
      if(mCallCounter > 0)
 
      {
 
          LogManager::logMessage("WARNING: Add message listener ignored.", "MessageCenter::addMessageListener");
 
          return;
 
      }
 
   
 
      mMsgListeners[messageType].insert(VoidCallbackPair(callingClass, msgCallback));
 
  }
 
   
 
  void MessageCenter::removeMessageListener(void *callingClass, const String &messageType)
 
  {
 
      if(mCallCounter > 0)
 
      {
 
          LogManager::logMessage("WARNING: Remove message listener ignored.", "MMOnsterMessageCenter::addMessageListener");
 
          return;
 
      }
 
   
 
      std::map::iterator listIter;
 
   
 
      if( (listIter = mMsgListeners[messageType].find(callingClass)) != mMsgListeners[messageType].end() )
 
      {
 
          mMsgListeners[messageType].erase(listIter);
 
      }
 
  }
 
   
 
  void MessageCenter::emitMessage(const String &messageType, const String &data)
 
  {
 
      std::map<void*, VoidCallbackStringString> callBacks = mMsgListeners[messageType];
 
   
 
      ++mCallCounter;
 
   
 
      BOOST_FOREACH(VoidCallbackPair callback, callBacks)
 
      {
 
          callback.second(messageType, data);
 
      }
 
   
 
      --mCallCounter;
 
  }

There are two issues I want to note:

  1. Adding a message listener inside a call to emitMessage() must be ignored because I’m using a std::map. Why? Because adding an element to a std::map causes the map to be resorted. Re-sorting of a std::map invalidates any existing iterators. Iterating with an invalid iterator will almost always crash and burn.
  2. Similarly, removing a message listener inside a call to emitMessage() will also be ignored for the same reason.

I wasn’t sure how to solve these two issues other than to block using a call counter. The blocking makes me uneasy but it works for a single threaded program.

Now, an arbitrary class with visibility of the game engine (which is just about everything) could emit a message with:

1
 
  
GameEngine::getMessageCenter()->emitMessage("MsgType", "Message");

To listen for a message, a listener class needs to register itself with a call like this:

1
 
  
GameEngine::getMessageCenter()->addMessageListener(boost::bind(&MyListenerClass::msgCenterCallback, this, _1, _2), this, "MsgType");

and the listener will obviously have to implement a callback function with a prototype like,

1
 
  
void msgCenterCallback(const String &msgType, const String &msg);

It was then trivial to connect the Achievement system to the game engine’s MessageCenter. However, I still had to do a lot of modifications to the classes that need to emit achievement events. The modifications took a great deal of time (about 2 weeks). Additionally, out of haste to finish the Achievements system I introduced a few bugs that needed more work to resolve, and some of them were not trivial.

Lessons Learned

Consider each part of the design doc and understand how it fits into the game before writing a single line of code.

If I had considered the needs of the achievement class I would have known that a message system was needed first. I also would have known to include the required callback functions in each class that needed to throw an achievement event. That would have spread the pain out over time and would have prevented the introduction of bugs.

We also might have coded the existing EventManager class in a way that included a lightweight and flexible message broadcast system.

Inheritance or Composition are not always the answer.

When I was learning C++ (way too many years ago) I found myself falling back onto my C ways to solve problems that I didn’t know how to solve with C++. I’ve learned that inheritance and composition are not the answer to every object oriented programming (OOP) problem. There are other programming techniques within the realm of OOP that I have yet to explore in depth. One of those techniques, the technique I used here and one that I’ve barely scratched the surface of, is messaging.

I find it really difficult to put aside the compiler when all I want to do is jump in feet first and start coding. But doing so can create problems. I have resolved to use my head before I use my fingers.

Until next time.