Let's Play!
It's play time. I'll take you now to a quick tour of the game itself. You will see how the PluginManager is initialized, how the monsters are created and how battles are conducted. Let's start with main().
Listing Six contains the main() function. You can skip all the #include statements and concentrate on the DummyInvokeService() function. This function serves as the invokeService in the PF_PlatformServices struct that the plugins receive. It doesn't do anything in this case, but in real applications it has a major role in providing system services to plugins.
#ifdef WIN32 #include "stdafx.h" #endif #include "plugin_framework/PluginManager.h" #include "plugin_framework/Path.h" #include "BattleManager.h" #include "static_plugin/static_plugin.h" #include <string> #include <iostream> using std::cout; using std::endl; apr_int32_t DummyInvokeService(const apr_byte_t * serviceName, void * serviceParams) { return 0; } #ifdef WIN32 int _tmain(int argc, _TCHAR* argv[]) #else int main (int argc, char * argv[]) #endif { cout << "Welcome to the great game!" << endl; if (argc != 2) { cout << "Usage: great_game <plugins dir>" << endl; return -1; } // Initialization ::apr_initialize(); PluginManager & pm = PluginManager::getInstance(); pm.getPlatformServices().invokeService = DummyInvokeService; pm.loadAll(Path::makeAbsolute(argv[1])); PluginManager::initializePlugin(StaticPlugin_InitPlugin); // Activate the battle manager BattleManager::getInstance().go(); ::apr_terminate(); return 0; }
The main() function is defined to conform to both Windows and UNIX systems. The game is very portable. I tested it on Windows XP SP2, Vista, Mac OS X 10.4 (Tiger), Mac OS X 10.5 (Leopard), and Kubuntu 7.10 (Gutsy Gibbon). These are the most common modern OS out there. It will probably work as-is or with tweaks to the build procedure on a range of other OSs.
Inside main() there is a check that the user passed the plugin directory as a command-line argument. The APR library is initialized and the PluginManager makes its entrance. It is a singleton and is destructed when the application terminates. The next step is to assign DummyInvokeService to the platform services struct. Once invokeService is ready, the plugins can be initialized. First, all of the dynamic plugins are loaded from the directory and passed as argv[1], then the static plugin is initialized explicitly. This is unpleasant, but I couldn't find a portable solution that works on Windows. Once all the plugins are initialized, BattleManager takes command. Finally, the APR library is cleaned up.
Pretty straightforward: Check command-line arguments, initialize global resources, load plugins, transfer control to the application logic, and clean up global resources.
BattleManager is the brain of the game. Example 9 contains the entire go() method. It starts by extracting all the registered monster types from the PluginManager.
void BattleManager::go() { // Get all monster types PluginManager & pm = PluginManager::getInstance(); const PluginManager::RegistrationMap & rm = pm.getRegistrationMap(); for (PluginManager::RegistrationMap::const_iterator i = rm.begin(); i != rm.end(); ++i) { monsterTypes_.push_back(i->first); } // Dump all the monsters for (MonsterTypeVec::iterator i = monsterTypes_.begin(); i != monsterTypes_.end(); ++i) { std::string m = *i; std::cout << m.c_str() << std::endl; } // Add the Hero to its faction (later allies may join) ActorInfo ai, heroInfo; hero_.getInitialInfo(&heroInfo); // Don't keep the hero's IActor *, because she is treated differently actors_.insert(std::make_pair((IActor *)0, heroInfo)); heroFaction_.push_back(&actors_[0]); // Instantiate some monsters randomly for (apr_int32_t i = 0; i < MONSTER_COUNT; ++i) { IActor * monster = createRandomMonster(rm); monster->getInitialInfo(&ai); ai.id = i+1; // Hero is id 0 actors_.insert(std::make_pair(monster, ai)); enemyFaction_.push_back(&actors_[monster]); } while (!gameOver_) { playTurn(); } heroInfo = actors_[0]; if (heroInfo.health > 0) std::cout << "Hero is victorious!!!" << std::endl; else std::cout << "Hero is dead :-(" << std::endl; }
This is a dynamic step. BattleManager doesn't have a clue what monsters are there -- and doesn't care. It doesn't even know about the FidgetyPhantom from the static plugin. It dumps all the monster types to the console for good measure (and for me to make sure all the monsters were registered properly). Then it puts the Hero (which is known and gets special treatment) in the actors list. BattleManager needs to know about Hero because the fate of Hero is linked with the fate of the entire game and the all important "game over" condition. Then the monsters are created randomly using the createRandomMonster() function. Finally, we get to the main loop: "while the game is not over play a turn". When the game is over, it's time to display a dazzling text message to the console, which states if the hero won or died. As you can see, my art budget for this game was pure fiction.
Example 10 contains the createRandomMonster() method. It selects a random monster by index, based on the total number of registered monster types, then creates it by calling the ActorFactory::createActor() method, passing it the monster type.
IActor * BattleManager::createRandomMonster(const PluginManager::RegistrationMap & rm) { // Select monster type apr_size_t index = ::rand() % monsterTypes_.size(); const std::string & key = monsterTypes_[index]; const PF_RegisterParams & rp = rm.find(key)->second; // Create it IActor * monster = ActorFactory::createActor(key); return monster; }
The ActorFactory is the application-specific object adapter that derives from the generic ObjectAdapter provided by the plugin framework. From the point of view of the BattleManager, all created monsters are just objects the implement the IActor interface. The fact that some of them are adapted C objects or that some hail from remote plugins is immaterial.
This is a turn-based game, so what happens when a turn is played in the main loop? Example 11 contains the playTurn() method, which provides the answer.
void BattleManager::playTurn() { // Iterate over all actors (start with Hero) //For each actor prepare turn info (friends and foes) Turn t; ActorInfo & ai = actors_[(IActor *)0]; t.self = &ai; std::copy(heroFaction_.begin(), heroFaction_.end(), std::back_inserter(t.friends.vec)); std::copy(enemyFaction_.begin(), enemyFaction_.end(), std::back_inserter(t.foes.vec)); hero_.play(&t); ActorInfo * p = NULL; ActorMap::iterator it; for (it = actors_.begin(); it != actors_.end(); ++it) { if (!it->first || isDead(&it->second)) continue; t.self = &(it->second); std::copy(heroFaction_.begin(), heroFaction_.end(), std::back_inserter(t.foes.vec)); std::copy(enemyFaction_.begin(), enemyFaction_.end(), std::back_inserter(t.friends.vec)); it->first->play(&t); } // Clean up dead enemies Faction::iterator last = std::remove_if(enemyFaction_.begin(), enemyFaction_.end(), isDead); while (last != enemyFaction_.end()) { enemyFaction_.erase(last++); } // Check if game is over (hero dead or all enemies are dead) if (isDead(&actors_[(IActor *)0]) || enemyFaction_.empty()) { gameOver_ = true; return; } }
The BattleManager starts by creating a Turn object on the stack. Each [live] actor getx his Turn object with proper information and acts on it. The Hero goes first. The BattleManager invokes its play() method, passing the turn object. The Hero moves about and attacks. The BattleManager is aware of all the action that transpires because the data structures that are manipulated are the ActorInfo structs the BattleManager manages. Once the hero is done, each of the other actors gets its chance to do some mayhem. After all the actors do their worst, it's time to remove the bodies from the battle field. This is done with the help of the standard std::remove_if algorithm and the isDead() predicate that checks if the actor has zero health points. Before the turn ends, the BattleManager checks if the Hero or all the monsters are dead. The game goes on until one of these conditions is fulfilled. Figure 1 shows the game in progress.
That's it. Go ahead and give it a try, but don't go addict on me :-)