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: 617



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

Integrando Laravel com o protocolo MQTT para comunicação entre dispositivos

Projeto de comunicação entre dois dispositivos ESP8266 e Raspberrypi4. Laravel irá funcionar como servidor e receptor de dados de temperatura e umidade coletados com o DHT11.

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.

Tutorial de programação do jogo da serpente em javascript

Programando o clássico jogo da serpente usando o framework p5.js. Tutorial indicado para iniciantes da programação que querem aprender os conceitos básico da área criando jogos.

Criando um jogo de pacman usando javascript e pixi.js (parte 1)

Desenvolvimento dos conceitos mais básicos do clássico pacman, como: mapa, animação, deslocamento e detector de colisões.

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

A internet é uma terra sem lei. O que vai proteger a nossa rede interna da internet é o firewall

Uma rede interna sem um firewall é como se fosse uma casa com a porta destrancada. Um indivíduo com más intenções pode se aproveitar para a invadir quando menos esperamos.

O DNS torna a interface do navegador mais amigável aos usuários

Antes de podermos visualizar o site, o endereço que digitamos na barra de endereço do nosso navegador passa por várias etapas, para só então podermos visualizar o site pela primeira vez...

Diversidade nas empresas

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

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.

Algoritmo A* (A-estrela)

Algoritmo que busca o caminho com o menor custo entre dois pontos. É usado em jogos e aplicativos de navegação para calcular a menor distância possível.

Busca linear

A busca linear é um algoritmo de força bruta não muito eficiente, mas com grande simplicidade, sendo utilizada regularmente por programadores.