Steering Behaviors no Godot 4: Movimento de IA que Não Parece Robô

Steering behaviors no Godot 4 em GDScript: seek, flee, arrive, pursuit, wander e flocking com código que roda. Faça a IA do seu jogo se mexer de forma orgânica.
Steering Behaviors no Godot 4: Movimento de IA que Não Parece Robô
Um inimigo que vira de direção na hora, sem inércia, denuncia o código na cara do jogador. Steering behaviors no Godot resolvem exatamente isso: dão à sua IA um movimento que acelera, curva e freia de forma orgânica, como se houvesse peso por trás dela. Neste guia eu mostro os comportamentos clássicos (seek, flee, arrive, pursuit, wander e flocking) em GDScript de Godot 4 que você copia, cola e roda.
Esse conjunto de técnicas vem de um trabalho do Craig Reynolds dos anos 80, e até hoje é a base de movimento de IA em jogo que parece vivo. A ideia é mais simples do que o nome sugere, e quando você entende a conta central, todos os comportamentos viram variação da mesma fórmula. Este post é um aprofundamento da seção de steering do nosso guia de IA para jogos no Godot; aqui a gente cobre os comportamentos que ficaram de fora lá.
A conta central: velocidade desejada menos velocidade atual
Todo steering behavior responde a uma única pergunta: para onde eu quero ir, e o quão diferente isso é de para onde eu já estou indo? A resposta é um vetor de força:
# desired = a velocidade que eu gostaria de ter agora
# velocity = a velocidade que eu realmente tenho
var steering := desired - velocity
Esse vetor steering é a correção. Se você o aplica de uma vez, volta ao movimento robótico. O truque é aplicar aos poucos, limitando a intensidade da correção por frame. É isso que cria a curva suave em vez do giro seco.
A estrutura base num CharacterBody2D fica assim:
extends CharacterBody2D
@export var max_speed := 250.0
@export var max_force := 600.0 # quão rápido ele consegue corrigir a rota
func apply_steering(steering: Vector2, delta: float) -> void:
# limita a força de correção por frame
steering = steering.limit_length(max_force)
velocity += steering * delta
# nunca passa da velocidade máxima
velocity = velocity.limit_length(max_speed)
move_and_slide()
max_speed define o teto de velocidade; max_force define a manobrabilidade. Força alta dá um agente ágil que vira quase na hora; força baixa dá um caminhão que demora a corrigir. Brincar com esses dois números é metade do trabalho de dar personalidade ao movimento.
Seek e Flee: perseguir e fugir
Seek é ir até um ponto. A velocidade desejada aponta para o alvo, na velocidade máxima:
func seek(target_pos: Vector2) -> Vector2:
var desired := (target_pos - global_position).normalized() * max_speed
return desired - velocity
Flee é o oposto exato: a velocidade desejada aponta para longe da ameaça. Mesma fórmula, sinal trocado:
func flee(threat_pos: Vector2) -> Vector2:
var desired := (global_position - threat_pos).normalized() * max_speed
return desired - velocity
Repare que os dois retornam só o vetor de steering, sem mover nada. Quem move é o apply_steering. Manter cada comportamento como uma função pura que devolve um Vector2 é o que vai te deixar somar vários deles mais pra frente.
Arrive: chegar sem ultrapassar o alvo
Seek tem um problema: o agente chega no alvo em velocidade máxima, ultrapassa, volta, ultrapassa de novo, e fica oscilando em cima do ponto. Arrive resolve fazendo o agente desacelerar conforme se aproxima.
A ideia é um raio de frenagem: fora dele, vai na velocidade máxima; dentro dele, a velocidade cai proporcional à distância.
@export var slowing_radius := 120.0
func arrive(target_pos: Vector2) -> Vector2:
var to_target := target_pos - global_position
var distance := to_target.length()
if distance < 1.0:
return -velocity # praticamente em cima do alvo, freia tudo
var ramped_speed := max_speed
if distance < slowing_radius:
ramped_speed = max_speed * (distance / slowing_radius)
var desired := to_target.normalized() * ramped_speed
return desired - velocity
Use arrive em vez de seek sempre que o alvo for um ponto fixo: um waypoint de patrulha, a posição de uma cobertura, o destino de um caminho calculado. O movimento fica muito mais natural, sem aquele tranco no fim.
Pursuit e Evade: mirar onde o alvo vai estar
Seek mira onde o alvo está agora. Contra um player que se mexe, isso faz o inimigo correr sempre atrás, como um cachorro perseguindo o próprio rabo. Pursuit corrige isso prevendo a posição futura do alvo e indo pra lá.
A previsão é simples: pega a velocidade do alvo, multiplica por um tempo de antecipação e soma na posição atual. Quanto mais longe o alvo está, mais à frente vale prever.
func pursuit(target: CharacterBody2D) -> Vector2:
var to_target := target.global_position - global_position
var distance := to_target.length()
# tempo de previsão cresce com a distância, mas com teto
var prediction := min(distance / max_speed, 1.0)
var future_pos := target.global_position + target.velocity * prediction
return seek(future_pos)
Evade é o pursuit ao contrário: prevê para onde a ameaça vai e foge desse ponto, trocando o seek por flee. Um inimigo que usa evade é muito mais difícil de encurralar, porque ele antecipa o seu movimento em vez de só reagir a ele.
Wander: vagar sem parecer bêbado
Para um inimigo em patrulha ou um NPC ambiente, você quer movimento aleatório, mas suave. Sortear uma direção nova todo frame dá um zigue-zague nervoso e feio. O wander clássico resolve projetando um círculo um pouco à frente do agente e mirando num ponto que desliza devagar pela borda desse círculo.
var wander_angle := 0.0
@export var wander_distance := 80.0 # quão à frente fica o círculo
@export var wander_radius := 40.0 # tamanho do círculo
@export var wander_jitter := 3.0 # quão rápido o alvo desliza
func wander(delta: float) -> Vector2:
# o ângulo muda pouco a cada frame, daí a suavidade
wander_angle += randf_range(-1.0, 1.0) * wander_jitter * delta
var heading := velocity.normalized()
if heading == Vector2.ZERO:
heading = Vector2.RIGHT # parado: escolhe uma direção qualquer
var circle_center := global_position + heading * wander_distance
var offset := Vector2(cos(wander_angle), sin(wander_angle)) * wander_radius
return seek(circle_center + offset)
O segredo está no wander_angle mudar pouquinho por frame. Como o alvo se desloca devagar pela borda do círculo, o agente faz curvas longas e orgânicas em vez de espasmos.
Flocking: vários agentes que se comportam como grupo
A parte mais bonita de steering aparece quando você tem muitos agentes. Três comportamentos somados, descritos por Reynolds como "boids", dão um bando que se move junto sem nenhuma coordenação central:
- Separation: afaste-se dos vizinhos muito próximos. É o que impede o empilhamento.
- Alignment: aponte na média da direção dos vizinhos. É o que faz o grupo andar junto.
- Cohesion: mova-se em direção ao centro do grupo. É o que mantém o bando coeso.
@export var neighbor_radius := 100.0
@export var separation_radius := 50.0
func flock(neighbors: Array) -> Vector2:
var separation := Vector2.ZERO
var alignment := Vector2.ZERO
var cohesion := Vector2.ZERO
var count := 0
for other in neighbors:
if other == self:
continue
var offset := global_position - other.global_position
var distance := offset.length()
if distance > neighbor_radius or distance == 0.0:
continue
# separação: empurra para longe, mais forte quanto mais perto
if distance < separation_radius:
separation += offset / distance
alignment += other.velocity
cohesion += other.global_position
count += 1
if count == 0:
return Vector2.ZERO
alignment = (alignment / count).normalized() * max_speed - velocity
cohesion = ((cohesion / count) - global_position).normalized() * max_speed - velocity
return separation * 1.5 + alignment * 1.0 + cohesion * 1.0
O neighbors aqui é a lista de agentes próximos. Em projeto pequeno dá pra passar o grupo inteiro; quando crescer, vale usar um Area2D em cada agente pra coletar só quem está perto, ou marcar todos com um grupo e filtrar por distância. Se você ainda não usa grupos pra isso, o nosso guia de IA de inimigo que persegue e patrulha mostra o padrão na prática.
Combinando forças com pesos
O poder real de steering é que tudo é vetor, e vetor soma. Um inimigo pode perseguir o player, vagar quando o perde de vista e ainda se separar dos colegas, tudo ao mesmo tempo, com pesos diferentes pra cada força:
func _physics_process(delta: float) -> void:
var steering := Vector2.ZERO
if player_spotted:
steering += pursuit(player) * 1.0
steering += flock(get_nearby_enemies()) * 0.6
else:
steering += wander(delta) * 0.4
apply_steering(steering, delta)
Os pesos (1.0, 0.6, 0.4) são chute inicial. Não espere acertar de primeira: você vai rodar, ver os agentes fazendo algo esquisito e ajustar no olho. É assim mesmo, faz parte do processo. Um peso de separação alto demais espalha o bando até dispersar; baixo demais e eles empilham. O ponto certo só aparece testando com o jogo rodando.
Steering não substitui pathfinding
Um aviso pra não te frustrar: steering sozinho não desvia de paredes. Seek vai mandar o agente em linha reta contra o muro. Steering resolve o movimento orgânico em espaço aberto e o desvio entre agentes; pra contornar a geometria do nível você precisa de pathfinding de verdade.
A combinação que funciona é usar os dois em camadas: o pathfinding no Godot com Navigation2D te dá o próximo ponto do caminho, e o steering (arrive nesse ponto, mais separation dos vizinhos) move o corpo até lá com suavidade. O caminho vem do navmesh; a vida no movimento vem do steering.
Por onde começar
Não tente jogar flocking completo no primeiro inimigo. Vá por partes, do mais simples pro mais complexo:
- Seek primeiro. Faça um agente ir até o mouse. Sinta a inércia mudando
max_force. - Troque por arrive. Veja a diferença na chegada: sem o tranco, ele para suave.
- Adicione separation quando tiver mais de um inimigo, pra eles pararem de se empilhar.
- Pursuit e wander entram quando você quiser inimigos que antecipam e NPCs que vagam.
Steering é uma daquelas ferramentas que parecem mágica quando você vê funcionando e óbvias quando entende a conta por trás. Abre o Godot, faz um CharacterBody2D seguir o mouse com seek, e vai empilhando comportamento por cima. Em pouco tempo você tem inimigos que se mexem de um jeito que o jogador sente, mesmo sem saber explicar por quê.
Perguntas frequentes
O que são steering behaviors em jogos?
São um conjunto de técnicas de movimento criadas por Craig Reynolds para fazer um agente se mover de forma orgânica. Em vez de teletransportar a velocidade para o valor desejado, você calcula uma força de direção a cada frame (a diferença entre a velocidade que você quer e a que tem agora) e aplica essa força aos poucos. O resultado é um movimento com inércia, que acelera e curva de forma natural em vez de parecer um robô trocando de direção no susto.
Qual a diferença entre steering behaviors e pathfinding no Godot?
Pathfinding decide o caminho (quais pontos seguir para ir de A até B sem bater em parede). Steering decide como percorrer esse caminho de um jeito que pareça vivo: acelerando, freando ao chegar, desviando dos vizinhos. Os dois trabalham juntos: o pathfinding te dá o próximo ponto, e o steering move o corpo até ele com suavidade. No Godot você usa NavigationServer para o caminho e steering para o movimento.
Steering behaviors funcionam com CharacterBody2D?
Funcionam, e é a combinação mais comum em 2D. Você acumula a força de steering num Vector2, soma na propriedade velocity do CharacterBody2D, limita o resultado à velocidade máxima e chama move_and_slide(). O CharacterBody2D cuida da colisão; o steering cuida só de para onde e com que intensidade o corpo quer ir.
Como fazer vários inimigos não se empilharem no mesmo ponto?
Use o comportamento de separação (separation) do flocking. Para cada vizinho dentro de um raio, some um vetor que aponta para longe dele, mais forte quanto mais perto estiver. Somando isso ao seek do alvo, os inimigos perseguem o player mas se espalham em vez de virar uma bola única em cima dele.


