This is the final article in a series about developing cross-platform plugins in C++. In previous articles -- Part 1, Part 2, Part 3, and Part 4 -- I examined the difficulties of working with C++ plugins in portable way.
In this installment, I cover the missing pieces of the sample game introduced in Part 4 and give a demonstration. I also take a quick tour of the source code that accompanies this series, tell a few good stories (it's about time), and finally compare the plugin framework to the NuPIC (Numenta's Platform for Intelligent Computing) plugin framework, which is its conceptual ancestor. But first, let's take a look at some monster plugins that will be loaded into the game.
Monster Plugins
I created four different plugins to demonstrate the scope and diversity of the plugin framework.
- A pure C++ plugin
- A pure C plugin
- A hybrid plugin deployed as dynamic/shared libraries
- One static C++ plugin that should be linked directly to the executable.
All these plugins register their monsters with the PluginManager as actors. In addition, the game itself implements the Hero as an object that implements the IActor interface and large parts of the code that work at the IActor interface don't (and can't distinguish) between the game-supplied Hero and any monster. The game could also provide some built-in monsters.
Dynamic C++ Plugins
The dynamic C++ plugin registers the KillerBunny and StationarySatan monsters. Listing One is the KillerBunny.h header where the KillerBunny class is defined. KillerBunny is derived directly from the C++ IActor interface (which makes it a pure C++ plugin). It implements the create() and destroy() static functions to support creation and destruction via the PluginManager. The StationarySatan and any other pure C++ plugin object should look exactly the same (except for private members if any).
#ifndef KILLER_BUNNY_H #define KILLER_BUNNY_H #include <object_model/object_model.h> struct PF_ObjectParams; class KillerBunny : public IActor { public: // static plugin interface static void * create(PF_ObjectParams *); static apr_int32_t destroy(void *); ~KillerBunny(); // IActor methods virtual void getInitialInfo(ActorInfo * info); virtual void play(ITurn * turnInfo); private: KillerBunny(); }; #endif
Example 1 contains the implementation of create() and destroy(). They are almost trivial. The create() function simply instantiates a new KillerBunny object and returns it (as opaque void pointer). The destroy() function accepts a void pointer, which is actually a pointer to an instance created earlier using the create() function. It casts the void pointer to a KillerBunny pointer and deletes it. The important part here is that these functions let the PluginManager create KillerBunny objects without "knowing" anything about the KillerBunny class. The returned instance is usable via the IActor interface later (even though it is returned as a void pointer).
void * KillerBunny::create(PF_ObjectParams *) { return new KillerBunny(); } apr_int32_t KillerBunny::destroy(void * p) { if (!p) return -1; delete (KillerBunny *)p; return 0; }
Example 2 contains the implementation of the IActor interface methods. These methods are trivial too. The getInitialInfo() method simply populates the ActorInfo struct with some data. The play() method is where a real KillerBunny will do the actual work, run around, avoid or attack enemies, and generally justify its name. Here, it just gets the list of friends from the ITurn interface to verify it works. This is just laziness on my part, and in fact all the monsters don't do anything. The Hero is the only one actually fighting. The monsters do defend themselves when attacked and even retaliate.
void KillerBunny::getInitialInfo(ActorInfo * info) { ::strcpy((char *)info->name, "KillerBunny"); info->attack = 10; info->damage = 3; info->defense = 8; info->health = 20; info->movement = 2; // Irrelevant. Will be assigned by system later info->id = 0; info->location_x = 0; info->location_y = 0; } void KillerBunny::play(ITurn * turnInfo) { IActorInfoIterator * friends = turnInfo->getFriends(); }
The main point here is that writing pure C++ plugin objects is pretty easy. Other than the boilerplate create() and destroy() static methods, you just implement a standard C++ class. No arcane incantations are required.
Listing Two contains the plugin initialization code. It's not too bad, but it's boring and error prone: Define an exit function, in the PF_initPlugin function, define a PF_RegisterParams struct, populate it, and register every plugin object. Make sure you return NULL if initialization failed. Less than exhilarating. That's all it takes to write a pure C++ monster plugin (with two monsters).
#include "cpp_plugin.h" #include "plugin_framework/plugin.h" #include "KillerBunny.h" #include "StationarySatan.h" extern "C" PLUGIN_API apr_int32_t ExitFunc() { return 0; } extern "C" PLUGIN_API PF_ExitFunc PF_initPlugin(const PF_PlatformServices * params) { int res = 0; PF_RegisterParams rp; rp.version.major = 1; rp.version.minor = 0; rp.programmingLanguage = PF_ProgrammingLanguage_CPP; // Register KillerBunny rp.createFunc = KillerBunny::create; rp.destroyFunc = KillerBunny::destroy; res = params->registerObject((const apr_byte_t *)"KillerBunny", &rp); if (res < 0) return NULL; // Regiater StationarySatan rp.createFunc = StationarySatan::create; rp.destroyFunc = StationarySatan::destroy; res = params->registerObject((const apr_byte_t *)"StationarySatan", &rp); if (res < 0) return NULL; return ExitFunc; }