Criando interações simples de IA com objetos do ambiente


Ao criar inteligência artificial para videogames, um dos aspectos mais importantes é a sua localização. A posição do personagem da IA ​​pode mudar completamente seus tipos de comportamento e decisões futuras. Neste tutorial, entenderemos como o ambiente do jogo pode afetar a IA e como usá-lo corretamente.

Este artigo foi retirado do livro Practical Game AI Programming , escrito por Michael Dagrack e publicado pela Packt Publishing. Este livro permite que você aprenda como criar uma IA de jogo e, do zero, implemente os algoritmos de IA mais avançados.

As interações visuais são básicas, elas não afetam diretamente a jogabilidade, mas permitem melhorar o videogame e seus personagens, tornando-os parte do ambiente que criamos, o que afeta bastante a imersão do jogador no jogo. Isso prova a importância do ambiente fazer parte do jogo, e não apenas ajudar a preencher o espaço na tela. Interações semelhantes são cada vez mais encontradas nos jogos, e os jogadores esperam vê-las. Se houver um objeto no jogo, ele deve cumprir alguma função, embora não a mais importante.

Um dos primeiros exemplos de interação com os ambientes pode ser encontrado no primeiro Castlevania, lançado em 1986 para o Nintendo Entertainment System. Desde o início, o jogador pode usar o chicote para destruir velas e fogos, que originalmente faziam parte do plano de fundo.


Este e alguns jogos da época abriram muitas portas e oportunidades em termos da percepção moderna dos antecedentes e ambientes dos personagens no jogo. Obviamente, devido às limitações de hardware dessa geração de consoles, era muito mais difícil criar coisas simples que geralmente são aceitas pelos padrões atuais. Mas cada geração de consoles trouxe novos recursos e os desenvolvedores os usaram para criar jogos incríveis.

Portanto, nosso primeiro exemplo de interação visual é um objeto em segundo plano que pode ser destruído sem afetar diretamente a jogabilidade. Esse tipo de interação é encontrado em muitos jogos. Implementá-lo é simples, basta animar o objeto quando for atacado. Depois disso, podemos decidir se algum ponto ou objeto deve ser descartado do objeto que recompensa o jogador por explorar o jogo.

Agora podemos seguir para o próximo exemplo - objetos no jogo que são animados ou se movem quando os personagens passam por eles. O princípio aqui é o mesmo que com objetos destrutíveis, mas desta vez a interação é mais sutil - requer que o personagem se mova para o ponto em que o objeto está. Isso pode ser aplicado a vários elementos do jogo, desde o movimento da grama, poeira ou água, até pássaros voando ou pessoas fazendo gestos engraçados; as possibilidades são infinitas.

Ao analisar essas interações, podemos facilmente determinar que elas não usam necessariamente a IA e, na maioria dos casos, é apenas uma função booleana que é ativada de acordo com alguma ação especificada. Mas eles fazem parte do ambiente e, portanto, devem ser levados em consideração ao implementar uma interação de alta qualidade entre o ambiente e a IA.

Crie interações simples com o ambiente


Como já vimos, o ambiente já se tornou parte da jogabilidade, e isso deu origem a muitos novos conceitos e idéias para jogos futuros. O próximo passo foi a integração dessas pequenas mudanças na jogabilidade e seu uso para alterar o comportamento do jogador no jogo. Isso definitivamente afetou positivamente a história dos videogames, porque todos os elementos da cena começaram a ganhar vida gradualmente e o jogador começou a perceber o quão rico era o ambiente. Usar ambientes para alcançar objetivos no jogo se tornou parte da jogabilidade.


Para demonstrar um exemplo de ambiente que afeta diretamente a jogabilidade, daremos um excelente exemplo - a franquia Tomb Raider. Neste exemplo, nossa personagem, Lara Croft, deve empurrar o cubo até que ele caia na área marcada. Isso mudará o ambiente e abrirá um novo caminho, permitindo que o jogador avance mais de nível.

Esses quebra-cabeças podem ser encontrados em muitos jogos: você deve executar uma ação em um determinado ponto do mapa para que algo aconteça em outra parte dele, e isso pode ser usado para atingir algum objetivo no jogo. Normalmente, precisamos alterar o próprio ambiente para avançar ainda mais em nível. Portanto, quando os desenvolvedores planejam um mapa ou nível, eles levam isso em consideração e criam todas as regras relacionadas a cada interação. Por exemplo:

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

Agora imagine por um momento que Lara Croft tem um personagem aliado cuja principal tarefa é ajudá-la a colocar esta caixa em seu lugar. E neste capítulo, consideraremos exatamente esse tipo de interação: o personagem da IA ​​entende como o ambiente funciona e como usá-lo.

Ambiente em movimento no Tomb Raider


Vamos direto a esse cenário e tentamos recriar uma situação em que haja um personagem de IA que possa ajudar o jogador a atingir seu objetivo. Neste exemplo, imaginaremos que um jogador está preso do qual não pode obter acesso a um objeto interativo que pode libertá-lo. O personagem que criamos deve ser capaz de encontrar o cubo e empurrá-lo na direção certa.


Então agora temos todos os personagens e objetos. Vamos planejar como o personagem da IA ​​deve se comportar nessa situação. Primeiro, ele deve ver que o jogador está perto, para que ele possa começar a procurar e mover o cubo para a posição desejada. Suponha que, se o cubo estiver na marca, um novo bloco aparecerá na areia, permitindo que o jogador avance ainda mais no nível. Um personagem de IA pode empurrar um cubo em quatro direções: esquerda, direita, frente e trás, para que ele se encaixe perfeitamente com a marca de posição.


O personagem do AI deve verificar e validar todas as ações mostradas nesta árvore de comportamento. A primeira e mais importante coisa para continuar a tarefa é que o personagem tenha certeza de que o jogador está localizado em sua marca.

Se o jogador ainda não chegou lá, nosso personagem deve esperar e permanecer no lugar. Se o jogador já chegou à marca, o personagem da IA ​​continua a execução e se pergunta a que distância ele está do objeto do cubo. Caso contrário, o personagem deve se mover em direção ao cubo e, uma vez confirmada essa ação, ele deve fazer a mesma pergunta. Quando a resposta se torna positiva e o personagem está ao lado do cubo, ele precisa descobrir de que maneira você deve primeiro empurrá-lo.

Então ele começa a empurrar o cubo ao longo do eixo Y ou X até que ele corresponda à posição de marcação e a tarefa seja concluída.

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 começando a adicionar informações ao código que permite ao personagem verificar se o jogador está próximo à sua posição marcada. Para isso, criamos todas as variáveis ​​necessárias para calcular as distâncias entre o jogador e a posição em que ele deve estar. playerMeshrefere-se ao modelo 3D do jogador, do qual extraímos sua posição e a usamos como currentPlayerPosition.

Para saber se está perto da marca, precisamos de uma variável que represente a posição da marca e, em nosso exemplo, criamos uma variável playerMarkna qual podemos escrever a posição em que o jogador deve estar. Em seguida, adicionamos três variáveis ​​que nos informam se o jogador está por perto. proximityValueXcalculará a distância entre o jogador e a marca do eixo X. proximityValueYcalcula a distância entre o jogador e a marca do eixo Y.

Em seguida, temos nearValue, em que podemos determinar a que distância o jogador pode estar da posição da marca, quando o personagem de IA pode começar a trabalhar para alcançar o objetivo. Assim que o player estiver próximo da marca, a variável booleana playerOnMarkaltera o valor para true.

Para calcular a distância entre o jogador e a marca, usamos o seguinte: a distância entre o jogador e sua marca, semelhante (mark.position - player.position).

Agora, para determinar se o caractere AI está próximo ao cubo, calculamos a mesma equação calculando a distância entre o AI e o cubo. Além disso, complementamos o código com posições nas duas marcas (jogador e 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;
  }
}

Agora que nosso personagem de IA sabe se ele está próximo ao cubo, isso nos permite responder à pergunta e determinar se ele pode passar para o próximo ramo que planejamos. Mas o que acontece quando o personagem não está próximo ao cubo? Ele precisará se aproximar do cubo. Portanto, adicionaremos isso ao 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);
}

Até agora, nosso personagem de IA é capaz de calcular a distância entre ele e o cubo; se estiverem muito distantes, ele irá para o cubo. Depois de concluir esta tarefa, ele pode prosseguir para a próxima fase e começar a empurrar o cubo. A última coisa que ele precisa calcular é a que distância o cubo está da posição da marca, após o que decide qual caminho empurrar, levando em consideração de que lado do cubo a marca está mais próxima.


O cubo só pode ser empurrado ao longo dos eixos X e Z, e sua rotação ainda não é importante para nós, porque o botão é ativado quando o cubo é instalado nele. Dado tudo isso, o caractere AI deve calcular a que distância o cubo está da posição da marca em X e da posição da marca em Z.

Em seguida, ele compara os dois valores em dois eixos e seleciona qual deles está mais distante da posição desejada, e então começa a empurrá-lo. eixo. O personagem continuará empurrando nessa direção até que o cubo esteja alinhado com a posição da marca e, em seguida, mude para o outro lado e o empurrará até que esteja completamente acima da posição da 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);
}

Depois de adicionar as últimas ações ao código, o personagem deve aprender a determinar seu objetivo, encontrar e empurrar o cubo para a posição desejada, para que o jogador possa passar e terminar o nível. Neste exemplo, focamos em como calcular distâncias entre objetos de cena e personagens. Isso nos ajudará a criar tipos semelhantes de interações nas quais precisamos colocar o objeto do jogo em uma determinada posição.

O exemplo demonstra um personagem AI amigável que ajuda o jogador, mas os mesmos princípios podem ser aplicados se precisarmos do efeito oposto (se o personagem for um inimigo), no qual o personagem precisa encontrar o cubo o mais rápido possível para parar o jogador.

Objetos de obstáculo em ambientes usando o exemplo do Age of Empires


Como vimos anteriormente, você pode usar ou mover objetos no jogo para atingir objetivos, mas o que acontece se algum objeto obstruir o caminho do personagem? O objeto pode ser colocado pelo jogador ou simplesmente localizado pelo designer nesta posição do mapa. De qualquer forma, o personagem da IA ​​deve ser capaz de determinar o que precisa ser feito nessa situação.

Podemos observar esse comportamento, por exemplo, em uma estratégia chamada Age of Empires II, desenvolvida pela Ensemble Studios. Cada vez que um personagem do jogo não pode alcançar o território inimigo devido ao fato de estar cercado por muros fortificados, a IA passa a destruir parte do muro para seguir em frente.

Esse tipo de interação também é muito inteligente e importante, porque, caso contrário, os personagens apenas perambulariam pela parede em busca de uma entrada, e isso não pareceria um comportamento razoável. Como a parede reforçada é criada pelo jogador, ela pode ser colocada em qualquer lugar e com qualquer forma. Portanto, você precisa pensar sobre isso ao desenvolver uma IA inimiga.


Este exemplo também se refere ao tópico do nosso artigo, porque, na fase de planejamento, quando criamos árvores de comportamento, precisamos pensar no que acontecerá se algo atrapalhar o personagem e ele não conseguir cumprir seus objetivos. Vamos considerar esse aspecto em detalhes nos próximos capítulos do livro, mas por enquanto simplificamos a situação e analisamos como o personagem da IA ​​deve se comportar se o objeto do ambiente o impedir de cumprir seu objetivo.


No nosso exemplo, o personagem da IA ​​deve entrar na casa, mas quando ele se aproxima, ele percebe que está cercado por uma cerca de madeira, pela qual você não pode passar. Queremos que o personagem escolha um alvo nesta fase e comece a atacar até que essa parte da cerca seja destruída e ele possa entrar na casa.

Neste exemplo, precisamos calcular a cerca que o personagem deve atacar, dada a distância e o status de saúde atual da cerca. Uma cerca com baixo HP deve ter uma prioridade mais alta para ataque do que uma cerca com HP total; portanto, levaremos isso em consideração em nossos cálculos.


Queremos definir a vizinhança em torno do personagem, dentro da qual as cercas mais próximas transmitem suas informações à inteligência artificial, para que ele possa decidir qual é mais fácil de destruir. Isso pode ser implementado de várias maneiras, usando o reconhecimento de colisões de cercas com o jogador ou forçando-os a calcular a distância entre cercas / objetos e o jogador; definimos o valor da distância em que o jogador começa a perceber o estado da cerca. Em nosso exemplo, calcularemos a distância e a usaremos para notificar o personagem sobre cercas HP.

Vamos começar criando o código que será aplicado ao objeto fence; todos eles terão o mesmo script:

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

Neste script, adicionamos informações básicas sobre HP e distâncias, que serão usadas para conectar-se ao caractere AI. Desta vez, adicionamos o script de cálculo da distância não ao personagem, mas aos objetos do ambiente; isso dá ao objeto mais dinamismo e nos permite criar mais oportunidades.

Por exemplo, se os personagens do jogo também estão envolvidos na criação de cercas, eles terão estados diferentes, por exemplo, "em construção", "concluído" ou "danificado"; então o personagem poderá receber essas informações e usá-las para seus próprios propósitos.

Vamos definir o personagem interagindo com o objeto do ambiente. Seu principal objetivo será obter acesso à casa, mas quando ele se aproxima dela, ele percebe que não pode entrar, devido ao fato de estar cercado por cercas de madeira. Queremos que, analisando a situação, nosso personagem destrua a cerca para cumprir seu objetivo e entrar na casa.

No script do personagem, adicionaremos uma função estática, cuja entrada será capaz de transmitir informações sobre sua "saúde" atual; isso ajudará o personagem a escolher a cerca mais adequada para a destruição.

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


Já adicionamos as informações mais básicas ao personagem. fenceHPserá uma variável estática na qual cada cerca que cai dentro do raio da vizinhança do personagem registrará informações sobre o HP atual. Em seguida, o personagem AI analisa as informações recebidas e as compara com a cerca com o mínimo de HP apresentado lowerFenceHP.

O personagem tem uma variável que timeWastedrepresenta o número de segundos que ele já passou procurando uma cerca adequada para destruir. fencesAnalyzedserá usado para descobrir se já existe uma cerca no código e, se não, a primeira cerca encontrada pelo personagem é adicionada; caso as cercas tenham o mesmo valor de HP, o personagem as ataca primeiro. Agora vamos adicionar o código das cercas para que eles possam acessar o script do personagem e inserir informações úteis.

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 concluímos este exemplo. Agora a cerca compara seu HP atual com os dados do personagem ( lowerFenceHP) e, se o HP for menor que o valor mais baixo que o personagem possui, essa cerca será considerada bestFence.

Este exemplo demonstra como adaptar um personagem de IA a vários objetos dinâmicos no jogo; o mesmo princípio pode ser estendido e usado para interagir com quase qualquer objeto. Também é aplicável e útil ao usar objetos para interagir com um personagem, vinculando informações entre eles.

Neste post, exploramos várias maneiras de interagir com o meio ambiente. As técnicas demonstradas neste capítulo podem ser estendidas a muitos gêneros diferentes de jogos e usadas para executar interações simples e complexas entre os personagens de IA e o ambiente.

All Articles