2.2.4 敌人
在前面的章节中已经讲述了那些比较友善的角色,如非玩家角色(NPCs),并且你也了解了用于和敌人打斗时所使用的物品和武器,那么敌人本身又应该具备什么样的能 力呢?
在PRG游戏中,敌人充当的是邪恶的敌对角色。他们在游戏世界中四处游荡并不停地袭击游戏玩家以阻止他们完成那些他们希望完成的任务。在打斗的过程当中,敌人的动作行为和游戏玩家以及其盟友的动作行为十分相似;双方都使用各种武器来袭击对方并渴望打败对手,也都使用像疗伤金丹和提升力量或者速度的药剂这样的物品来帮助自己。
概括地说,这也就是你选择RPG游戏的最为重要的一个原因了。除了对话、探测地图以及解谜题之外,至少一半的游戏时间(有时要比这更多一些,这取决于它到底是哪个游戏)都用在与别人的打斗上。因此下面这种现象也就不足为奇了,RPG游戏中实现敌人时所采用的方法对于这个工程本身以及整个项目的最终结果都会有很大的影响。图2.8所示就是从游戏Breath of Fire中获取的一张屏幕截图。Breath of Fire是一个商业的角色扮演游戏,它的打斗场景就采用了现在正在讨论的方法。

图2.8 Capcom的Breath of Fire系列游戏中的一个打斗场景
但是有关敌人的最为主要的部分则在于它同时利用了你前面已经学习的两个概念:它具有非玩家角色的特征或者是个性为中心的方面,同时它还具有物品和武器的功能以及破坏特性。因此,怎样在你的RPG游戏中定义敌人基本上就是一个怎样将这两个实体背后的概念相结合的过程。
解决方法
你可以用很多种方法来解决这个问题,但最终它们都可以归结到一些非常相似的领域。就像在游戏中设计非玩家角色一样,在描述敌人的过程中最需要考虑的重要特性就是这个敌人的个性和行为。它是不是一头强壮、行动快捷,而且还很强大的野兽?它能不能很无情地袭击它的敌人而又能很容易地避过对方的反击?还是它是一只温顺的生物,它袭击的非常慢并且对敌人造成的伤害也非常小?它可能是这二者之中的一种,但它更有可能会介于两者之间——这是一个灰色区域,它的描述方法需要对参数敏感而且同时又要便于调节。
你可能会试图通过使用一组参数来定义你的敌人从而解决这个问题。例如,游戏中敌人的行为可以描述为以下方面:
● 力量:每一次攻击具有多大的威力。
● 速度:每一次攻击需要多长时间才会作用到敌人的身上,也就是敌人有多大的可能性躲避掉对方的攻击。
● 忍耐力:当敌人受到很明显的伤害后它还能坚持多久。较高的忍耐力可以使得你的敌人在情况变得很坏的情况下还能保持它的攻击力。
● 装甲/防御:对方的攻击会对你造成多大的伤害。装甲/防御的水平越低,在打斗的过程中因为自身的弱点,它的体力值就会减少得越快。
● 恐惧:在交手的过程中,敌人有多大的可能性会逃跑。
● 智力:决定打斗过程中敌人行为的整体策略。具备较高智能的敌人可能会故意攻击玩家一方中最弱的成员,或者可能保存它们最有力、最具伤害性的攻击手段以攻击玩家中最强大的一个。较低智能的生物就很少考虑这些问题,它们可能会浪费时间去采用错误的进攻手段攻击敌人,完全采用一种暴力的手段直到对手被 打败。
你可以持续不断地添加类似的参数,这个看起来也像是一个很不错的单子。很明显你可以用这种方法来描绘许多种敌人。毫无疑问,一个巨大的魔鬼般的野兽可能会有特别大的力量、无穷的忍耐力、坚如岩石的防御力,并且几乎不会恐惧。然而,它不会很聪明很敏捷。与此类似,一个日本武士或者一个刺客将会具备很好的速度和忍耐力,以及较高的智能和相当可观的力量。一个地位低贱的下人或许所有这些水平都很低,而最后的大恶人或许在所有这些方面都又会是最强的。总体来说,这是一个简单的系统,但是它使得你能足够灵活地快速定义一大批各种各样的敌人。
但是,这个看起来相当令人怀疑。就像在第1章有关物品描述文件的课程中了解到的那样,在你的游戏中定义一大组实体,而它们之间就只是具有很少的一些公共参数,这种做法很快就会把你逼入死角,使得你没有什么可以创作的空间。但是,就像你极有可能已经猜到的那样,脚本又一次能够很好地解决这个问题。
不过,你又该如何真正组织这些脚本的代码呢?除了前面我提到的描述敌人的脚本与描述物品和非玩家角色的脚本之间的相似性之外,机敏的读者可能已经发现了二者之间还存在一个很大的差别。物品、武器和非玩家角色都是基于同样的问题发生的:它们都会因为某种触发条件或者是某个事件的激发而执行它们的功能,当任务完成后终止运行。Fire Sword在你使用它之前一直都保持着静止的状态。当你使用它以后,它就会在屏幕上投掷出一个火球,减少敌人的体力值,然后迅速地将控制权交给游戏引擎。非玩家角色采用同样的方式工作,唯一真正的不同就是他们谈论的是大蒜而不是攻击别人。但是在两种情况下,非玩家角色和武器都是在被使用的情况下才开始工作的。
但是,敌人看起来更像是游戏玩家,他们在打斗的过程中不停地影响游戏的进展。从游戏开始的那一刻到敌人或者是游戏玩家被打败的那一刻,敌人都要理解游戏玩家的输入并且基于对手的输入做出决策。这个活动的状态是连续的,因此,它的脚本也应该采用不同的方法编写。基本上,差别在于你应该把这个时候的代码编写成为一个更大的,并且不断循环的形式,而不再是简单的、自包含的事件。如图2.9所示,你不能够更加形象地理解这个概念。RPG游戏中的打斗场面的主体框架是循环。在循环过程的每一个迭代过程中,游戏玩家和敌人都需要对输入进行分析。就游戏玩家而言,这就意味着他要处理从输入设备输入的数据;而对于他的敌人而言,这就意味着他要执行相应的打斗脚本。
事实上,就像所有类型的游戏一样,RPG游戏中的打斗也就是一个持续不断的循环过程。在循环的每一次迭代过程中,游戏接受游戏玩家和敌人的输入,管理他们之间的相互交互,并计算他们行为的最终结果。它不停地完成这种动作直到游戏双方有一方被打败。这个时候,循环终止,一个胜利者也就产生了。所以,我们不能编写只运行一次,然后就不用再管了的代码,我们需要编写一段更加详细的、结构清晰的代码。当每次打斗循环重复的时候,游戏引擎可以自动地调用它。一段智能的敌人脚本并不是迅速地做完一件事情,它需要重复处理上次执行结束后所有接收到的输入并立即对输入做出反应。下面就是一段简单的例子:

图2.9 游戏中的循环过程
void Act ()
{
int iWeakestPlayer, iLastAttacker;
if ( iHitPoints < 20 )
if ( rand () % 10 == 1 )
Flee ();
else
{
iWeakestPlayer = GetWeakestPlayer ();
if ( Player [ iWeakestPlayer ].iHitPoints < 20 )
Attack ( iWeakestPlayer, METEOR_SHOWER );
else
{
iLastAttacker = GetLastAttacker ();
switch ( Player [ iLastAttacker ].iType )
{
case NINJA:
{
Attack ( iLastAttacker, THROW_FIREBALL );
break;
}
case MAGE:
{
Attack ( iLastAttacker, BROADSWORD );
break;
}
case WARRIOR:
{
Attack ( iLastAttacker, SUMMON_DEMON );
break;
}
}
}
}
}
就像你看到的那样,这是一段相当简单的代码。更为重要的是,你要注意到它并不真正具有一个开始和结束地点。它会被“插入”到一段已经正在运行的循环当中,这个循环过程负责为它提供原始输入,它也是基于这个输入做出决策。
简单地说,这段智能代码的工作过程是:首先,敌人脚本会确定它会被打败的可能性有多大。如果这个结果低于一定的临界值(这个例子中是低于20个体力值),那么它就会模拟一个试图从打斗现场逃跑的动作,例如,在1~10之间随机产生一个随机数的结果,如果是1的话,它就会逃跑。然而,如果他觉得非常有必要接着继续打斗,那么它就会调用有游戏引擎提供的一个函数去确定哪个是对手中最弱的。如果敌人觉得这个游戏玩家比较接近被打败的状态(在这个例子中,如果玩家的体力值低于20),他就会使用颇具攻击力的Meteor Shower来将对手打败。如果这个最弱的敌人并不是十分的弱小以至于很容易被打败出局,敌人就会转去袭击那个最后攻击他的人,并且会基于对手的类型选择一个具体的进攻策略。
这个处理过程是比较公平的。看看这些面向过程的代码所能够处理的问题之后,现在基于参数的敌人描述方法很有可能就不是那么吸引人了。
好了,到目前为止,我们已经能够比较好地解决RPG游戏的脚本编写问题了,所以现在你可以把注意力转向一个更加动作型的游戏种类——第一人称射击游戏(first-person shooters)。






