在之前的文章中我们主要围绕着我们所了解的人工智能背后的概念展开。在本文中我们将把执行带入一款完整的冰球游戏中。你将从中学到如何添加必要组件到游戏中,如分数,道具以及一些游戏设计。
最终结果
以下是将使用本教程中所描述的所有元素而执行的游戏。

Hockey(from tutplus)
想想游戏设计
在之前的文章中我们主要着眼于解释游戏AI是如何运行的。每一部分都详细描述了游戏的特定方面,如角色如何移动,攻击和防御动作如何执行等等。它们都是以操纵行为以及成堆的有限状态机器等概念为基础。
为了创造一款完整的游戏,我们必须将所有这些元素整合到一个核心的游戏机制中。最显著的选择便是执行一场官方冰球比赛中的所有官方规则,但这需要我们投入更多精力与时间。让我们采取一种更加简单的方法。
所有的冰球规则将被唯一一个规则所取代:如果你正运行着冰球并撞到了对手,你便会被冻起来并被彻底粉碎!如此对于双方玩家来说游戏将变得更简单且更有趣:即一方尝试着运行冰球而另一方尝试着抢球。
为了加强这一机制,我们将添加一些道具。它们将帮助玩家获得分数并让游戏变得更加动态化。
添加能力到分数中
让我们先从分数系统开始,即决定玩家输赢的要素。每当一支团队将冰球带进对方的球门时他们便能够获得分数。
执行这一行动的最简单方式便是使用两个重叠的矩形:

goal-rectangles(from tutplus)
绿色矩形代表的是球门结构(框和网)所占有的区域。它就像是一个固体组块,所以冰球和运动员不可能穿越它;他们将被反弹回去。
红色矩形代表的是“分数区域”。如果冰球与这一矩形重叠,这便代表团队得分了。
红色矩形小于绿色,并且位于前方,所以如果冰球碰触到除前方外球门的任何一面,它便会被反弹并且玩家也不会得到分数:

goal-rectangles-possible-collisions(from tutplus)
在有人得分后组织一切内容
在一支队伍得分后,所有的运动员必须回到最初的位置,冰球也必须再次被置于溜冰场中心位置。在这个过程之后,比赛将继续进行。
将运动员移至最初位置
就像之前文章中所提到的,所有运动员都有一个名为prepareForMatch的AI状态,即将把他们移至最初位置上,并会让他们顺畅地停在那里。
当冰球与其中一个“分数区域”重叠时,所有运动员的任何当前有效的AI状态都会被删除。不管这时候运动员位于何处,他们都将在几秒钟后回到最初的位置上:
将摄像机移至溜冰场中央
因为摄像机总是跟着冰球,所以如果在有人得分后它直接转向溜冰场中央位置,那么当前的视图便会突然发生改变,从而会让玩家感到困惑。
解决这种情况的一种有效方法便是顺畅地将冰球朝溜冰场中央位置移动;因为摄像机将跟着冰球,所以这恩能够够将视图中球门慢慢转向中央位置。
我们可以通过在冰球进入任何球门区域后改变它的速度矢量而做到这点。全新的速度矢量必须能够“推动”冰球朝着溜冰场中央移动,即:
var c :Vector3D = getRinkCenter();
var p :Vector3D = puck.position;
var v :Vector3D = c – p;
v = normalize(v) * 100;
puck.velocity = v;通过从冰球当前位置减去溜冰场中央的位置,我们便能够计算到达中位置的点数矢量。
在标准化这一矢量后,它可以扩展成任何值,如100,并且能够控制冰球朝中央位置移动的快慢。
以下是关于全新速度矢量的一张图:

move-towards-center(from tutplus)
矢量V是作为冰球的速度矢量,所以冰球将按照预期朝着溜冰场中央移动。
为了确保冰球朝着溜冰场中央移动的同时能够避免任何奇怪的行为,如与运动员的互动,在这个过程中我们需要将冰球设为无效。结果便是,它将不再与运动员产生互动并且将被标记为不可见。玩家将看不到冰球的移动,但是摄像机仍然能够跟着它。
为了判断冰球是否已经位于正确的位置上,我们可以在移动过程中计算它与中央位置间的距离。如果距离小于10,冰球便非常接近于中央位置,这时我们便可以再次激活它以推动比赛的继续。
添加道具
道具背后的理念是帮助玩家获得游戏的主要目标,即通过将冰球运到对方的球门而得分。
关于范围,我们的游戏只拥有2种道具:Ghost Help和Fear The Puck。前者能够添加额外的3名运动员而提高给玩家的团队暂时的帮助,后者则能够让对方远离冰球几秒钟。
当任何人得分时,双方队伍都能够获得道具。
执行“Ghost Help”
因为所有拥有Ghost Help道具的玩家都只能得到暂时的帮助,所以运动员类别将得到修改从而让运动员带有“ghost”的标志。如果一名运动员变成了ghost,那么在几秒钟后它的这一标志便会消失。
以下便是运动员类,只突出添加内容以适应ghost功能:
public class Athlete
{
// (…)
private var mGhost :Boolean; // tells if the athlete is a ghost (a powerup that adds new athletes to help steal the puck).
private var mGhostCounter :Number; // counts the time a ghost will remain active
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
// (…)
mGhost = false;
mGhostCounter = 0;
// (…)
}
public function setGhost(theStatus :Boolean, theDuration :Number) :void {
mGhost = theStatus;
mGhostCounter = theDuration;
}
public function amIAGhost() :Boolean {
return mGhost;
}
public function update() :void {
// (…)
// Update powerup counters and stuff
updatePowerups();
// (…)
}
public function updatePowerups() :void {
// TODO.
}
}mGhost的性质属于布尔数学体系,能够判断运动员是否属于ghost,同时mGhostGounter包含了运动员在失去ghost功能前的时间数。
这两种性质都是基于updatePowerups()方法:
private function updatePowerups():void {
// If the athlete is a ghost, it has a counter that controls
// when it must be removed.
if (amIAGhost()) {
mGhostCounter -= time_elapsed;
if (mGhostCounter <= 2) {
// Make athlete flicker when it is about to be removed.
flicker(0.5);
}
if (mGhostCounter <= 0) {
// Time to leave this world! (again)
kill();
}
}
}updatePowerups()方法是在运动员的update()程序中进行调用,能够处理运动员的所有升级过程。现在它将判断当前的运动员是否属于ghost。如果是的话,mGhostCounter性质将随着时间流逝而递减。
当mGhostCounter值到达0时,这便意味着临时运动员所存活时间已过,它将被删除。为了让玩家意识到这点,运动员会在消失前2秒开始闪烁。
最后,当道具被激活时,我们便可以执行添加临时运动员到游戏中的过程。这是基于powerupGhostHelp()方法执行的,这在主要游戏逻辑中是可行的:
private function powerupGhostHelp() :void {
var aAthlete :Athlete;
for (var i:int = 0; i < 3; i++) {
// Add the new athlete to the list of athletes
aAthlete = addAthlete(RINK_WIDTH / 2, RINK_HEIGHT – 100);
// Mark the athlete as a ghost which will be removed after 10 seconds.
aAthlete.setGhost(true, 10);
}
}这一方法将在一个循环过程中跌带着,即相当于被添加到游戏中的临时运动员的数量。每个新运动员将被添加到溜冰场底部并标记为ghost。
就像上面所描述的那样,ghost运动员在时间到达后会被删除。
执行“Fear The Puck”
Fear The Puck道具将让所有对手远离冰球几秒。
就像Ghost Help那样,我们必须修改运动员类以适应这一功能:
public class Athlete
{
// (…)
private var mFearCounter :Number; // counts the time the athlete should evade from puck (when fear powerup is active).
public function Athlete(thePosX :Number, thePosY :Number, theTotalMass :Number) {
// (…)
mFearCounter = 0;
// (…)
}
public function fearPuck(theDuration: Number = 2) :void {
mFearCounter = theDuration;
}
// Returns true if the mFearCounter has a value and the athlete
// is not idle or preparing for a match.
private function shouldIEvadeFromPuck() :Boolean {
return mFearCounter > 0 && mBrain.getCurrentState() != idle && mBrain.getCurrentState() != prepareForMatch;
}
private function updatePowerups():void {
if(mFearCounter > 0) {
mFearCounter -= elapsed_time;
}
// (…)
}
public function update() :void {
// (…)
// Update powerup counters and stuff
updatePowerups();
// If the athlete is an AI-controlled opponent
if (amIAnAiControlledOpponent()) {
// Check if “fear of the puck” power-up is active.
// If that’s true, evade from puck.
if(shouldIEvadeFromPuck()) {
evadeFromPuck();
}
}
// (…)
}
public function evadeFromPuck() :void {
// TODO
}
}首先updatePowerups()方法将渐减mFearCounter属性,即包含了运动员离开冰球的时间。每当调用fearPuck()方法时,mFearCounter性质便会发生改变。
在Athlete的update()方法中,我们将添加测试去判断是否出现了升级道具。如果运动员是由AI(游戏邦注:amIAnAiControlledOpponent()变成true)控制的对手,evadeFromPuck()方法将被调用。
evadeFromPuck()方法使用了躲避行为,即能让一个实体避开任何对象及其轨迹:
private function evadeFromPuck() :void {
mBoid.steering = mBoid.steering + mBoid.evade(getPuck().getBoid());
}所有的evadeFromPuck()方法都能够添加躲避机制到当前的运动员转向力中,从而让运动员无需忽视已经添加的转向力而避开冰球,这就像当前激活的AI状态所创造的内容。
为了被躲避,冰球必须像boid一般,即所有的运动员所做的那样。结果便是包含了冰球当前位置和矢量的boid属性必须被添加到冰球类中:
class Puck {
// (…)
private var mBoid :Boid;
// (…)
public function update() {
// (…)
mBoid.update();
}
public function getBoid() :Boid {
return mBoid;
}
// (…)
}最后我们将在道具被激活时升级主要游戏逻辑从而让对手害怕冰球而避开它:
private function powerupFearPuck() :void {
var i :uint,
athletes :Array = rightTeam.members,
size :uint = athletes.length;
for (i = 0; i < size; i++) {
if (athletes[i] != null) {
// Make athlete fear the puck for 3 seconds.
athletes[i].fearPuck(3);
}
}
}这一方法将在所有对方运动员身上进行迭代,并调用其中的每个fearkPuck()方法。这将触发让运动员在几秒钟内害怕冰球的逻辑。
冰冻与碎裂
最后需要添加到游戏中的便是冰冻与碎裂部分。他们是基于主要的游戏逻辑,即程序将判断左边团队的运动员是否与右边团队的运动员发生重叠。
这一重叠检查是由Flixel游戏引擎自动执行的,每当发现重叠时便能够唤醒回调函数:
private function athletesOverlapped(theLeftAthlete :Athlete, theRightAthlete :Athlete) :void {
// Does the puck have an owner?
if (mPuck.owner != null) {
// Yes, it does.
if (mPuck.owner == theLeftAthlete) {
//Puck’s owner is the left athlete
theLeftAthlete.shatter();
mPuck.setOwner(theRightAthlete);
} else if (mPuck.owner == theRightAthlete) {
//Puck’s owner is the right athlete
theRightAthlete.shatter();
mPuck.setOwner(theLeftAthlete);
}
}
}这一回调函数是作为每个重叠的团队运动员的参数。测试将检查冰球的持有者是否是无效的,这意味着冰球是由某人所控制着。
在这种情况下,冰球的持有者将与被重叠的运动员相比较。如果其中的一名运动员正控制着冰球(游戏邦注:所以他便是冰球持有者),他便会被粉碎,而冰球的所有权也会转交到其他运动员手上。
运动员类中的shatter()方法将把运动员标记为暂停,并在几秒钟后将其置于溜冰场底部。这同时也将释放一些微粒去代表冰块,不过这并不属于本文要讨论的内容。
结论
在本篇教程中我们执行了将我们的冰球原型变成一款真正完整的游戏所需要的一些元素。我希望将注意力放在这些元素的背后概念而不是如何在游戏引擎X或Y中执行它们。
也许在游戏中使用冰冻与碎裂方法听起来太过疯狂,但这却能够确保这个项目是容易控制的。体育规则总是非常明确,而它们的执行又非常棘手。
通过添加一些画面和HUD元素,你便能够基于这一演示版本而创造出属于自己的完整冰球游戏了。
