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_CreateFunc 和PF_DestroyFunc 函数指针。这就是全部条件,没有其他要求了。因为 ActorBaseTemplate 已经满足了这个要求,所以凡是继承自 ActorBaseTemplate 的类都可以与 PluginHelper 兼容。
PluginHelper 以 PF_initPlugin() 作为入口点,并在其内部使用。我们会在后面的文章中看到究竟如何使用这个类。现在,我们浏览一下 PluginHelper 提供给插件开发者哪些有用的服务。入口点函数用于注册所有支持的插件对象,如果成功,则返回具有特定签名的指向 PF_ExitFunc 的函数指针。如果有问题则返回 NULL。
PluginHelper 构造函数接受一个指向 PF_PlatfromServices 结构的指针。该结构体包含了主系统插件 API 的版本、invokeService 和 registerObject 两个函数指针并且将其保存下来。如果插件初始化成功,它也会在 result 成员中保存 exitPlugin 函数指针。
PluginHelper 提供一个模板化的 registerObject 函数,完成了我们所需要的大多数工作。模板参数 T 代表需要注册的对象类型。该类型需要有 create() 和 destroy() static 函数,用于赋值给PF_CreateFunc 和 PF_DestroyFunc。它接受一个对象类型字符串和一个可选的编程语言类型(默认是 PF_ProgrammingLanguage_C)。这个函数需要执行版本检测,以保证插件版本与主系统兼容。如果这些检测都通过了,就会准备一个 RegisterObjectParams 结构,调用registerObject() 函数,然后检查其返回值。如果版本检测或者 registerObject 函数指针调用失败,则会报告错误(这一点是由 CHECK 宏实现的),并且将 result_ 设置为 NULL ,抛出异常。之所以不会让异常扩散,是因为 PF_initPlugin(这是 PluginHelper 假设会使用的)是一个 C 函数,不应该将异常发送到二进制兼容边界之外。在 registerObject 中捕获所有异常,能够减轻开发者的处理负担(甚至让他们忘记这件事)。这是使用 THROW、CHECK 和 ASSERT 宏以获得方便的好例子。错误信息则使用流运算符构建,不需要分配缓存、合并字符串或者使用 printf。reportError 调用的结果会包含错误位置信息(__FILE__ 和 __LINE__),无需手动指定。
一般的,一个插件会注册多于一个对象类型。如果有一个对象类型注册失败,result_ 就会是 NULL。对于某些对象,这么做是可以的。例如,你可能需要注册同一对象的多个版本,其中一个版本主系统不支持。此时,该对象类型就会注册失败。插件开发者需要在每一个 PluginHelper::registerObject() 调用后检测 result_ 的值,来确定是不是致命错误。如果不是致命错误,只需要在最后返回 PluginHelper::ExitPlugin。
默认行为是,每一个失败都是致命的,插件开发者应该返回 PluginHelper::getResult(),这个函数会返回 result_ 的值,该值就是 PluginHelper::ExitPlugin(所有注册都是成功的)或者 NULL(任一注册失败)。
我喜欢 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 创建一个新的 Turn。Turn 对象传递给每一个 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 游戏编写插件,了解系统目录结构等内容。