译者:林公子
出处:木木的二进制人生
转载请注明作者和出处,谢谢!
第六章 基本的人工智能
人工智能,哈?或许听起来有一点点吓人和酷。我们在之前的章节中接触到了人工智能这个概念,不过现在让我们看看人工智能到底是什么。
自从计算机时代开始,研究者们就开始考虑和讨论让机器的行动更像人类或给它们某种形式人工智能的方法。整个人工智能科学最大的问题就是实际上没有办法来定义“智能”。是什么使某人或某物拥有了智能?这是个很棒的问题,而且可能是一个我们无法完美解答的问题。很多其他的问题也同样暴露出来。您怎样定义典型的人类行为?人类的行为以什么形式构成智能?什么形式的人类行为值得机器来复制?
您可以说您编写的应用程序是“智能的”因为精灵能够自己进行动画(就是说,用户不需要告诉它们持续进行动画)。所以,它们必定是智能的,对吗?其他人会争辩说他们不是智能的,因为它们没有“做”任何事;它们只是呆在那里旋转着。甚至在这个例子里,很显然精灵不是真正智能的,您能开始理解这个研究领域是怎样的一种固有二义性。
在这个科研领域,创造有人工智能的存在的想法对人类有巨大的吸引力,是好事,也是坏事。是好事是因为这是促进这门学科开始的原因:研究者和反对者同样对这里领域的可能性很感兴趣并且每年有大量的时间和金钱花费在人工智能上。
与此同时,它是坏事是因为始于早期文化的魅力,导致了书本,电影里的高等人工智能存在。这个领域的期望值被好莱坞和作者设置得太高使得科学可能永远都没有办法达到最新的科幻小说的水平。
图灵测试
阿兰•图灵,作为现代计算机之父而广为人知,发明了用来检测一台机器是否智能的最著名的方法之一。图灵称这种方法为模仿游戏,但是更普遍为人所知的是叫法是图灵测试。
一般说来,一个图灵测试从一个人类坐在一张键盘前面开始。使用键盘同时询问一台计算机和另一个人类。其他参与者的身份没有暴露给询问者。如果询问者不能辨认哪一个是计算机,哪一个是人类,测试中使用的计算机就被认为是“智能的”。尽管这个看起来很简单,编程实现一个东西能够在不管提出什么样问题的情况下欺骗某人确实相当的困难。
我们怎么把那些运用到XNA中?嗯,即使图灵测试不是一个视频游戏,和视频游戏有关的大部分人工智能本质背后的原理是相同的。当编程一个任意游戏中由计算机控制的实体时,想法就是使这个实体行动起来像一个人类,使真正的人类对手感觉不到不同。
说起来容易做起来难,并且在这个游戏中我们不会做到那样的程度。不过,您可以清楚的认识到如果您使用图灵测试作为您的标准,那么您现有的程序不可能做到这一点。
那么,下一步是什么呢?让我们为您的动画精灵编码基本的移动,然后我们通过使用基本的人工智能算法使事情有些进展。
随机创建精灵
这一章使用第5章完成的代码。打开那个项目,本章自始至终都使用它。
您已经创建了一个精灵管理器来绘制和更新程序中的所有精灵。不过,目前您拥有的全部就是一些在程序启动时创建的头骨精灵。甚至更糟,那些精灵不会动——它们只是呆在那里不断的产生动画。这个并不能做什么,您需要一些动作和使人兴奋的东西。在这一部分,您会添加一些代码来随机的产生自动精灵并使它们飞过屏幕迫使玩家得四处移动花点功夫来避免撞上它们。
与其一波一波的创建对象或一次创建所有,您更希望它们在一个随机区间创建。这个为游戏添加了一些多样性并且使玩家保持猜测。要做的第一件事就是创建一些变量来帮助您定义创建自动精灵的频率。
首先,为了持有游戏中的随机因子,创建下面的变量到Game1类中:
然后在Game1的构造函数中初始化这个Random对象。
现在您有了一个在游戏中所有随机部分使用Random变量。当使用随机数生成器时,很重要的一点是您要确保不要在一个循环里创建多个随机数生成器。这是因为如果您在一个足够接近的时间帧里创建多个随机数生成器,那么有可能它们使用同一个随机数种子创建。种子是随机数生成器用来检测哪些数被生成了和以什么样的顺序生成。您可能可以猜到,用同一个种子创建多个随机数生成器是一件很糟糕的事情:很可能每个生成的是同样的一组随机数,然后您的精灵们会被乱扔到窗口上。
一个避免这种情况的方法是在程序中使用一个随机数生成器对象然后重用它用作所有的随机数。否则,就要保证您创建的随机数生成器们在程序的不同部分使它们不会在很短的时间间隔内相继执行。
System.Random实际上不是最好的随机数生成工具,不过现在用这个就好。
下面,为SpriteManager类添加一些成员变量用来量产精灵:
int enemySpawnMaxMilliseconds = 2000; // 等待产生精灵的最大时间。
int enemyMinSpeed = 2; // 最小移动速度。
int enemyMaxSpeed = 6; // 最大移动速度。
这两组变量表示产生一个敌人需要等待的最小时间(秒)和最大时间(秒),还有敌人最小速度和最大速度。接下来就要在SpriteManager类中使用这两个时间变量之间和两个速度值之间的随机值产生敌人。
下面,您需要移除之前创建不会移动的自动精灵的代码。因为您现在要周期性的产生精灵,不再需要那些测试精灵了。创建精灵的代码在LoadContent方法中,一旦您移除创建自动精灵的代码,LoadContent方法看起来应该是这样:
{
spriteBatch = new SpriteBatch(Game.GraphicsDevice);
player = new UserControlledSprite(
Game.Content.Load<Texture2D>(@"Images/threerings"),
Vector2.Zero, new Point(75, 75), 10, new Point(0, 0),
new Point(6, 8), new Vector2(6, 6));
base.LoadContent();
}
为什么使用随机数? 那么,为什么要有一个最小/最大产生时间,并且为什么用一个这个两个时间之间的随机数来产生敌人呢? 答案又回到人工智能上。作为人类,我们的想法不是无意识的。加入一些随机因素使程序让您觉得更像和人类对手游戏。同时也增加了某种程度的不可预测性,使得游戏变得更加有趣更有挑战性。 好的,因此这里有另一个问题:为什么用变量来表示最小/最大时间和最小/最大速度? 一般来说,游戏不会一直只有一种难度。当您玩到一定程度,游戏会变得越来越难。使用变量来表示这些值让您很容易提高难度水平。在玩家玩游戏期间,您会使敌人更频繁的产生并且移动更迅速。 那么,再多一个问题:“这个很棒,Aaron! 为什么这么有趣呢?” 这就是XNA, 我的朋友,XNA很酷! |
现在,您会想要使游戏窗口变得更大一些以便更好的工作。添加这些代码到Game1类的构造方法的底部:
graphics.PreferredBackBufferWidth = 1024;
随机产生精灵
好,让我们产生一些精灵。您希望精灵是随机产生的,并且会从屏幕的上,左,右,下方产生。目前您只是让它们沿着直线方向移动,现在它们会以不同的速度移动。
您必须让SpriteManager类知道何时去产生下一个敌人精灵。在SpriteManager类中创建一个成员变量来储存标识下一次产生时间的值:
下面,您需要初始化这个变量为下一次产生时间。创建一个独立的函数来设置产生时间为之前定义的等待时间之间的某个值:
{
nextSpawnTime = ((Game1)Game).random.Next(enemySpawnMinMilliseconds,
enemySpawnMaxMilliseconds);
}
然后您需要在SpriteManager类的Initialize方法中调用新的ResetSpawnTime方法,这样变量在游戏一开始就会被初始化。添加以下代码到Initialize方法的底部,在base.Initialize调用之前:
现在您需要在SpriteManager的Update方法中使用GameTime变量来检查是否到了该产生一个新的敌人的时候。添加下面的代码到Update方法的开始处:
if (nextSpawnTime < 0)
{
SpawnEnemy();
// Reset spawn timer.
ResetSpawnTime();
}
这段代码首先从nextSpawnTime变量减去上一次Update调用到现在经过的毫秒数,一旦nextSpawnTime小于0,产生时间就到了,那么是时候将您的新敌人精灵狂暴的愤怒释放给可怜的玩家,呃,我的意思是,是时候产生一个自动精灵了。您通过SpawnEnemy方法来产生一个新敌人,很快您就会定义它。然后您重置了nextSpawnTime来检查下一次产生敌人的时间。
SpawnEnemy方法需要能够,嗯,产生一个敌人。您需要为这个敌人选择一个随机的出现地点,在屏幕的上边,左边,右边或下边。您还要基于之前定义的速度变量为这个敌人选择一个起始速度。要添加敌人精灵到游戏中,您所需要做的就是添加一个新的AutomatedSprite到SpriteList变量中。如下添加SpawnEnemy的代码:
{
Vector2 speed = Vector2.Zero;
Vector2 position = Vector2.Zero;
// 默认帧尺寸。
Point frameSize = new Point(75, 75);
int backBufferWidth = Game.GraphicsDevice.PresentationParameters.BackBufferWidth;
int backBufferHeight = Game.GraphicsDevice.PresentationParameters.BackBufferHeight;
// 随机选择一个放置敌人的地点。
// 并随机选择一个敌人的起始速度。
switch (((Game1)Game).random.Next(4))
{
// 从左到右。
case 0:
position = new Vector2(-frameSize.X, ((Game1)Game).random.Next(0,
backBufferHeight - frameSize.Y));
speed = new Vector2(((Game1)Game).random.Next(enemyMinSpeed, enemyMaxSpeed), 0);
break;
// 从右到左.
case 1:
position = new Vector2(backBufferWidth, ((Game1)Game).random.Next(0, backBufferHeight - frameSize.Y));
speed = new Vector2(-((Game1)Game).random.Next(enemyMinSpeed, enemyMaxSpeed), 0);
break;
// 从下到上。
case 2:
position = new Vector2(backBufferHeight, ((Game1)Game).random.Next(0, backBufferWidth - frameSize.X));
speed = new Vector2(0, -((Game1)Game).random.Next(enemyMinSpeed, enemyMaxSpeed));
break;
// 从上到下。
case 3:
position = new Vector2(((Game1)Game).random.Next(0, backBufferWidth - frameSize.Y), -frameSize.Y);
speed = new Vector2(0, ((Game1)Game).random.Next(enemyMinSpeed, enemyMaxSpeed));
break;
}
spriteList.Add(new AutomatedSprite(Game.Content.Load<Texture2D>(@"images\skullball"),
position, new Point(75, 75), 10, new Point(0, 0),
new Point(6, 8), speed, "skullcollision"));
}
首先,这个方法为很快要创建的精灵的速度和位置添加了变量。接下来,速度和位置被随机选择。方法顶部定义的frameSize变量用来确定精灵要从窗口边缘偏移多远。
现在编译并运行程序,您会发现现在看起来越来越像一个游戏了。敌人精灵从屏幕的四周产生,然后他们以不同的速度沿着一条直线穿越屏幕(见图6-1)。
图6-1 随机产生的敌人正在攻击我们!
好了,现在是问答时间,让我们看看您对这里发生的事情了解多少,还要看下您会遇到什么问题。让游戏运行一分钟左右,不进行用户输入。有一些精灵可能碰到用户精灵消失掉了,但是它们大部分飞到屏幕意外。这样会有什么问题,如何修正呢?
如果您的答案是没有删除飞到屏幕以外的物体,那么您就真正理解了游戏理念——干的好!如果您感到困惑,让我解释一下:当一个自动精灵碰到用户精灵时,它从精灵列表中被移除并且销毁。然而,如果一个自动精灵穿过屏幕,它只是简单的消失了;您没有做任何事去销毁那些精灵,而且玩家也不会再碰到那些精灵中的任何一个从而销毁它。结果就是那些精灵永远在游戏之外,并且每帧进行更新和绘制——更不用说对它们进行无意义的碰撞检测了。这个问题会越来越糟直到某个时刻开始影响您的游戏性能。
无关的对象
这将带我们回到游戏开发的基本元素。一件对所有游戏都很重要的事情是什么使一个对象变得“无关”的定义。当一个对象不会再影响游戏中的任何事物时,可以认为它是“无关的”。
无关性的处理在每个游戏中都不同。有些游戏允许对象离开屏幕并最终返回。其他一些游戏在对象离开屏幕之前就销毁它们。后者的例子可以在游戏Asteroid中看到。在大多数Asteroid游戏版本中,当从屏幕的一边向另一边射击时,飞船的子弹实际上在离开屏幕前就消失了。因为射击有个最大距离让子弹可以移动直到被删除。虽然我对这样的设置不太感冒(是的,我喜欢可以射击任何我看得到的地方的枪),开发者做主让子弹不能从屏幕的一头达到另一头。您可以说这样做有这样做的好处,不过重点不在这里。重点是开发者决定是什么构成了子弹的无关性,当满足这一点时,就删除子弹。
深入的观察Asteroids游戏是很有趣的,因为虽然开发者决定在子弹碰到屏幕边缘之前就移除它们,不过他们对小行星做了相反的事情,当小行星离开屏幕时会被再利用,让它们从屏幕的另一边出来。再次的,您可以说您是否喜欢这样的行为,这样是否合理,但是这不是重点。游戏开发最棒的一件事就是你主宰着那个世界,并且您可以为所欲为。Asteroid的开发者那样做了,嘿,谁又会去和一个空前经典的游戏争辩呢,对吧?
目前,您没有对您的无关精灵做任何事情。您的精灵们离开了屏幕并且再也没有机会返回(您只有使精灵向前移动的,没有转弯或原路返回的逻辑),因此就此变成无关的了。一旦您的自动精灵离开了屏幕,您需要检测并移除它们使它们不会浪费宝贵的CPU时钟周期去更新和绘制永远不会回到游戏中的对象。
要这样做,您需要添加一个方法到Sprite基类中,这个方法接受一个表示窗体矩形Rectangle类型的参数并返回true或false来表明精灵是否在界限之外。添加下面的方法到Sprite类中:
{
if (position.X < -frameSize.X ||
position.Y > clientRect.Width ||
position.Y < -frameSize.Y ||
position.Y > clientRect.Height)
{
return true;
}
return false;
}
下面,您需要添加一些代码到SpriteManager类的Update方法中来遍历AutomatedSprites列表并对每个精灵调用IsOutOfBounds,删除那些界限之外的精灵。SpriteManager类的Update方法已经有了遍历精灵的代码。现在代码应该看起来是这样:
{
Sprite s = spriteList[i];
s.Update(gameTime, Game.Window.ClientBounds);
// Check for collisions
if (s.collisionRect.Intersects(player.collisionRect))
{
// Play collision sound
if (s.collisionCueName != null)
((Game1)Game).PlayCue(s.collisionCueName);
// Remove collided sprite from the game
spriteList.RemoveAt(i);
--i;
}
}
添加一些代码来检查精灵是否越界。如果是,从游戏中移除它们。之前的循环现在看起来应该是这样(新添加的代码用粗体表示):
{
Sprite s = spriteList[i];
s.Update(gameTime, Game.Window.ClientBounds);
// Check for collisions
if (s.collisionRect.Intersects(player.collisionRect))
{
// Play collision sound
if (s.collisionCueName != null)
((Game1)Game).PlayCue(s.collisionCueName);
// Remove collided sprite from the game
spriteList.RemoveAt(i);
--i;
}
// Remove object if it is out of bounds.
if (s.IsOutOfBounds(Game.Window.ClientBounds))
{
spriteList.RemoveAt(i);
--i;
}
}
现在您的无关精灵在它们离开屏幕之后就会被删除。您的游戏只会为屏幕中的对象进行更新,绘制,运行碰撞,并且这会大幅提高性能,特别是游戏进行过程中。