自己动手写插件框架(15)

开始游戏!

到了玩游戏的时候啦!现在我将告诉你如何来玩这个游戏。你可以在此了解到PluginManager如何初始化、怪物怎样被创建、战斗如何开始等。我们从main() 开始。

下面是main() 函数的代码。直接跳过所有的#include 语句,集中精力看看DummyInvokeService() 函数。这个函数作为由插件接收的 PF_PlatformServices 结构体的 invokeService。这里它实际不会做任何事情,但在正式程序中,它将是主系统为插件提供服务的主要角色。

#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;
}

main() 函数可以自动适应 Windows 和 UNIX 系统。游戏是可移植的。我测试过 Windows XP SP2、Vista、Mac OS X 10.4 (Tiger)、Mac OS X 10.5 (Leopard) 和 Kubuntu 7.10 (Gutsy Gibbon)。这是我认为的一些比较重要的操作系统。对于另外的系统或者这些系统的不同版本,我们的游戏应该也可以正常运行。

main() 函数中有一个检测,用于查看用户是否通过命令行参数传入了插件目录。APR 库在一开始就被初始化,同样也获得了PluginManager实例。这个类是一个单例,在应用程序结束时销毁。下一步是将DummyInvokeService 赋值给平台服务结构体。一旦invokeService 准备好,插件就可以初始化。首先,插件目录以argv[1]的形式传入,经过处理后,其中的所有动态插件都被加载进来,静态插件则显式初始化。这并不是一个令人愉快的方法,但是在 Windows 下,我们找不到更合适的替代方案。一旦所有插件都初始化完成,BattleManager获得控制权。最后,APR 库清理。

我们的解决方案很直接:检查命令行参数、初始化全局资源、加载插件、向应用程序业务逻辑移交控制权,最后清理全局资源。

BattleManager是游戏的核心。下面是完整的go() 函数。它首先导出所有PluginManager已注册的怪物类型。

void BattleManager::go()
{
    // 获取所有怪物类型
    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);
    }

    // 显示所有怪物
    for (MonsterTypeVec::iterator i = monsterTypes_.begin();
         i != monsterTypes_.end();
         ++i)
    {
        std::string m = *i;
        std::cout << m.c_str() << std::endl;
    }

    // 将主角添加进来(随后添加那些怪物)
    ActorInfo ai, heroInfo;
    hero_.getInitialInfo(&heroInfo);
    // 不要保留主角的 IActor *,因为它需要特殊处理
    actors_.insert(std::make_pair((IActor *)0, heroInfo));
    heroFaction_.push_back(&actors_[0]);

    // 随机初始化某些怪物
    for (apr_int32_t i = 0; i < MONSTER_COUNT; ++i)
    {
        IActor * monster = createRandomMonster(rm);
        monster->getInitialInfo(&ai);
        ai.id = i+1; // 主角的 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;
}

这是一个动态的步骤。BattleManager并不知道有什么样的怪物——也不需要关心。它甚至不知道FidgetyPhantom 是由静态插件提供的。为清楚起见,我们将所有怪物类型输出到控制台(也为了能够知道是不是所有怪物都正确注册)。然后,它将Hero(这是需要特殊对待的)添加到 actor 列表。BattleManager需要知道Hero,因为Hero 的命运关系着整个游戏的命运,Hero 如果阵亡,游戏也就结束。怪物使用createRandomMonster() 函数随机创建。最后,我们开始主循环:“如果游戏没有结束,开始下一轮”。当游戏结束时,需要显示总结信息:主角胜利还是失败。正如你看到的那样,我们的游戏实在是太简单了。

下面是createRandomMonster() 函数的代码。它使用索引随机选择怪物,基于已注册怪物的总数,使用ActorFactory::createActor() 函数创建怪物,其参数为怪物类型。

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;
}

ActorFactory 是应用程序相关的对象适配器,继承自由插件框架提供的通用的 ObjectAdapter。从BattleManager的角度来看,所有创建的怪物都实现了IActor 接口。事实上,有些是适配的 C 对象,有些则是从另外的插件获取的。

这是一个回合策略制游戏,那么,在主循环中,一个回合代表着什么?下面就是playTurn() 函数的代码,这就是答案。

void BattleManager::playTurn()
{
    // 遍历所有 actor(从主角开始)
    // 为每一个 actor 准备回合信息(友军和敌人)
    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);
    }

    // 清理已死亡的敌人
    Faction::iterator last = std::remove_if(enemyFaction_.begin(),
                                            enemyFaction_.end(),
                                            isDead);
    while (last != enemyFaction_.end())
    {
        enemyFaction_.erase(last++);
    }

    // 检查游戏是否结束(主角死亡或者所有敌人死亡)
    if (isDead(&actors_[(IActor *)0]) || enemyFaction_.empty())
    {
        gameOver_ = true;
        return;
    }
}

BattleManager开始的时候在栈上创建一个Turn 对象。每一个存活的 actor 都要获取其带有适当信息的 Turn 对象,并在其基础上做出一定的动作。首先是HeroBattleManager调用其play()函数,将回合对象传递给它。Hero进行移动和攻击。BattleManager知道这所有的动作,因为保存动作信息的数据结构ActorInfo就是BattleManager管理的。一旦主角完成,其它 actor 则获得各自的机会。当所有 actor 都完成各自需要的动作之后,就需要将死亡的对象从战场移除。这由标准算法std::remove_ifisDead()谓词决定。该谓词需要检查 actor 的血量是否为 0。回合结束之前,BattleManager检查Hero是不是死亡,或者所有的怪物是不是都已经死亡。如果这些条件都不满足,游戏将继续。下图是我们的游戏截图:RPG Game Sample

这就是我们的简单的游戏了。

Leave a Reply