开发真正吸引人的游戏经常涉及在计算机的范畴内有效地模拟人类的思想。本文重点介绍人工智能的基础理论以及如何将其应用在游戏中,最后将介绍一个例子,说明如何将简单的人工智能结合到游戏中。
本文内容:
接上文 游戏编程入门(15):开发 MeteorDefense(抵御流星)游戏
人工智能(AI)的定义是,用来在计算机中模拟人类思维过程的技术。AI 是一个很广泛的研究领域,与游戏相关的 AI 是全部 AI 知识中相当小的部分。本章的目的不是研究 AI 的所有方面,只是研究 AI 背后的一些适用于游戏的基本概念。
在过去,基于信息的 AI 算法完全是确定性的,即每一个决定都可以回溯到一个可预知的逻辑流。近年来,AI 研究的重点是模糊逻辑,它试图做出最佳猜测决定而不是传统 AI 系统的具体决定。另一个与游戏有关的有趣 AI 研究领域涉及遗传算法,它尝试模拟进化的思想。
我将游戏相关的 AI 划分为3种基本类型:
这3种 AI 绝对没有包括在游戏中使用的所有 AI 方法,它们只不过是人们认识和使用的最多的类型。如果读者认为 AI 是一个值得进一步学习的有趣主题,那么可以在这3种 AI 的基础上随意自行研究和发挥。
漫游 AI 是指模拟游戏对象的移动的 AI,即游戏对象作出在虚拟的游戏世界中如何漫游的决定。
实现漫游 AI 很简单,它通常需要根据一个对象的位置来改变另一个对象的速度或者位置。对象的漫游移动还会受到随机模式或者预定模式的影响。存在三种不同的漫游 AI :追逐、逃避和有模式漫游 AI 。
追逐是这样的一种漫游 AI :一个游戏对象跟踪并追逐另一个或多个游戏对象。追逐这种方法用在许多射击类游戏中,例如外星人追逐玩家的船只。它的实现方法是根据玩家船只的当前位置来改变外星人的速度或者位置。
下面是一个简单的追逐算法的例子,它涉及一个外星人和一艘船:
if(iXAlien > iXShip)
iXAlien--;
else if(iXAlien < iXShip)
iXAlien++;
if(iYAlien > iYShip)
iYAlien--;
else if(iYAlien < iYShip)
iYAlien++;
可以看出,根据船只的位置(iXShip 和 iYShip)更改外星人的 xy 位置(iXAlien 和 iYAlien)。这段代码的问题在于,外星人会毫不犹豫的跟上玩家,不给玩家任何躲避的机会。我们可能希望外星人有追逐玩家的“倾向”,而不是竭力使用闪电战击败玩家。
使追逐算法更平滑的一种方法是添加一点随机性,如下所示:
if((rand()%3)==0)
{
if(iXAlien > iXShip)
iXAlien--;
else if(iXAlien < iXShip)
iXAlien++;
}
if((rand()%3)==0)
{
if(iYAlien > iYShip)
iYAlien--;
else if(iYAlien < iYShip)
iYAlien++;
}
在这段代码上,外星人在各个方向有1/3的机会追逐玩家,这使玩家还有战斗的机会,可以避开外星人。
逃避在逻辑上与追逐相对,它是另一种漫游 AI ,游戏对象明确地避开另一个或一些对象。逃避的实现方式与追逐相似,其代码如下所示:
if(iXAlien > iXShip)
iXAlien++;
else if(iXAlien < iXShip)
iXAlien--;
if(iYAlien > iYShip)
iYAlien++;
else if(iYAlien < iYShip)
iYAlien--;
与追逐类似,可以使用随机或者有模式移动使得逃避变得更缓和。
有模式移动是指游戏对象使用一组预定义的移动的漫游 AI 类型。
模式通常存储为一个速度或者位置偏移量(或者系数)的数组,每次需要使用有模式移动时,都将其应用于一个对象,如下所示:
int iZigZag[2][2]={ {3,2},{-3,2} };
iXAlien +=iZigZag[iPatternStep][0];
iYAlien +=iZigZag[iPatternStep][1];
整型数组iZigZag 包含用来对外星人应用模式的一对 xy 偏移量。iPatternStep 变量是一个表示模式中的当前步骤的整数。在应用这个模式时,外星人以每个游戏周期2个像素的速度在垂直方向上移动,同时在水平方向以每个游戏周期3个像素的速度来来回回地曲折行进。
虽然漫游 AI 策略的各种类型本身很有用,但实际的游戏方案经常需要混合使用这 3种类型。
行为 AI 是游戏 AI 的另一种基本类型,它经常结合使用漫游 AI 算法来使游戏对象具有特定的行为。如果希望外星人有时追逐、有时逃避、有时遵循一种模式、甚至完全随机地移动,那么该如何实现呢?使用行为 AI 的另一个极好理由是改变游戏的难度。
要想实现行为 AI ,需要为外星人创建一组行为。为游戏对象创建行为并不难,需要为游戏系统中存在的各种行为创建一个等级系统,然后将其应用于各个对象。例如,在外星人系统中,可以包含以下行为:追逐、逃避、以一种模式飞行以及随机飞行。对于各种不同类型的外星人,为不同的行为指定不同的百分比,从而使其具有各种不同的个性。例如,一个攻击性的外星人可能有如下行为比例划分:50%的时间在追逐、10%的时间在逃避,30%的时间以一种模式飞行,10%的时间随机飞行。
这种行为方法的典型实现是使用 switch语句或者嵌套的 if-else 语句,以便选定某个特定的行为。行为上具有攻击性的外星人的简单实现如下:
int iBehavior=abs(rand() % 100);
if(iBehavior <50 )
//追逐
else if(iBehavior <60 )
//逃避
else if(iBehavior <90 )
//以某种模式飞行
else
//随机飞行
策略 AI 就是使用一套固定的明确定义好的规则来玩游戏而设计的 AI 。例如,一个计算机控制的象棋玩家会使用策略 AI ,根据尝试增加赢得游戏的机会来确定每一步移动。
在理解了游戏中的 AI 背后的基本概念之后,就可以开始考虑在自己特定的游戏中使用的 AI 策略了。决定了如何在游戏中实现 AI 时,需要做一些初步的工作来准确估计是和使用什么类型和级别的 AI ,还需要确定什么级别的计算机应满足自己的需求、能力、资源以及项目的时间要求。
Roids 2 是游戏编程入门(14):创建子画面背景 的 Roids程序的升级版本。原来的Roids 程序显示了一个动画小行星区域。现在,我们,我们将向这个程序添加一个飞碟,它具有足够的只能,可以避开小行星,或者至少会尽力避开小行星。
Roids 2示例与原来的Roids 程序非常相似,只是添加了飞碟子画面。本章后面的内容将重点介绍这个程序的开发以及 AI 对飞碟子画面的影响。
注意:若出现编译错误,请在项目设置->连接->对象/库模块中 加入 msimg32.lib winmm.lib
Roids 2 目录结构:
Roids 2 效果图::
(飞碟会灵活的躲避行星,非常可爱)
#pragma once
//-----------------------------------------------------------------
// 包含的文件
//-----------------------------------------------------------------
#include
#include "Resource.h"
#include "GameEngine.h"
#include "Bitmap.h"
#include "Sprite.h"
#include "Background.h"
//-----------------------------------------------------------------
// 全局变量
//-----------------------------------------------------------------
HINSTANCE g_hInstance; //程序实例句柄
GameEngine* g_pGame; //游戏引擎
HDC g_hOffscreenDC; //屏幕外设备环境
HBITMAP g_hOffscreenBitmap;//屏幕外位图
Bitmap* g_pAsteroidBitmap; //小行星位图
Bitmap* g_pSaucerBitmap; //飞碟位图
StarryBackground* g_pBackground; //星空动画背景
Sprite* g_pAsteroids[3]; //小行星子画面
Sprite* g_pSaucer; //飞碟子画面
//-----------------------------------------------------------------
// 该游戏独有的方法,更新飞碟子画面(根据它与附近的小行星接近程度来改变飞碟的速度)
//-----------------------------------------------------------------
void UpdateSaucer();
UpdateSaucer( ) 函数更新飞碟子画面(根据它与附近的小行星接近程度来改变飞碟的速度),以便帮助飞碟避开小行星。
// 根据它与附近的小行星接近程度来改变飞碟的速度
void UpdateSaucer()
{
// 获得飞碟
RECT rcSaucer, rcRoid;
rcSaucer = g_pSaucer->GetPosition();
// 找出距离飞碟最接近的行星
int iXCollision = 500, iYCollision = 400, iXYCollision = 900;
for (int i = 0; i < 3; i++)
{
// 获得行星的位置
rcRoid = g_pAsteroids[i]->GetPosition();
// 计算最小的 xy 碰撞距离
int iXCollisionDist = (rcSaucer.left +
(rcSaucer.right - rcSaucer.left) / 2) -
(rcRoid.left +
(rcRoid.right - rcRoid.left) / 2);
int iYCollisionDist = (rcSaucer.top +
(rcSaucer.bottom - rcSaucer.top) / 2) -
(rcRoid.top +
(rcRoid.bottom - rcRoid.top) / 2);
if ((abs(iXCollisionDist) < abs(iXCollision)) ||
(abs(iYCollisionDist) < abs(iYCollision)))
if ((abs(iXCollisionDist) + abs(iYCollisionDist)) < iXYCollision)
{
iXYCollision = abs(iXCollision) + abs(iYCollision);
iXCollision = iXCollisionDist;
iYCollision = iYCollisionDist;
}
}
POINT ptVelocity;
ptVelocity = g_pSaucer->GetVelocity();
// 查看飞碟是否需要在X方向上躲避
if (abs(iXCollision) < 60)
{
// 调整x方向的速度
if (iXCollision < 0)
ptVelocity.x = max(ptVelocity.x - 1, -8);
else
ptVelocity.x = min(ptVelocity.x + 1, 8);
}
// 查看飞碟是否需要在X方向上躲避
if (abs(iYCollision) < 60)
{
// 调整y方向的速度
if (iYCollision < 0)
ptVelocity.y = max(ptVelocity.y - 1, -8);
else
ptVelocity.y = min(ptVelocity.y + 1, 8);
}
// 在新位置更新飞碟
g_pSaucer->SetVelocity(ptVelocity);
}
首先查看距离飞碟最近的小行星,然后改变飞碟的速度,使其具有在于这个小行星相反的方向上飞行的倾向。因为我们不希望飞碟突然变向并且立即开始远离小行星,而是希望逐渐改变其速度,使其看起来慢慢驶离校行星。
UpdateSaucer( ) 函数中的第一步是获取飞碟的位置。然后遍历小行星,找出最短的xy 碰撞距离,这就是小行星与飞碟之间的最短距离。在循环内部,首先获取小行星位置,它对于确定碰撞距离至关重要。然后,计算最短的xy 碰撞距离并将这作为确定这个小行星当前是否距离飞碟最近的基础。
在退出小行星循环时,获得了两点重要信息:X碰撞距离和Y碰撞距离。现在可以查看这些距离是否小于某个最小距离。在这个距离内,就认为小行星有相撞的危险。然后调整飞碟的速度。
GameStart( ) 函数初始化飞碟位图和子画面。
// 开始游戏
void GameStart(HWND hWindow)
{
// 生成随机数生成器种子
srand(GetTickCount());
// 创建屏幕外设备环境和位图
g_hOffscreenDC = CreateCompatibleDC(GetDC(hWindow));
g_hOffscreenBitmap = CreateCompatibleBitmap(GetDC(hWindow),
g_pGame->GetWidth(), g_pGame->GetHeight());
SelectObject(g_hOffscreenDC, g_hOffscreenBitmap);
// 创建并加载行星和飞碟位图
HDC hDC = GetDC(hWindow);
g_pAsteroidBitmap = new Bitmap(hDC, IDB_ASTEROID, g_hInstance);
g_pSaucerBitmap = new Bitmap(hDC, IDB_SAUCER, g_hInstance);
// 创建布满星星的动画子背景
g_pBackground = new StarryBackground(500, 400);
// 创建行星子画面
RECT rcBounds = { 0, 0, 500, 400 };
g_pAsteroids[0] = new Sprite(g_pAsteroidBitmap, rcBounds, BA_WRAP);
g_pAsteroids[0]->SetNumFrames(14);
g_pAsteroids[0]->SetFrameDelay(1);
g_pAsteroids[0]->SetPosition(250, 200);
g_pAsteroids[0]->SetVelocity(-3, 1);
g_pGame->AddSprite(g_pAsteroids[0]);
g_pAsteroids[1] = new Sprite(g_pAsteroidBitmap, rcBounds, BA_WRAP);
g_pAsteroids[1]->SetNumFrames(14);
g_pAsteroids[1]->SetFrameDelay(2);
g_pAsteroids[1]->SetPosition(250, 200);
g_pAsteroids[1]->SetVelocity(3, -2);
g_pGame->AddSprite(g_pAsteroids[1]);
g_pAsteroids[2] = new Sprite(g_pAsteroidBitmap, rcBounds, BA_WRAP);
g_pAsteroids[2]->SetNumFrames(14);
g_pAsteroids[2]->SetFrameDelay(3);
g_pAsteroids[2]->SetPosition(250, 200);
g_pAsteroids[2]->SetVelocity(-2, -4);
g_pGame->AddSprite(g_pAsteroids[2]);
// 创建飞碟子画面
g_pSaucer = new Sprite(g_pSaucerBitmap, rcBounds, BA_WRAP);
g_pSaucer->SetPosition(0, 0);
g_pSaucer->SetVelocity(3, 1);
g_pGame->AddSprite(g_pSaucer);
}
GameCycle( ) 函数现在调用UpdateSaucer( ) 函数来使飞碟“思考”。
// 游戏循环
void GameCycle()
{
// 更新背景
g_pBackground->Update();
// 更新子画面
g_pGame->UpdateSprites();
// 根据小行星位置移动飞碟
UpdateSaucer();
// 获得用于重新绘制游戏的设备环境
HWND hWindow = g_pGame->GetWindow();
HDC hDC = GetDC(hWindow);
// 在屏幕外设备环境上绘制游戏
GamePaint(g_hOffscreenDC);
// 将屏幕外位图位块传送到游戏屏幕
BitBlt(hDC, 0, 0, g_pGame->GetWidth(), g_pGame->GetHeight(),
g_hOffscreenDC, 0, 0, SRCCOPY);
// 清理
ReleaseDC(hWindow, hDC);
}
http://pan.baidu.com/s/1ge2Vzr1