译者:林公子
出处:木木的二进制人生
转载请注明作者和出处,谢谢!
第六章 基本的人工智能(2)
创建一个追逐精灵
如同之前提到过的,当由计算机控制对象时,任何游戏的目标是使得对象表现的智能程度让玩家无法分辨是由人类在控制还是计算机在控制。很明显的,我们远远没有做到这一点。
您添加的自动精灵仅仅只是沿着直线前进。虽然您对SpriteManager做了些很棒的工作,我们还没有讨论如何去改善自动精灵的移动。
让我们添加一些不同的对象来做些比直线移动更智能的事情。
在这一部分里,您将创建一种新类型的精灵,将会在屏幕上四处追逐用户控制的精灵。您可以用下面很简单的追踪逻辑做到这一点:
if
(player.X
<
chasingSprite.X)
{
chasingSprite.X
-=
1
;
}
else
if
(player.X
>
chasingSprite.X)
{
chasingSprite.X
+=
1
;
}
if
(player.Y
<
chasingSprite.Y)
{
chasingSprite.Y
-=
1
;
}
else
if
(player.Y
>
chasingSprite.Y)
{
chasingSprite.Y
+=
1
;
}
基本上,这个算法比较玩家精灵和追逐精灵的位置。如果玩家的X坐标小于追逐精灵的X坐标,就减少追逐精灵的X坐标。如果玩家的X坐标大于追逐精灵的X坐标,就增加追逐精灵的X坐标。对于Y坐标也是如此。
要实现追逐精灵,您应该创建一个派生自Sprite的新类,不过在做那之前,您可以在之前的算法中看到新类需要知道玩家对象的位置。看看现在的Sprite类和它的派生类,没有办法得到这个信息。所以,您需要添加一个公共的访问器到Sprite基类中来返回精灵对象的位置:
public
Vector2 GetPosition()
{
return
position;
}
好,您需要在项目中添加一个新类(右键点击解决方案管理器中的项目,选择添加->类,命名新类文件名为ChasingSprite.cs,然后用以下代码替换自动生成的代码:
using
System;
using
Microsoft.Xna.Framework;
using
Microsoft.Xna.Framework.Graphics;
namespace
AnimatedSprites
{
class
ChasingSprite : Sprite
{
SpriteManager spriteManager;
public
ChasingSprite(Texture2D textureImage, Vector2 position,
Point frameSize,
int
collisionOffset, Point currentFrame,
Point sheetSize, Vector2 speed,
string
collisionCueName,
SpriteManager spriteManager)
:
base
(textureImage, position, frameSize, collisionOffset,
currentFrame, sheetSize, speed, collisionCueName)
{
this
.spriteManager
=
spriteManager;
}
public
ChasingSprite(Texture2D textureImage, Vector2 position,
Point frameSize,
int
collisionOffset, Point currentFrame,
Point sheetSize, Vector2 speed,
int
millisecondsPerFrame,
string
collisionCueName, SpriteManager spriteManager)
:
base
(textureImage, position, frameSize, collisionOffset,
currentFrame, sheetSize, speed, millisecondsPerFrame,
collisionCueName)
{
this
.spriteManager
=
spriteManager;
}
public
override
Vector2 direction
{
get
{
return
speed; }
}
public
override
void
Update(GameTime gameTime, Rectangle clientBounds)
{
//
使用玩家精灵的位置来使追逐精灵在X和/或Y方向靠近玩家精灵。
Vector2 player
=
spriteManager.GetPlayerPosition();
//
因为精灵可能会往X或Y方向移动,
//
取两个数字中的最大值作为精灵的速度。
float
speedVal
=
Math.Max(Math.Abs(speed.X), Math.Abs(speed.Y));
if
(player.X
<
position.X)
{
position.X
-=
speedVal;
}
else
if
(player.X
>
position.X)
{
position.X
+=
speedVal;
}
if
(player.Y
<
position.Y)
{
position.Y
-=
speedVal;
}
else
if
(player.Y
>
position.Y)
{
position.Y
+=
speedVal;
}
base
.Update(gameTime, clientBounds);
}
}
}
这一段代码有一些要注意的地方。首先,您使用的命名空间是AnimatedSprites。这是您在本书开始几章中应该使用的命名方式。如果命名空间有问题,您可能希望为您的项目起个别的名字。看看Game1类的命名空间,使用的同样的名字。
接下来,注意构造方法几乎和AutomatedSprite类的中的一个样,只有一个关键的差异:这里,您添加了一个SpriteManager类型参数和一个局部SpriteManager类型变量来保持通过这个参数传递进来的对象。在Update方法调用期间,这个变量通过之前添加的方法来得到玩家的位置。
另一个要了解的重要事情是Update方法中发生了什么。您获得玩家的位置并使用Sprite基类中指定的speed成员两个坐标中的最大值来运行追踪逻辑(因为精灵只会在X或Y方向移动,而不是两者)。
您要做的最后一件事是修改SpriteManager类的SpawnEnemy方法来使追逐精灵起作用。您要用ChasingSprite类型代替AutomatedSprite类型来创建精灵。结果就可以再随机间隔的时产生追逐精灵,然后当您运行时会觉得它们的表现很不错。SpriteManager类中的SpawnEnemy方法的尾部,spriteList.Add方法现在看起来应该是这样:
spriteList.Add(
new
ChasingSprite (Game.Content.Load
<
Texture2D
>
(
@"
images\skullball
"
),
position,
new
Point(
75
,
75
),
10
,
new
Point(
0
,
0
),
new
Point(
6
,
8
), speed,
"
skullcollision
"
,
this
));
运行程序,然后准备好开始逃亡生涯吧。您可以试着躲避精灵,但是最终会因为精灵数量太多而撞上它们。试着使用一个游戏手柄或键盘来迎接更困难的挑战。您的程序现在看起来应该和图6-2一样:
图6-2 我无法逃离那些精灵!
您可以很简单的用Sprite类的speed成员乘以某个值来增加或减少算法的难度。增加速度会使追逐精灵更快的追逐玩家,减少速度会减慢它们的步伐。如您所见,追逐精灵一定会在屏幕上追上玩家,不过我们会为了这个游戏的目的做些小小的调整。您要编程使它们在玩家水平移动时,从垂直方向追逐;当玩家垂直移动时,从水平方向追逐,而不是无期限的紧追玩家。这样一来,如果玩家成功躲避了追逐精灵,它们就会越过屏幕然后从游戏中被删除。
为了实现这个,您首先需要指出给定精灵朝哪个方向移动(记住那些精灵只会朝一个方向移动——上,下,左或右)。如果精灵朝水平方向移动,您只调整精灵的垂直移动去追逐玩家;如果精灵垂直移动,你只调整精灵的水平移动来追逐玩家。这会使精灵在追逐玩家的同时保持它们的原始方向。
用以下代码替换ChasingSprite类中的Update方法:
public
override
void
Update(GameTime gameTime, Rectangle clientBounds)
{
//
First, move the sprite along its direction vector
position
+=
speed;
//
Use the player position to move the sprite closer in
//
the X and/or Y directions
Vector2 player
=
spriteManager.GetPlayerPosition();
//
If player is moving vertically, chase horizontally
if
(speed.X
==
0
)
{
if
(player.X
<
position.X)
position.X
-=
Math.Abs(speed.Y);
else
if
(player.X
>
position.X)
position.X
+=
Math.Abs(speed.Y);
}
//
If player is moving horizontally, chase vertically
if
(speed.Y
==
0
)
{
if
(player.Y
<
position.Y)
position.Y
-=
Math.Abs(speed.X);
else
if
(player.Y
>
position.Y)
position.Y
+=
Math.Abs(speed.X);
}
base
.Update(gameTime, clientBounds);
}
这个小小的修改使追逐只在一个方向进行。这个方法以精灵position成员加上speed成员开始。这将使精灵沿着speed向量的方向移动。
在位置更新后,获得玩家的位置。当您编写产生自动精灵的代码时,代码随机产生一个Vector2值作为速度,X或Y坐标会有一个0值(比如,精灵只会在水平或垂直方向移动,而不会沿对角线移动)。因此,算法下一步检查speed的哪一个坐标为0来确定追逐精灵朝哪个方向移动。如果X坐标为0,意味着精灵沿垂直方向移动,然后算法仅仅调整精灵的X坐标来水平追逐玩家。结果就是精灵持续移动或穿过屏幕。然后算法对水平移动的精灵运行同样的检查。
现在编译并运行游戏,您会看到精灵水平或垂直穿越屏幕,但是会稍稍的靠向玩家所在的位置。游戏中您可能注意到了,移动快速的精灵比移动慢的更难躲避。因为它们以和穿过屏幕时相同的速度来追逐玩家。
恭喜!现在您的确是有所进展了。不光是您的游戏看起来和感觉起来更像一个真正的游戏,并且编写了一个人工智能算法使精灵能够对真人玩家的移动做出反应。酷毙了!
创建一个躲避精灵
现在您的程序中有了两种类型的自动精灵:一种穿越屏幕不会改变方向,另一种会逐渐改变方向来追逐玩家。
在这一部分,您会创建另一种和追逐精灵很类似的精灵,不过这一种会尝试躲避玩家。为什么您要编写一个精灵来躲避玩家呢?这种精灵会用在某些玩家想要得到的东西上面(比如一个Power Up, 额外的生命数或其他东西),所以精灵会捉弄玩家,先让玩家接近它,不过当玩家离它足够近时,它会往另一个方向逃走。这会为游戏添加一个不错的元素,让它更具有挑战性。
让我们开始吧。添加一个新类文件到项目中,叫做EvadingSprite.cs。这个精灵的代码将会和刚刚为ChasingSprite精灵编写的代码很类似——如此的相似,实际上从那段代码开始要比从头开始写容易得多。删除EvadingSprite.cs中自动生成的代码,然后把ChasingSprite.cs中的代码拷贝过来,不过记得修改类的名字和构造方法的名字。现在EvadingSprite.cs看起来应该是这样:
using
System;
using
Microsoft.Xna.Framework;
using
Microsoft.Xna.Framework.Graphics;
namespace
AnimatedSprites
{
class
EvadingSprite : Sprite
{
SpriteManager spriteManager;
public
EvadingSprite(Texture2D textureImage, Vector2 position,
Point frameSize,
int
collisionOffset, Point currentFrame,
Point sheetSize, Vector2 speed,
string
collisionCueName,
SpriteManager spriteManager)
:
base
(textureImage, position, frameSize, collisionOffset,
currentFrame, sheetSize, speed, collisionCueName)
{
this
.spriteManager
=
spriteManager;
}
public
EvadingSprite(Texture2D textureImage, Vector2 position,
Point frameSize,
int
collisionOffset, Point currentFrame,
Point sheetSize, Vector2 speed,
int
millisecondsPerFrame,
string
collisionCueName, SpriteManager spriteManager)
:
base
(textureImage, position, frameSize, collisionOffset,
currentFrame, sheetSize, speed, millisecondsPerFrame,
collisionCueName)
{
this
.spriteManager
=
spriteManager;
}
public
override
Vector2 direction
{
get
{
return
speed; }
}
public
override
void
Update(GameTime gameTime, Rectangle clientBounds)
{
//
First, move the sprite along its direction vector
position
+=
speed;
//
Use the player position to move the sprite closer in
//
the X and/or Y directions
Vector2 player
=
spriteManager.GetPlayerPosition();
//
If player is moving vertically, chase horizontally
if
(speed.X
==
0
)
{
if
(player.X
<
position.X)
position.X
-=
Math.Abs(speed.Y);
else
if
(player.X
>
position.X)
position.X
+=
Math.Abs(speed.Y);
}
//
If player is moving horizontally, chase vertically
if
(speed.Y
==
0
)
{
if
(player.Y
<
position.Y)
position.Y
-=
Math.Abs(speed.X);
else
if
(player.Y
>
position.Y)
position.Y
+=
Math.Abs(speed.X);
}
base
.Update(gameTime, clientBounds);
}
}
}
因为这些代码和您的ChasingSprite对象使用的代码相同,现在创建一个这个类型的对象也会在屏幕上追逐玩家。不过,您想要编写从玩家身边逃离的对象。
首先,您需要告诉SpriteManager来创建EvadingSprite类型而不是ChasingSprite类型的对象。修改SpriteManager类中SpawnEnemy方法的spriteList.Add调用:
spriteList.Add(
new
EvadingSprite(Game.Content.Load
<
Texture2D
>
(
@"
images\skullball
"
),
position,
new
Point(
75
,
75
),
10
,
new
Point(
0
,
0
),
new
Point(
6
,
8
), speed,
"
skullcollision
"
,
this
));
现在您就准备编写躲避算法了。算法很简单,实际上就是追逐算法的反转:如果玩家位置的X坐标小于躲避精灵的X坐标,就增加躲避精灵的X坐标使它远离玩家。
您可以交换追逐算法中的加法和减法。并且,因为现在是躲避玩家,您不关心之后精灵的移动方向,所以您可以移除两个判断精灵移动方向的语句,做了这些改变之后,EvadingSprite类的Update方法看起来应该是这样:
public
override
void
Update(GameTime gameTime, Rectangle clientBounds)
{
//
First, move the sprite along its direction vector
position
+=
speed;
//
Use the player position to move the sprite closer in
//
the X and/or Y directions
Vector2 player
=
spriteManager.GetPlayerPosition();
//
If player is moving vertically, chase horizontally
if
(player.X
<
position.X)
position.X +
=
Math.Abs(speed.Y);
else
if
(player.X
>
position.X)
position.X -
=
Math.Abs(speed.Y);
//
If player is moving horizontally, chase vertically
if
(player.Y
<
position.Y)
position.Y +
=
Math.Abs(speed.X);
else
if
(player.Y
>
position.Y)
position.Y -
=
Math.Abs(speed.X);
base
.Update(gameTime, clientBounds);
}
现在编译并运行项目,您会发现几乎不可能追上精灵。它们在还没有靠近玩家之前就跑向另一边了。
当精灵可以这么能干的躲开玩家的时候,实在是不怎么有趣。您在自己的游戏中失败,这很差劲。让我们修改一下新精灵让它可以像自动精灵一样在屏幕上移动,不过,当玩家进入一个特定范围时,开启躲避算法然后精灵开始逃走。
添加一些新变量到EvadingSprite类中:一个用来检测什么时候激活躲避算法,一个用来决定精灵以多大的速度从玩家身边逃离,然后另一个用来保持精灵的状态(可能的状态是躲避和不躲避)。默认的,您应该使这个变量将精灵标识为不躲避状态:
float
evasionSpeedModifier;
int
evasionRange;
bool
evade
=
false
;
为什么为躲避单独使用一个速度呢?您不是必须这样做,不过躲避策略会使玩家稍微有些意外。游戏中的其他精灵不是只朝着一个方向移动就是追逐玩家。有一个可以改变方向移动的精灵会有一些惊奇并使玩家更难于应付。提供一个像这样的变量可以让您增减或减少精灵在躲避模式时的速度。您能够调整这个数字到一个作为玩家/开发者觉得合适的数值。
接下来,修改构造方法来接受evasionSpeedModifier和evasionRange变量。您应该在构造方法体内将传递进来的变量赋值给成员变量:
public
EvadingSprite(Texture2D textureImage, Vector2 position,
Point frameSize,
int
collisionOffset, Point currentFrame,
Point sheetSize, Vector2 speed,
string
collisionCueName,
SpriteManager spriteManager,
float
evasionSpeedModifier,
int
evasionRange)
:
base
(textureImage, position, frameSize, collisionOffset,
currentFrame, sheetSize, speed, collisionCueName)
{
this
.spriteManager
=
spriteManager;
this
.evasionSpeedModifier
=
evasionSpeedModifier;
this
.evasionRange
=
evasionRange;
}
public
EvadingSprite(Texture2D textureImage, Vector2 position,
Point frameSize,
int
collisionOffset, Point currentFrame,
Point sheetSize, Vector2 speed,
int
millisecondsPerFrame,
string
collisionCueName, SpriteManager spriteManager,
float
evasionSpeedModifier,
int
evasionRange)
:
base
(textureImage, position, frameSize, collisionOffset,
currentFrame, sheetSize, speed, millisecondsPerFrame,
collisionCueName)
{
this
.spriteManager
=
spriteManager;
this
.evasionSpeedModifier
=
evasionSpeedModifier;
this
.evasionRange
=
evasionRange;
}
现在您需要修改Update方法来添加一些逻辑使精灵在和玩家的位置小于evasionRange值之前都像一个自动精灵那样移动。您可以用Vector2.Distance方法来确定两个向量之间的距离。
一旦玩家和精灵之间的距离小于evasionRange,您需要反转精灵的方向并激活躲避算法。算法运行直到精灵被销毁。
您的Update方法看起来应该是这样:
public
override
void
Update(GameTime gameTime, Rectangle clientBounds)
{
//
First, move the sprite along its direction vector
position
+=
speed;
//
Use the player position to move the sprite closer in
//
the X and/or Y directions
Vector2 player
=
spriteManager.GetPlayerPosition();
//
If player is moving vertically, chase horizontally
if
(evade)
{
if
(player.X
<
position.X)
position.X
+=
Math.Abs(speed.Y);
else
if
(player.X
>
position.X)
position.X
-=
Math.Abs(speed.Y);
//
If player is moving horizontally, chase vertically
if
(player.Y
<
position.Y)
position.Y
+=
Math.Abs(speed.X);
else
if
(player.Y
>
position.Y)
position.Y
-=
Math.Abs(speed.X);
}
else
{
if
(Vector2.Distance(position, player)
<
evasionRange)
{
//
Player is within evasionrange,
//
reverse direction and mofify speed
speed
*=
-
evasionSpeedModifier;
evade
=
true
;
}
}
base
.Update(gameTime, clientBounds);
}
最后,您需要再次修改SpriteManager类SpawnEnemy方法中的spriteList.Add调用,添加
新加入的成员变量参数到EvadingSprite的构造方法中。对于新手来说,.75f作为速度值,150作为躲避范围值。这些值使精灵和玩家进入150个单位的范围时以正常速度的1/3开始躲避玩家:
spriteList.Add(
new
EvadingSprite(Game.Content.Load
<
Texture2D
>
(
@"
images\skullball
"
),
position,
new
Point(
75
,
75
),
10
,
new
Point(
0
,
0
),
new
Point(
6
,
8
), speed,
"
skullcollision
"
,
this
,
0.75f
,
150
));
现在编译并运行游戏,您会看到精灵似乎有了一点“智能”。它们检测玩家何时靠近,然后转向相反的方向逃走。并且您会发现您可以追上它们中的大部分,但是它们仍然有一些狡猾。一旦您在屏幕上添加玩家实际需要躲避的精灵之外的精灵,这个躲避技巧将足够用来呈现一个不错的挑战。
所以这个算是人工智能吗?
嗯, 不算是。我们甚至还没有触及到人工智能算法的表层。实际上,一些中坚AI专家会说这完全不是人工智能——而且很可能他们是对的。此外,这个学科有些模棱两可,谁又能说您做的是否真的实现了人工智能呢?
重点是您有些时候可以游离于科学称谓之外。真正的人工智能研究和算法绝对存在于世界上的某处,不过大概不会在一个2D躲避精灵游戏中。当编程AI对象时——特别是视频游戏中——通常会有一个“足够好”的确定等级的“智能”。您可以花费数月或数年的时间来为这个游戏微调算法因此您可以说这是真正的人工智能,不过要花费多少成本,对玩家的益处又何在呢?
遗憾的是,对于应该达到的人工智能程度或质量,没有一个对或错的答案,而且作为一个开发者,这变成您必须做出的决定。最终,由您决定算法什么时候需要改进,并且对于你的目的来说,什么样的程度算是足够好。
刚刚您做了些什么
现在您快有成就了。在下一章里,您会微调游戏并且包装起来。与此同时,让我们回顾一下您刚刚做了些什么:
•您学习了一些人工智能的背景。
•您创建了一个随机产生精灵的工厂。
•您了解了无关对象和应该怎么处理它们来提高性能。
•您创建了一个在屏幕上追逐玩家的精灵。
•您创建了一个会躲避玩家的精灵。
•您沉醉于XNA的精髓。
摘要
•人工智能意味着很多东西,主要是因为“智能”这个术语本身就是模棱两可难于定义的。
•阿兰•图灵在人工智能领域取得了巨大进步。他的许多工作直接与游戏开发者的目的相关。
•无关对象是那些不会再影响游戏的对象(例如,发射到天空中并没有击中任何东西的子弹)。这些对象必须被删除以便不会因为它们的增加而对游戏性能产生负面影响。
•为了实现追逐算法,检查玩家的位置和追踪者目前位置的关系,然后使追逐对象向玩家方向移动。
•说“人工智能”这个词很多遍使您听起来更智能一些,当然,要人工的说。
======================================================================
知识测试:问答
1.什么是图灵测试?
2.为什么人工智能如此难于理想化?
3.在视频游戏中是什么是对象变成无关的?要怎么处理无关对象?为什么?
4.如果您有一个位置保存在一个叫做PlayerPos的Vector2对象中的玩家和一个位置保存在ChasePos(同PlayerPos)的追逐精灵,什么样的算法可以使追逐精灵追逐您的玩家?
======================================================================
知识测试:练习
1.用您在这一章里学到的知识创建另一种精灵对象。使这种精灵在屏幕上随机移动。要做到这一点,您应该创建一个随机计时器表示何时对象应该改变方向。当计时结束时,让对象往一个不同的方向移动,然后重置随机计时器为一个新的随机时间作为精灵下一次改变方向的时间。