与环境对象创建简单的AI交互


在为视频游戏创建人工智能时,最重要的方面之一就是它的位置。 AI角色的位置可以完全改变他的行为类型和未来的决策。在本教程中,我们将了解游戏环境如何影响AI以及如何正确使用AI。

本文摘自Michael Dagrack撰写并由Packt Publishing出版的《Practical Game AI Programming》一书。本书可让您学习如何创建游戏AI并从头开始实施最先进的AI算法。

视觉互动是基本的操作,它们不会直接影响游戏玩法,但可以通过将它们纳入我们创造的环境中来使您改善视频游戏及其角色,从而极大地影响玩家对游戏的沉浸感。这向我们证明了环境成为游戏一部分的重要性,而不仅仅是帮助填充屏幕上的空间。在游戏中越来越发现类似的互动,玩家希望看到它们。如果游戏中有一个对象,那么它必须履行某些功能,尽管不是最重要的功能。

与环境互动的第一个例子可以在1986年为任天堂娱乐系统发行的第一部《恶魔城》中找到。从一开始,玩家就可以使用鞭子破坏蜡烛和火,这些蜡烛和火原本是背景的一部分。


就现代人对游戏中角色背景和环境的感知而言,这段时间以及当时的一些游戏打开了许多大门和机会。显然,由于那一代控制台的硬件限制,要创建当前标准普遍接受的简单内容要困难得多。但是每一代游戏机都带来了新功能,开发人员使用它们来创造出色的游戏。

因此,我们第一个视觉交互的示例是背景上的一个对象,可以在不直接影响游戏玩法的情况下对其进行破坏。在许多游戏中都可以找到这种互动方式。实现起来很简单,只要在对象受到攻击时对其进行动画处理即可。之后,我们可以决定是否应该从对象中丢弃任何奖励玩家探索游戏的点或对象。

现在,我们可以继续下一个示例-游戏中具有动画效果的对象,或者当角色通过它们时移动的对象。此处的原理与可破坏对象相同,但是这次的交互更加微妙-它要求角色移动到对象所在的位置。这可以应用于游戏的各种元素,从草地,灰尘或水的运动到飞鸟或做出有趣手势的人;可能性是无止境。

在分析这些交互时,我们可以轻松地确定它们不一定使用AI,并且在大多数情况下,它只是根据某些给定操作激活的布尔函数。但是它们是环境的一部分,因此在实现环境与AI之间的高质量交互时应予以考虑。

与环境进行简单的交互


正如我们已经看到的,环境曾经成为游戏玩法的一部分,这为未来的游戏带来了许多新的概念和想法。下一步是整合游戏过程中的这些小变化,以及将其用于改变玩家在游戏中的行为。这无疑对视频游戏的历史产生了积极的影响,因为场景中的所有元素逐渐开始栩栩如生,并且玩家开始意识到环境的丰富程度。使用环境实现游戏内目标已成为游戏的一部分。


为了演示一个直接影响游戏玩法的环境示例,我们将举一个很好的例子-《古墓丽影》系列。在此示例中,我们的角色Lara Croft必须推动立方体,直到它落在标记的区域上。这将改变环境并开辟新的道路,使玩家可以在水平上进一步移动。

在许多游戏中都可以找到这样的难题:您必须在地图上的特定位置执行一个动作,以便在其中的另一部分发生某些事情,这可以用来实现游戏中的某些目标。通常,我们需要更改环境本身才能进一步提高水平。因此,开发人员在计划地图或关卡时,会考虑到这一点并创建与每个交互相关的所有规则。例如:

if(cube.transform.position == mark.transform.position)
{
  openDoor = true;
}

现在想象一下,劳拉·克罗夫特(Lara Croft)有一个盟友角色,其主要任务是帮助她将盒子放置在原处。在本章中,我们将只考虑这种类型的交互:AI角色了解环境如何工作以及如何使用它。

古墓丽影的移动环境


让我们直接转到这种情况,并尝试重新创建一种可以帮助玩家实现目标的AI角色。在此示例中,我们将想象一个被困的玩家,他无法从中访问可以释放他的交互式对象。我们创建的角色必须能够找到多维数据集并将其推向正确的方向。


现在我们有了所有的角色和对象。让我们计划一下在这种情况下AI角色应该如何表现。首先,他应该看到玩家在附近,以便他可以开始搜索并将多维数据集移动到所需位置。假设如果立方体在标记处,则沙子上会出现一个新的方块,从而使玩家可以进一步提升水平。一个AI角色可以在四个方向上推立方体:左,右,前进和后退,从而使其与位置标记完全吻合。


AI角色必须验证并验证此行为树中显示的每个动作。继续执行任务的首要也是最重要的一点是,角色必须确保玩家​​位于自己的标记上。

如果玩家尚未到达那里,那么我们的角色必须等待并留在原地。如果玩家已经到达标记,那么AI角色会继续执行并问自己距立方体对象有多远。如果不是,则角色应该向立方体移动,一旦确认该动作,他就应该问同样的问题。当答案是肯定的,并且角色在立方体旁边时,他需要弄清楚必须先按哪种方式推立方体。

然后,他开始沿Y轴或X轴推动多维数据集,直到其与标记位置匹配,任务完成。

public GameObject playerMesh;
public Transform playerMark;
public Transform cubeMark;
public Transform currentPlayerPosition;
public Transform currentCubePosition;

public float proximityValueX;
public float proximityValueY;
public float nearValue;

private bool playerOnMark;


void Start () {

}

void Update () {

  // Calculates the current position of the player
  currentPlayerPosition.transform.position = playerMesh.transform.position;

  // Calculates the distance between the player and the player mark of the X axis
  proximityValueX = playerMark.transform.position.x - currentPlayerPosition.transform.position.x;

  // Calculates the distance between the player and the player mark of the Y axis
  proximityValueYplayerMark.transform.position.y - currentPlayerPosition.transform.position.y;

  // Calculates if the player is near of his MARK POSITION
  if((proximityValueX + proximityValueY) < nearValue)
  {
     playerOnMark = true;
  }
}

我们开始在代码中添加信息,以允许角色检查玩家是否在其标记位置附近。为此,我们创建了所有必要的变量,以计算玩家与其应处于的位置之间的距离。playerMesh指玩家的3D模型,我们从中提取他的位置并将其用作currentPlayerPosition

要知道它是否接近标记,我们需要一个代表标记位置的变量,在我们的示例中,我们创建了一个变量playerMark,可以在其中写入玩家应该放置的位置。然后,我们添加了三个变量,让我们知道玩家是否在附近。proximityValueX将计算播放器与X轴标记proximityValueY之间的距离计算播放器与Y轴标记之间的距离。

接下来,我们nearValue可以确定AI角色何时可以开始努力实现目标,从而确定玩家离标记位置有多远。玩家接近标记后,布尔变量playerOnMark将值更改为true

要计算玩家与标记之间的距离,我们使用以下方法:玩家与标记之间的距离,相似(mark.position - player.position)

现在,要确定AI字符是否在立方体附近,我们通过计算AI与立方体之间的距离来计算相同的方程式。另外,我们在两个标记(玩家和立方体)上的位置上补充了代码:

public GameObject playerMesh;
public Transform playerMark;
public Transform cubeMark;
public Transform currentPlayerPosition;
public Transform currentCubePosition;

public float proximityValueX;
public float proximityValueY;
public float nearValue;

public float cubeProximityX;
public float cubeProximityY;
public float nearCube;

private bool playerOnMark;
private bool cubeIsNear;


void Start () {

   Vector3 playerMark = new Vector3(81.2f, 32.6f, -31.3f);
   Vector3 cubeMark = new Vector3(81.9f, -8.3f, -2.94f);
   nearValue = 0.5f;
   nearCube = 0.5f;
}

void Update () {

  // Calculates the current position of the player
  currentPlayerPosition.transform.position = playerMesh.transform.position;

  // Calculates the distance between the player and the player mark of the X axis
  proximityValueX = playerMark.transform.position.x - currentPlayerPosition.transform.position.x;

  // Calculates the distance between the player and the player mark of the Y axis
  proximityValueY = playerMark.transform.position.y - currentPlayerPosition.transform.position.y;

  // Calculates if the player is near of his MARK POSITION
  if((proximityValueX + proximityValueY) < nearValue)
  {
     playerOnMark = true;
  }

  cubeProximityX = currentCubePosition.transform.position.x - this.transform.position.x;
  cubeProximityY = currentCubePosition.transform.position.y - this.transform.position.y;

  if((cubeProximityX + cubeProximityY) < nearCube)
  {
     cubeIsNear = true;
  }

  else
  {
     cubeIsNear = false;
  }
}

现在我们的AI角色知道他是否在立方体旁边,这使我们能够回答问题并确定他是否可以继续进行我们计划的下一个分支。但是,当角色不在立方体旁边时会发生什么?他将需要接近立方体。因此,我们将其添加到代码中:

public GameObject playerMesh;
public Transform playerMark;
public Transform cubeMark;
public Transform cubeMesh;
public Transform currentPlayerPosition;
public Transform currentCubePosition;

public float proximityValueX;
public float proximityValueY;
public float nearValue;

public float cubeProximityX;
public float cubeProximityY;
public float nearCube;

private bool playerOnMark;
private bool cubeIsNear;

public float speed;
public bool Finding;


void Start () {

   Vector3 playerMark = new Vector3(81.2f, 32.6f, -31.3f);
   Vector3 cubeMark = new Vector3(81.9f, -8.3f, -2.94f);
   nearValue = 0.5f;
   nearCube = 0.5f;
   speed = 1.3f;
}

void Update () {

  // Calculates the current position of the player
  currentPlayerPosition.transform.position = playerMesh.transform.position;

  // Calculates the distance between the player and the player mark of the X axis
  proximityValueX = playerMark.transform.position.x - currentPlayerPosition.transform.position.x;

  // Calculates the distance between the player and the player mark of the Y axis
  proximityValueY = playerMark.transform.position.y - currentPlayerPosition.transform.position.y;

  // Calculates if the player is near of his MARK POSITION
  if((proximityValueX + proximityValueY) < nearValue)
  { 
      playerOnMark = true;
  }

  cubeProximityX = currentCubePosition.transform.position.x - this.transform.position.x;
  cubeProximityY = currentCubePosition.transform.position.y - this.transform.position.y;

  if((cubeProximityX + cubeProximityY) < nearCube)
  {
      cubeIsNear = true;
  }

  else
  {
      cubeIsNear = false;
  }

  if(playerOnMark == true && cubeIsNear == false && Finding == false)
  {
     PositionChanging();
  }

  if(playerOnMark == true && cubeIsNear == true)
  {
     Finding = false;
  }

}

void PositionChanging () {

  Finding = true;
  Vector3 positionA = this.transform.position;
  Vector3 positionB = cubeMesh.transform.position;
  this.transform.position = Vector3.Lerp(positionA, positionB, Time.deltaTime * speed);
}

到目前为止,我们的AI角色已经能够计算自己与立方体之间的距离。如果它们相距太远,则他将转到立方体。完成此任务后,他可以进入下一个阶段并开始推动多维数据集。他需要计算的最后一件事是立方体离标记的位置有多远,然后他考虑标记靠近立方体的哪一侧来决定推入哪种方式。


只能沿X和Z轴推动多维数据集,并且多维数据集的旋转对我们而言并不重要,因为在将多维数据集安装到多维数据集后,该按钮便被激活。鉴于所有这些,AI角色必须计算立方体距X上标记位置和Z上标记位置的距离。

然后他比较两个轴上的两个值并选择距离所需位置更远的那个值,然后开始沿此方向推动轴。角色将继续朝这个方向推动,直到立方体与标记的位置对齐,然后切换到另一侧,并将其推动直到完全超过标记的位置:

public GameObject playerMesh;
public Transform playerMark;
public Transform cubeMark;
public Transform cubeMesh;
public Transform currentPlayerPosition;
public Transform currentCubePosition;

public float proximityValueX;
public float proximityValueY;
public float nearValue;

public float cubeProximityX;
public float cubeProximityY;
public float nearCube;

public float cubeMarkProximityX;
public float cubeMarkProximityZ;

private bool playerOnMark;
private bool cubeIsNear;

public float speed;
public bool Finding;


void Start () {

        Vector3 playerMark = new Vector3(81.2f, 32.6f, -31.3f);
        Vector3 cubeMark = new Vector3(81.9f, -8.3f, -2.94f);
        nearValue = 0.5f;
        nearCube = 0.5f;
        speed = 1.3f;
}

void Update () {

  // Calculates the current position of the player
  currentPlayerPosition.transform.position = playerMesh.transform.position;

  // Calculates the distance between the player and the player mark of the X axis
  proximityValueX = playerMark.transform.position.x - currentPlayerPosition.transform.position.x;

  // Calculates the distance between the player and the player mark of the Y axis
  proximityValueY = playerMark.transform.position.y - currentPlayerPosition.transform.position.y;

  // Calculates if the player is near of his MARK POSITION
  if((proximityValueX + proximityValueY) < nearValue)
  {
     playerOnMark = true;
  }

  cubeProximityX = currentCubePosition.transform.position.x - this.transform.position.x;
  cubeProximityY = currentCubePosition.transform.position.y - this.transform.position.y;

  if((cubeProximityX + cubeProximityY) < nearCube)
  {
     cubeIsNear = true;
  }

  else
  {
     cubeIsNear = false;
  }

  if(playerOnMark == true && cubeIsNear == false && Finding == false)
  {
      PositionChanging();
  }

  if(playerOnMark == true && cubeIsNear == true)
  {
      Finding = false;
    }

   cubeMarkProximityX = cubeMark.transform.position.x - currentCubePosition.transform.position.x;
   cubeMarkProximityZ = cubeMark.transform.position.z - currentCubePosition.transform.position.z;

   if(cubeMarkProximityX > cubeMarkProximityZ)
   {
     PushX();
   }

   if(cubeMarkProximityX < cubeMarkProximityZ)
   {
     PushZ();
   }

}

void PositionChanging () {

  Finding = true;
  Vector3 positionA = this.transform.position;
  Vector3 positionB = cubeMesh.transform.position;
  this.transform.position = Vector3.Lerp(positionA, positionB, Time.deltaTime * speed);
}

在代码中添加了最新的动作之后,角色必须学会确定自己的目标,找到并将立方体推到所需位置,以便玩家可以完成并完成关卡。在此示例中,我们集中于如何计算场景对象与角色之间的距离。这将帮助我们创建类似类型的交互,其中需要将游戏对象放置在特定位置。

该示例演示了一个友好的AI角色,该角色可以帮助玩家,但是如果我们需要相反的效果(如果角色是敌人),则可以应用相同的原理,其中角色需要尽快找到立方体以阻止玩家。

以帝国时代为例,在环境中设置障碍物


如前所述,您可以在游戏中使用或移动对象来达成目标,但是如果某些对象阻塞了角色的路径会发生什么呢?对象可以由玩家放置,也可以由设计师简单地放置在地图的此位置。无论如何,AI角色应该能够确定在这种情况下需要做什么。

例如,我们可以在Ensemble Studios开发的称为帝国时代II的策略中观察到这种行为。每当游戏角色由于被坚固的城墙包围而无法到达敌方领土时,AI会切换到破坏城墙的一部分以继续进行。

这种交互方式也非常聪明和重要,因为否则角色将只是沿着墙壁徘徊以寻找入口,这看起来不像是合理的行为。由于加固墙是由玩家创建的,因此可以将其放置在任何位置并具有任何形状。因此,在开发敌方AI时需要考虑这一点。


这个例子也与我们的文章主题有关,因为在计划阶段,当我们创建行为树时,我们需要考虑如果某些东西阻碍了角色并且他无法实现目标,将会发生什么。我们将在本书的下一章中详细考虑这方面,但是现在我们简化了情况并分析了如果环境对象阻止AI角色实现目标,AI角色应该如何表现。


在我们的示例中,AI角色必须进入房屋,但是当他走近时,他意识到自己被木栅栏所包围,您不能通过它。我们希望角色在此阶段选择一个目标,然后开始进攻,直到篱笆的这一部分被破坏,他才能进入房屋。

在此示例中,我们需要根据给定的距离和栅栏的当前健康状况,计算角色应该攻击哪个栅栏。 HP较低的篱笆应比HP较高的篱笆具有更高的攻击优先级,因此我们在计算时会考虑到这一点。


我们想在角色周围设置邻域,在该邻域内最近的栅栏将其信息传递给人工智能,以便它可以确定哪个更容易破坏。这可以通过多种方式实现,要么使用围栏与玩家的碰撞识别,要么迫使他们计算围栏/物体与玩家之间的距离。我们设置玩家开始感知围栏状态的距离值。在我们的示例中,我们将计算距离并将其用于通知角色有关HP围栏的信息。

让我们从创建将应用到fence对象的代码开始;它们都将具有相同的脚本:

public float HP;
public float distanceValue;
private Transform characterPosition;
private GameObject characterMesh;

private float proximityValueX;
private float proximityValueY;
private float nearValue;

// Use this for initialization
void Start () {

  HP = 100f;
  distanceValue = 1.5f;

  // Find the Character Mesh
  characterMesh = GameObject.Find("AICharacter");
}

// Update is called once per frame
void Update () {

  // Obtain the Character Mesh Position
  characterPosition = characterMesh.transform;

  //Calculate the distance between this object and the AI Character
  proximityValueX = characterPosition.transform.position.x - this.transform.position.x;
  proximityValueY = characterPosition.transform.position.y - this.transform.position.y;

  nearValue = proximityValueX + proximityValueY;
}

在此脚本中,我们添加了有关HP和距离的基本信息,这些信息将用于与AI字符连接。这次我们不将距离计算脚本添加到角色,而是添加到环境对象。这使对象更具活力,并允许我们创造更多机会。

例如,如果游戏中的角色也参与围栏的创建,则它们将具有不同的状态,例如“正在建设中”,“已完成”或“已损坏”;那么角色将能够接收此信息并将其用于自己的目的。

让我们设置与环境对象交互的角色。他的主要目标是进入房屋,但是当他接近房屋时,他意识到由于自己被木栅栏包围,因此无法进入房屋。我们希望,在分析了情况之后,我们的角色会破坏篱笆以实现他的目标并进入房屋。

在角色脚本中,我们将添加一个静态函数,栅栏将能够在该输入中传输有关其当前“健康状况”的信息;这将帮助角色选择最适合破坏的围栏。

public static float fenceHP;
public static float lowerFenceHP;
public static float fencesAnalyzed;
public static GameObject bestFence;

private Transform House;

private float timeWasted;
public float speed;



void Start () {

        fenceHP = 100f;
        lowerFenceHP = fenceHP;
        fencesAnalyzed = 0;
        speed = 0.8;

        Vector3 House = new Vector3(300.2f, 83.3f, -13.3f);

}

void Update () {

        timeWasted += Time.deltaTime;

        if(fenceHP > lowerFenceHP)
        {
            lowerFenceHP = fenceHP;
        }

        if(timeWasted > 30f)
        {
            GoToFence();  
        }
}

void GoToFence() {

        Vector3 positionA = this.transform.position;
        Vector3 positionB = bestFence.transform.position;
        this.transform.position = Vector3.Lerp(positionA, positionB, Time.deltaTime * speed);
}


我们已经为角色添加了最基本的信息。fenceHP将是一个静态变量,落入角色邻域半径内的每个围栏都将记录有关当前HP的信息。然后AI角色分析收到的信息,并将其与HP最少的栅栏进行比较lowerFenceHP

角色有一个变量,timeWasted表示他已经花费了几秒钟来寻找合适的栅栏来摧毁。fencesAnalyzed将用于查找代码中是否已经有围栏,如果没有,则添加角色找到的第一个围栏;如果围栏具有相同的HP值,角色将首先攻击它们。现在,我们添加围栏的代码,以便围栏可以访问角色的脚本并输入有用的信息。

public float HP;
public float distanceValue;
private Transform characterPosition;
private GameObject characterMesh;

private float proximityValueX;
private float proximityValueY;
private float nearValue;
void Start () {

        HP = 100f;
        distanceValue = 1.5f;

        // Find the Character Mesh
        characterMesh = GameObject.Find("AICharacter");
}

void Update () {

        // Obtain the Character Mesh Position
        characterPosition = characterMesh.transform;

        //Calculate the distance between this object and the AI Character
        proximityValueX = characterPosition.transform.position.x - this.transform.position.x;
        proximityValueY = characterPosition.transform.position.y - this.transform.position.y;

        nearValue = proximityValueX + proximityValueY;

        if(nearValue <= distanceValue){
            if(AICharacter.fencesAnalyzed == 0){
                AICharacter.fencesAnalyzed = 1;
                AICharacter.bestFence = this.gameObject;
            }

            AICharacter.fenceHP = HP;

            if(HP < AICharacter.lowerFenceHP){
                AICharacter.bestFence = this.gameObject;
            }
        }
}

我们终于完成了这个例子。现在,围栏将其当前HP与角色的数据(lowerFenceHP)进行比较,如果其HP小于角色拥有的最小值,则将考虑该围栏bestFence

该示例演示了如何使AI角色适应游戏中的各种动态对象。可以扩展相同的原理,并将其与几乎任何对象进行交互。当使用对象与角色交互,链接对象之间的信息时,它也是适用和有用的。

在本文中,我们探索了与环境互动的各种方式。本章中演示的技术可以扩展到许多不同类型的游戏,并可以用于在AI角色和环境之间进行简单而复杂的交互。

All Articles