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_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 游戏
我喜欢 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->health == 0)
{
std::cout << self->name << " was defeated by "
<< foe->name << std::endl;
return;
}
}
} 攻击逻辑很简单。当 actor 攻击另一个 actor(通过 id 识别)时,攻击者需要遍历其敌人列表,如果不是敌人则立刻终止。actor(通过doSingleFightSequence() 函数)攻击敌人,攻击将会减少敌人的血量。如果敌人仍然存活,则会反击攻击者,直到一方死亡。
这是我们本节的内容。下面我们将讨论BattleManager以及游戏主循环的设计。我们将深入研究如何为我们的 RPG 游戏编写插件,了解系统目录结构等内容。