Cursos / Jogos Digitais / Inteligência Artificial para Jogos / Aula

arrow_back Aula 10 - Descoberta de caminho – Parte 04

3 - Caminho eficaz: Algoritmo A*

Ehr... sem pressão.

O algoritmo A* é um dos mais utilizados em situações de pathfinding, ou busca de caminhos. Ele otimiza o algoritmo de Dijkstra em dois aspectos para tornar o seu funcionamento mais eficiente:

  • Ele utiliza uma estrutura de dados chamada Fila de prioridade para organizar os vértices que serão explorados;
  • Além de salvar os caminhos já calculados (como Dijkstra faz), ele também utiliza heurísticas para estimar em cada ponto quanto ainda falta para o final, buscando direcionar a escolha do próximo vértice.

A ideia do algoritmo é tentar acertar o menor caminho o mais rápido possível! Por isso ele olha tanto para o passado (o quanto ele já percorreu) como para o futuro (o quanto ainda falta) na hora de estimar se um caminho é bom ou não. Calma, não estou dizendo que ele acerta de primeira, mas ele converge para o menor caminho muito mais rápida e precisamente do que Dijkstra ou as buscas por DFS e BFS conseguem fazer. De certa forma, esse algoritmo é uma combinação das duas abordagens que foram apresentadas nas seções anteriores dessa aula, utilizando o que há de melhor em cada um dos métodos.

Por convergir mais rápido, o algoritmo A* acaba explorando menos vértices para achar a solução, e consequentemente possui uma performance melhor (por isso o povo de jogos usa bastante ele!). Se você não usar nenhuma heurística para mensurar o caminho para a frente do vértice atual, estará olhando apenas o caminho já percorrido, e o algoritmo se iguala ao de Dijkstra.

Você deve estar se perguntando: o que é uma Fila de prioridade? É a mesma estrutura que eu já estudei?

Não! Essa seria uma estrutura de dados nova. Nessa estrutura, foi definido um critério de prioridade, e os elementos que mais atendem esse critério são os primeiros a serem acessados em uma consulta à estrutura. Por exemplo, se for definida uma Fila de prioridade para valores inteiros, e que o critério é o menor valor, então o menor valor existente entre os elementos da Fila será o primeiro a ser acessado em uma consulta. Independentemente da sequência em que você realiza a inserção dos valores, o menor valor sempre será o primeiro a sair da Fila. Ou seja, o menor critério sempre fura para o começo da fila, porque tem a prioridade, captaram?

A implementação desse tipo de estrutura exigiria que você tivesse estudado outras estruturas, como a heap… então essa parte eu vou abstrair. Tudo o que você precisa saber é que, numa Fila de prioridade eu sempre consigo pegar o menor elemento de acordo com o meu critério de forma imediata, ok?

Com esses elementos em mente, o pseudocódigo do A* pode ser descrito como no código abaixo.

x
1
    Busca_A*(Grafo g, Vértice inicio, Vértice fim){
2
        Visitados = {} //Nenhum vértice foi visitado ainda
3
        Abertos ={inicio} //Vértices na fila para exploração
4
5
        custo[] // Vetor que representa os custos dos caminhos para cada 
6
        
7
        //vértice
8
        anterior[] // Vetor que representa o vértice anterior no caminho
9
        futuro[] //Vetor que representa o custo futuro do caminho
10
11
        Enquanto Abertos não for vazio{
12
            atual = vértice de Abertos que possui menor valor de caminho futuro
13
14
            Se atual == fim
15
                Retorne o caminho
16
17
            Remove atual de Abertos
18
            Insere atual em Visitados
19
20
            Para cada vizinho de atual{
21
                Se o vizinho não foi visitado{
22
                    Se ele não estiver em Abertos
23
                        Insere vizinho em Abertos
24
25
                    custo_vizinho = custo[atual] + g[atual, vizinho]
26
27
                    Se custo_vizinho < custo[vizinho]{
28
                        anterior[vizinho] = atual
29
                        custo[vizinho] = custo_vizinho
30
                        futuro[vizinho] = custo[vizinho]+ HEURÍSTICA
31
                    }
32
                }   
33
            }
34
        }
35
        Retorna ERRO
36
    }

Perceba que o código fica um pouco mais complexo na medida em que você adiciona as otimizações. Que tal uma simulação com o exemplo dos casos anteriores, para você poder visualizar melhor o funcionamento do algoritmo?

O bom e velho mapa!

Figura 37 - Passo inicial!
Passo inicial!

O primeiro passo é processar o vértice inicial e calcular os primeiros valores. Como heurística, resolvi fazer esse com a distância Euclidiana, que é uma heurística bastante utilizada em diversas aplicações da computação. A conta fica um pouquinho maior, mas como quem vai fazer é o computador, então tá tudo tranquilo.

Lembrando: a posição do destino é (4,8) e a de partida do jogador é (4,1). Todas as contas de coordenadas são feitas da posição do vértice com a posição do destino, ok?

Aqui serão dois conjuntos de valores calculados para cada um dos vértices: Denomine de C o valor do caminho percorrido até o vértice, e F o valor estimado do restante do caminho a partir do vértice. O custo de movimentação C é unitário, então você precisa fazer a conta do valor da heurística para somar com esses custos:

F(4,1) = RAIZ_QUADRADA(0 + 49) + C(4,1) = 7 + 0 = 7
F(3,1) = RAIZ_QUADRADA(1 + 49) + C(3,1) = 7.07 + 1 = 8.07
F(3,2) = RAIZ_QUADRADA(1 + 36) + C(3,2) = 6.08 + 1 = 7.08
F(4,2) = RAIZ_QUADRADA(0 + 36) + C(4,2) = 6 + 1 = 7
F(5,1) = RAIZ_QUADRADA(1 + 49) + C(5,1) = 7.07 + 1 = 8.07
F(5,2) = RAIZ_QUADRADA(1 + 36) + C(5,2) = 6.08 + 1 = 7.08

Entendeu a ideia? Cada vértice vai ter duas informações: C e F.

Figura 38 - Passo 01 do patrulheiro estelar
Passo 01 do patrulheiro estelar

A partir desse ponto, escolhe-se sempre o vértice que tiver o menor F como o próximo a ser explorado. Em caso de empate, vai ser o primeiro que foi adicionado na fila.

Figura 39 - Passo 02 do patrulheiro estelar
Passo 02 do patrulheiro estelar

Valide as contas do valor de F, para ver se você está seguro:

F(3,3) = RAIZ_QUADRADA(1 + 25) + C(3,3) = 5.09 + 2 = 7.09
F(4,3) = RAIZ_QUADRADA(0 + 25) + C(4,3) = 5 + 2 = 7
F(5,3) = RAIZ_QUADRADA(1 + 25) + C(5,3) = 5.09 + 2 = 7.09

A partir daqui vou colocar as contas dos passos diretamente, mas você pode fazer a conta para validar, certo?

Figura 40 - Passo 03 do patrulheiro estelar
Passo 03 do patrulheiro estelar

Quase lá! Substitui a figura da nave pelos valores que são calculados para o vértice destino, para mostrar que ele seria a escolha certa ao final do algoritmo.

Figura 57 - Passo 20 do patrulheiro estela
Passo 20 do patrulheiro estela

Chegou!Ufa, 20 passos para convergir para o menor caminho.

Eu não adicionei a informação do mapa anterior nessa imagem, mas o caminho traçado como menor foi o seguinte:

Figura 58 - Até que enfim!
Até que enfim!

Não deixe a quantidade de imagens lhe enganar: o algoritmo A* convergiu para o menor caminho após explorar 20 vértices. Eu não coloquei todos do Dijkstra, mas contei aqui: foi necessário explorar 35, quase o dobro para achar o caminho! Isso é uma pequena mostra de como esse algoritmo consegue aumentar a performance de execução mantendo a corretude do caminho encontrado. Mas é claro, tanto o meu nome como o dele começam com a mesma letra.

E é com esse assunto que encerro o estudo do conteúdo de IA para jogos.

Fonte: TENOR. Disponível em: <https://tenor.com/search/desespero-gifs>. Acesso em: 07 jun. 2018.

É, eu também fico sentimental nessas horas…

Eu sei que alguns assuntos podem parecer muito complexos à primeira vista, mas lembre-se: para alguns dos tópicos da disciplina já existem ferramentas prontas, e você entender como elas funcionam, para quais situações são adequadas e a forma de utilizar já é metade do caminho andado!

E quem sabe alguns dos assuntos da disciplina tenham despertado seu interesse para outras áreas também. Que o conhecimento aqui sirva de plataforma para você progredir ainda mais como programador.

Até uma próxima aula! (Essa não é a última? )

Versão 5.3 - Todos os Direitos reservados