Crear interacciones simples de IA con objetos del entorno


Al crear inteligencia artificial para videojuegos, uno de los aspectos más importantes es su ubicación. La posición del personaje de IA puede cambiar completamente sus tipos de comportamiento y decisiones futuras. En este tutorial entenderemos cómo el entorno del juego puede afectar a la IA y cómo usarla correctamente.

Este artículo está tomado del libro Practical Game AI Programming , escrito por Michael Dagrack y publicado por Packt Publishing. Este libro te permite aprender cómo crear un juego de IA y desde cero implementar los algoritmos de IA más avanzados.

Las interacciones visuales son básicas, no afectan directamente el juego, pero te permiten mejorar el videojuego y sus personajes al hacerlos parte del entorno que creamos, lo que afecta en gran medida la inmersión del jugador en el juego. Esto nos demuestra la importancia de que el entorno sea parte del juego, y no solo de ayudar a llenar el espacio en la pantalla. Interacciones similares se encuentran cada vez más en los juegos, y los jugadores esperan verlas. Si hay un objeto en el juego, entonces debe cumplir alguna función, aunque no la más importante.

Uno de los primeros ejemplos de interacción con entornos se puede encontrar en el primer Castlevania, lanzado en 1986 para Nintendo Entertainment System. Desde el principio, el jugador puede usar el látigo para destruir velas e incendios, que originalmente formaban parte del fondo.


Este y algunos juegos de esa época abrieron muchas puertas y oportunidades en términos de la percepción moderna de los entornos y entornos de los personajes del juego. Obviamente, debido a las limitaciones de hardware de esa generación de consolas, fue mucho más difícil crear cosas simples que generalmente son aceptadas por los estándares actuales. Pero cada generación de consolas trajo nuevas características y los desarrolladores las usaron para crear juegos increíbles.

Entonces, nuestro primer ejemplo de interacción visual es un objeto en el fondo que puede destruirse sin afectar directamente el juego. Este tipo de interacción se encuentra en muchos juegos. Implementarlo es simple, solo anima el objeto cuando es atacado. Después de eso, podemos decidir si se deben soltar puntos u objetos del objeto que recompense al jugador por explorar el juego.

Ahora podemos pasar al siguiente ejemplo: objetos en el juego que están animados o se mueven cuando los personajes los atraviesan. El principio aquí es el mismo que con los objetos destructibles, pero esta vez la interacción es más sutil: requiere que el personaje se mueva al punto donde está el objeto. Esto se puede aplicar a varios elementos del juego, desde el movimiento de hierba, polvo o agua, hasta pájaros voladores o personas que hacen gestos divertidos; Las posibilidades son infinitas.

Al analizar estas interacciones, podemos determinar fácilmente que no necesariamente usan AI, y en la mayoría de los casos es solo una función booleana que se activa de acuerdo con alguna acción dada. Pero son parte del entorno y, por lo tanto, deben tenerse en cuenta al implementar una interacción de alta calidad entre el entorno y la IA.

Crear interacciones simples con el entorno.


Como ya hemos visto, el entorno una vez se convirtió en parte de la jugabilidad, y esto dio lugar a muchos nuevos conceptos e ideas para futuros juegos. El siguiente paso fue la integración de estos pequeños cambios en el juego y su uso para cambiar el comportamiento del jugador en el juego. Esto definitivamente afectó positivamente la historia de los videojuegos, porque todos los elementos de la escena gradualmente comenzaron a cobrar vida y el jugador comenzó a darse cuenta de lo rico que era el entorno. El uso de entornos para lograr objetivos en el juego se ha convertido en parte del juego.


Para demostrar un ejemplo de un entorno que afecta directamente el juego, tomaremos un excelente ejemplo: la franquicia Tomb Raider. En este ejemplo, nuestro personaje, Lara Croft, debe empujar el cubo hasta que aterrice en el área marcada. Esto cambiará el entorno y abrirá un nuevo camino, permitiendo que el jugador avance más de nivel.

Estos acertijos se pueden encontrar en muchos juegos: debes realizar una acción en cierto punto del mapa para que algo suceda en otra parte del mismo, y esto se puede usar para lograr algún objetivo en el juego. Por lo general, necesitamos cambiar el entorno mismo para avanzar más de nivel. Por lo tanto, cuando los desarrolladores planifican un mapa o nivel, lo tienen en cuenta y crean todas las reglas relacionadas con cada interacción. Por ejemplo:

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

Ahora imagine por un momento que Lara Croft tiene un personaje aliado cuya tarea principal es ayudarla a poner esta caja en su lugar. Y en este capítulo consideraremos solo este tipo de interacción: el personaje de IA comprende cómo funciona el entorno y cómo usarlo.

Entorno en movimiento en Tomb Raider


Vayamos directamente a este escenario e intentemos recrear una situación en la que haya un personaje de IA que pueda ayudar al jugador a lograr su objetivo. En este ejemplo, imaginaremos que un jugador está atrapado del cual no puede obtener acceso a un objeto interactivo que pueda liberarlo. El personaje que creamos debe poder encontrar el cubo y empujarlo en la dirección correcta.


Entonces ahora tenemos todos los personajes y objetos. Planifiquemos cómo debería comportarse el personaje AI en esta situación. Primero, debe ver que el jugador está cerca, para poder comenzar a buscar y mover el cubo a la posición deseada. Suponga que si el cubo está en la marca, entonces aparece un nuevo bloque de la arena, lo que permite al jugador avanzar más de nivel. Un personaje AI puede empujar un cubo en cuatro direcciones: izquierda, derecha, adelante y atrás, para que se ajuste perfectamente con la marca de posición.


El personaje AI debe verificar y validar cada acción mostrada en este árbol de comportamiento. Lo primero y más importante para continuar la tarea es que el personaje debe estar seguro de que el jugador se encuentra en su marca.

Si el jugador aún no ha llegado allí, entonces nuestro personaje debe esperar y permanecer en su lugar. Si el jugador ya ha llegado a la marca, entonces el personaje AI continúa la ejecución y se pregunta a qué distancia está del objeto cubo. Si no, entonces el personaje debe moverse hacia el cubo, y una vez que se confirma esta acción, debe hacer la misma pregunta. Cuando la respuesta se vuelve positiva y el personaje está al lado del cubo, necesita descubrir de qué manera primero debes empujar el cubo.

Luego comienza a empujar el cubo a lo largo del eje Y o X hasta que coincida con la posición de marcado y se completa la tarea.

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;
  }
}

Estamos comenzando a agregar información al código que permite al personaje verificar si el jugador está al lado de su posición marcada. Para hacer esto, creamos todas las variables necesarias para calcular las distancias entre el jugador y la posición en la que debería estar. playerMeshse refiere al modelo 3D del jugador, del cual extraemos su posición y la usamos como currentPlayerPosition.

Para saber si está cerca de la marca, necesitamos una variable que represente la posición de la marca, y en nuestro ejemplo creamos una variable playerMarken la que podemos escribir la posición en la que debería estar el jugador. Luego agregamos tres variables que nos permiten saber si el jugador está cerca. proximityValueXcalculará la distancia entre el jugador y la marca del eje X. proximityValueYcalcula la distancia entre el jugador y la marca del eje Y.

A continuación, tenemos nearValue, en el que podemos determinar qué tan lejos puede estar el jugador de la posición de la marca, cuando el personaje AI puede comenzar a trabajar para lograr el objetivo. Tan pronto como el jugador esté cerca de la marca, la variable booleana playerOnMarkcambia el valor a true.

Para calcular la distancia entre el jugador y la marca, utilizamos lo siguiente: la distancia entre el jugador y su marca, similar (mark.position - player.position).

Ahora, para determinar si el personaje AI está cerca del cubo, calculamos la misma ecuación calculando la distancia entre la IA y el cubo. Además, complementamos el código con posiciones en ambas marcas (jugador y cubo):

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;
  }
}

Ahora que nuestro personaje de IA sabe si está al lado del cubo, esto nos permite responder la pregunta y determinar si puede pasar a la siguiente rama que planeamos. Pero, ¿qué sucede cuando el personaje no está al lado del cubo? Tendrá que acercarse al cubo. Por lo tanto, agregaremos esto al código:

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);
}

Hasta ahora, nuestro personaje AI puede calcular la distancia entre él y el cubo; si están demasiado separados, él irá al cubo. Después de completar esta tarea, puede pasar a la siguiente fase y comenzar a empujar el cubo. Lo último que necesita calcular es qué tan lejos está el cubo de la posición de la marca, después de lo cual decide qué forma de empujar, teniendo en cuenta a qué lado del cubo está más cerca la marca.


El cubo solo se puede empujar a lo largo de los ejes X y Z, y su rotación aún no es importante para nosotros, porque el botón se activa cuando el cubo está instalado en él. Dado todo esto, el personaje AI debe calcular qué tan lejos está el cubo de la posición de la marca en X y la posición de la marca en Z.

Luego compara los dos valores en dos ejes y selecciona cuál está más lejos de la posición deseada, y luego comienza a avanzar. eje. El personaje continuará empujando en esta dirección hasta que el cubo esté alineado con la posición de la marca, y luego cambie al otro lado, y lo empujará hasta que esté completamente sobre la posición de la marca:

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);
}

Después de agregar las últimas acciones al código, el personaje debe aprender a determinar su objetivo, encontrar y empujar el cubo a la posición deseada para que el jugador pueda pasar y terminar el nivel. En este ejemplo, nos centramos en cómo calcular distancias entre objetos de escena y personajes. Esto nos ayudará a crear tipos similares de interacciones en las que necesitamos colocar el objeto del juego en una determinada posición.

El ejemplo demuestra un personaje AI amigable que ayuda al jugador, pero los mismos principios se pueden aplicar si necesitamos el efecto contrario (si el personaje es un enemigo), en el que el personaje necesita encontrar el cubo lo antes posible para detener al jugador.

Obstáculos en entornos que utilizan el ejemplo de Age of Empires


Como vimos anteriormente, puedes usar o mover objetos en el juego para lograr objetivos, pero ¿qué sucede si algún objeto obstruye el camino del personaje? El jugador puede colocar el objeto o simplemente ubicarlo el diseñador en esta posición del mapa. En cualquier caso, el personaje AI debería poder determinar qué debe hacerse en esta situación.

Podemos observar este comportamiento, por ejemplo, en una estrategia llamada Age of Empires II desarrollada por Ensemble Studios. Cada vez que un personaje del juego no puede llegar al territorio enemigo debido al hecho de que está rodeado de muros fortificados, la IA cambia a destruir parte del muro para seguir adelante.

Este tipo de interacción también es muy inteligente e importante, porque de lo contrario los personajes simplemente deambularían por la pared en busca de una entrada, y esto no se vería como un comportamiento razonable. Dado que el jugador crea la pared reforzada, se puede colocar en cualquier lugar y tener cualquier forma. Por lo tanto, debes pensar en esto cuando desarrolles una IA enemiga.


Este ejemplo también se relaciona con el tema de nuestro artículo, porque en la etapa de planificación, cuando creamos árboles de comportamiento, necesitamos pensar en lo que sucederá si algo se interpone en el camino del personaje y él no puede cumplir sus objetivos. Consideraremos este aspecto en detalle en los próximos capítulos del libro, pero por ahora simplificamos la situación y analizamos cómo debe comportarse el personaje AI si el objeto del entorno le impide cumplir su objetivo.


En nuestro ejemplo, el personaje AI debe entrar en la casa, pero cuando se acerca, se da cuenta de que está rodeado por una valla de madera, a través de la cual no puede pasar. Queremos que el personaje elija un objetivo en este punto y comience a atacar hasta que esta parte de la cerca se destruya y pueda entrar en la casa.

En este ejemplo, necesitamos calcular a qué cerca debe atacar el personaje, dada la distancia y el estado de salud actual de la cerca. Una cerca con bajo HP debería tener una prioridad más alta para el ataque, a diferencia de una cerca con HP completo, por lo que lo tendremos en cuenta en nuestros cálculos.


Queremos establecer el vecindario alrededor del personaje, dentro del cual las cercas más cercanas transmiten su información a la inteligencia artificial para que pueda decidir cuál es más fácil de destruir. Esto se puede implementar de varias maneras, ya sea utilizando el reconocimiento de colisiones de cercas con el jugador o forzándolas a calcular la distancia entre cercas / objetos y el jugador; establecemos el valor de la distancia a la que el jugador comienza a percibir el estado de la cerca. En nuestro ejemplo, calcularemos la distancia y la usaremos para notificar al personaje sobre las cercas de HP.

Comencemos creando el código que se aplicará al objeto de valla; Todos tendrán el mismo guión:

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;
}

En este script, agregamos información básica sobre HP y las distancias, que se utilizarán para conectarse con el personaje AI. Esta vez agregamos el guión de cálculo de distancia no al personaje, sino a los objetos del entorno; Esto le da más dinamismo al objeto y nos permite crear más oportunidades.

Por ejemplo, si los personajes del juego también participan en la creación de cercas, entonces tendrán diferentes estados, por ejemplo, "en construcción", "completado" o "dañado"; entonces el personaje podrá recibir esta información y usarla para sus propios fines.

Vamos a configurar el personaje interactuando con el objeto del entorno. Su objetivo principal será obtener acceso a la casa, pero cuando se acerca, se da cuenta de que no puede entrar debido al hecho de que está rodeado de cercas de madera. Queremos que, después de analizar la situación, nuestro personaje destruya la cerca para cumplir su objetivo y entrar en la casa.

En el script de caracteres, agregaremos una función estática, a la entrada de las cuales las cercas podrán transmitir información sobre su "estado" actual; Esto ayudará al personaje a elegir la cerca más adecuada para la destrucción.

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);
}


Ya hemos agregado la información más básica al personaje. fenceHPserá una variable estática en la que cada cerca que se encuentre dentro del radio del vecindario del personaje registrará información sobre el HP actual. Luego, el personaje de AI analiza la información recibida y la compara con la valla con el menor HP presentado lowerFenceHP.

El personaje tiene una variable que timeWastedrepresenta la cantidad de segundos que ya pasó buscando una cerca adecuada para destruir. fencesAnalyzedse usará para averiguar si ya hay una cerca en el código, y si no, se agrega la primera cerca encontrada por el personaje; en caso de que las cercas tengan el mismo valor de HP, el personaje las ataca primero. Ahora agreguemos el código de las cercas para que puedan acceder al guión del personaje e ingresar información útil.

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;
            }
        }
}

Finalmente completamos este ejemplo. Ahora la cerca compara su HP actual con los datos del personaje ( lowerFenceHP), y si su HP es menor que el valor más bajo que tiene el personaje, entonces se considerará esta cerca bestFence.

Este ejemplo demuestra cómo adaptar un personaje AI a varios objetos dinámicos en el juego; El mismo principio puede extenderse y utilizarse para interactuar con casi cualquier objeto. También es aplicable y útil cuando se usan objetos para interactuar con un personaje, vinculando información entre ellos.

En esta publicación, exploramos varias formas de interactuar con el medio ambiente. Las técnicas demostradas en este capítulo pueden extenderse a muchos géneros diferentes de juegos y usarse para realizar interacciones simples y complejas entre los personajes de IA y el entorno.

All Articles