在此文中,我将讲述如何做RPG游戏中人物类的技术设计,以及如何使用简单程序模型让游戏开发更灵活、可变、而且连贯。
若要此文对你有用,你必须大致了解面向对象编程(Object Orientated Programming)和接口(Interfaces)、继承(Inheritance)、多态(Polymorphism)等概念有基础的了解。
还需要注意的是,本文不是一篇编程教程,只是一篇关于事情该如何发生的概述。我会讲到一些功能,但不会放很多代码。大部分了解上述概念的游戏开发者都应该可以把这个蓝图应用在游戏中。
我还想说明这篇文章只是关于基础人物设计,我们不会涉及到技能树、技能作用和人物塑造,那些对于此文来说还是太深了。
在开头,为了简便,我们举例说我们要做一个RPG游戏中的英雄人物模型,这个游戏中只有三类英雄:战士、魔法师和弓箭手。
为了让这三类人的架构设计得灵活、容易维护又方便延伸,我们将开发一个非常有用的接口和类层次结构。
英雄类人物的技术模型
从总体开发上说,这个任务并不难,但问题是大多数时候最终的成品都并不灵活可变,所以也无法重复利用。一个很常见的问题是,在做这类人物设计时,新手开发者常常会专门开发三类(class):战士类、魔法师类和弓箭手类,然后就没有了,就只有这三类。别误会,这样可行。但它很容易导致问题。
为什么这样不好?以下列了几个问题:
- 你必须重复写三个类中都有的基础东西
- 如果你想改人物的基础概念,你得在每个类中都改一遍
- 如果一个bug老是出现,你必须在每一个类中把它修改好
- 还有很多其他可能出现的麻烦
所以我们如何避免这个问题呢?
很简单。我们要设计一个可以把所有人物的共同特征记录下来的人物接口(character interface)。然后,我们要设计一个可以通过英雄共有的基础程序设计代码将这个接口应用起来的基础人物类。最后,我们再在这个基础人物类的基础上延伸设计出战士、魔法师和弓箭手三类人物。
开发技术设计
我们从最基础的开始。这是我们的类层次结构大致的样子。
人物接口
首先我们要设计一个人物接口,在这篇文章中我们暂时叫它ICharacter。
让我们开始思考,这些人物共有的特征是什么?他们都能攻击(attack)、移动(move)、触发动作(trigger action)和死亡(die)。当然,每类英雄之后都会对他们对这些特征有不同的表达方式。
所以,这个接口会有以下的方法(method):
- onMove()
- onAttack()
- onActionTrigger()
- onCharacterDeath()
人物类
现在我们已经准备好了接口,我们应该创造一个基础的类来应用它。在这篇文章中我们叫它CharacterBase(人物基础)。它的原理是什么呢?我们在应用这个接口前,应该写好将被启用在每个人物中的基础代码(code)。每个方法都应只含基本数量的代码。
- onMove() — 这个方法将处理人物的运动形式,也会触发运动的动画。
- onAttack() — 因为每类人物都有自己的攻击的形式,这里我们只要处理动画的触发并保证 (对游戏设计而言这一步或许不同) 我们有开始攻击所需的调配。同时也要计算对目标的伤害值。
- onActionTrigger() — 这个基本上就是在人物与其他东西互动时会触发动画。三类人物可能会有不同的与其他事物互动的方式。
- onCharacterDeath() — 这个会触发死亡的动画,然后记录下所有之后的游戏需要的资料和信息。
战士类
基础部分已经搞定了。现在我们来制作战士类。我们暂时叫它WarriorCharacter(战士人物)。它将延伸CharacterBase并基本上覆盖所有的方法。
这是为了适合战士类这些方法应做出的改变。
- onMove() — 在这儿我们需要给人物加上一些具体的运动形式。因为这是一名战士,他需要运动得较慢但非常稳定。所以我们将减缓他的运动速度。处理完这个之后,我们将用super.onMove() 引入CharacterBase里的基础代码。这样,当我们需要解析运动形式时我们早就已经完成了改变,所以现在它按照我们想要的方式启动。它也会触发战士移动的动画。
- onAttack() — 这个方法应首先检查战士到他的目标需要进行的位置移动。如果在战士的攻击范围以内,我们就将用super.onAttack() 或其他你的编程语言里用来这样做的命令来引入CharacterBase中的onAttack() 方法。
- onActionTrigger() — 战士要能够接触可破坏物品,并破坏它,因此我们将在这个方法中准备一个触发器来触发破坏一个具体物件的动作。在实际操作中,这个应该在物件而不是人物的基础上设计。总之,开始的时候我们应从CharacterBase中引入基础的代码然后通过让战士能破坏物品来延伸代码。
- onCharacterDeath() — 为了延伸死亡动画的代码,当一名战士死亡时,我们会希望加入一些具体的效果,例如掉落武器。引入过基础代码后,我们可以也加上这些。
请记住,为了应用CharacterBase中的基础代码,你需要用super.onAttack() 或者你想要应用的方法来完成。
魔法师类
现在是完善魔法师的代码的时候了。我们叫它MageCharacter(魔法师人物)。它将延伸CharacterBase,而且原理会与战士类人物很相似。魔法师的攻击范围会和战士非常不同,伤害也因距离而逐渐不同,所以我们要在攻击上加一个导弹效果。重申一下,当你要在方法中加额外的代码的时候,一定记住把基础代码从CharacterBase中引入过来。否则他们的基础功能就不会被执行,那就不会有任何用处。所以这是我们基础的方法大致会像这个样子:
- onMove() — 在这里人物的运动形式和速度应该都比较普通。从物理的角度来说,魔法师是一个普通人,所以他应该像一个比较平均的人一样移动。不过我们可能用到魔法。假设我们用某种技能(最开始就提到,我们不会深讲技能的东西)让魔法师飞起来,在这个方法中我们就需要检查这个技能是否已经生效。如果是的话,我们就需要改变魔法师的运动形式、速度甚至动画效果。
- onAttack() — 就像我们对战士类做的一样,在找出攻击范围后,魔法师就可以攻击了,但这个范围应该比战士的大。在那一点上我们应加上一个导弹效果,这个效果可以通过额外的代码完成,不过那就超过了本文讨论的范围。
- onActionTrigger() — 魔法师要能够给物件施法,就是说能够将技能用在物件上或攻击物件,比如说魔法书。
- onCharacterDeath() — 如果魔法师能够使用自救技能的话,我们就要设置一个死亡的时间间隔(time span) 好方便他使用技能。接着我们就可以触发标准代码了。
弓箭手类
就像魔法师类和战士类一样,我们把CharacterBase延伸为一个叫ArcherCharacter(弓箭手人物)的类。现在你多半知道接下来要如何了。我们需要非常快的运动速度以及快速从可见地图一边到另一边的运动形式。对于这类人物我们还需要设置快速、频繁的攻击,也许还有多倍攻击一类的技能,就是在一击的时间内释放两波攻击。我们肯定也要用到让人物与物件互动的代码,比如弓箭手也许能藏在灌木丛中。在临死时,我们可以设置让弓箭手倒下前对目标放出最后一箭。
有趣的观点
在此文中我只是想讲述如何最大程度做基础人物类设计。如果你要真的将这个做到最好,你的类层次结构会大很多,给近身人物、用魔法的人物和远程人物都要有不同的类,然后再做具体的人物类的继承(Inheritance)。
还有,说实话,要让这个东西真正称得上好,你需要使用更多的方法(method),例如处理人物各项属性、处理人物技能使用的方法。你还会需要添加人物的体力、耐力等的变量,还有具体人物有的变量,比如魔法师的魔力值。
不过这些都会让这篇文章太复杂。本文的目标读者是刚开始接触到继承、多态等概念的开发者。如果你可以坐下写出你的基本技术设计,那你就多半可以进入更高一层了。
总结
人物类设计的技术方面并不是很难理解或应用的东西,但是要记住,面向对象编程的理论和理论背后的重复利用的因素可以帮你节省很多时间、bug,而且真的能让你的代码在很多方面都更好一些。