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

PluginHelper

PluginHelper是另外一个帮助类,用于帮助插件开发者编写插件胶水代码。下面是它的实现:

#ifndef PF_PLUGIN_HELPER_H
#define PF_PLUGIN_HELPER_H

#include "plugin.h"
#include "base.h"

class PluginHelper
{
    struct RegisterParams : public PF_RegisterParams
    {
        RegisterParams(PF_PluginAPI_Version v,
                       PF_CreateFunc cf,
                       PF_DestroyFunc df,
                       PF_ProgrammingLanguage pl)
        {
            version = v;
            createFunc = cf;
            destroyFunc = df;
            programmingLanguage = pl;
        }
    };

public:
    PluginHelper(const PF_PlatformServices * params) :
        params_(params),
        result_(exitPlugin)
    {
    }

    PF_ExitFunc getResult()
    {
        return result_;
    } 

    template <typename T>
    void registerObject(const apr_byte_t * objectType,
                        PF_ProgrammingLanguage pl = PF_ProgrammingLanguage_C,
                        PF_PluginAPI_Version v = {1, 0})
    {
        RegisterParams rp(v, T::create, T::destroy, pl);
        apr_int32_t rc = params_->registerObject(objectType, &rp);
        if (rc < 0)
        {
            result_ = NULL;
            THROW << "Registration of object type "
                  << objectType << "failed. "
                  << "Error code=" << rc;
        }
    }

private:
    static apr_int32_t exitPlugin()
    {
        return 0;
    }

private:
    const PF_PlatformServices * params_;
    PF_ExitFunc result_;
};

#endif // PF_PLUGIN_HELPER_H

这个类可以同插件对象协同工作。这些插件对象应该用 static 函数实现PF_CreateFuncPF_DestroyFunc函数指针。这就是全部条件,没有其他要求了。因为ActorBaseTemplate已经满足了这个要求,所以凡是继承自ActorBaseTemplate的类都可以与PluginHelper兼容。

PluginHelperPF_initPlugin()作为入口点,并在其内部使用。我们会在后面的文章中看到究竟如何使用这个类。现在,我们浏览一下PluginHelper提供给插件开发者哪些有用的服务。入口点函数用于注册所有支持的插件对象,如果成功,则返回具有特定签名的指向PF_ExitFunc的函数指针。如果有问题则返回 NULL。

PluginHelper构造函数接受一个指向PF_PlatfromServices结构的指针。该结构体包含了主系统插件 API 的版本、invokeServiceregisterObject两个函数指针并且将其保存下来。如果插件初始化成功,它也会在result成员中保存exitPlugin函数指针。

PluginHelper提供一个模板化的registerObject函数,完成了我们所需要的大多数工作。模板参数T代表需要注册的对象类型。该类型需要有create()destroy()static 函数,用于赋值给PF_CreateFuncPF_DestroyFunc。它接受一个对象类型字符串和一个可选的编程语言类型(默认是PF_ProgrammingLanguage_C)。这个函数需要执行版本检测,以保证插件版本与主系统兼容。如果这些检测都通过了,就会准备一个 RegisterObjectParams结构,调用registerObject()函数,然后检查其返回值。如果版本检测或者registerObject函数指针调用失败,则会报告错误(这一点是由CHECK宏实现的),并且将result_设置为 NULL,抛出异常。之所以不会让异常扩散,是因为PF_initPlugin(这是 PluginHelper假设会使用的)是一个 C 函数,不应该将异常发送到二进制兼容边界之外。在registerObject中捕获所有异常,能够减轻开发者的处理负担(甚至让他们忘记这件事)。这是使用THROWCHECKASSERT宏以获得方便的好例子。错误信息则使用流运算符构建,不需要分配缓存、合并字符串或者使用printf()reportError调用的结果会包含错误位置信息(__FILE____LINE__),无需手动指定。

一般的,一个插件会注册多于一个对象类型。如果有一个对象类型注册失败,result_就会是 NULL。对于某些对象,这么做是可以的。例如,你可能需要注册同一对象的多个版本,其中一个版本主系统不支持。此时,该对象类型就会注册失败。插件开发者需要在每一个 PluginHelper::registerObject()调用后检测result_的值,来确定是不是致命错误。如果不是致命错误,只需要在最后返回PluginHelper::ExitPlugin

默认行为是,每一个失败都是致命的,插件开发者应该返回PluginHelper::getResult(),这个函数会返回result_的值,该值就是PluginHelper::ExitPlugin(所有注册都是成功的)或者 NULL(任一注册失败)。

RPG 游戏

我喜欢 RPG 游戏。作为一个程序开发者,我希望编写自己的游戏。但是,问题在于,严肃的游戏开发远比单纯的编程复杂得多。我曾经在 Sony Playstation 工作,但只是相关项目,不是游戏本身。

这里,我们选择一个简单的 RPG 游戏,来测试下插件框架。这个游戏不需要很复杂,仅仅是一个演示,因为我们是由程序而不是用户控制主角。下面,我们来介绍下其中的概念。

概念

游戏的概念很基础。有一个游戏主角,什么都不怕。主角被一股神秘力量传送到一个战场上,周围有很多各种各样的怪物。我们的主角必须战斗,直到打败所有怪物取得胜利。

主角和所有怪物都是 actor。actor 有许多属性,例如在战场的位置、血量和速度。当 actor 的血量降为 0(或者更低)时,它就死了。

游戏发生在一个 2D 表格(战场)上。这是一个回合制游戏。每一个回合都有 actor 发出动作。actor 的动作可以是移动或者攻击(如果在一个怪物旁边的话)。每个 actor 都有一个友军和敌军的列表。这允许有帮派、部落的概念。在我们的游戏中,没有友军的概念,所有的怪物都是敌人。

设计接口

接口当然需要满足概念模型。actor 由ActorInfo结构描述,其中包含了所有状态。actor 应当实现IActor接口,以允许BattleManager获取初始化状态以及指导它们的动作。ITurn接口就是轮到 actor 反应的时候,它会有哪些信息。ITurn接口允许 actor 获取自己的信息(如果没有保存的话)以便移动或者攻击。思路是,BattleManager管理数据,actor 在一个受管理的环境中接受各自的信息以及做出自己的动作。当 actor 移动的时候,BattleManager应当基于移动点强制移动,确保不会超出边界等等。BattleManager还应该忽略 actor 的非法操作(根据游戏规则),比如多次攻击或者攻击友军等。这是它的任务。actor 由各自不同的 id 进行区分。这些 id 每回合都会刷新,因为可能有 actor 死亡,也可能有新的出现。由于这只是一个简单的游戏,我们不会指定很多规则。在网络游戏中(特别是 MMORPG),用户还得使用客户端通过网络协议与服务器交互,这就必须要验证客户端的动作以防止作弊行为。有些游戏还有虚拟的或者真实的交易行为,这些都可能让那些非法用户轻易地摧毁用户体验。

实现对象模型

如果你了解了 C/C++ 双模型,那么我们的对象模型实现就会很容易理解。真实的实现是使用的 C++ 函数。ActorInfo 是一个带有数据的结构体。ActorInfoIterator 则是ActorInfo 的集合。然后我们来看看Turn 对象。在某种程度上,这是相当重要的一个对象,因为我们的游戏正是基于回合的。当 actor 需要动作时,会为每一个 actor 创建一个新的TurnTurn 对象传递给每一个 actor 的IActor::play() 函数。Turn 对象保存其 actor 的信息(因此 actor 不需要保存这些信息)以及友军和敌人的列表。它提供三个访问函数:getSelfInfo()getFriends()getFoes(),以及两个动作函数attack() 和 move()

下面的代码显示,上述访问器只是返回了相关数据,而move() 函数则更新的当前 actor 的位置信息。

ActorInfo * Turn::getSelfInfo()
{
    return self;
}

IActorInfoIterator * Turn::getFriends()
{
    return &friends;
}

IActorInfoIterator * Turn::getFoes()
{
    return &foes;
}

void Turn::move(apr_uint32_t x, apr_uint32_t y)
{
    self->location_x += x;
    self->location_y += y;
}

我们在这里不做任何验证。actor 有可能直接移动到地图之外,或者移动了超出其限制的距离。这些在正式的游戏中都不应该发生。

下面则是attack() 函数的代码,注意,我们还需要一个帮助函数doSingleFightSequence()

static void doSingleFightSequence(ActorInfo & attacker, ActorInfo & defender)
{
    // Check if attacker hits or misses
    bool hit = (::rand() % attacker.attack - ::rand() % defender.defense) > 0;
    if (!hit) // miss
    {
        std::cout << attacker.name << " misses " << defender.name << std::endl;
        return;
    }
    // Deal damage
    apr_uint32_t damage = 1 + ::rand() % attacker.damage;
    defender.health -= std::min(defender.health, damage);
    std::cout << attacker.name << "(" << attacker.health << ") hits "
              << defender.name << "(" << defender.health << "), damage: "
              << damage << std::endl;
}

void Turn::attack(apr_uint32_t id)
{
    ActorInfo * foe = NULL;
    foes.reset();
    while ((foe = foes.next()))
        if (foe->id == id)
            break;
    // Attack only foes
    if (!foe)
        return;

    std::cout << self->name << "(" << self->health << ") attacks "
              << foe->name << "(" << foe->health << ")" << std::endl;
    while (true)
    {
        // first attacker attacks
        doSingleFightSequence(*self, *foe);
        if (foe->health == 0)
        {
            std::cout << self->name << " defeated " << foe->name << std::endl;
            return;
        }
        // then foe retaliates
        doSingleFightSequence(*foe, *self);
        if (self-&tl;health == 0)
        {
            std::cout << self->name << " was defeated by "
                      << foe->name << std::endl;
            return;
        }
    }
}

攻击逻辑很简单。当 actor 攻击另一个 actor(通过 id 识别)时,攻击者需要遍历其敌人列表,如果不是敌人则立刻终止。actor(通过doSingleFightSequence() 函数)攻击敌人,攻击将会减少敌人的血量。如果敌人仍然存活,则会反击攻击者,直到一方死亡。

这是我们本节的内容。下面我们将讨论BattleManager以及游戏主循环的设计。我们将深入研究如何为我们的 RPG 游戏编写插件,了解系统目录结构等内容。

Leave a Reply