游戏邦在:
杂志专栏:
gamerboom.com订阅到鲜果订阅到抓虾google reader订阅到有道订阅到QQ邮箱订阅到帮看

如何创造一款冰球游戏的AI:游戏机制

发布时间:2015-04-17 11:51:35 Tags:,,,,

作者:Fernando Bevilacqua

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

最终结果

以下是将使用本教程中所描述的所有元素而执行的游戏。

Hockey(from tutplus)

Hockey(from tutplus)

想想游戏设计

在之前的文章中我们主要着眼于解释游戏AI是如何运行的。每一部分都详细描述了游戏的特定方面,如角色如何移动,攻击和防御动作如何执行等等。它们都是以操纵行为以及成堆的有限状态机器等概念为基础。

为了创造一款完整的游戏,我们必须将所有这些元素整合到一个核心的游戏机制中。最显著的选择便是执行一场官方冰球比赛中的所有官方规则,但这需要我们投入更多精力与时间。让我们采取一种更加简单的方法。

所有的冰球规则将被唯一一个规则所取代:如果你正运行着冰球并撞到了对手,你便会被冻起来并被彻底粉碎!如此对于双方玩家来说游戏将变得更简单且更有趣:即一方尝试着运行冰球而另一方尝试着抢球。

为了加强这一机制,我们将添加一些道具。它们将帮助玩家获得分数并让游戏变得更加动态化。

添加能力到分数中

让我们先从分数系统开始,即决定玩家输赢的要素。每当一支团队将冰球带进对方的球门时他们便能够获得分数。

执行这一行动的最简单方式便是使用两个重叠的矩形:

goal-rectangles(from tutplus)

goal-rectangles(from tutplus)

绿色矩形代表的是球门结构(框和网)所占有的区域。它就像是一个固体组块,所以冰球和运动员不可能穿越它;他们将被反弹回去。

红色矩形代表的是“分数区域”。如果冰球与这一矩形重叠,这便代表团队得分了。

红色矩形小于绿色,并且位于前方,所以如果冰球碰触到除前方外球门的任何一面,它便会被反弹并且玩家也不会得到分数:

goal-rectangles-possible-collisions(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)

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元素,你便能够基于这一演示版本而创造出属于自己的完整冰球游戏了。

本文为游戏邦/gamerboom.com编译,拒绝任何不保留版权的转发,如需转载请联系:游戏邦

Create a Hockey Game AI Using Steering Behaviors: Game Mechanics

by Fernando Bevilacqua

In past posts in this series, we’ve focused on the concepts behind the artificial intelligence we’ve been learning about. In this part, we’ll wrap all the implementation into an entirely playable hockey game. You’ll learn how to add the missing pieces required to turn this into a game, such as score, power-ups, and a bit of game design.

Final Result

Below is the game that will be implemented using all the elements described in this tutorial.

Thinking Game Design

The previous parts of this series focused on explaining how the game AI works. Each part detailed a particular aspect of the game, like how athletes move and how attack and defense are implemented. They were based on concepts like steering behaviors and stack-based finite state machines.

In order to make a fully playable game, however, all those aspects must be wrapped into a core game mechanic. The most obvious choice would be to implement all the official rules of an official hockey match, but that would require a lot of work and time. Let’s take a simpler fantasy approach instead.

All hockey rules will be replaced with a single one: if you are carrying the puck and are touched by an opponent, you freeze and shatter into a million pieces! It will make the game simpler to play and fun for both players: the one carrying the puck and the one trying to recover it.

In order to enhance this mechanic, we’ll add a few power-ups. They will help the player to score and make the game a bit more dynamic.

Adding the Ability to Score

Let’s begin with the scoring system, responsible for determining who wins or loses. A team scores every time the puck enters the opponent’s goal.

The easiest way to implement this is by using two overlapped rectangles:

The green rectangle represents the area occupied by the goal structure (the frame and the net). It works like a solid block, so the puck and the athletes will not be able to move through it; they will bounce back.

The red rectangle represents the “score area”. If the puck overlaps this rectangle, it means a team just scored.

The red rectangle is smaller than the green one, and placed in front of it, so if the puck touches the goal on any side but the front, it will bounce back and no score will be added:

Organizing Everything After Someone Scores

After a team scores, all athletes must return to their initial position and the puck must be placed at the rink center again. After this process, the match can continue.

Moving Athletes To Their Initial Position

As explained in the first part of this series, all athletes have an AI state called prepareForMatch that will move them towards the initial position, and cause them to smoothly come to a stop there.

When the puck overlaps one of the “score areas”, any currently active AI state of all athlete is removed and prepareForMatch is pushed into the brain. Wherever the athletes are, they will return to their initial position after a few seconds:

Moving the Camera Towards the Rink Center

Since the camera always follows the puck, if it is directly teleported to the rink center after someone scores, the current view will abruptly change, which would be ugly and confusing.

A better way to do this is to move the puck smoothly towards the rink center; since the camera follows the puck, this will gracefully slide the view from the goal to the rink center.

This can be achieved by changing the puck’s velocity vector after it hits any goal area. The new velocity vector must “push” the puck towards the rink center, so it can be calculated as:

By subtracting the rink center’s position from the puck’s current position, it is possible to calculate a vector that points directly towards the rink center.

After normalizing this vector, it can be scaled by any value, like 100, which controls how fast the puck moves towards the rink center.

Below is an image with a representation of the new velocity vector:

This vector V is used as the puck’s velocity vector, so the puck will move towards the rink center as intended.

To prevent any weird behavior while the puck is moving towards the rink center, such as an interaction with an athlete, the puck is deactivated during the process. As a consequence, it stops interacting with athletes and is marked as invisible. The player will not see the puck moving, but the camera will still follow it.

In order to decide whether the puck is already in position, the distance between it and the rink center is calculated during the movement. If it is less than 10, for instance, the puck is close enough to be directly placed at the rink center and reactivated so that the match can continue.

Adding Power-Ups

The idea behind power-ups is to help the player achieve the game’s primary objective, which is to score by carrying the puck to the opponent’s goal.

For the sake of scope, our game will have only two power-ups: Ghost Help and Fear The Puck. The former adds three additional athletes to the player’s team for some time, while the latter makes the opponents flee the puck for a few seconds.

Power-ups are added to both teams when anyone scores.

Implementing the “Ghost Help” Power-up

Since all athletes added by the Ghost Help power-up are temporary, the Athlete class must be modified to allow an athlete to be marked as a “ghost”. If an athlete is a ghost, it will remove itself from the game after a few seconds.

Below is the Athlete class, highlighting only the additions made to accommodate the ghost functionality:

The property mGhost is a boolean that tells if the athlete is a ghost or not, while mGhostCounter contains the amount of seconds the athlete should wait before removing himself from the game.

Those two properties are used by the updatePowerups() method:

The updatePowerups() method, called within the athlete’s update() routine, will handle all power-up processing in the athlete. Right now all it does is check whether the current athlete is a ghost or not. If it is, then the mGhostCounter property is decremented by the amount of time elapsed since the last update.

When the value of mGhostCounter reaches zero, it means that the temporary athlete has been active for long enough, so it must remove itself from the game. To make the player aware of that, the athlete will start flickering its last two seconds before disappearing.

Finally, it is time to implement the process of adding the temporary athletes when the power-up is activated. That is performed in the powerupGhostHelp() method, available in the main game logic:

This method iterates over a loop that corresponds to the amount of temporary athletes being added. Each new athlete is added to the bottom of the rink and marked as a ghost.

As previously described, ghost athletes will remove themselves from the game.

Implementing the “Fear The Puck” Power-Up

The Fear The Puck power-up makes all opponents flee the puck for a few seconds.

Just like the Ghost Help power-up, the Athlete class must be modified to accommodate that functionality:

First the updatePowerups() method is changed to decrement the mFearCounter property, which contains the amount of time the athlete should avoid the puck. The mFearCounter property is changed every time the method fearPuck() is called.

In the Athlete’s update() method, a test is added to check if the power-up should take place. If the athlete is an opponent controlled by the AI (amIAnAiControlledOpponent() returns true) and the athlete should evade the puck (shouldIEvadeFromPuck() returns true as well), the evadeFromPuck() method is invoked.

The evadeFromPuck() method uses the evade behavior, which makes an entity avoid any object and its trajectory altogether:

All the evadeFromPuck() method does is to add an evade force to the current athlete’s steering force. It makes him evade the puck without ignoring the already added steering forces, such as the one created by the currently active AI state.

In order to be evadable, the puck must behave like a boid, as all athletes do (more information about that in the first part of the series). As a consequence, a boid property, which contains the puck’s current position and velocity, must be added to the Puck class:

Finally, we update the main game logic to make opponents fear the puck when the power-up is activated:

The method iterates over all opponent athletes (the right team, in this case), calling the fearkPuck() method of each one of them. This will trigger the logic that makes the athletes fear the puck during a few seconds, as previously explained.

Freezing and Shattering

The last addition to the game is the freezing and shattering part. It is performed in the main game logic, where a routine checks whether the athletes of the left team are overlapping with the athletes of the right team.

This overlapping check is automatically performed by the Flixel game engine, which invokes a callback every time an overlap is found:

This callback receives as parameters the athletes of each team that overlapped. A test checks if the puck’s owner is not null, which means it is being carried by someone.

In that case, the puck’s owner is compared to the athletes that just overlapped. If one of them is carrying the puck (so he is the puck’s owner), he is shattered and the puck’s ownership passes to the other athlete.

The shatter() method in the Athlete class will mark the athlete as inactive and place it at the bottom of the rink after a few seconds. It will also emit several particles representing ice pieces, but this topic will be covered in another post.

Conclusion

In this tutorial, we implemented a few elements required to turn our hockey prototype into a fully playable game. I intentionally placed the focus on the concepts behind each of those elements, instead of how to actually implement them in game engine X or Y.

The freeze and shatter approach used for the game might sound too fantastical, but it helps keep the project manageable. Sports rules are very specific, and their implementation can be tricky.

By adding a few screens and some HUD elements, you can create your own full hockey game from this demo!(source:tutplus)

 


上一篇:

下一篇: