Prevenção de colisões (collision avoidance)

Comportamento usado em steering behaviors para evitar que o agente se choque contra obstáculos durante o percurso do seu trajeto.

Categoria de Programação

Postado em 19 junho 2023

Atualizado em 19 junho 2023

Palavras-chave: steering,behavior,collision,avoid,avoidance,comportamento,colisao,seek,busca

Visualizações: 407



Os comportamentos de navegação (steering behaviors) são um conjunto de algoritmos que simulam comportamentos de seres vivos. Uma porção desse conjunto pode ser eficiente para o desenvolvimento de jogos ou máquinas que podem se locomover. Alguns desses comportamentos já foram documentados aqui:

Usando como exemplo jogos de duas dimensões, é bastante comum o desenvolvedor fazer o inimigo perseguir o jogador. Se usarmos vetores geométricos, podemos calcular facilmente a direção que o inimigo deve ir para se aproximar do jogador e definir sua velocidade a cada frame. Porém, quando há obstáculos no meio do caminho, simplesmente calcular o caminho mais curto entre o jogador e o inimigo não é mais viável. Nesse caso, o inimigo deve ser capaz de desviar dos obstáculos para chegar ao local onde o jogador se encontra. Existem diversas técnicas para realizar esse movimento, o algoritmo A-estrela é um exemplo de pathfinding inteligente que encontra o caminho mais curto entre o usuário e o inimigo desviando dos obstáculos. Entretanto, alguns desenvolvedores podem optar por um algoritmo mais simples para fazer o inimigo desviar dos obstáculos. Uma boa opção é a prevenção de colisão (collision avoidance) desenvolvido por Craig Reynolds.

O que é a prevenção de colisões (collision avoidance) nos comportamentos de navegação?

A prevenção de colisões é um algoritmo que pertence ao steering behaviors. O agente é capaz de prever uma colisão com obstáculos usando parâmetros como velocidade e coordenada atual. As coordenadas usadas como base para comparação são a posição atual do agente e a posição atual do obstáculo. Além disso, o agente possui habilidade de “ver adiante” usando a velocidade atual como base. Ao multiplicar a velocidade atual por um valor arbitrário, o agente tem capacidade de ver adiante e detectar colisões com antecedência, evitando obstáculos antes de colidir com eles.

ver adiante prevenção de colisões steering behaviors

Ao normalizar a velocidade do agente e multiplicar por um valor arbitrário, é possível manter a direção do agente com uma magnitude diferente. É assim que é calculado a previsão de colisões. A linha na frente do agente é quem diz se o agente vai desviar ou não do obstáculo. Caso a linha esteja dentro de um obstáculo, ela vai interferir na direção do agente e mudar a sua rota para evitar a colisão.

Entretanto, esse comportamento não é capaz de prevenir uma colisão com total certeza. Dependendo da velocidade e a força exercida sobre a direção do agente, o agente ainda pode colidir com o obstáculo. Por isso, os valores definidos nos parâmetros de velocidade e força interferem fortemente no comportamento de prevenção de colisões. Porém, isso não é necessariamente um ponto negativo. O desenvolvedor pode aumentar o número de colisões de um agente propositalmente para diversas finalidades, como por exemplo, no desenvolvimento de um jogo de corrida. Caso os agentes (carros de corrida) estejam em alta velocidade e não conseguem virar uma curva, eles acabam colidindo com o obstáculo que eles tentaram desviar. Esse tipo de comportamento pode tornar um jogo mais desafiador, uma vez que os agentes também tem que diminuir a velocidade para não colidir com obstáculos.

Como aplicar a prevenção de colisões?

O comportamento de prevenção de colisões pode ser eficiente em alguns casos, mas ineficientes em outros casos. O contexto onde esse algoritmo é implementado influencia fortemente na sua funcionalidade. Simplesmente implementar esse comportamento em um jogo de RPG em um plano 2D pode ser ineficiente se o seu contexto não for compatível. O algoritmo “collision avoidance” possui alguns requerimentos para funcionar corretamente.

Em alguns jogos de RPG, um personagem pode se locomover um quadrado por comando. Ou seja, ao pressionar a tecla “direita” uma vez, o personagem irá se locomover um quadrado para a direita, independente de valores como velocidade e aceleração. Além disso, não há inércia na locomoção dos personagens, um NPC pode mudar a sua direção repentinamente sem se preocupar com a sua velocidade atual. Em casos como esse, o comportamento de prevenção de colisões não irá funcionar com eficiência. Para funcionar corretamente, há a necessidade de implementar mais alguns dos comportamentos de navegação, como o comportamento de busca.

O comportamento de busca faz o agente se locomover em direção a um local no mapa. Essa direção é um vetor e é chamada de velocidade desejada (desired velocity). Por mais que um agente deseje se locomover em uma direção, a sua velocidade atual não pode ser anulada repentinamente. Em outras palavras, a direção desejada do agente sofrerá variações com valores como velocidade atual e força de navegação (steer force).

Em resumo, dependendo do contexto onde o algoritmo de prevenção de colisões é aplicado, seu comportamento pode variar. Na maioria das vezes o desenvolvedor é quem deve ajustar o algoritmo para funcionar corretamente em um determinado contexto.

Pré-implementação de prevenção de colisões

O agente é o objeto que desviará dos obstáculos para chegar em um determinado local. Um cenário pode ter vários agentes com destinos diferentes. A grande vantagem dos comportamentos de colisões é que cada agente é autônomo.

class boid {
    constructor(position, acceleration) {
        this.position = position.clone()
        this.velocity = new Vector(0, 0)
        this.acceleration = acceleration.clone()
        this.maxVelocity = 4
        this.maxForce = 0.5
    }

    draw() {
        circle(this.position.x, this.position.y, BOID_SIZE)
    }
    
	move() {
	    if (this.position.x > width || this.position.x < 0) {
	        this.velocity.x *= -1
	    }

	    if (this.position.y > height || this.position.y < 0) {
	        this.velocity.y *= -1
	    }

	    this.velocity.add(this.acceleration)
	    this.velocity.limit(this.maxVelocity)
	    this.position.add(this.velocity)
	    this.acceleration.mult(0)
	}
}

Nesse exemplo, a classe do agente será chamada de “boid”. Ela terá cinco atributos: coordenada, velocidade, aceleração, velocidade máxima e força máxima. Com excessão dos atributos “maxVelocity” e “maxForce”, todos os outros atributos são dinâmicos e mudam com frequência.

Dentro da função local “move”, também está sendo verificado se o agente não está saindo das extremidades da tela. Caso o agente esteja quase ultrapassando as extremidades da tela, a direção dele é invertida para a direção contrária.

Em seguida, na mesma função, o valor da aceleração é adicionado na velocidade. A velocidade é limitada para não exceder a velocidade máxima (maxVelocity). O resultado é adicionado na coordenada atual do agente, que finalmente irá se locomover.

let boids = []

function start() {
    let initialPosition = new Vector(width / 2, height / 2)
    let initialVelocity = new Vector(1, 0)
    let boid = new boid(initialPosition, initialVelocity)
    boids.push(boid)
}

function update() {
    for (let i = 0; i < boids.length; i++) {
        boids[i].draw()
        boids[i].move()
    }
}

Na variável “boids” são listadas as instâncias de cada agente. Nesse exemplo, apenas um agente estará sendo adicionado, mas múltiplos agentes também podem ser instanciados sem problemas, pois cada agente atua de forma independente. Na função “update” simplesmente estamos exibindo o agente e atualizando as suas coordenadas.

Em seguida, criamos uma classe para os obstáculos. Um obstáculo é um simples objeto que possui apenas coordenada e tamanho.

class obstacle {
    constructor(position, size) {
        this.position = position
        this.size = size
    }

    draw() {
        circle(this.position.x, this.position.y, this.size)
    }
}

Os obstáculos são instanciados da mesma forma que o agente. Até aqui é a pré-implementação da prevenção de colisões.

Implementando o comportamento de prevenção de colisões

Dentro da classe “boid”, adicionamos a função “steer”, que é responsável por calcular a velocidade desejada pelo agente.

steer(destination) {
    let desiredVelocity = destination - this.position
    desiredVelocity = desiredVelocity.normalize() * this.maxVelocity
    
    let steer = desiredVelocity - this.velocity
    steer.limit(this.maxForce)
    this.acceleration += steer
}

A velocidade desejada é resultado da subtração do destino pela posição atual do agente. Essa velocidade é normalizada e multiplicada pela velocidade máxima do agente. Em seguida, a velocidade desejada é subtraída pela velocidade atual, limitada a força máxima e adicionada com a aceleração. Essa força adicionada a aceleração é a força de navegação (steer force).

O motivo de subtrair a velocidade desejada pela velocidade atual é que a velocidade atual vai sendo atualizada pela aceleração a cada frame. Depois de um certo tempo, a velocidade atual irá ceder aos valores impostos pela aceleração.

Finalmente, adicionamos a função “avoid” que é responsável por prevenir colisões com obstáculos.

avoid(obstacles) {
  	// Coordenada do agente
    let pos = this.position.clone()
    
    // Direção que o agente está se locomovendo
    let nVel = this.velocity.clone().normalize()
    
    // Distância que o agente pode ver
    let future = nVel * SEE_AHEAD
    let ahead = pos + future
    line(pos.x, pos.y, ahead.x, ahead.y)

    for (let i = 0; i < obstacles.length; i++) {
      	// Distância entre os obstáculos e a extremidade que o agente pode ver
        let distanceInFuture = obstacles[i].position.distance(ahead)
        
        // Se o agente ver um obstáculo, tenta desviar
        if (distanceInFuture < obstacles[i].size / 2) {
          	// Diferença da extremidade que o agente pode ver e o centro do obstáculo
            let direction = ahead - obstacles[i].position
            line(obstacles[i].position.x, obstacles[i].position.y, obstacles[i].position.x + direction.x, obstacles[i].position.y + direction.y)

          	// Quanto mais perto do obstáculo, mais forte será a repulsão
            let avoidanceForce = pos.distance(obstacles[i].position)
            direction *= avoidanceForce
          
          	// Aplica a força
            this.steer(direction)
        }
    }
    // Caso não haja obstáculos, acelera o agente até a velocidade máxima
    this.steer(pos.clone() + (nVel * this.velocity.mag()))
}

Antes mesmo do agente chegar perto do obstáculo, ele irá ser empurrado para fora do obstáculo. A distância que o agente pode ver no futuro e a quantidade da força de repulsão podem ser expressadas com linhas. Isso facilita a depuração (debug) dos comportamentos.

O resultado final é um agente capaz de desviar de obstáculos com efetividade. O algoritmo acima foi desenvolvido para funcionar corretamente nesse tipo de contexto. Como dito anteriormente, em alguns outros tipos de ambientes e contextos, ajustes extras podem ser necessários.

Conclusão

A prevenção de colisões (collision avoidance) é um algoritmo que prevê colisões no futuro baseando-se em atributos do agente. Quando uma colisão é prevista pelo agente, ele muda a sua direção prevenindo que aconteça colisões. Dependendo da velocidade e força de navegação, as chances de colisão podem ser diminuídas ou aumentadas. Enfim, a prevenção de colisões não é um comportamento que consegue prevenir em todas as ocasiões colisões entre agentes e obstáculos.

Projetos práticos

Usando dados fornecidos pelo TSE para simular o gráfico das eleições presidenciais de 2022

Simulação dos gráficos do segundo turno das eleições presidenciais, utilizando python e ferramentas de análise de dados, pandas e jupyter.

Criando artes de texto usando imagens

Convertendo imagens para ascii art usando o valor da intensidade das cores cinzentas.

Criando um sistema de integração contínua (CI/CD)

Fazendo a integração contínua de Jenkins, Sonatype Nexus, Sonatype, JUnit e Gradle para automatizar processos repetitivos. Prática bastante usada em tecnologias de DevOps.

Integrando o PHP com Elasticsearch no desenvolvimento de um sistema de busca

Projeto de criação de um sistema de busca usando o framework Symfony e Elasticsearch. A integração com Kibana também é feito de modo remoto com um raspberrypi.

Implementando um algoritmo de pathfinding

Implementando um programa que encontra a menor distância entre dois pontos dentro de um labirinto usando o algoritmo A* (a-estrela).

Veja também

O usuário malicioso joga a isca e espera a vítima pacientemente

Phishing tem esse nome pois a vítima se torna só mais um peixe na rede. Ter conhecimento de phishing é o melhor jeito de evitar ser um desses peixes

O endereçamento de dispositivos na internet é automatizado graças ao DHCP

Antigamente o endereçamento de dispositivos era feito manualmente, porém isso traz muitas dificuldades em questão de administração. O DHCP resolve esses problemas

Seguir caminho (path following)

Um dos comportamentos de navegação (steering behaviors). Faz com que um agente siga um caminho pré-determinado pelo desenvolvedor.

Busca binária

A busca binária usa o método de divisão de conquista que visa em dividir os problemas em pequenos problemas até que eles se resolvam sozinhos.

Steering Behaviors (Comportamentos de navegação)

Conjunto de técnicas de algoritmo que simulam comportamentos realísticos. Usado em jogos e em pesquisas biológicas para o estudo de comportamentos.

Diversidade nas empresas

Política e filosofia que tem como objetivo aceitar pessoas de diferentes etnias, comportamentos e características como funcionário.